refactor into fewer crates, pt.2

This commit is contained in:
🪞👃🪞 2025-05-01 18:03:27 +03:00
parent 77703d83a5
commit a22a793c31
48 changed files with 2155 additions and 2157 deletions

1
Cargo.lock generated
View file

@ -1540,6 +1540,7 @@ dependencies = [
"symphonia",
"tek_engine",
"tengri",
"uuid",
"wavers",
]

View file

@ -1,4 +1,5 @@
use crate::*;
use std::path::PathBuf;
type MaybeClip = Option<Arc<RwLock<MidiClip>>>;
@ -89,7 +90,7 @@ impose!([app: Tek]
(SceneCommand:
("add" [] Some(Self::Add))
("delete" [a: Option<usize>] Some(Self::Del(a.flatten().unwrap())))
("delete" [a: Option<usize>] Some(Self::Del(a.flatten().unwrap())))
("zoom" [a: usize] Some(Self::SetZoom(a.unwrap())))
("color" [a: usize] Some(Self::SetColor(a.unwrap(), ItemTheme::G[128])))
("enqueue" [a: usize] Some(Self::Enqueue(a.unwrap())))
@ -193,3 +194,411 @@ fn delegate_to_pool (app: &mut Tek, cmd: PoolCommand) -> Perhaps<TekCommand> {
None
})
}
handle!(TuiIn: |self: MidiPool, input|{
//Ok(if let Some(command) = match self.mode() {
//Some(PoolMode::Rename(..)) => self.keys_rename,
//Some(PoolMode::Length(..)) => self.keys_length,
//Some(PoolMode::Import(..)) | Some(PoolMode::Export(..)) => self.keys_file,
//_ => self.keys
//}.command::<Self, PoolCommand, TuiIn>(self, input) {
Ok(if let Some(command) = self.keys.command(self, input) {
let _undo = command.execute(self)?;
Some(true)
} else {
None
})
});
provide!(bool: |self: MidiPool| {});
provide!(MidiClip: |self: MidiPool| {
":new-clip" => self.new_clip(),
":cloned-clip" => self.cloned_clip(),
});
provide!(PathBuf: |self: MidiPool| {});
provide!(Arc<str>: |self: MidiPool| {});
provide!(usize: |self: MidiPool| {
":current" => 0,
":after" => 0,
":previous" => 0,
":next" => 0
});
provide!(ItemColor: |self: MidiPool| {
":random-color" => ItemColor::random()
});
#[derive(Clone, PartialEq, Debug)] pub enum PoolCommand {
/// Toggle visibility of pool
Show(bool),
/// Select a clip from the clip pool
Select(usize),
/// Rename a clip
Rename(ClipRenameCommand),
/// Change the length of a clip
Length(ClipLengthCommand),
/// Import from file
Import(FileBrowserCommand),
/// Export to file
Export(FileBrowserCommand),
/// Update the contents of the clip pool
Clip(PoolClipCommand),
}
atom_command!(PoolCommand: |state: MidiPool| {
("show" [a: bool] Some(Self::Show(a.expect("no flag"))))
("select" [i: usize] Some(Self::Select(i.expect("no index"))))
("rename" [,..a] ClipRenameCommand::try_from_expr(state, a).map(Self::Rename))
("length" [,..a] ClipLengthCommand::try_from_expr(state, a).map(Self::Length))
("import" [,..a] FileBrowserCommand::try_from_expr(state, a).map(Self::Import))
("export" [,..a] FileBrowserCommand::try_from_expr(state, a).map(Self::Export))
("clip" [,..a] PoolClipCommand::try_from_expr(state, a).map(Self::Clip))
});
command!(|self: PoolCommand, state: MidiPool|{
use PoolCommand::*;
match self {
Rename(ClipRenameCommand::Begin) => { state.begin_clip_rename(); None }
Rename(command) => command.delegate(state, Rename)?,
Length(ClipLengthCommand::Begin) => { state.begin_clip_length(); None },
Length(command) => command.delegate(state, Length)?,
Import(FileBrowserCommand::Begin) => { state.begin_import()?; None },
Import(command) => command.delegate(state, Import)?,
Export(FileBrowserCommand::Begin) => { state.begin_export()?; None },
Export(command) => command.delegate(state, Export)?,
Clip(command) => command.execute(state)?.map(Clip),
Show(visible) => { state.visible = visible; Some(Self::Show(!visible)) },
Select(clip) => { state.set_clip_index(clip); None },
}
});
#[derive(Clone, Debug, PartialEq)] pub enum PoolClipCommand {
Add(usize, MidiClip),
Delete(usize),
Swap(usize, usize),
Import(usize, PathBuf),
Export(usize, PathBuf),
SetName(usize, Arc<str>),
SetLength(usize, usize),
SetColor(usize, ItemColor),
}
atom_command!(PoolClipCommand: |state: MidiPool| {
("add" [i: usize, c: MidiClip]
Some(Self::Add(i.expect("no index"), c.expect("no clip"))))
("delete" [i: usize]
Some(Self::Delete(i.expect("no index"))))
("swap" [a: usize, b: usize]
Some(Self::Swap(a.expect("no index"), b.expect("no index"))))
("import" [i: usize, p: PathBuf]
Some(Self::Import(i.expect("no index"), p.expect("no path"))))
("export" [i: usize, p: PathBuf]
Some(Self::Export(i.expect("no index"), p.expect("no path"))))
("set-name" [i: usize, n: Arc<str>]
Some(Self::SetName(i.expect("no index"), n.expect("no name"))))
("set-length" [i: usize, l: usize]
Some(Self::SetLength(i.expect("no index"), l.expect("no length"))))
("set-color" [i: usize, c: ItemColor]
Some(Self::SetColor(i.expect("no index"), c.expect("no color"))))
});
impl<T: HasClips> Command<T> for PoolClipCommand {
fn execute (self, model: &mut T) -> Perhaps<Self> {
use PoolClipCommand::*;
Ok(match self {
Add(mut index, clip) => {
let clip = Arc::new(RwLock::new(clip));
let mut clips = model.clips_mut();
if index >= clips.len() {
index = clips.len();
clips.push(clip)
} else {
clips.insert(index, clip);
}
Some(Self::Delete(index))
},
Delete(index) => {
let clip = model.clips_mut().remove(index).read().unwrap().clone();
Some(Self::Add(index, clip))
},
Swap(index, other) => {
model.clips_mut().swap(index, other);
Some(Self::Swap(index, other))
},
Import(index, path) => {
let bytes = std::fs::read(&path)?;
let smf = Smf::parse(bytes.as_slice())?;
let mut t = 0u32;
let mut events = vec![];
for track in smf.tracks.iter() {
for event in track.iter() {
t += event.delta.as_int();
if let TrackEventKind::Midi { channel, message } = event.kind {
events.push((t, channel.as_int(), message));
}
}
}
let mut clip = MidiClip::new("imported", true, t as usize + 1, None, None);
for event in events.iter() {
clip.notes[event.0 as usize].push(event.2);
}
Self::Add(index, clip).execute(model)?
},
Export(_index, _path) => {
todo!("export clip to midi file");
},
SetName(index, name) => {
let clip = &mut model.clips_mut()[index];
let old_name = clip.read().unwrap().name.clone();
clip.write().unwrap().name = name;
Some(Self::SetName(index, old_name))
},
SetLength(index, length) => {
let clip = &mut model.clips_mut()[index];
let old_len = clip.read().unwrap().length;
clip.write().unwrap().length = length;
Some(Self::SetLength(index, old_len))
},
SetColor(index, color) => {
let mut color = ItemTheme::from(color);
std::mem::swap(&mut color, &mut model.clips()[index].write().unwrap().color);
Some(Self::SetColor(index, color.base))
},
})
}
}
#[derive(Clone, Debug, PartialEq)] pub enum ClipRenameCommand {
Begin,
Cancel,
Confirm,
Set(Arc<str>),
}
atom_command!(ClipRenameCommand: |state: MidiPool| {
("begin" [] Some(Self::Begin))
("cancel" [] Some(Self::Cancel))
("confirm" [] Some(Self::Confirm))
("set" [n: Arc<str>] Some(Self::Set(n.expect("no name"))))
});
command!(|self: ClipRenameCommand, state: MidiPool|if let Some(
PoolMode::Rename(clip, ref mut old_name)
) = state.mode_mut().clone() {
match self {
Self::Set(s) => {
state.clips()[clip].write().unwrap().name = s;
return Ok(Some(Self::Set(old_name.clone().into())))
},
Self::Confirm => {
let old_name = old_name.clone();
*state.mode_mut() = None;
return Ok(Some(Self::Set(old_name)))
},
Self::Cancel => {
state.clips()[clip].write().unwrap().name = old_name.clone().into();
return Ok(None)
},
_ => unreachable!()
}
} else {
unreachable!()
});
atom_command!(FileBrowserCommand: |state: MidiPool| {
("begin" [] Some(Self::Begin))
("cancel" [] Some(Self::Cancel))
("confirm" [] Some(Self::Confirm))
("select" [i: usize] Some(Self::Select(i.expect("no index"))))
("chdir" [p: PathBuf] Some(Self::Chdir(p.expect("no path"))))
("filter" [f: Arc<str>] Some(Self::Filter(f.expect("no filter"))))
});
command!(|self: FileBrowserCommand, state: MidiPool|{
use PoolMode::*;
use FileBrowserCommand::*;
let mode = &mut state.mode;
match mode {
Some(Import(index, ref mut browser)) => match self {
Cancel => { *mode = None; },
Chdir(cwd) => { *mode = Some(Import(*index, FileBrowser::new(Some(cwd))?)); },
Select(index) => { browser.index = index; },
Confirm => if browser.is_file() {
let index = *index;
let path = browser.path();
*mode = None;
PoolClipCommand::Import(index, path).execute(state)?;
} else if browser.is_dir() {
*mode = Some(Import(*index, browser.chdir()?));
},
_ => todo!(),
},
Some(Export(index, ref mut browser)) => match self {
Cancel => { *mode = None; },
Chdir(cwd) => { *mode = Some(Export(*index, FileBrowser::new(Some(cwd))?)); },
Select(index) => { browser.index = index; },
_ => unreachable!()
},
_ => unreachable!(),
};
None
});
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum ClipLengthCommand {
Begin,
Cancel,
Set(usize),
Next,
Prev,
Inc,
Dec,
}
atom_command!(ClipLengthCommand: |state: MidiPool| {
("begin" [] Some(Self::Begin))
("cancel" [] Some(Self::Cancel))
("next" [] Some(Self::Next))
("prev" [] Some(Self::Prev))
("inc" [] Some(Self::Inc))
("dec" [] Some(Self::Dec))
("set" [l: usize] Some(Self::Set(l.expect("no length"))))
});
command!(|self: ClipLengthCommand, state: MidiPool|{
use ClipLengthCommand::*;
use ClipLengthFocus::*;
if let Some(
PoolMode::Length(clip, ref mut length, ref mut focus)
) = state.mode_mut().clone() {
match self {
Cancel => { *state.mode_mut() = None; },
Prev => { focus.prev() },
Next => { focus.next() },
Inc => match focus {
Bar => { *length += 4 * PPQ },
Beat => { *length += PPQ },
Tick => { *length += 1 },
},
Dec => match focus {
Bar => { *length = length.saturating_sub(4 * PPQ) },
Beat => { *length = length.saturating_sub(PPQ) },
Tick => { *length = length.saturating_sub(1) },
},
Set(length) => {
let old_length;
{
let clip = state.clips()[clip].clone();//.write().unwrap();
old_length = Some(clip.read().unwrap().length);
clip.write().unwrap().length = length;
}
*state.mode_mut() = None;
return Ok(old_length.map(Self::Set))
},
_ => unreachable!()
}
} else {
unreachable!();
}
None
});
provide!(bool: |self: MidiEditor| {
":true" => true,
":false" => false,
":time-lock" => self.time_lock().get(),
":time-lock-toggle" => !self.time_lock().get(),
});
provide!(usize: |self: MidiEditor| {
":note-length" => self.note_len(),
":note-pos" => self.note_pos(),
":note-pos-next" => self.note_pos() + 1,
":note-pos-prev" => self.note_pos().saturating_sub(1),
":note-pos-next-octave" => self.note_pos() + 12,
":note-pos-prev-octave" => self.note_pos().saturating_sub(12),
":note-len" => self.note_len(),
":note-len-next" => self.note_len() + 1,
":note-len-prev" => self.note_len().saturating_sub(1),
":note-range" => self.note_axis().get(),
":note-range-prev" => self.note_axis().get() + 1,
":note-range-next" => self.note_axis().get().saturating_sub(1),
":time-pos" => self.time_pos(),
":time-pos-next" => self.time_pos() + self.time_zoom().get(),
":time-pos-prev" => self.time_pos().saturating_sub(self.time_zoom().get()),
":time-zoom" => self.time_zoom().get(),
":time-zoom-next" => self.time_zoom().get() + 1,
":time-zoom-prev" => self.time_zoom().get().saturating_sub(1).max(1),
});
atom_command!(MidiEditCommand: |state: MidiEditor| {
("note/append" [] Some(Self::AppendNote))
("note/put" [] Some(Self::PutNote))
("note/del" [] Some(Self::DelNote))
("note/pos" [a: usize] Some(Self::SetNoteCursor(a.expect("no note cursor"))))
("note/len" [a: usize] Some(Self::SetNoteLength(a.expect("no note length"))))
("time/pos" [a: usize] Some(Self::SetTimeCursor(a.expect("no time cursor"))))
("time/zoom" [a: usize] Some(Self::SetTimeZoom(a.expect("no time zoom"))))
("time/lock" [a: bool] Some(Self::SetTimeLock(a.expect("no time lock"))))
("time/lock" [] Some(Self::SetTimeLock(!state.time_lock().get())))
});
#[derive(Clone, Debug)] pub enum MidiEditCommand {
// TODO: 1-9 seek markers that by default start every 8th of the clip
AppendNote,
PutNote,
DelNote,
SetNoteCursor(usize),
SetNoteLength(usize),
SetNoteScroll(usize),
SetTimeCursor(usize),
SetTimeScroll(usize),
SetTimeZoom(usize),
SetTimeLock(bool),
Show(Option<Arc<RwLock<MidiClip>>>),
}
handle!(TuiIn: |self: MidiEditor, input|Ok(if let Some(command) = self.keys.command(self, input) {
command.execute(self)?;
Some(true)
} else {
None
}));
impl Command<MidiEditor> for MidiEditCommand {
fn execute (self, state: &mut MidiEditor) -> Perhaps<Self> {
use MidiEditCommand::*;
match self {
Show(clip) => { state.set_clip(clip.as_ref()); },
DelNote => {},
PutNote => { state.put_note(false); },
AppendNote => { state.put_note(true); },
SetTimeZoom(x) => { state.time_zoom().set(x); state.redraw(); },
SetTimeLock(x) => { state.time_lock().set(x); },
SetTimeScroll(x) => { state.time_start().set(x); },
SetNoteScroll(x) => { state.note_lo().set(x.min(127)); },
SetNoteLength(x) => {
let note_len = state.note_len();
let time_zoom = state.time_zoom().get();
state.set_note_len(x);
//if note_len / time_zoom != x / time_zoom {
state.redraw();
//}
},
SetTimeCursor(x) => { state.set_time_pos(x); },
SetNoteCursor(note) => { state.set_note_pos(note.min(127)); },
//_ => todo!("{:?}", self)
}
Ok(None)
}
}

View file

@ -19,11 +19,8 @@
pub type Usually<T> = std::result::Result<T, Box<dyn std::error::Error>>;
/// Standard optional result type.
pub type Perhaps<T> = std::result::Result<Option<T>, Box<dyn std::error::Error>>;
pub use ::tek_time::{self, *};
pub use ::tek_jack::{self, *, jack::*};
pub use ::tek_midi::{self, *, midly::{MidiMessage, num::*, live::*}};
pub use ::tek_sampler::{self, *};
#[cfg(feature = "host")] pub use ::tek_plugin::{self, *};
pub use ::tek_engine:: *;
pub use ::tek_device::{self, *};
pub use ::tengri::dsl::*;
pub use ::tengri::input::*;
pub use ::tengri::output::*;
@ -34,7 +31,8 @@ pub use ::tengri::tui::ratatui::prelude::Color::{self, *};
pub use ::tengri::tui::ratatui::prelude::{Style, Stylize, Buffer, Modifier};
pub use ::tengri::tui::crossterm;
pub use ::tengri::tui::crossterm::event::{Event, KeyCode::{self, *}};
pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicBool, Ordering::Relaxed}};
pub(crate) use std::sync::{Arc, RwLock};
pub(crate) use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed};
mod api; pub use self::api::*;
mod audio; pub use self::audio::*;

View file

@ -789,9 +789,391 @@ impl HasTracks for Tek {
}
#[derive(Debug)]
pub enum Device {
Sequencer(MidiPlayer),
Sampler(Sampler),
#[cfg(feature="host")]
Plugin(Plugin),
pub struct MidiPool {
pub visible: bool,
/// Collection of clips
pub clips: Arc<RwLock<Vec<Arc<RwLock<MidiClip>>>>>,
/// Selected clip
pub clip: AtomicUsize,
/// Mode switch
pub mode: Option<PoolMode>,
pub keys: InputMap<'static, Self, PoolCommand, TuiIn, SourceIter<'static>>,
//pub keys: SourceIter<'static>,
//pub keys_rename: SourceIter<'static>,
//pub keys_length: SourceIter<'static>,
//pub keys_file: SourceIter<'static>,
}
impl Default for MidiPool {
fn default () -> Self {
use PoolMode::*;
Self {
visible: true,
clips: Arc::from(RwLock::from(vec![])),
clip: 0.into(),
mode: None,
keys: InputMap::new(SourceIter(include_str!("../../../config/keys_pool.edn")))
.layer_if(|pool: &Self|matches!(pool.mode, Some(Import(..))|Some(Export(..))),
SourceIter(include_str!("../../../config/keys_pool_file.edn")))
.layer_if(|pool: &Self|matches!(pool.mode, Some(Rename(..))),
SourceIter(include_str!("../../../config/keys_clip_rename.edn")))
.layer_if(|pool: &Self|matches!(pool.mode, Some(Length(..))),
SourceIter(include_str!("../../../config/keys_clip_length.edn")))
}
}
}
has_clips!(|self: MidiPool|self.clips);
has_clip!(|self: MidiPool|self.clips().get(self.clip_index()).map(|c|c.clone()));
from!(|clip:&Arc<RwLock<MidiClip>>|MidiPool = {
let model = Self::default();
model.clips.write().unwrap().push(clip.clone());
model.clip.store(1, Relaxed);
model
});
impl MidiPool {
pub fn clip_index (&self) -> usize {
self.clip.load(Relaxed)
}
pub fn set_clip_index (&self, value: usize) {
self.clip.store(value, Relaxed);
}
pub fn mode (&self) -> &Option<PoolMode> {
&self.mode
}
pub fn mode_mut (&mut self) -> &mut Option<PoolMode> {
&mut self.mode
}
pub fn begin_clip_length (&mut self) {
let length = self.clips()[self.clip_index()].read().unwrap().length;
*self.mode_mut() = Some(PoolMode::Length(
self.clip_index(),
length,
ClipLengthFocus::Bar
));
}
pub fn begin_clip_rename (&mut self) {
let name = self.clips()[self.clip_index()].read().unwrap().name.clone();
*self.mode_mut() = Some(PoolMode::Rename(
self.clip_index(),
name
));
}
pub fn begin_import (&mut self) -> Usually<()> {
*self.mode_mut() = Some(PoolMode::Import(
self.clip_index(),
FileBrowser::new(None)?
));
Ok(())
}
pub fn begin_export (&mut self) -> Usually<()> {
*self.mode_mut() = Some(PoolMode::Export(
self.clip_index(),
FileBrowser::new(None)?
));
Ok(())
}
pub fn new_clip (&self) -> MidiClip {
MidiClip::new("Clip", true, 4 * PPQ, None, Some(ItemTheme::random()))
}
pub fn cloned_clip (&self) -> MidiClip {
let index = self.clip_index();
let mut clip = self.clips()[index].read().unwrap().duplicate();
clip.color = ItemTheme::random_near(clip.color, 0.25);
clip
}
pub fn add_new_clip (&self) -> (usize, Arc<RwLock<MidiClip>>) {
let clip = Arc::new(RwLock::new(self.new_clip()));
let index = {
let mut clips = self.clips.write().unwrap();
clips.push(clip.clone());
clips.len().saturating_sub(1)
};
self.clip.store(index, Relaxed);
(index, clip)
}
pub fn delete_clip (&mut self, clip: &MidiClip) -> bool {
let index = self.clips.read().unwrap().iter().position(|x|*x.read().unwrap()==*clip);
if let Some(index) = index {
self.clips.write().unwrap().remove(index);
return true
}
false
}
}
/// Modes for clip pool
#[derive(Debug, Clone)]
pub enum PoolMode {
/// Renaming a pattern
Rename(usize, Arc<str>),
/// Editing the length of a pattern
Length(usize, usize, ClipLengthFocus),
/// Load clip from disk
Import(usize, FileBrowser),
/// Save clip to disk
Export(usize, FileBrowser),
}
/// Focused field of `ClipLength`
#[derive(Copy, Clone, Debug)]
pub enum ClipLengthFocus {
/// Editing the number of bars
Bar,
/// Editing the number of beats
Beat,
/// Editing the number of ticks
Tick,
}
impl ClipLengthFocus {
pub fn next (&mut self) {
*self = match self { Self::Bar => Self::Beat, Self::Beat => Self::Tick, Self::Tick => Self::Bar, }
}
pub fn prev (&mut self) {
*self = match self { Self::Bar => Self::Tick, Self::Beat => Self::Bar, Self::Tick => Self::Beat, }
}
}
/// Displays and edits clip length.
#[derive(Clone)]
pub struct ClipLength {
/// Pulses per beat (quaver)
ppq: usize,
/// Beats per bar
bpb: usize,
/// Length of clip in pulses
pulses: usize,
/// Selected subdivision
pub focus: Option<ClipLengthFocus>,
}
impl ClipLength {
pub fn _new (pulses: usize, focus: Option<ClipLengthFocus>) -> Self {
Self { ppq: PPQ, bpb: 4, pulses, focus }
}
pub fn bars (&self) -> usize {
self.pulses / (self.bpb * self.ppq)
}
pub fn beats (&self) -> usize {
(self.pulses % (self.bpb * self.ppq)) / self.ppq
}
pub fn ticks (&self) -> usize {
self.pulses % self.ppq
}
pub fn bars_string (&self) -> Arc<str> {
format!("{}", self.bars()).into()
}
pub fn beats_string (&self) -> Arc<str> {
format!("{}", self.beats()).into()
}
pub fn ticks_string (&self) -> Arc<str> {
format!("{:>02}", self.ticks()).into()
}
}
pub type ClipPool = Vec<Arc<RwLock<MidiClip>>>;
pub trait HasClips {
fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>;
fn clips_mut <'a> (&'a self) -> std::sync::RwLockWriteGuard<'a, ClipPool>;
fn add_clip (&self) -> (usize, Arc<RwLock<MidiClip>>) {
let clip = Arc::new(RwLock::new(MidiClip::new("Clip", true, 384, None, None)));
self.clips_mut().push(clip.clone());
(self.clips().len() - 1, clip)
}
}
#[macro_export] macro_rules! has_clips {
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? {
fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> {
$cb.read().unwrap()
}
fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> {
$cb.write().unwrap()
}
}
}
}
/// Contains state for viewing and editing a clip
pub struct MidiEditor {
/// Size of editor on screen
pub size: Measure<TuiOut>,
/// View mode and state of editor
pub mode: PianoHorizontal,
/// Input keymap
pub keys: InputMap<'static, Self, MidiEditCommand, TuiIn, SourceIter<'static>>
}
impl std::fmt::Debug for MidiEditor {
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
f.debug_struct("MidiEditor")
.field("mode", &self.mode)
.finish()
}
}
impl Default for MidiEditor {
fn default () -> Self {
Self {
size: Measure::new(),
mode: PianoHorizontal::new(None),
keys: InputMap::new(SourceIter(include_str!("../../../config/keys_editor.edn"))),
}
}
}
has_size!(<TuiOut>|self: MidiEditor|&self.size);
content!(TuiOut: |self: MidiEditor| {
self.autoscroll();
//self.autozoom();
self.size.of(&self.mode)
});
from!(|clip: &Arc<RwLock<MidiClip>>|MidiEditor = {
let model = Self::from(Some(clip.clone()));
model.redraw();
model
});
from!(|clip: Option<Arc<RwLock<MidiClip>>>|MidiEditor = {
let mut model = Self::default();
*model.clip_mut() = clip;
model.redraw();
model
});
impl MidiEditor {
/// Put note at current position
pub fn put_note (&mut self, advance: bool) {
let mut redraw = false;
if let Some(clip) = self.clip() {
let mut clip = clip.write().unwrap();
let note_start = self.time_pos();
let note_pos = self.note_pos();
let note_len = self.note_len();
let note_end = note_start + (note_len.saturating_sub(1));
let key: u7 = u7::from(note_pos as u8);
let vel: u7 = 100.into();
let length = clip.length;
let note_end = note_end % length;
let note_on = MidiMessage::NoteOn { key, vel };
if !clip.notes[note_start].iter().any(|msg|*msg == note_on) {
clip.notes[note_start].push(note_on);
}
let note_off = MidiMessage::NoteOff { key, vel };
if !clip.notes[note_end].iter().any(|msg|*msg == note_off) {
clip.notes[note_end].push(note_off);
}
if advance {
self.set_time_pos(note_end);
}
redraw = true;
}
if redraw {
self.mode.redraw();
}
}
pub fn clip_status (&self) -> impl Content<TuiOut> + '_ {
let (color, name, length, looped) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) {
(clip.color, clip.name.clone(), clip.length, clip.looped)
} else { (ItemTheme::G[64], String::new().into(), 0, false) };
Bsp::e(
FieldH(color, "Edit", format!("{name} ({length})")),
FieldH(color, "Loop", looped.to_string())
)
}
pub fn edit_status (&self) -> impl Content<TuiOut> + '_ {
let (color, length) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) {
(clip.color, clip.length)
} else { (ItemTheme::G[64], 0) };
let time_pos = self.time_pos();
let time_zoom = self.time_zoom().get();
let time_lock = if self.time_lock().get() { "[lock]" } else { " " };
let note_pos = format!("{:>3}", self.note_pos());
let note_name = format!("{:4}", Note::pitch_to_name(self.note_pos()));
let note_len = format!("{:>4}", self.note_len());
Bsp::e(
FieldH(color, "Time", format!("{length}/{time_zoom}+{time_pos} {time_lock}")),
FieldH(color, "Note", format!("{note_name} {note_pos} {note_len}")),
)
}
//fn clip_length (&self) -> usize {
//self.clip().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1)
//}
}
impl TimeRange for MidiEditor {
fn time_len (&self) -> &AtomicUsize { self.mode.time_len() }
fn time_zoom (&self) -> &AtomicUsize { self.mode.time_zoom() }
fn time_lock (&self) -> &AtomicBool { self.mode.time_lock() }
fn time_start (&self) -> &AtomicUsize { self.mode.time_start() }
fn time_axis (&self) -> &AtomicUsize { self.mode.time_axis() }
}
impl NoteRange for MidiEditor {
fn note_lo (&self) -> &AtomicUsize { self.mode.note_lo() }
fn note_axis (&self) -> &AtomicUsize { self.mode.note_axis() }
}
impl NotePoint for MidiEditor {
fn note_len (&self) -> usize { self.mode.note_len() }
fn set_note_len (&self, x: usize) -> usize { self.mode.set_note_len(x) }
fn note_pos (&self) -> usize { self.mode.note_pos() }
fn set_note_pos (&self, x: usize) -> usize { self.mode.set_note_pos(x) }
}
impl TimePoint for MidiEditor {
fn time_pos (&self) -> usize { self.mode.time_pos() }
fn set_time_pos (&self, x: usize) -> usize { self.mode.set_time_pos(x) }
}
impl MidiViewer for MidiEditor {
fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { self.mode.buffer_size(clip) }
fn redraw (&self) { self.mode.redraw() }
fn clip (&self) -> &Option<Arc<RwLock<MidiClip>>> { self.mode.clip() }
fn clip_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>> { self.mode.clip_mut() }
fn set_clip (&mut self, p: Option<&Arc<RwLock<MidiClip>>>) { self.mode.set_clip(p) }
}
pub trait HasEditor {
fn editor (&self) -> &Option<MidiEditor>;
fn editor_mut (&mut self) -> &Option<MidiEditor>;
fn is_editing (&self) -> bool { true }
fn editor_w (&self) -> usize { 0 }
fn editor_h (&self) -> usize { 0 }
}
#[macro_export] macro_rules! has_editor {
(|$self:ident: $Struct:ident|{
editor = $e0:expr;
editor_w = $e1:expr;
editor_h = $e2:expr;
is_editing = $e3:expr;
}) => {
impl HasEditor for $Struct {
fn editor (&$self) -> &Option<MidiEditor> { &$e0 }
fn editor_mut (&mut $self) -> &Option<MidiEditor> { &mut $e0 }
fn editor_w (&$self) -> usize { $e1 }
fn editor_h (&$self) -> usize { $e2 }
fn is_editing (&$self) -> bool { $e3 }
}
};
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? HasEditor for $Struct $(<$($L),*$($T),*>)? {
fn editor (&$self) -> &MidiEditor { &$cb }
}
};
}

View file

@ -388,16 +388,15 @@ impl<'a> ArrangerView<'a> {
let queued = track.player.next_clip.is_some();
let queued_blank = Thunk::new(||Tui::bg(Reset, " ------ "));
let queued_clip = Thunk::new(||{
let title = if let Some((_, clip)) = track.player.next_clip.as_ref() {
Tui::bg(Reset, if let Some((_, clip)) = track.player.next_clip.as_ref() {
if let Some(clip) = clip {
clip.read().unwrap().name.as_ref().clone()
clip.read().unwrap().name.clone()
} else {
"Stop"
"Stop".into()
}
} else {
""
};
Tui::bg(Reset, title)
"".into()
})
});
Either(queued, queued_clip, queued_blank)
}))
@ -917,3 +916,408 @@ impl ViewCache {
}
}
}
pub struct PoolView<'a>(pub bool, pub &'a MidiPool);
content!(TuiOut: |self: PoolView<'a>| {
let Self(compact, model) = self;
let MidiPool { clips, .. } = self.1;
//let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||Tui::g(32).into());
let on_bg = |x|x;//Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, x));
let border = |x|x;//Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x);
let iter = | |model.clips().clone().into_iter();
let height = clips.read().unwrap().len() as u16;
Tui::bg(Color::Reset, Fixed::y(height, on_bg(border(Map::new(iter, move|clip: Arc<RwLock<MidiClip>>, i|{
let item_height = 1;
let item_offset = i as u16 * item_height;
let selected = i == model.clip_index();
let MidiClip { ref name, color, length, .. } = *clip.read().unwrap();
let bg = if selected { color.light.rgb } else { color.base.rgb };
let fg = color.lightest.rgb;
let name = if *compact { format!(" {i:>3}") } else { format!(" {i:>3} {name}") };
let length = if *compact { String::default() } else { format!("{length} ") };
Fixed::y(1, map_south(item_offset, item_height, Tui::bg(bg, lay!(
Fill::x(Align::w(Tui::fg(fg, Tui::bold(selected, name)))),
Fill::x(Align::e(Tui::fg(fg, Tui::bold(selected, length)))),
Fill::x(Align::w(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), ""))))),
Fill::x(Align::e(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), ""))))),
))))
})))))
});
content!(TuiOut: |self: ClipLength| {
let bars = ||self.bars_string();
let beats = ||self.beats_string();
let ticks = ||self.ticks_string();
match self.focus {
None =>
row!(" ", bars(), ".", beats(), ".", ticks()),
Some(ClipLengthFocus::Bar) =>
row!("[", bars(), "]", beats(), ".", ticks()),
Some(ClipLengthFocus::Beat) =>
row!(" ", bars(), "[", beats(), "]", ticks()),
Some(ClipLengthFocus::Tick) =>
row!(" ", bars(), ".", beats(), "[", ticks()),
}
});
/// A clip, rendered as a horizontal piano roll.
#[derive(Clone)]
pub struct PianoHorizontal {
pub clip: Option<Arc<RwLock<MidiClip>>>,
/// Buffer where the whole clip is rerendered on change
pub buffer: Arc<RwLock<BigBuffer>>,
/// Size of actual notes area
pub size: Measure<TuiOut>,
/// The display window
pub range: MidiRangeModel,
/// The note cursor
pub point: MidiPointModel,
/// The highlight color palette
pub color: ItemTheme,
/// Width of the keyboard
pub keys_width: u16,
}
impl PianoHorizontal {
pub fn new (clip: Option<&Arc<RwLock<MidiClip>>>) -> Self {
let size = Measure::new();
let mut range = MidiRangeModel::from((12, true));
range.time_axis = size.x.clone();
range.note_axis = size.y.clone();
let piano = Self {
keys_width: 5,
size,
range,
buffer: RwLock::new(Default::default()).into(),
point: MidiPointModel::default(),
clip: clip.cloned(),
color: clip.as_ref().map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]),
};
piano.redraw();
piano
}
}
pub(crate) fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16) -> impl Iterator<Item=(usize, u16, usize)> {
(note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n))
}
content!(TuiOut:|self: PianoHorizontal| Tui::bg(Tui::g(40), Bsp::s(
Bsp::e(
Fixed::x(5, format!("{}x{}", self.size.w(), self.size.h())),
self.timeline()
),
Bsp::e(
self.keys(),
self.size.of(Tui::bg(Tui::g(32), Bsp::b(
Fill::xy(self.notes()),
Fill::xy(self.cursor()),
)))
),
)));
impl PianoHorizontal {
/// Draw the piano roll background.
///
/// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄
fn draw_bg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize, note_len: usize) {
for (y, note) in (0..=127).rev().enumerate() {
for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) {
let cell = buf.get_mut(x, y).unwrap();
cell.set_bg(clip.color.darkest.rgb);
if time % 384 == 0 {
cell.set_fg(clip.color.darker.rgb);
cell.set_char('│');
} else if time % 96 == 0 {
cell.set_fg(clip.color.dark.rgb);
cell.set_char('╎');
} else if time % note_len == 0 {
cell.set_fg(clip.color.darker.rgb);
cell.set_char('┊');
} else if (127 - note) % 12 == 0 {
cell.set_fg(clip.color.darker.rgb);
cell.set_char('=');
} else if (127 - note) % 6 == 0 {
cell.set_fg(clip.color.darker.rgb);
cell.set_char('—');
} else {
cell.set_fg(clip.color.darker.rgb);
cell.set_char('·');
}
}
}
}
/// Draw the piano roll foreground.
///
/// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄
fn draw_fg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize) {
let style = Style::default().fg(clip.color.base.rgb);//.bg(Rgb(0, 0, 0));
let mut notes_on = [false;128];
for (x, time_start) in (0..clip.length).step_by(zoom).enumerate() {
for (_y, note) in (0..=127).rev().enumerate() {
if let Some(cell) = buf.get_mut(x, note) {
if notes_on[note] {
cell.set_char('▂');
cell.set_style(style);
}
}
}
let time_end = time_start + zoom;
for time in time_start..time_end.min(clip.length) {
for event in clip.notes[time].iter() {
match event {
MidiMessage::NoteOn { key, .. } => {
let note = key.as_int() as usize;
if let Some(cell) = buf.get_mut(x, note) {
cell.set_char('█');
cell.set_style(style);
}
notes_on[note] = true
},
MidiMessage::NoteOff { key, .. } => {
notes_on[key.as_int() as usize] = false
},
_ => {}
}
}
}
}
}
fn notes (&self) -> impl Content<TuiOut> {
let time_start = self.time_start().get();
let note_lo = self.note_lo().get();
let note_hi = self.note_hi();
let buffer = self.buffer.clone();
ThunkRender::new(move|to: &mut TuiOut|{
let source = buffer.read().unwrap();
let [x0, y0, w, _h] = to.area().xywh();
//if h as usize != note_axis {
//panic!("area height mismatch: {h} <> {note_axis}");
//}
for (area_x, screen_x) in (x0..x0+w).enumerate() {
for (area_y, screen_y, _note) in note_y_iter(note_lo, note_hi, y0) {
let source_x = time_start + area_x;
let source_y = note_hi - area_y;
// TODO: enable loop rollover:
//let source_x = (time_start + area_x) % source.width.max(1);
//let source_y = (note_hi - area_y) % source.height.max(1);
let is_in_x = source_x < source.width;
let is_in_y = source_y < source.height;
if is_in_x && is_in_y {
if let Some(source_cell) = source.get(source_x, source_y) {
if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) {
*cell = source_cell.clone();
}
}
}
}
}
})
}
fn cursor (&self) -> impl Content<TuiOut> {
let style = Some(Style::default().fg(self.color.lightest.rgb));
let note_hi = self.note_hi();
let note_lo = self.note_lo().get();
let note_pos = self.note_pos();
let note_len = self.note_len();
let time_pos = self.time_pos();
let time_start = self.time_start().get();
let time_zoom = self.time_zoom().get();
ThunkRender::new(move|to: &mut TuiOut|{
let [x0, y0, w, _] = to.area().xywh();
for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
if note == note_pos {
for x in 0..w {
let screen_x = x0 + x;
let time_1 = time_start + x as usize * time_zoom;
let time_2 = time_1 + time_zoom;
if time_1 <= time_pos && time_pos < time_2 {
to.blit(&"", screen_x, screen_y, style);
let tail = note_len as u16 / time_zoom as u16;
for x_tail in (screen_x + 1)..(screen_x + tail) {
to.blit(&"", x_tail, screen_y, style);
}
break
}
}
break
}
}
})
}
fn keys (&self) -> impl Content<TuiOut> {
let state = self;
let color = state.color;
let note_lo = state.note_lo().get();
let note_hi = state.note_hi();
let note_pos = state.note_pos();
let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0)));
let off_style = Some(Style::default().fg(Tui::g(255)));
let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.rgb).bold());
Fill::y(Fixed::x(self.keys_width, ThunkRender::new(move|to: &mut TuiOut|{
let [x, y0, _w, _h] = to.area().xywh();
for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
to.blit(&to_key(note), x, screen_y, key_style);
if note > 127 {
continue
}
if note == note_pos {
to.blit(&format!("{:<5}", Note::pitch_to_name(note)), x, screen_y, on_style)
} else {
to.blit(&Note::pitch_to_name(note), x, screen_y, off_style)
};
}
})))
}
fn timeline (&self) -> impl Content<TuiOut> + '_ {
Fill::x(Fixed::y(1, ThunkRender::new(move|to: &mut TuiOut|{
let [x, y, w, _h] = to.area();
let style = Some(Style::default().dim());
let length = self.clip.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1);
for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) {
let t = area_x as usize * self.time_zoom().get();
if t < length {
to.blit(&"|", screen_x, y, style);
}
}
})))
}
}
has_size!(<TuiOut>|self:PianoHorizontal|&self.size);
impl TimeRange for PianoHorizontal {
fn time_len (&self) -> &AtomicUsize { self.range.time_len() }
fn time_zoom (&self) -> &AtomicUsize { self.range.time_zoom() }
fn time_lock (&self) -> &AtomicBool { self.range.time_lock() }
fn time_start (&self) -> &AtomicUsize { self.range.time_start() }
fn time_axis (&self) -> &AtomicUsize { self.range.time_axis() }
}
impl NoteRange for PianoHorizontal {
fn note_lo (&self) -> &AtomicUsize { self.range.note_lo() }
fn note_axis (&self) -> &AtomicUsize { self.range.note_axis() }
}
impl NotePoint for PianoHorizontal {
fn note_len (&self) -> usize { self.point.note_len() }
fn set_note_len (&self, x: usize) -> usize { self.point.set_note_len(x) }
fn note_pos (&self) -> usize { self.point.note_pos() }
fn set_note_pos (&self, x: usize) -> usize { self.point.set_note_pos(x) }
}
impl TimePoint for PianoHorizontal {
fn time_pos (&self) -> usize { self.point.time_pos() }
fn set_time_pos (&self, x: usize) -> usize { self.point.set_time_pos(x) }
}
impl MidiViewer for PianoHorizontal {
fn clip (&self) -> &Option<Arc<RwLock<MidiClip>>> {
&self.clip
}
fn clip_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>> {
&mut self.clip
}
/// Determine the required space to render the clip.
fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) {
(clip.length / self.range.time_zoom().get(), 128)
}
fn redraw (&self) {
*self.buffer.write().unwrap() = if let Some(clip) = self.clip.as_ref() {
let clip = clip.read().unwrap();
let buf_size = self.buffer_size(&clip);
let mut buffer = BigBuffer::from(buf_size);
let note_len = self.note_len();
let time_zoom = self.time_zoom().get();
self.time_len().set(clip.length);
PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom, note_len);
PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom);
buffer
} else {
Default::default()
}
}
fn set_clip (&mut self, clip: Option<&Arc<RwLock<MidiClip>>>) {
*self.clip_mut() = clip.cloned();
self.color = clip.map(|p|p.read().unwrap().color)
.unwrap_or(ItemTheme::G[64]);
self.redraw();
}
}
impl std::fmt::Debug for PianoHorizontal {
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
let buffer = self.buffer.read().unwrap();
f.debug_struct("PianoHorizontal")
.field("time_zoom", &self.range.time_zoom)
.field("buffer", &format!("{}x{}", buffer.width, buffer.height))
.finish()
}
}
// Update sequencer playhead indicator
//self.now().set(0.);
//if let Some((ref started_at, Some(ref playing))) = self.player.play_clip {
//let clip = clip.read().unwrap();
//if *playing.read().unwrap() == *clip {
//let pulse = self.current().pulse.get();
//let start = started_at.pulse.get();
//let now = (pulse - start) % clip.length as f64;
//self.now().set(now);
//}
//}
fn to_key (note: usize) -> &'static str {
match note % 12 {
11 => "████▌",
10 => " ",
9 => "████▌",
8 => " ",
7 => "████▌",
6 => " ",
5 => "████▌",
4 => "████▌",
3 => " ",
2 => "████▌",
1 => " ",
0 => "████▌",
_ => unreachable!(),
}
}
pub struct OctaveVertical {
on: [bool; 12],
colors: [Color; 3]
}
impl Default for OctaveVertical {
fn default () -> Self {
Self {
on: [false; 12],
colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)]
}
}
}
impl OctaveVertical {
fn color (&self, pitch: usize) -> Color {
let pitch = pitch % 12;
self.colors[if self.on[pitch] { 2 } else {
match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 }
}]
}
}
impl Content<TuiOut> for OctaveVertical {
fn content (&self) -> impl Render<TuiOut> {
row!(
Tui::fg_bg(self.color(0), self.color(1), ""),
Tui::fg_bg(self.color(2), self.color(3), ""),
Tui::fg_bg(self.color(4), self.color(5), ""),
Tui::fg_bg(self.color(6), self.color(7), ""),
Tui::fg_bg(self.color(8), self.color(9), ""),
Tui::fg_bg(self.color(10), self.color(11), ""),
)
}
}

View file

@ -6,12 +6,14 @@ version = { workspace = true }
[dependencies]
tengri = { workspace = true }
tek_engine = { workspace = true }
uuid = { workspace = true, optional = true }
livi = { workspace = true, optional = true }
symphonia = { workspace = true, optional = true }
wavers = { workspace = true, optional = true }
[features]
default = [ "sequencer", "sampler" ]
default = [ "clock", "sequencer", "sampler" ]
lv2 = [ "livi" ]
sampler = [ "symphonia", "wavers" ]
sequencer = []
sequencer = [ "clock", "uuid" ]
clock = []

View file

@ -17,11 +17,11 @@ provide_num!(u32: |self: Clock| {});
provide!(f64: |self: Clock| {});
atom_command!(ClockCommand: |state: Clock| {
("play" [] Some(Self::Play(None)))
("play" [] Some(Self::Play(None)))
("play" [t: u32] Some(Self::Play(t)))
("pause" [] Some(Self::Pause(None)))
("pause" [] Some(Self::Pause(None)))
("pause" [t: u32] Some(Self::Pause(t)))
("toggle" [] Some(if state.is_rolling() { Self::Pause(None) } else { Self::Play(None) }))
("toggle" [] Some(if state.is_rolling() { Self::Pause(None) } else { Self::Play(None) }))
("toggle" [t: u32] Some(if state.is_rolling() { Self::Pause(t) } else { Self::Play(t) }))
("seek/usec" [t: f64] Some(Self::SeekUsec(t.expect("no usec"))))
("seek/pulse" [t: f64] Some(Self::SeekPulse(t.expect("no pulse"))))

View file

@ -3,7 +3,7 @@ use crate::*;
#[derive(Clone, Default)]
pub struct Clock {
/// JACK transport handle.
pub transport: Arc<Option<Transport>>,
pub transport: Arc<Option<tek_engine::jack::Transport>>,
/// Global temporal resolution (shared by [Moment] fields)
pub timebase: Arc<Timebase>,
/// Current global sample and usec (monotonic from JACK clock)
@ -127,7 +127,7 @@ impl Clock {
self.set_chunk(scope.n_frames() as usize);
// Store reported global frame and usec
let CycleTimes { current_frames, current_usecs, .. } = scope.cycle_times()?;
let tek_engine::jack::CycleTimes { current_frames, current_usecs, .. } = scope.cycle_times()?;
self.global.sample.set(current_frames as f64);
self.global.usec.set(current_usecs as f64);
@ -157,12 +157,12 @@ impl Clock {
Ok(())
}
pub fn bbt (&self) -> PositionBBT {
pub fn bbt (&self) -> tek_engine::jack::PositionBBT {
let pulse = self.playhead.pulse.get() as i32;
let ppq = self.timebase.ppq.get() as i32;
let bpm = self.timebase.bpm.get();
let bar = (pulse / ppq) / 4;
PositionBBT {
tek_engine::jack::PositionBBT {
bar: 1 + bar,
beat: 1 + (pulse / ppq) % 4,
tick: (pulse % ppq),

View file

@ -0,0 +1,38 @@
#![feature(let_chains)]
pub(crate) use std::cmp::Ord;
pub(crate) use std::fmt::{Debug, Formatter};
pub(crate) use std::thread::JoinHandle;
pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::Relaxed}};
pub(crate) use std::fs::File;
pub(crate) use std::path::PathBuf;
pub(crate) use std::error::Error;
pub(crate) use std::ffi::OsString;
pub(crate) use ::tengri::{dsl::*, input::*, output::*, tui::{*, ratatui::prelude::*}};
pub(crate) use ::tek_engine::*;
pub(crate) use ::tek_engine::midi::{u7, LiveEvent, MidiMessage};
pub(crate) use ::tek_engine::jack::{Control, ProcessScope, MidiWriter, RawMidi};
pub(crate) use ratatui::{prelude::Rect, widgets::{Widget, canvas::{Canvas, Line}}};
#[cfg(feature = "clock")] mod clock;
#[cfg(feature = "clock")] pub use self::clock::*;
#[cfg(feature = "sequencer")] mod sequencer;
#[cfg(feature = "sequencer")] pub use self::sequencer::*;
#[cfg(feature = "sampler")] mod sampler;
#[cfg(feature = "sampler")] pub use self::sampler::*;
#[cfg(feature = "plugin")] mod plugin;
#[cfg(feature = "plugin")] pub use self::plugin::*;
#[derive(Debug)]
pub enum Device {
#[cfg(feature = "sequencer")]
Sequencer(MidiPlayer),
#[cfg(feature = "sampler")]
Sampler(Sampler),
#[cfg(feature = "plugin")]
Plugin(Plugin),
}

View file

@ -1,8 +1,281 @@
mod plugin; pub use self::plugin::*;
mod lv2; pub use self::lv2::*;
pub(crate) use std::cmp::Ord;
pub(crate) use std::fmt::{Debug, Formatter};
pub(crate) use std::sync::{Arc, RwLock};
pub(crate) use std::thread::JoinHandle;
pub(crate) use ::tek_jack::{*, jack::*};
pub(crate) use ::tengri::{output::*, tui::{*, ratatui::prelude::*}};
use crate::*;
mod lv2;
mod lv2_gui;
mod lv2_tui;
mod vst2_tui;
mod vst3_tui;
/// A plugin device.
#[derive(Debug)]
pub struct Plugin {
/// JACK client handle (needs to not be dropped for standalone mode to work).
pub jack: Jack,
pub name: Arc<str>,
pub path: Option<Arc<str>>,
pub plugin: Option<PluginKind>,
pub selected: usize,
pub mapping: bool,
pub midi_ins: Vec<Port<MidiIn>>,
pub midi_outs: Vec<Port<MidiOut>>,
pub audio_ins: Vec<Port<AudioIn>>,
pub audio_outs: Vec<Port<AudioOut>>,
}
/// Supported plugin formats.
#[derive(Default)]
pub enum PluginKind {
#[default] None,
LV2(LV2Plugin),
VST2 { instance: () /*::vst::host::PluginInstance*/ },
VST3,
}
impl Debug for PluginKind {
fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
write!(f, "{}", match self {
Self::None => "(none)",
Self::LV2(_) => "LV2",
Self::VST2{..} => "VST2",
Self::VST3 => "VST3",
})
}
}
impl Plugin {
pub fn new_lv2 (
jack: &Jack,
name: &str,
path: &str,
) -> Usually<Self> {
Ok(Self {
jack: jack.clone(),
name: name.into(),
path: Some(String::from(path).into()),
plugin: Some(PluginKind::LV2(LV2Plugin::new(path)?)),
selected: 0,
mapping: false,
midi_ins: vec![],
midi_outs: vec![],
audio_ins: vec![],
audio_outs: vec![],
})
}
}
pub struct PluginAudio(Arc<RwLock<Plugin>>);
from!(|model: &Arc<RwLock<Plugin>>| PluginAudio = Self(model.clone()));
audio!(|self: PluginAudio, _client, scope|{
let state = &mut*self.0.write().unwrap();
match state.plugin.as_mut() {
Some(PluginKind::LV2(LV2Plugin {
features,
ref mut instance,
ref mut input_buffer,
..
})) => {
let urid = features.midi_urid();
input_buffer.clear();
for port in state.midi_ins.iter() {
let mut atom = ::livi::event::LV2AtomSequence::new(
&features,
scope.n_frames() as usize
);
for event in port.iter(scope) {
match event.bytes.len() {
3 => atom.push_midi_event::<3>(
event.time as i64,
urid,
&event.bytes[0..3]
).unwrap(),
_ => {}
}
}
input_buffer.push(atom);
}
let mut outputs = vec![];
for _ in state.midi_outs.iter() {
outputs.push(::livi::event::LV2AtomSequence::new(
features,
scope.n_frames() as usize
));
}
let ports = ::livi::EmptyPortConnections::new()
.with_atom_sequence_inputs(input_buffer.iter())
.with_atom_sequence_outputs(outputs.iter_mut())
.with_audio_inputs(state.audio_ins.iter().map(|o|o.as_slice(scope)))
.with_audio_outputs(state.audio_outs.iter_mut().map(|o|o.as_mut_slice(scope)));
unsafe {
instance.run(scope.n_frames() as usize, ports).unwrap()
};
},
_ => todo!("only lv2 is supported")
}
Control::Continue
});
//fn jack_from_lv2 (name: &str, plugin: &::livi::Plugin) -> Usually<Jack> {
//let counts = plugin.port_counts();
//let mut jack = Jack::new(name)?;
//for i in 0..counts.atom_sequence_inputs {
//jack = jack.midi_in(&format!("midi-in-{i}"))
//}
//for i in 0..counts.atom_sequence_outputs {
//jack = jack.midi_out(&format!("midi-out-{i}"));
//}
//for i in 0..counts.audio_inputs {
//jack = jack.audio_in(&format!("audio-in-{i}"));
//}
//for i in 0..counts.audio_outputs {
//jack = jack.audio_out(&format!("audio-out-{i}"));
//}
//Ok(jack)
//}
impl Plugin {
/// Create a plugin host device.
pub fn new (
jack: &Jack,
name: &str,
) -> Usually<Self> {
Ok(Self {
//_engine: Default::default(),
jack: jack.clone(),
name: name.into(),
path: None,
plugin: None,
selected: 0,
mapping: false,
audio_ins: vec![],
audio_outs: vec![],
midi_ins: vec![],
midi_outs: vec![],
//ports: JackPorts::default()
})
}
}
impl Content<TuiOut> for Plugin {
fn render (&self, to: &mut TuiOut) {
let area = to.area();
let [x, y, _, height] = area;
let mut width = 20u16;
match &self.plugin {
Some(PluginKind::LV2(LV2Plugin { port_list, instance, .. })) => {
let start = self.selected.saturating_sub((height as usize / 2).saturating_sub(1));
let end = start + height as usize - 2;
//draw_box(buf, Rect { x, y, width, height });
for i in start..end {
if let Some(port) = port_list.get(i) {
let value = if let Some(value) = instance.control_input(port.index) {
value
} else {
port.default_value
};
//let label = &format!("C·· M·· {:25} = {value:.03}", port.name);
let label = &format!("{:25} = {value:.03}", port.name);
width = width.max(label.len() as u16 + 4);
let style = if i == self.selected {
Some(Style::default().green())
} else {
None
} ;
to.blit(&label, x + 2, y + 1 + i as u16 - start as u16, style);
} else {
break
}
}
},
_ => {}
};
draw_header(self, to, x, y, width);
}
}
fn draw_header (state: &Plugin, to: &mut TuiOut, x: u16, y: u16, w: u16) {
let style = Style::default().gray();
let label1 = format!(" {}", state.name);
to.blit(&label1, x + 1, y, Some(style.white().bold()));
if let Some(ref path) = state.path {
let label2 = format!("{}", &path[..((w as usize - 10).min(path.len()))]);
to.blit(&label2, x + 2 + label1.len() as u16, y, Some(style.not_dim()));
}
//Ok(Rect { x, y, width: w, height: 1 })
}
//handle!(TuiIn: |self:Plugin, from|{
//match from.event() {
//kpat!(KeyCode::Up) => {
//self.selected = self.selected.saturating_sub(1);
//Ok(Some(true))
//},
//kpat!(KeyCode::Down) => {
//self.selected = (self.selected + 1).min(match &self.plugin {
//Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1,
//_ => unimplemented!()
//});
//Ok(Some(true))
//},
//kpat!(KeyCode::PageUp) => {
//self.selected = self.selected.saturating_sub(8);
//Ok(Some(true))
//},
//kpat!(KeyCode::PageDown) => {
//self.selected = (self.selected + 10).min(match &self.plugin {
//Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1,
//_ => unimplemented!()
//});
//Ok(Some(true))
//},
//kpat!(KeyCode::Char(',')) => {
//match self.plugin.as_mut() {
//Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => {
//let index = port_list[self.selected].index;
//if let Some(value) = instance.control_input(index) {
//instance.set_control_input(index, value - 0.01);
//}
//},
//_ => {}
//}
//Ok(Some(true))
//},
//kpat!(KeyCode::Char('.')) => {
//match self.plugin.as_mut() {
//Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => {
//let index = port_list[self.selected].index;
//if let Some(value) = instance.control_input(index) {
//instance.set_control_input(index, value + 0.01);
//}
//},
//_ => {}
//}
//Ok(Some(true))
//},
//kpat!(KeyCode::Char('g')) => {
//match self.plugin {
////Some(PluginKind::LV2(ref mut plugin)) => {
////plugin.ui_thread = Some(run_lv2_ui(LV2PluginUI::new()?)?);
////},
//Some(_) => unreachable!(),
//None => {}
//}
//Ok(Some(true))
//},
//_ => Ok(None)
//}
//});
//from_atom!("plugin/lv2" => |jack: &Jack, args| -> Plugin {
//let mut name = String::new();
//let mut path = String::new();
//atom!(atom in args {
//Atom::Map(map) => {
//if let Some(Atom::Str(n)) = map.get(&Atom::Key(":name")) {
//name = String::from(*n);
//}
//if let Some(Atom::Str(p)) = map.get(&Atom::Key(":path")) {
//path = String::from(*p);
//}
//},
//_ => panic!("unexpected in lv2 '{name}'"),
//});
//Plugin::new_lv2(jack, &name, &path)
//});

View file

@ -1,275 +0,0 @@
use crate::*;
/// A plugin device.
#[derive(Debug)]
pub struct Plugin {
/// JACK client handle (needs to not be dropped for standalone mode to work).
pub jack: Jack,
pub name: Arc<str>,
pub path: Option<Arc<str>>,
pub plugin: Option<PluginKind>,
pub selected: usize,
pub mapping: bool,
pub midi_ins: Vec<Port<MidiIn>>,
pub midi_outs: Vec<Port<MidiOut>>,
pub audio_ins: Vec<Port<AudioIn>>,
pub audio_outs: Vec<Port<AudioOut>>,
}
/// Supported plugin formats.
#[derive(Default)]
pub enum PluginKind {
#[default] None,
LV2(LV2Plugin),
VST2 { instance: () /*::vst::host::PluginInstance*/ },
VST3,
}
impl Debug for PluginKind {
fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
write!(f, "{}", match self {
Self::None => "(none)",
Self::LV2(_) => "LV2",
Self::VST2{..} => "VST2",
Self::VST3 => "VST3",
})
}
}
impl Plugin {
pub fn new_lv2 (
jack: &Jack,
name: &str,
path: &str,
) -> Usually<Self> {
Ok(Self {
jack: jack.clone(),
name: name.into(),
path: Some(String::from(path).into()),
plugin: Some(PluginKind::LV2(LV2Plugin::new(path)?)),
selected: 0,
mapping: false,
midi_ins: vec![],
midi_outs: vec![],
audio_ins: vec![],
audio_outs: vec![],
})
}
}
pub struct PluginAudio(Arc<RwLock<Plugin>>);
from!(|model: &Arc<RwLock<Plugin>>| PluginAudio = Self(model.clone()));
audio!(|self: PluginAudio, _client, scope|{
let state = &mut*self.0.write().unwrap();
match state.plugin.as_mut() {
Some(PluginKind::LV2(LV2Plugin {
features,
ref mut instance,
ref mut input_buffer,
..
})) => {
let urid = features.midi_urid();
input_buffer.clear();
for port in state.midi_ins.iter() {
let mut atom = ::livi::event::LV2AtomSequence::new(
&features,
scope.n_frames() as usize
);
for event in port.iter(scope) {
match event.bytes.len() {
3 => atom.push_midi_event::<3>(
event.time as i64,
urid,
&event.bytes[0..3]
).unwrap(),
_ => {}
}
}
input_buffer.push(atom);
}
let mut outputs = vec![];
for _ in state.midi_outs.iter() {
outputs.push(::livi::event::LV2AtomSequence::new(
features,
scope.n_frames() as usize
));
}
let ports = ::livi::EmptyPortConnections::new()
.with_atom_sequence_inputs(input_buffer.iter())
.with_atom_sequence_outputs(outputs.iter_mut())
.with_audio_inputs(state.audio_ins.iter().map(|o|o.as_slice(scope)))
.with_audio_outputs(state.audio_outs.iter_mut().map(|o|o.as_mut_slice(scope)));
unsafe {
instance.run(scope.n_frames() as usize, ports).unwrap()
};
},
_ => todo!("only lv2 is supported")
}
Control::Continue
});
//fn jack_from_lv2 (name: &str, plugin: &::livi::Plugin) -> Usually<Jack> {
//let counts = plugin.port_counts();
//let mut jack = Jack::new(name)?;
//for i in 0..counts.atom_sequence_inputs {
//jack = jack.midi_in(&format!("midi-in-{i}"))
//}
//for i in 0..counts.atom_sequence_outputs {
//jack = jack.midi_out(&format!("midi-out-{i}"));
//}
//for i in 0..counts.audio_inputs {
//jack = jack.audio_in(&format!("audio-in-{i}"));
//}
//for i in 0..counts.audio_outputs {
//jack = jack.audio_out(&format!("audio-out-{i}"));
//}
//Ok(jack)
//}
impl Plugin {
/// Create a plugin host device.
pub fn new (
jack: &Jack,
name: &str,
) -> Usually<Self> {
Ok(Self {
//_engine: Default::default(),
jack: jack.clone(),
name: name.into(),
path: None,
plugin: None,
selected: 0,
mapping: false,
audio_ins: vec![],
audio_outs: vec![],
midi_ins: vec![],
midi_outs: vec![],
//ports: JackPorts::default()
})
}
}
impl Content<TuiOut> for Plugin {
fn render (&self, to: &mut TuiOut) {
let area = to.area();
let [x, y, _, height] = area;
let mut width = 20u16;
match &self.plugin {
Some(PluginKind::LV2(LV2Plugin { port_list, instance, .. })) => {
let start = self.selected.saturating_sub((height as usize / 2).saturating_sub(1));
let end = start + height as usize - 2;
//draw_box(buf, Rect { x, y, width, height });
for i in start..end {
if let Some(port) = port_list.get(i) {
let value = if let Some(value) = instance.control_input(port.index) {
value
} else {
port.default_value
};
//let label = &format!("C·· M·· {:25} = {value:.03}", port.name);
let label = &format!("{:25} = {value:.03}", port.name);
width = width.max(label.len() as u16 + 4);
let style = if i == self.selected {
Some(Style::default().green())
} else {
None
} ;
to.blit(&label, x + 2, y + 1 + i as u16 - start as u16, style);
} else {
break
}
}
},
_ => {}
};
draw_header(self, to, x, y, width);
}
}
fn draw_header (state: &Plugin, to: &mut TuiOut, x: u16, y: u16, w: u16) {
let style = Style::default().gray();
let label1 = format!(" {}", state.name);
to.blit(&label1, x + 1, y, Some(style.white().bold()));
if let Some(ref path) = state.path {
let label2 = format!("{}", &path[..((w as usize - 10).min(path.len()))]);
to.blit(&label2, x + 2 + label1.len() as u16, y, Some(style.not_dim()));
}
//Ok(Rect { x, y, width: w, height: 1 })
}
//handle!(TuiIn: |self:Plugin, from|{
//match from.event() {
//kpat!(KeyCode::Up) => {
//self.selected = self.selected.saturating_sub(1);
//Ok(Some(true))
//},
//kpat!(KeyCode::Down) => {
//self.selected = (self.selected + 1).min(match &self.plugin {
//Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1,
//_ => unimplemented!()
//});
//Ok(Some(true))
//},
//kpat!(KeyCode::PageUp) => {
//self.selected = self.selected.saturating_sub(8);
//Ok(Some(true))
//},
//kpat!(KeyCode::PageDown) => {
//self.selected = (self.selected + 10).min(match &self.plugin {
//Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1,
//_ => unimplemented!()
//});
//Ok(Some(true))
//},
//kpat!(KeyCode::Char(',')) => {
//match self.plugin.as_mut() {
//Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => {
//let index = port_list[self.selected].index;
//if let Some(value) = instance.control_input(index) {
//instance.set_control_input(index, value - 0.01);
//}
//},
//_ => {}
//}
//Ok(Some(true))
//},
//kpat!(KeyCode::Char('.')) => {
//match self.plugin.as_mut() {
//Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => {
//let index = port_list[self.selected].index;
//if let Some(value) = instance.control_input(index) {
//instance.set_control_input(index, value + 0.01);
//}
//},
//_ => {}
//}
//Ok(Some(true))
//},
//kpat!(KeyCode::Char('g')) => {
//match self.plugin {
////Some(PluginKind::LV2(ref mut plugin)) => {
////plugin.ui_thread = Some(run_lv2_ui(LV2PluginUI::new()?)?);
////},
//Some(_) => unreachable!(),
//None => {}
//}
//Ok(Some(true))
//},
//_ => Ok(None)
//}
//});
//from_atom!("plugin/lv2" => |jack: &Jack, args| -> Plugin {
//let mut name = String::new();
//let mut path = String::new();
//atom!(atom in args {
//Atom::Map(map) => {
//if let Some(Atom::Str(n)) = map.get(&Atom::Key(":name")) {
//name = String::from(*n);
//}
//if let Some(Atom::Str(p)) = map.get(&Atom::Key(":path")) {
//path = String::from(*p);
//}
//},
//_ => panic!("unexpected in lv2 '{name}'"),
//});
//Plugin::new_lv2(jack, &name, &path)
//});

View file

@ -1,13 +1,5 @@
#![feature(let_chains)]
use crate::*;
pub(crate) use ::tek_jack::{*, jack::*};
pub(crate) use ::tek_midi::{*, midly::{*, live::*, num::*}};
pub(crate) use ::tengri::{dsl::*, input::*, output::*, tui::{*, ratatui::prelude::*}};
pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::Relaxed}};
pub(crate) use std::fs::File;
pub(crate) use std::path::PathBuf;
pub(crate) use std::error::Error;
pub(crate) use std::ffi::OsString;
pub(crate) use symphonia::{
core::{
formats::Packet,
@ -19,7 +11,6 @@ pub(crate) use symphonia::{
},
default::get_codecs,
};
pub(crate) use ratatui::{prelude::Rect, widgets::{Widget, canvas::{Canvas, Line}}};
mod sampler_api; pub use self::sampler_api::*;
mod sampler_audio; pub use self::sampler_audio::*;

View file

@ -53,10 +53,8 @@ impl Sampler {
const _EMPTY: &[(f64, f64)] = &[(0., 0.), (1., 1.), (2., 2.), (0., 2.), (2., 0.)];
pub fn view_list <'a> (
&'a self,
compact: bool,
editor: &MidiEditor
pub fn view_list <'a, T: NotePoint + NoteRange> (
&'a self, compact: bool, editor: &T
) -> impl Content<TuiOut> + 'a {
let note_lo = editor.note_lo().load(Relaxed);
let note_pt = editor.note_pos();

View file

@ -1,38 +1,9 @@
mod clip_editor; pub use self::clip_editor::*;
mod clip_launch; pub use self::clip_launch::*;
mod clip_model; pub use self::clip_model::*;
mod clip_play; pub use self::clip_play::*;
mod clip_view; pub use self::clip_view::*;
use crate::*;
pub trait HasEditor {
fn editor (&self) -> &Option<MidiEditor>;
fn editor_mut (&mut self) -> &Option<MidiEditor>;
fn is_editing (&self) -> bool { true }
fn editor_w (&self) -> usize { 0 }
fn editor_h (&self) -> usize { 0 }
}
#[macro_export] macro_rules! has_editor {
(|$self:ident: $Struct:ident|{
editor = $e0:expr;
editor_w = $e1:expr;
editor_h = $e2:expr;
is_editing = $e3:expr;
}) => {
impl HasEditor for $Struct {
fn editor (&$self) -> &Option<MidiEditor> { &$e0 }
fn editor_mut (&mut $self) -> &Option<MidiEditor> { &mut $e0 }
fn editor_w (&$self) -> usize { $e1 }
fn editor_h (&$self) -> usize { $e2 }
fn is_editing (&$self) -> bool { $e3 }
}
};
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? HasEditor for $Struct $(<$($L),*$($T),*>)? {
fn editor (&$self) -> &MidiEditor { &$cb }
}
};
}
mod seq_clip; pub use self::seq_clip::*;
mod seq_launch; pub use self::seq_launch::*;
mod seq_model; pub use self::seq_model::*;
mod seq_view; pub use self::seq_view::*;
#[cfg(test)] #[test] pub fn test_midi_clip () {
let clip = MidiClip::stop_all();

View file

@ -87,4 +87,3 @@ pub trait HasPlayClip: HasClock {
FieldV(color, "Next:", format!("{} {}", time, name))
}
}

View file

@ -0,0 +1,453 @@
//! MIDI player
use crate::*;
use tek_engine::jack::*;
pub trait HasPlayer {
fn player (&self) -> &impl MidiPlayerApi;
fn player_mut (&mut self) -> &mut impl MidiPlayerApi;
}
#[macro_export] macro_rules! has_player {
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? HasPlayer for $Struct $(<$($L),*$($T),*>)? {
fn player (&$self) -> &impl MidiPlayerApi { &$cb }
fn player_mut (&mut $self) -> &mut impl MidiPlayerApi { &mut$cb }
}
}
}
pub trait MidiPlayerApi: MidiRecordApi + MidiPlaybackApi + Send + Sync {}
impl MidiPlayerApi for MidiPlayer {}
/// Contains state for playing a clip
pub struct MidiPlayer {
/// State of clock and playhead
pub clock: Clock,
/// Start time and clip being played
pub play_clip: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
/// Start time and next clip
pub next_clip: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
/// Play input through output.
pub monitoring: bool,
/// Write input to sequence.
pub recording: bool,
/// Overdub input to sequence.
pub overdub: bool,
/// Send all notes off
pub reset: bool, // TODO?: after Some(nframes)
/// Record from MIDI ports to current sequence.
pub midi_ins: Vec<JackMidiIn>,
/// Play from current sequence to MIDI ports
pub midi_outs: Vec<JackMidiOut>,
/// Notes currently held at input
pub notes_in: Arc<RwLock<[bool; 128]>>,
/// Notes currently held at output
pub notes_out: Arc<RwLock<[bool; 128]>>,
/// MIDI output buffer
pub note_buf: Vec<u8>,
}
impl Default for MidiPlayer {
fn default () -> Self {
Self {
play_clip: None,
next_clip: None,
recording: false,
monitoring: false,
overdub: false,
notes_in: RwLock::new([false;128]).into(),
notes_out: RwLock::new([false;128]).into(),
note_buf: vec![0;8],
reset: true,
midi_ins: vec![],
midi_outs: vec![],
clock: Clock::default(),
}
}
}
impl MidiPlayer {
pub fn new (
name: impl AsRef<str>,
jack: &Jack,
clock: Option<&Clock>,
clip: Option<&Arc<RwLock<MidiClip>>>,
midi_from: &[PortConnect],
midi_to: &[PortConnect],
) -> Usually<Self> {
let _name = name.as_ref();
let clock = clock.cloned().unwrap_or_default();
Ok(Self {
midi_ins: vec![JackMidiIn::new(jack, format!("M/{}", name.as_ref()), midi_from)?,],
midi_outs: vec![JackMidiOut::new(jack, format!("{}/M", name.as_ref()), midi_to)?, ],
play_clip: clip.map(|clip|(Moment::zero(&clock.timebase), Some(clip.clone()))),
clock,
note_buf: vec![0;8],
reset: true,
recording: false,
monitoring: false,
overdub: false,
next_clip: None,
notes_in: RwLock::new([false;128]).into(),
notes_out: RwLock::new([false;128]).into(),
})
}
}
impl std::fmt::Debug for MidiPlayer {
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
f.debug_struct("MidiPlayer")
.field("clock", &self.clock)
.field("play_clip", &self.play_clip)
.field("next_clip", &self.next_clip)
.finish()
}
}
has_clock!(|self: MidiPlayer|self.clock);
impl HasMidiIns for MidiPlayer {
fn midi_ins (&self) -> &Vec<JackMidiIn> { &self.midi_ins }
fn midi_ins_mut (&mut self) -> &mut Vec<JackMidiIn> { &mut self.midi_ins }
}
impl HasMidiOuts for MidiPlayer {
fn midi_outs (&self) -> &Vec<JackMidiOut> { &self.midi_outs }
fn midi_outs_mut (&mut self) -> &mut Vec<JackMidiOut> { &mut self.midi_outs }
fn midi_note (&mut self) -> &mut Vec<u8> { &mut self.note_buf }
}
/// Hosts the JACK callback for a single MIDI player
pub struct PlayerAudio<'a, T: MidiPlayerApi>(
/// Player
pub &'a mut T,
/// Note buffer
pub &'a mut Vec<u8>,
/// Note chunk buffer
pub &'a mut Vec<Vec<Vec<u8>>>,
);
/// JACK process callback for a sequencer's clip player/recorder.
impl<T: MidiPlayerApi> Audio for PlayerAudio<'_, T> {
fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
let model = &mut self.0;
let note_buf = &mut self.1;
let midi_buf = &mut self.2;
// Clear output buffer(s)
model.clear(scope, midi_buf, false);
// Write chunk of clip to output, handle switchover
if model.play(scope, note_buf, midi_buf) {
model.switchover(scope, note_buf, midi_buf);
}
if model.has_midi_ins() {
if model.recording() || model.monitoring() {
// Record and/or monitor input
model.record(scope, midi_buf)
} else if model.has_midi_outs() && model.monitoring() {
// Monitor input to output
model.monitor(scope, midi_buf)
}
}
// Write to output port(s)
model.write(scope, midi_buf);
Control::Continue
}
}
impl MidiRecordApi for MidiPlayer {
fn recording (&self) -> bool {
self.recording
}
fn recording_mut (&mut self) -> &mut bool {
&mut self.recording
}
fn monitoring (&self) -> bool {
self.monitoring
}
fn monitoring_mut (&mut self) -> &mut bool {
&mut self.monitoring
}
fn overdub (&self) -> bool {
self.overdub
}
fn overdub_mut (&mut self) -> &mut bool {
&mut self.overdub
}
fn notes_in (&self) -> &Arc<RwLock<[bool; 128]>> {
&self.notes_in
}
}
impl MidiPlaybackApi for MidiPlayer {
fn notes_out (&self) -> &Arc<RwLock<[bool; 128]>> {
&self.notes_out
}
}
impl HasPlayClip for MidiPlayer {
fn reset (&self) -> bool {
self.reset
}
fn reset_mut (&mut self) -> &mut bool {
&mut self.reset
}
fn play_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
&self.play_clip
}
fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
&mut self.play_clip
}
fn next_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
&self.next_clip
}
fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
&mut self.next_clip
}
}
pub trait MidiRecordApi: HasClock + HasPlayClip + HasMidiIns {
fn notes_in (&self) -> &Arc<RwLock<[bool;128]>>;
fn recording (&self) -> bool;
fn recording_mut (&mut self) -> &mut bool;
fn toggle_record (&mut self) {
*self.recording_mut() = !self.recording();
}
fn monitoring (&self) -> bool;
fn monitoring_mut (&mut self) -> &mut bool;
fn toggle_monitor (&mut self) {
*self.monitoring_mut() = !self.monitoring();
}
fn overdub (&self) -> bool;
fn overdub_mut (&mut self) -> &mut bool;
fn toggle_overdub (&mut self) {
*self.overdub_mut() = !self.overdub();
}
fn monitor (&mut self, scope: &ProcessScope, midi_buf: &mut Vec<Vec<Vec<u8>>>) {
// For highlighting keys and note repeat
let notes_in = self.notes_in().clone();
let monitoring = self.monitoring();
for input in self.midi_ins_mut().iter() {
for (sample, event, bytes) in parse_midi_input(input.port().iter(scope)) {
if let LiveEvent::Midi { message, .. } = event {
if monitoring {
midi_buf[sample].push(bytes.to_vec());
}
// FIXME: don't lock on every event!
update_keys(&mut notes_in.write().unwrap(), &message);
}
}
}
}
fn record (&mut self, scope: &ProcessScope, midi_buf: &mut Vec<Vec<Vec<u8>>>) {
if self.monitoring() {
self.monitor(scope, midi_buf);
}
if !self.clock().is_rolling() {
return
}
if let Some((started, ref clip)) = self.play_clip().clone() {
self.record_clip(scope, started, clip, midi_buf);
}
if let Some((_start_at, _clip)) = &self.next_clip() {
self.record_next();
}
}
fn record_clip (
&mut self,
scope: &ProcessScope,
started: Moment,
clip: &Option<Arc<RwLock<MidiClip>>>,
_midi_buf: &mut Vec<Vec<Vec<u8>>>
) {
if let Some(clip) = clip {
let sample0 = scope.last_frame_time() as usize;
let start = started.sample.get() as usize;
let _recording = self.recording();
let timebase = self.clock().timebase().clone();
let quant = self.clock().quant.get();
let mut clip = clip.write().unwrap();
let length = clip.length;
for input in self.midi_ins_mut().iter() {
for (sample, event, _bytes) in parse_midi_input(input.port().iter(scope)) {
if let LiveEvent::Midi { message, .. } = event {
clip.record_event({
let sample = (sample0 + sample - start) as f64;
let pulse = timebase.samples_to_pulse(sample);
let quantized = (pulse / quant).round() * quant;
quantized as usize % length
}, message);
}
}
}
}
}
fn record_next (&mut self) {
// TODO switch to next clip and record into it
}
}
pub trait MidiPlaybackApi: HasPlayClip + HasClock + HasMidiOuts {
fn notes_out (&self) -> &Arc<RwLock<[bool;128]>>;
/// Clear the section of the output buffer that we will be using,
/// emitting "all notes off" at start of buffer if requested.
fn clear (
&mut self, scope: &ProcessScope, out: &mut [Vec<Vec<u8>>], reset: bool
) {
let n_frames = (scope.n_frames() as usize).min(out.len());
for frame in &mut out[0..n_frames] {
frame.clear();
}
if reset {
all_notes_off(out);
}
}
/// Output notes from clip to MIDI output ports.
fn play (
&mut self, scope: &ProcessScope, note_buf: &mut Vec<u8>, out: &mut [Vec<Vec<u8>>]
) -> bool {
if !self.clock().is_rolling() {
return false
}
// If a clip is playing, write a chunk of MIDI events from it to the output buffer.
// If no clip is playing, prepare for switchover immediately.
self.play_clip().as_ref().map_or(true, |(started, clip)|{
self.play_chunk(scope, note_buf, out, started, clip)
})
}
/// Handle switchover from current to next playing clip.
fn switchover (
&mut self, scope: &ProcessScope, note_buf: &mut Vec<u8>, out: &mut [Vec<Vec<u8>>]
) {
if !self.clock().is_rolling() {
return
}
let sample0 = scope.last_frame_time() as usize;
//let samples = scope.n_frames() as usize;
if let Some((start_at, clip)) = &self.next_clip() {
let start = start_at.sample.get() as usize;
let sample = self.clock().started.read().unwrap()
.as_ref().unwrap().sample.get() as usize;
// If it's time to switch to the next clip:
if start <= sample0.saturating_sub(sample) {
// Samples elapsed since clip was supposed to start
let _skipped = sample0 - start;
// Switch over to enqueued clip
let started = Moment::from_sample(self.clock().timebase(), start as f64);
// Launch enqueued clip
*self.play_clip_mut() = Some((started, clip.clone()));
// Unset enqueuement (TODO: where to implement looping?)
*self.next_clip_mut() = None;
// Fill in remaining ticks of chunk from next clip.
self.play(scope, note_buf, out);
}
}
}
fn play_chunk (
&self,
scope: &ProcessScope,
note_buf: &mut Vec<u8>,
out: &mut [Vec<Vec<u8>>],
started: &Moment,
clip: &Option<Arc<RwLock<MidiClip>>>
) -> bool {
// First sample to populate. Greater than 0 means that the first
// pulse of the clip falls somewhere in the middle of the chunk.
let sample = (scope.last_frame_time() as usize).saturating_sub(
started.sample.get() as usize +
self.clock().started.read().unwrap().as_ref().unwrap().sample.get() as usize
);
// Iterator that emits sample (index into output buffer at which to write MIDI event)
// paired with pulse (index into clip from which to take the MIDI event) for each
// sample of the output buffer that corresponds to a MIDI pulse.
let pulses = self.clock().timebase().pulses_between_samples(sample, sample + scope.n_frames() as usize);
// Notes active during current chunk.
let notes = &mut self.notes_out().write().unwrap();
let length = clip.as_ref().map_or(0, |p|p.read().unwrap().length);
for (sample, pulse) in pulses {
// If a next clip is enqueued, and we're past the end of the current one,
// break the loop here (FIXME count pulse correctly)
let past_end = if clip.is_some() { pulse >= length } else { true };
if self.next_clip().is_some() && past_end {
return true
}
// If there's a currently playing clip, output notes from it to buffer:
if let Some(ref clip) = clip {
Self::play_pulse(clip, pulse, sample, note_buf, out, notes)
}
}
false
}
fn play_pulse (
clip: &RwLock<MidiClip>,
pulse: usize,
sample: usize,
note_buf: &mut Vec<u8>,
out: &mut [Vec<Vec<u8>>],
notes: &mut [bool;128]
) {
// Source clip from which the MIDI events will be taken.
let clip = clip.read().unwrap();
// Clip with zero length is not processed
if clip.length > 0 {
// Current pulse index in source clip
let pulse = pulse % clip.length;
// Output each MIDI event from clip at appropriate frames of output buffer:
for message in clip.notes[pulse].iter() {
// Clear output buffer for this MIDI event.
note_buf.clear();
// TODO: support MIDI channels other than CH1.
let channel = 0.into();
// Serialize MIDI event into message buffer.
LiveEvent::Midi { channel, message: *message }
.write(note_buf)
.unwrap();
// Append serialized message to output buffer.
out[sample].push(note_buf.clone());
// Update the list of currently held notes.
update_keys(&mut*notes, message);
}
}
}
/// Write a chunk of MIDI data from the output buffer to all assigned output ports.
fn write (&mut self, scope: &ProcessScope, out: &[Vec<Vec<u8>>]) {
let samples = scope.n_frames() as usize;
for port in self.midi_outs_mut().iter_mut() {
Self::write_port(&mut port.port_mut().writer(scope), samples, out)
}
}
/// Write a chunk of MIDI data from the output buffer to an output port.
fn write_port (writer: &mut MidiWriter, samples: usize, out: &[Vec<Vec<u8>>]) {
for (time, events) in out.iter().enumerate().take(samples) {
for bytes in events.iter() {
writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{
panic!("Failed to write MIDI data: {bytes:?}");
});
}
}
}
}

View file

@ -1,17 +1,18 @@
#![feature(type_alias_impl_trait)]
mod jack_client; pub use self::jack_client::*;
mod jack_event; pub use self::jack_event::*;
mod jack_port; pub use self::jack_port::*;
pub use ::jack::{*, contrib::{*, ClosureProcessHandler}};
//contrib::ClosureProcessHandler,
//NotificationHandler,
//Client, AsyncClient, ClientOptions, ClientStatus,
//ProcessScope, Control, Frames,
//Port, PortId, PortSpec, PortFlags,
//Unowned, MidiIn, MidiOut, AudioIn, AudioOut,
//};
pub(crate) use PortConnectName::*;
pub(crate) use PortConnectScope::*;
pub(crate) use PortConnectStatus::*;
pub(crate) use std::sync::{Arc, RwLock};
pub use ::jack; pub(crate) use ::jack::{
//contrib::ClosureProcessHandler,
NotificationHandler,
Client, AsyncClient, ClientOptions, ClientStatus,
ProcessScope, Control, Frames,
Port, PortId, PortSpec, PortFlags,
Unowned, MidiIn, MidiOut, AudioIn, AudioOut,
};
pub(crate) type Usually<T> = Result<T, Box<dyn std::error::Error>>;
mod jack_client; pub use self::jack_client::*;
mod jack_event; pub use self::jack_event::*;
mod jack_port; pub use self::jack_port::*;

View file

@ -1,5 +1,5 @@
use crate::*;
use ::jack::contrib::*;
use super::*;
use self::JackState::*;
/// Things that can provide a [jack::Client] reference.

View file

@ -1,4 +1,5 @@
use crate::*;
use super::*;
/// Event enum for JACK events.
#[derive(Debug, Clone, PartialEq)] pub enum JackEvent {

View file

@ -1,4 +1,5 @@
use crate::*;
use super::*;
macro_rules! impl_port {
($Name:ident : $Spec:ident -> $Pair:ident |$jack:ident, $name:ident|$port:expr) => {

76
crates/engine/src/lib.rs Normal file
View file

@ -0,0 +1,76 @@
#![feature(type_alias_impl_trait)]
mod time; pub use self::time::*;
mod note; pub use self::note::*;
pub mod jack; pub use self::jack::*;
pub mod midi; pub use self::midi::*;
pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicUsize, AtomicBool, Ordering::Relaxed}};
pub(crate) use std::path::PathBuf;
pub(crate) use std::fmt::Debug;
pub(crate) use std::ops::{Add, Sub, Mul, Div, Rem};
pub(crate) use ::tengri::input::*;
pub(crate) use ::tengri::output::*;
pub(crate) use ::tengri::dsl::*;
pub(crate) use ::tengri::tui::*;
pub(crate) use ::tengri::tui::ratatui::style::{Style, Stylize, Color};
pub use ::atomic_float; pub(crate) use atomic_float::*;
/// Standard result type.
pub(crate) type Usually<T> = std::result::Result<T, Box<dyn std::error::Error>>;
/// Standard optional result type.
pub(crate) type Perhaps<T> = std::result::Result<Option<T>, Box<dyn std::error::Error>>;
pub trait Gettable<T> {
/// Returns current value
fn get (&self) -> T;
}
pub trait Mutable<T>: Gettable<T> {
/// Sets new value, returns old
fn set (&mut self, value: T) -> T;
}
pub trait InteriorMutable<T>: Gettable<T> {
/// Sets new value, returns old
fn set (&self, value: T) -> T;
}
impl Gettable<bool> for AtomicBool {
fn get (&self) -> bool { self.load(Relaxed) }
}
impl InteriorMutable<bool> for AtomicBool {
fn set (&self, value: bool) -> bool { self.swap(value, Relaxed) }
}
impl Gettable<usize> for AtomicUsize {
fn get (&self) -> usize { self.load(Relaxed) }
}
impl InteriorMutable<usize> for AtomicUsize {
fn set (&self, value: usize) -> usize { self.swap(value, Relaxed) }
}
#[cfg(test)] #[test] fn test_time () -> Usually<()> {
// TODO!
Ok(())
}
#[cfg(test)] #[test] fn test_midi_range () {
let model = MidiRangeModel::from((1, false));
let _ = model.time_len();
let _ = model.time_zoom();
let _ = model.time_lock();
let _ = model.time_start();
let _ = model.time_axis();
let _ = model.time_end();
let _ = model.note_lo();
let _ = model.note_axis();
let _ = model.note_hi();
}

View file

@ -1,21 +1,61 @@
pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicUsize, AtomicBool, Ordering::Relaxed}};
pub(crate) use std::path::PathBuf;
pub(crate) use std::fmt::Debug;
use crate::*;
pub use ::midly;
pub(crate) use ::midly::{*, num::*, live::*};
pub use ::midly::{
Smf,
TrackEventKind,
MidiMessage,
num::*,
live::*,
};
pub(crate) use ::tek_time::*;
pub(crate) use ::tek_jack::{*, jack::*};
pub(crate) use ::tengri::input::*;
pub(crate) use ::tengri::output::*;
pub(crate) use ::tengri::dsl::*;
pub(crate) use ::tengri::tui::*;
pub(crate) use ::tengri::tui::ratatui::style::{Style, Stylize, Color};
/// Update notes_in array
pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) {
match message {
MidiMessage::NoteOn { key, .. } => { keys[key.as_int() as usize] = true; }
MidiMessage::NoteOff { key, .. } => { keys[key.as_int() as usize] = false; },
_ => {}
}
}
mod clip; pub use self::clip::*;
mod mode; pub use self::mode::*;
mod note; pub use self::note::*;
mod piano; pub use self::piano::*;
mod pool; pub use self::pool::*;
mod port; pub use self::port::*;
/// Return boxed iterator of MIDI events
pub fn parse_midi_input <'a> (input: ::jack::MidiIter<'a>) -> Box<dyn Iterator<Item=(usize, LiveEvent<'a>, &'a [u8])> + 'a> {
Box::new(input.map(|::jack::RawMidi { time, bytes }|(
time as usize,
LiveEvent::parse(bytes).unwrap(),
bytes
)))
}
/// Add "all notes off" to the start of a buffer.
pub fn all_notes_off (output: &mut [Vec<Vec<u8>>]) {
let mut buf = vec![];
let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() };
let evt = LiveEvent::Midi { channel: 0.into(), message: msg };
evt.write(&mut buf).unwrap();
output[0].push(buf);
}
/// Trait for thing that may receive MIDI.
pub trait HasMidiIns {
fn midi_ins (&self) -> &Vec<JackMidiIn>;
fn midi_ins_mut (&mut self) -> &mut Vec<JackMidiIn>;
fn has_midi_ins (&self) -> bool {
!self.midi_ins().is_empty()
}
}
/// Trait for thing that may output MIDI.
pub trait HasMidiOuts {
fn midi_outs (&self) -> &Vec<JackMidiOut>;
fn midi_outs_mut (&mut self) -> &mut Vec<JackMidiOut>;
fn has_midi_outs (&self) -> bool {
!self.midi_outs().is_empty()
}
/// Buffer for serializing a MIDI event. FIXME rename
fn midi_note (&mut self) -> &mut Vec<u8>;
}

View file

@ -1,244 +0,0 @@
//! MIDI editor.
use crate::*;
/// Contains state for viewing and editing a clip
pub struct MidiEditor {
/// Size of editor on screen
pub size: Measure<TuiOut>,
/// View mode and state of editor
pub mode: PianoHorizontal,
/// Input keymap
pub keys: InputMap<'static, Self, MidiEditCommand, TuiIn, SourceIter<'static>>
}
impl std::fmt::Debug for MidiEditor {
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
f.debug_struct("MidiEditor")
.field("mode", &self.mode)
.finish()
}
}
impl Default for MidiEditor {
fn default () -> Self {
Self {
size: Measure::new(),
mode: PianoHorizontal::new(None),
keys: InputMap::new(SourceIter(include_str!("../../../../config/keys_editor.edn"))),
}
}
}
has_size!(<TuiOut>|self: MidiEditor|&self.size);
content!(TuiOut: |self: MidiEditor| {
self.autoscroll();
//self.autozoom();
self.size.of(&self.mode)
});
from!(|clip: &Arc<RwLock<MidiClip>>|MidiEditor = {
let model = Self::from(Some(clip.clone()));
model.redraw();
model
});
from!(|clip: Option<Arc<RwLock<MidiClip>>>|MidiEditor = {
let mut model = Self::default();
*model.clip_mut() = clip;
model.redraw();
model
});
provide!(bool: |self: MidiEditor| {
":true" => true,
":false" => false,
":time-lock" => self.time_lock().get(),
":time-lock-toggle" => !self.time_lock().get(),
});
provide!(usize: |self: MidiEditor| {
":note-length" => self.note_len(),
":note-pos" => self.note_pos(),
":note-pos-next" => self.note_pos() + 1,
":note-pos-prev" => self.note_pos().saturating_sub(1),
":note-pos-next-octave" => self.note_pos() + 12,
":note-pos-prev-octave" => self.note_pos().saturating_sub(12),
":note-len" => self.note_len(),
":note-len-next" => self.note_len() + 1,
":note-len-prev" => self.note_len().saturating_sub(1),
":note-range" => self.note_axis().get(),
":note-range-prev" => self.note_axis().get() + 1,
":note-range-next" => self.note_axis().get().saturating_sub(1),
":time-pos" => self.time_pos(),
":time-pos-next" => self.time_pos() + self.time_zoom().get(),
":time-pos-prev" => self.time_pos().saturating_sub(self.time_zoom().get()),
":time-zoom" => self.time_zoom().get(),
":time-zoom-next" => self.time_zoom().get() + 1,
":time-zoom-prev" => self.time_zoom().get().saturating_sub(1).max(1),
});
impl MidiEditor {
/// Put note at current position
pub fn put_note (&mut self, advance: bool) {
let mut redraw = false;
if let Some(clip) = self.clip() {
let mut clip = clip.write().unwrap();
let note_start = self.time_pos();
let note_pos = self.note_pos();
let note_len = self.note_len();
let note_end = note_start + (note_len.saturating_sub(1));
let key: u7 = u7::from(note_pos as u8);
let vel: u7 = 100.into();
let length = clip.length;
let note_end = note_end % length;
let note_on = MidiMessage::NoteOn { key, vel };
if !clip.notes[note_start].iter().any(|msg|*msg == note_on) {
clip.notes[note_start].push(note_on);
}
let note_off = MidiMessage::NoteOff { key, vel };
if !clip.notes[note_end].iter().any(|msg|*msg == note_off) {
clip.notes[note_end].push(note_off);
}
if advance {
self.set_time_pos(note_end);
}
redraw = true;
}
if redraw {
self.mode.redraw();
}
}
pub fn clip_status (&self) -> impl Content<TuiOut> + '_ {
let (color, name, length, looped) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) {
(clip.color, clip.name.clone(), clip.length, clip.looped)
} else { (ItemTheme::G[64], String::new().into(), 0, false) };
Bsp::e(
FieldH(color, "Edit", format!("{name} ({length})")),
FieldH(color, "Loop", looped.to_string())
)
}
pub fn edit_status (&self) -> impl Content<TuiOut> + '_ {
let (color, length) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) {
(clip.color, clip.length)
} else { (ItemTheme::G[64], 0) };
let time_pos = self.time_pos();
let time_zoom = self.time_zoom().get();
let time_lock = if self.time_lock().get() { "[lock]" } else { " " };
let note_pos = format!("{:>3}", self.note_pos());
let note_name = format!("{:4}", Note::pitch_to_name(self.note_pos()));
let note_len = format!("{:>4}", self.note_len());
Bsp::e(
FieldH(color, "Time", format!("{length}/{time_zoom}+{time_pos} {time_lock}")),
FieldH(color, "Note", format!("{note_name} {note_pos} {note_len}")),
)
}
//fn clip_length (&self) -> usize {
//self.clip().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1)
//}
}
impl TimeRange for MidiEditor {
fn time_len (&self) -> &AtomicUsize { self.mode.time_len() }
fn time_zoom (&self) -> &AtomicUsize { self.mode.time_zoom() }
fn time_lock (&self) -> &AtomicBool { self.mode.time_lock() }
fn time_start (&self) -> &AtomicUsize { self.mode.time_start() }
fn time_axis (&self) -> &AtomicUsize { self.mode.time_axis() }
}
impl NoteRange for MidiEditor {
fn note_lo (&self) -> &AtomicUsize { self.mode.note_lo() }
fn note_axis (&self) -> &AtomicUsize { self.mode.note_axis() }
}
impl NotePoint for MidiEditor {
fn note_len (&self) -> usize { self.mode.note_len() }
fn set_note_len (&self, x: usize) -> usize { self.mode.set_note_len(x) }
fn note_pos (&self) -> usize { self.mode.note_pos() }
fn set_note_pos (&self, x: usize) -> usize { self.mode.set_note_pos(x) }
}
impl TimePoint for MidiEditor {
fn time_pos (&self) -> usize { self.mode.time_pos() }
fn set_time_pos (&self, x: usize) -> usize { self.mode.set_time_pos(x) }
}
impl MidiViewer for MidiEditor {
fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { self.mode.buffer_size(clip) }
fn redraw (&self) { self.mode.redraw() }
fn clip (&self) -> &Option<Arc<RwLock<MidiClip>>> { self.mode.clip() }
fn clip_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>> { self.mode.clip_mut() }
fn set_clip (&mut self, p: Option<&Arc<RwLock<MidiClip>>>) { self.mode.set_clip(p) }
}
atom_command!(MidiEditCommand: |state: MidiEditor| {
("note/append" [] Some(Self::AppendNote))
("note/put" [] Some(Self::PutNote))
("note/del" [] Some(Self::DelNote))
("note/pos" [a: usize] Some(Self::SetNoteCursor(a.expect("no note cursor"))))
("note/len" [a: usize] Some(Self::SetNoteLength(a.expect("no note length"))))
("time/pos" [a: usize] Some(Self::SetTimeCursor(a.expect("no time cursor"))))
("time/zoom" [a: usize] Some(Self::SetTimeZoom(a.expect("no time zoom"))))
("time/lock" [a: bool] Some(Self::SetTimeLock(a.expect("no time lock"))))
("time/lock" [] Some(Self::SetTimeLock(!state.time_lock().get())))
});
#[derive(Clone, Debug)] pub enum MidiEditCommand {
// TODO: 1-9 seek markers that by default start every 8th of the clip
AppendNote,
PutNote,
DelNote,
SetNoteCursor(usize),
SetNoteLength(usize),
SetNoteScroll(usize),
SetTimeCursor(usize),
SetTimeScroll(usize),
SetTimeZoom(usize),
SetTimeLock(bool),
Show(Option<Arc<RwLock<MidiClip>>>),
}
handle!(TuiIn: |self: MidiEditor, input|Ok(if let Some(command) = self.keys.command(self, input) {
command.execute(self)?;
Some(true)
} else {
None
}));
impl Command<MidiEditor> for MidiEditCommand {
fn execute (self, state: &mut MidiEditor) -> Perhaps<Self> {
use MidiEditCommand::*;
match self {
Show(clip) => { state.set_clip(clip.as_ref()); },
DelNote => {},
PutNote => { state.put_note(false); },
AppendNote => { state.put_note(true); },
SetTimeZoom(x) => { state.time_zoom().set(x); state.redraw(); },
SetTimeLock(x) => { state.time_lock().set(x); },
SetTimeScroll(x) => { state.time_start().set(x); },
SetNoteScroll(x) => { state.note_lo().set(x.min(127)); },
SetNoteLength(x) => {
let note_len = state.note_len();
let time_zoom = state.time_zoom().get();
state.set_note_len(x);
//if note_len / time_zoom != x / time_zoom {
state.redraw();
//}
},
SetTimeCursor(x) => { state.set_time_pos(x); },
SetNoteCursor(note) => { state.set_note_pos(note.min(127)); },
//_ => todo!("{:?}", self)
}
Ok(None)
}
}

View file

@ -1,208 +0,0 @@
//! MIDI player
use crate::*;
pub trait HasPlayer {
fn player (&self) -> &impl MidiPlayerApi;
fn player_mut (&mut self) -> &mut impl MidiPlayerApi;
}
#[macro_export] macro_rules! has_player {
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? HasPlayer for $Struct $(<$($L),*$($T),*>)? {
fn player (&$self) -> &impl MidiPlayerApi { &$cb }
fn player_mut (&mut $self) -> &mut impl MidiPlayerApi { &mut$cb }
}
}
}
pub trait MidiPlayerApi: MidiRecordApi + MidiPlaybackApi + Send + Sync {}
impl MidiPlayerApi for MidiPlayer {}
/// Contains state for playing a clip
pub struct MidiPlayer {
/// State of clock and playhead
pub clock: Clock,
/// Start time and clip being played
pub play_clip: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
/// Start time and next clip
pub next_clip: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
/// Play input through output.
pub monitoring: bool,
/// Write input to sequence.
pub recording: bool,
/// Overdub input to sequence.
pub overdub: bool,
/// Send all notes off
pub reset: bool, // TODO?: after Some(nframes)
/// Record from MIDI ports to current sequence.
pub midi_ins: Vec<JackMidiIn>,
/// Play from current sequence to MIDI ports
pub midi_outs: Vec<JackMidiOut>,
/// Notes currently held at input
pub notes_in: Arc<RwLock<[bool; 128]>>,
/// Notes currently held at output
pub notes_out: Arc<RwLock<[bool; 128]>>,
/// MIDI output buffer
pub note_buf: Vec<u8>,
}
impl Default for MidiPlayer {
fn default () -> Self {
Self {
play_clip: None,
next_clip: None,
recording: false,
monitoring: false,
overdub: false,
notes_in: RwLock::new([false;128]).into(),
notes_out: RwLock::new([false;128]).into(),
note_buf: vec![0;8],
reset: true,
midi_ins: vec![],
midi_outs: vec![],
clock: Clock::default(),
}
}
}
impl MidiPlayer {
pub fn new (
name: impl AsRef<str>,
jack: &Jack,
clock: Option<&Clock>,
clip: Option<&Arc<RwLock<MidiClip>>>,
midi_from: &[PortConnect],
midi_to: &[PortConnect],
) -> Usually<Self> {
let _name = name.as_ref();
let clock = clock.cloned().unwrap_or_default();
Ok(Self {
midi_ins: vec![JackMidiIn::new(jack, format!("M/{}", name.as_ref()), midi_from)?,],
midi_outs: vec![JackMidiOut::new(jack, format!("{}/M", name.as_ref()), midi_to)?, ],
play_clip: clip.map(|clip|(Moment::zero(&clock.timebase), Some(clip.clone()))),
clock,
note_buf: vec![0;8],
reset: true,
recording: false,
monitoring: false,
overdub: false,
next_clip: None,
notes_in: RwLock::new([false;128]).into(),
notes_out: RwLock::new([false;128]).into(),
})
}
}
impl std::fmt::Debug for MidiPlayer {
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
f.debug_struct("MidiPlayer")
.field("clock", &self.clock)
.field("play_clip", &self.play_clip)
.field("next_clip", &self.next_clip)
.finish()
}
}
has_clock!(|self: MidiPlayer|self.clock);
impl HasMidiIns for MidiPlayer {
fn midi_ins (&self) -> &Vec<JackMidiIn> { &self.midi_ins }
fn midi_ins_mut (&mut self) -> &mut Vec<JackMidiIn> { &mut self.midi_ins }
}
impl HasMidiOuts for MidiPlayer {
fn midi_outs (&self) -> &Vec<JackMidiOut> { &self.midi_outs }
fn midi_outs_mut (&mut self) -> &mut Vec<JackMidiOut> { &mut self.midi_outs }
fn midi_note (&mut self) -> &mut Vec<u8> { &mut self.note_buf }
}
/// Hosts the JACK callback for a single MIDI player
pub struct PlayerAudio<'a, T: MidiPlayerApi>(
/// Player
pub &'a mut T,
/// Note buffer
pub &'a mut Vec<u8>,
/// Note chunk buffer
pub &'a mut Vec<Vec<Vec<u8>>>,
);
/// JACK process callback for a sequencer's clip player/recorder.
impl<T: MidiPlayerApi> Audio for PlayerAudio<'_, T> {
fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
let model = &mut self.0;
let note_buf = &mut self.1;
let midi_buf = &mut self.2;
// Clear output buffer(s)
model.clear(scope, midi_buf, false);
// Write chunk of clip to output, handle switchover
if model.play(scope, note_buf, midi_buf) {
model.switchover(scope, note_buf, midi_buf);
}
if model.has_midi_ins() {
if model.recording() || model.monitoring() {
// Record and/or monitor input
model.record(scope, midi_buf)
} else if model.has_midi_outs() && model.monitoring() {
// Monitor input to output
model.monitor(scope, midi_buf)
}
}
// Write to output port(s)
model.write(scope, midi_buf);
Control::Continue
}
}
impl MidiRecordApi for MidiPlayer {
fn recording (&self) -> bool {
self.recording
}
fn recording_mut (&mut self) -> &mut bool {
&mut self.recording
}
fn monitoring (&self) -> bool {
self.monitoring
}
fn monitoring_mut (&mut self) -> &mut bool {
&mut self.monitoring
}
fn overdub (&self) -> bool {
self.overdub
}
fn overdub_mut (&mut self) -> &mut bool {
&mut self.overdub
}
fn notes_in (&self) -> &Arc<RwLock<[bool; 128]>> {
&self.notes_in
}
}
impl MidiPlaybackApi for MidiPlayer {
fn notes_out (&self) -> &Arc<RwLock<[bool; 128]>> {
&self.notes_out
}
}
impl HasPlayClip for MidiPlayer {
fn reset (&self) -> bool {
self.reset
}
fn reset_mut (&mut self) -> &mut bool {
&mut self.reset
}
fn play_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
&self.play_clip
}
fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
&mut self.play_clip
}
fn next_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
&self.next_clip
}
fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
&mut self.next_clip
}
}

View file

@ -1,18 +0,0 @@
use crate::*;
mod mode_length; pub use self::mode_length::*;
mod mode_rename; pub use self::mode_rename::*;
mod mode_browse;
/// Modes for clip pool
#[derive(Debug, Clone)]
pub enum PoolMode {
/// Renaming a pattern
Rename(usize, Arc<str>),
/// Editing the length of a pattern
Length(usize, usize, ClipLengthFocus),
/// Load clip from disk
Import(usize, FileBrowser),
/// Save clip to disk
Export(usize, FileBrowser),
}

View file

@ -1,40 +0,0 @@
use crate::*;
atom_command!(FileBrowserCommand: |state: MidiPool| {
("begin" [] Some(Self::Begin))
("cancel" [] Some(Self::Cancel))
("confirm" [] Some(Self::Confirm))
("select" [i: usize] Some(Self::Select(i.expect("no index"))))
("chdir" [p: PathBuf] Some(Self::Chdir(p.expect("no path"))))
("filter" [f: Arc<str>] Some(Self::Filter(f.expect("no filter"))))
});
command!(|self: FileBrowserCommand, state: MidiPool|{
use PoolMode::*;
use FileBrowserCommand::*;
let mode = &mut state.mode;
match mode {
Some(Import(index, ref mut browser)) => match self {
Cancel => { *mode = None; },
Chdir(cwd) => { *mode = Some(Import(*index, FileBrowser::new(Some(cwd))?)); },
Select(index) => { browser.index = index; },
Confirm => if browser.is_file() {
let index = *index;
let path = browser.path();
*mode = None;
PoolClipCommand::Import(index, path).execute(state)?;
} else if browser.is_dir() {
*mode = Some(Import(*index, browser.chdir()?));
},
_ => todo!(),
},
Some(Export(index, ref mut browser)) => match self {
Cancel => { *mode = None; },
Chdir(cwd) => { *mode = Some(Export(*index, FileBrowser::new(Some(cwd))?)); },
Select(index) => { browser.index = index; },
_ => unreachable!()
},
_ => unreachable!(),
};
None
});

View file

@ -1,133 +0,0 @@
use crate::*;
/// Displays and edits clip length.
#[derive(Clone)]
pub struct ClipLength {
/// Pulses per beat (quaver)
ppq: usize,
/// Beats per bar
bpb: usize,
/// Length of clip in pulses
pulses: usize,
/// Selected subdivision
focus: Option<ClipLengthFocus>,
}
impl ClipLength {
fn _new (pulses: usize, focus: Option<ClipLengthFocus>) -> Self {
Self { ppq: PPQ, bpb: 4, pulses, focus }
}
fn bars (&self) -> usize {
self.pulses / (self.bpb * self.ppq)
}
fn beats (&self) -> usize {
(self.pulses % (self.bpb * self.ppq)) / self.ppq
}
fn ticks (&self) -> usize {
self.pulses % self.ppq
}
fn bars_string (&self) -> Arc<str> {
format!("{}", self.bars()).into()
}
fn beats_string (&self) -> Arc<str> {
format!("{}", self.beats()).into()
}
fn ticks_string (&self) -> Arc<str> {
format!("{:>02}", self.ticks()).into()
}
}
content!(TuiOut: |self: ClipLength| {
let bars = ||self.bars_string();
let beats = ||self.beats_string();
let ticks = ||self.ticks_string();
match self.focus {
None =>
row!(" ", bars(), ".", beats(), ".", ticks()),
Some(ClipLengthFocus::Bar) =>
row!("[", bars(), "]", beats(), ".", ticks()),
Some(ClipLengthFocus::Beat) =>
row!(" ", bars(), "[", beats(), "]", ticks()),
Some(ClipLengthFocus::Tick) =>
row!(" ", bars(), ".", beats(), "[", ticks()),
}
});
/// Focused field of `ClipLength`
#[derive(Copy, Clone, Debug)]
pub enum ClipLengthFocus {
/// Editing the number of bars
Bar,
/// Editing the number of beats
Beat,
/// Editing the number of ticks
Tick,
}
impl ClipLengthFocus {
fn next (&mut self) {
*self = match self { Self::Bar => Self::Beat, Self::Beat => Self::Tick, Self::Tick => Self::Bar, }
}
fn prev (&mut self) {
*self = match self { Self::Bar => Self::Tick, Self::Beat => Self::Bar, Self::Tick => Self::Beat, }
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum ClipLengthCommand {
Begin,
Cancel,
Set(usize),
Next,
Prev,
Inc,
Dec,
}
atom_command!(ClipLengthCommand: |state: MidiPool| {
("begin" [] Some(Self::Begin))
("cancel" [] Some(Self::Cancel))
("next" [] Some(Self::Next))
("prev" [] Some(Self::Prev))
("inc" [] Some(Self::Inc))
("dec" [] Some(Self::Dec))
("set" [l: usize] Some(Self::Set(l.expect("no length"))))
});
command!(|self: ClipLengthCommand, state: MidiPool|{
use ClipLengthCommand::*;
use ClipLengthFocus::*;
if let Some(
PoolMode::Length(clip, ref mut length, ref mut focus)
) = state.mode_mut().clone() {
match self {
Cancel => { *state.mode_mut() = None; },
Prev => { focus.prev() },
Next => { focus.next() },
Inc => match focus {
Bar => { *length += 4 * PPQ },
Beat => { *length += PPQ },
Tick => { *length += 1 },
},
Dec => match focus {
Bar => { *length = length.saturating_sub(4 * PPQ) },
Beat => { *length = length.saturating_sub(PPQ) },
Tick => { *length = length.saturating_sub(1) },
},
Set(length) => {
let old_length;
{
let clip = state.clips()[clip].clone();//.write().unwrap();
old_length = Some(clip.read().unwrap().length);
clip.write().unwrap().length = length;
}
*state.mode_mut() = None;
return Ok(old_length.map(Self::Set))
},
_ => unreachable!()
}
} else {
unreachable!();
}
None
});

View file

@ -1,38 +0,0 @@
use crate::*;
#[derive(Clone, Debug, PartialEq)] pub enum ClipRenameCommand {
Begin,
Cancel,
Confirm,
Set(Arc<str>),
}
atom_command!(ClipRenameCommand: |state: MidiPool| {
("begin" [] Some(Self::Begin))
("cancel" [] Some(Self::Cancel))
("confirm" [] Some(Self::Confirm))
("set" [n: Arc<str>] Some(Self::Set(n.expect("no name"))))
});
command!(|self: ClipRenameCommand, state: MidiPool|if let Some(
PoolMode::Rename(clip, ref mut old_name)
) = state.mode_mut().clone() {
match self {
Self::Set(s) => {
state.clips()[clip].write().unwrap().name = s;
return Ok(Some(Self::Set(old_name.clone().into())))
},
Self::Confirm => {
let old_name = old_name.clone();
*state.mode_mut() = None;
return Ok(Some(Self::Set(old_name)))
},
Self::Cancel => {
state.clips()[clip].write().unwrap().name = old_name.clone().into();
return Ok(None)
},
_ => unreachable!()
}
} else {
unreachable!()
});

View file

@ -1,2 +0,0 @@
mod piano_h; pub use self::piano_h::*;
mod piano_v; pub use self::piano_v::*;

View file

@ -1,327 +0,0 @@
use crate::*;
use Color::*;
/// A clip, rendered as a horizontal piano roll.
#[derive(Clone)]
pub struct PianoHorizontal {
pub clip: Option<Arc<RwLock<MidiClip>>>,
/// Buffer where the whole clip is rerendered on change
pub buffer: Arc<RwLock<BigBuffer>>,
/// Size of actual notes area
pub size: Measure<TuiOut>,
/// The display window
pub range: MidiRangeModel,
/// The note cursor
pub point: MidiPointModel,
/// The highlight color palette
pub color: ItemTheme,
/// Width of the keyboard
pub keys_width: u16,
}
impl PianoHorizontal {
pub fn new (clip: Option<&Arc<RwLock<MidiClip>>>) -> Self {
let size = Measure::new();
let mut range = MidiRangeModel::from((12, true));
range.time_axis = size.x.clone();
range.note_axis = size.y.clone();
let piano = Self {
keys_width: 5,
size,
range,
buffer: RwLock::new(Default::default()).into(),
point: MidiPointModel::default(),
clip: clip.cloned(),
color: clip.as_ref().map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]),
};
piano.redraw();
piano
}
}
pub(crate) fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16) -> impl Iterator<Item=(usize, u16, usize)> {
(note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n))
}
content!(TuiOut:|self: PianoHorizontal| Tui::bg(Tui::g(40), Bsp::s(
Bsp::e(
Fixed::x(5, format!("{}x{}", self.size.w(), self.size.h())),
self.timeline()
),
Bsp::e(
self.keys(),
self.size.of(Tui::bg(Tui::g(32), Bsp::b(
Fill::xy(self.notes()),
Fill::xy(self.cursor()),
)))
),
)));
impl PianoHorizontal {
/// Draw the piano roll background.
///
/// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄
fn draw_bg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize, note_len: usize) {
for (y, note) in (0..=127).rev().enumerate() {
for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) {
let cell = buf.get_mut(x, y).unwrap();
cell.set_bg(clip.color.darkest.rgb);
if time % 384 == 0 {
cell.set_fg(clip.color.darker.rgb);
cell.set_char('│');
} else if time % 96 == 0 {
cell.set_fg(clip.color.dark.rgb);
cell.set_char('╎');
} else if time % note_len == 0 {
cell.set_fg(clip.color.darker.rgb);
cell.set_char('┊');
} else if (127 - note) % 12 == 0 {
cell.set_fg(clip.color.darker.rgb);
cell.set_char('=');
} else if (127 - note) % 6 == 0 {
cell.set_fg(clip.color.darker.rgb);
cell.set_char('—');
} else {
cell.set_fg(clip.color.darker.rgb);
cell.set_char('·');
}
}
}
}
/// Draw the piano roll foreground.
///
/// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄
fn draw_fg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize) {
let style = Style::default().fg(clip.color.base.rgb);//.bg(Rgb(0, 0, 0));
let mut notes_on = [false;128];
for (x, time_start) in (0..clip.length).step_by(zoom).enumerate() {
for (_y, note) in (0..=127).rev().enumerate() {
if let Some(cell) = buf.get_mut(x, note) {
if notes_on[note] {
cell.set_char('▂');
cell.set_style(style);
}
}
}
let time_end = time_start + zoom;
for time in time_start..time_end.min(clip.length) {
for event in clip.notes[time].iter() {
match event {
MidiMessage::NoteOn { key, .. } => {
let note = key.as_int() as usize;
if let Some(cell) = buf.get_mut(x, note) {
cell.set_char('█');
cell.set_style(style);
}
notes_on[note] = true
},
MidiMessage::NoteOff { key, .. } => {
notes_on[key.as_int() as usize] = false
},
_ => {}
}
}
}
}
}
fn notes (&self) -> impl Content<TuiOut> {
let time_start = self.time_start().get();
let note_lo = self.note_lo().get();
let note_hi = self.note_hi();
let buffer = self.buffer.clone();
ThunkRender::new(move|to: &mut TuiOut|{
let source = buffer.read().unwrap();
let [x0, y0, w, _h] = to.area().xywh();
//if h as usize != note_axis {
//panic!("area height mismatch: {h} <> {note_axis}");
//}
for (area_x, screen_x) in (x0..x0+w).enumerate() {
for (area_y, screen_y, _note) in note_y_iter(note_lo, note_hi, y0) {
let source_x = time_start + area_x;
let source_y = note_hi - area_y;
// TODO: enable loop rollover:
//let source_x = (time_start + area_x) % source.width.max(1);
//let source_y = (note_hi - area_y) % source.height.max(1);
let is_in_x = source_x < source.width;
let is_in_y = source_y < source.height;
if is_in_x && is_in_y {
if let Some(source_cell) = source.get(source_x, source_y) {
if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) {
*cell = source_cell.clone();
}
}
}
}
}
})
}
fn cursor (&self) -> impl Content<TuiOut> {
let style = Some(Style::default().fg(self.color.lightest.rgb));
let note_hi = self.note_hi();
let note_lo = self.note_lo().get();
let note_pos = self.note_pos();
let note_len = self.note_len();
let time_pos = self.time_pos();
let time_start = self.time_start().get();
let time_zoom = self.time_zoom().get();
ThunkRender::new(move|to: &mut TuiOut|{
let [x0, y0, w, _] = to.area().xywh();
for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
if note == note_pos {
for x in 0..w {
let screen_x = x0 + x;
let time_1 = time_start + x as usize * time_zoom;
let time_2 = time_1 + time_zoom;
if time_1 <= time_pos && time_pos < time_2 {
to.blit(&"", screen_x, screen_y, style);
let tail = note_len as u16 / time_zoom as u16;
for x_tail in (screen_x + 1)..(screen_x + tail) {
to.blit(&"", x_tail, screen_y, style);
}
break
}
}
break
}
}
})
}
fn keys (&self) -> impl Content<TuiOut> {
let state = self;
let color = state.color;
let note_lo = state.note_lo().get();
let note_hi = state.note_hi();
let note_pos = state.note_pos();
let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0)));
let off_style = Some(Style::default().fg(Tui::g(255)));
let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.rgb).bold());
Fill::y(Fixed::x(self.keys_width, ThunkRender::new(move|to: &mut TuiOut|{
let [x, y0, _w, _h] = to.area().xywh();
for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
to.blit(&to_key(note), x, screen_y, key_style);
if note > 127 {
continue
}
if note == note_pos {
to.blit(&format!("{:<5}", Note::pitch_to_name(note)), x, screen_y, on_style)
} else {
to.blit(&Note::pitch_to_name(note), x, screen_y, off_style)
};
}
})))
}
fn timeline (&self) -> impl Content<TuiOut> + '_ {
Fill::x(Fixed::y(1, ThunkRender::new(move|to: &mut TuiOut|{
let [x, y, w, _h] = to.area();
let style = Some(Style::default().dim());
let length = self.clip.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1);
for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) {
let t = area_x as usize * self.time_zoom().get();
if t < length {
to.blit(&"|", screen_x, y, style);
}
}
})))
}
}
has_size!(<TuiOut>|self:PianoHorizontal|&self.size);
impl TimeRange for PianoHorizontal {
fn time_len (&self) -> &AtomicUsize { self.range.time_len() }
fn time_zoom (&self) -> &AtomicUsize { self.range.time_zoom() }
fn time_lock (&self) -> &AtomicBool { self.range.time_lock() }
fn time_start (&self) -> &AtomicUsize { self.range.time_start() }
fn time_axis (&self) -> &AtomicUsize { self.range.time_axis() }
}
impl NoteRange for PianoHorizontal {
fn note_lo (&self) -> &AtomicUsize { self.range.note_lo() }
fn note_axis (&self) -> &AtomicUsize { self.range.note_axis() }
}
impl NotePoint for PianoHorizontal {
fn note_len (&self) -> usize { self.point.note_len() }
fn set_note_len (&self, x: usize) -> usize { self.point.set_note_len(x) }
fn note_pos (&self) -> usize { self.point.note_pos() }
fn set_note_pos (&self, x: usize) -> usize { self.point.set_note_pos(x) }
}
impl TimePoint for PianoHorizontal {
fn time_pos (&self) -> usize { self.point.time_pos() }
fn set_time_pos (&self, x: usize) -> usize { self.point.set_time_pos(x) }
}
impl MidiViewer for PianoHorizontal {
fn clip (&self) -> &Option<Arc<RwLock<MidiClip>>> {
&self.clip
}
fn clip_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>> {
&mut self.clip
}
/// Determine the required space to render the clip.
fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) {
(clip.length / self.range.time_zoom().get(), 128)
}
fn redraw (&self) {
*self.buffer.write().unwrap() = if let Some(clip) = self.clip.as_ref() {
let clip = clip.read().unwrap();
let buf_size = self.buffer_size(&clip);
let mut buffer = BigBuffer::from(buf_size);
let note_len = self.note_len();
let time_zoom = self.time_zoom().get();
self.time_len().set(clip.length);
PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom, note_len);
PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom);
buffer
} else {
Default::default()
}
}
fn set_clip (&mut self, clip: Option<&Arc<RwLock<MidiClip>>>) {
*self.clip_mut() = clip.cloned();
self.color = clip.map(|p|p.read().unwrap().color)
.unwrap_or(ItemTheme::G[64]);
self.redraw();
}
}
impl std::fmt::Debug for PianoHorizontal {
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
let buffer = self.buffer.read().unwrap();
f.debug_struct("PianoHorizontal")
.field("time_zoom", &self.range.time_zoom)
.field("buffer", &format!("{}x{}", buffer.width, buffer.height))
.finish()
}
}
// Update sequencer playhead indicator
//self.now().set(0.);
//if let Some((ref started_at, Some(ref playing))) = self.player.play_clip {
//let clip = clip.read().unwrap();
//if *playing.read().unwrap() == *clip {
//let pulse = self.current().pulse.get();
//let start = started_at.pulse.get();
//let now = (pulse - start) % clip.length as f64;
//self.now().set(now);
//}
//}
fn to_key (note: usize) -> &'static str {
match note % 12 {
11 => "████▌",
10 => " ",
9 => "████▌",
8 => " ",
7 => "████▌",
6 => " ",
5 => "████▌",
4 => "████▌",
3 => " ",
2 => "████▌",
1 => " ",
0 => "████▌",
_ => unreachable!(),
}
}

View file

@ -1,34 +0,0 @@
use crate::*;
use Color::*;
pub struct OctaveVertical {
on: [bool; 12],
colors: [Color; 3]
}
impl Default for OctaveVertical {
fn default () -> Self {
Self {
on: [false; 12],
colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)]
}
}
}
impl OctaveVertical {
fn color (&self, pitch: usize) -> Color {
let pitch = pitch % 12;
self.colors[if self.on[pitch] { 2 } else {
match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 }
}]
}
}
impl Content<TuiOut> for OctaveVertical {
fn content (&self) -> impl Render<TuiOut> {
row!(
Tui::fg_bg(self.color(0), self.color(1), ""),
Tui::fg_bg(self.color(2), self.color(3), ""),
Tui::fg_bg(self.color(4), self.color(5), ""),
Tui::fg_bg(self.color(6), self.color(7), ""),
Tui::fg_bg(self.color(8), self.color(9), ""),
Tui::fg_bg(self.color(10), self.color(11), ""),
)
}
}

View file

@ -1,4 +0,0 @@
mod pool_api; pub use self::pool_api::*;
mod pool_clips; pub use self::pool_clips::*;
mod pool_model; pub use self::pool_model::*;
mod pool_view; pub use self::pool_view::*;

View file

@ -1,178 +0,0 @@
use crate::*;
handle!(TuiIn: |self: MidiPool, input|{
//Ok(if let Some(command) = match self.mode() {
//Some(PoolMode::Rename(..)) => self.keys_rename,
//Some(PoolMode::Length(..)) => self.keys_length,
//Some(PoolMode::Import(..)) | Some(PoolMode::Export(..)) => self.keys_file,
//_ => self.keys
//}.command::<Self, PoolCommand, TuiIn>(self, input) {
Ok(if let Some(command) = self.keys.command(self, input) {
let _undo = command.execute(self)?;
Some(true)
} else {
None
})
});
provide!(bool: |self: MidiPool| {});
provide!(MidiClip: |self: MidiPool| {
":new-clip" => self.new_clip(),
":cloned-clip" => self.cloned_clip(),
});
provide!(PathBuf: |self: MidiPool| {});
provide!(Arc<str>: |self: MidiPool| {});
provide!(usize: |self: MidiPool| {
":current" => 0,
":after" => 0,
":previous" => 0,
":next" => 0
});
provide!(ItemColor: |self: MidiPool| {
":random-color" => ItemColor::random()
});
#[derive(Clone, PartialEq, Debug)] pub enum PoolCommand {
/// Toggle visibility of pool
Show(bool),
/// Select a clip from the clip pool
Select(usize),
/// Rename a clip
Rename(ClipRenameCommand),
/// Change the length of a clip
Length(ClipLengthCommand),
/// Import from file
Import(FileBrowserCommand),
/// Export to file
Export(FileBrowserCommand),
/// Update the contents of the clip pool
Clip(PoolClipCommand),
}
atom_command!(PoolCommand: |state: MidiPool| {
("show" [a: bool] Some(Self::Show(a.expect("no flag"))))
("select" [i: usize] Some(Self::Select(i.expect("no index"))))
("rename" [,..a] ClipRenameCommand::try_from_expr(state, a).map(Self::Rename))
("length" [,..a] ClipLengthCommand::try_from_expr(state, a).map(Self::Length))
("import" [,..a] FileBrowserCommand::try_from_expr(state, a).map(Self::Import))
("export" [,..a] FileBrowserCommand::try_from_expr(state, a).map(Self::Export))
("clip" [,..a] PoolClipCommand::try_from_expr(state, a).map(Self::Clip))
});
command!(|self: PoolCommand, state: MidiPool|{
use PoolCommand::*;
match self {
Rename(ClipRenameCommand::Begin) => { state.begin_clip_rename(); None }
Rename(command) => command.delegate(state, Rename)?,
Length(ClipLengthCommand::Begin) => { state.begin_clip_length(); None },
Length(command) => command.delegate(state, Length)?,
Import(FileBrowserCommand::Begin) => { state.begin_import()?; None },
Import(command) => command.delegate(state, Import)?,
Export(FileBrowserCommand::Begin) => { state.begin_export()?; None },
Export(command) => command.delegate(state, Export)?,
Clip(command) => command.execute(state)?.map(Clip),
Show(visible) => { state.visible = visible; Some(Self::Show(!visible)) },
Select(clip) => { state.set_clip_index(clip); None },
}
});
#[derive(Clone, Debug, PartialEq)] pub enum PoolClipCommand {
Add(usize, MidiClip),
Delete(usize),
Swap(usize, usize),
Import(usize, PathBuf),
Export(usize, PathBuf),
SetName(usize, Arc<str>),
SetLength(usize, usize),
SetColor(usize, ItemColor),
}
atom_command!(PoolClipCommand: |state: MidiPool| {
("add" [i: usize, c: MidiClip]
Some(Self::Add(i.expect("no index"), c.expect("no clip"))))
("delete" [i: usize]
Some(Self::Delete(i.expect("no index"))))
("swap" [a: usize, b: usize]
Some(Self::Swap(a.expect("no index"), b.expect("no index"))))
("import" [i: usize, p: PathBuf]
Some(Self::Import(i.expect("no index"), p.expect("no path"))))
("export" [i: usize, p: PathBuf]
Some(Self::Export(i.expect("no index"), p.expect("no path"))))
("set-name" [i: usize, n: Arc<str>]
Some(Self::SetName(i.expect("no index"), n.expect("no name"))))
("set-length" [i: usize, l: usize]
Some(Self::SetLength(i.expect("no index"), l.expect("no length"))))
("set-color" [i: usize, c: ItemColor]
Some(Self::SetColor(i.expect("no index"), c.expect("no color"))))
});
impl<T: HasClips> Command<T> for PoolClipCommand {
fn execute (self, model: &mut T) -> Perhaps<Self> {
use PoolClipCommand::*;
Ok(match self {
Add(mut index, clip) => {
let clip = Arc::new(RwLock::new(clip));
let mut clips = model.clips_mut();
if index >= clips.len() {
index = clips.len();
clips.push(clip)
} else {
clips.insert(index, clip);
}
Some(Self::Delete(index))
},
Delete(index) => {
let clip = model.clips_mut().remove(index).read().unwrap().clone();
Some(Self::Add(index, clip))
},
Swap(index, other) => {
model.clips_mut().swap(index, other);
Some(Self::Swap(index, other))
},
Import(index, path) => {
let bytes = std::fs::read(&path)?;
let smf = Smf::parse(bytes.as_slice())?;
let mut t = 0u32;
let mut events = vec![];
for track in smf.tracks.iter() {
for event in track.iter() {
t += event.delta.as_int();
if let TrackEventKind::Midi { channel, message } = event.kind {
events.push((t, channel.as_int(), message));
}
}
}
let mut clip = MidiClip::new("imported", true, t as usize + 1, None, None);
for event in events.iter() {
clip.notes[event.0 as usize].push(event.2);
}
Self::Add(index, clip).execute(model)?
},
Export(_index, _path) => {
todo!("export clip to midi file");
},
SetName(index, name) => {
let clip = &mut model.clips_mut()[index];
let old_name = clip.read().unwrap().name.clone();
clip.write().unwrap().name = name;
Some(Self::SetName(index, old_name))
},
SetLength(index, length) => {
let clip = &mut model.clips_mut()[index];
let old_len = clip.read().unwrap().length;
clip.write().unwrap().length = length;
Some(Self::SetLength(index, old_len))
},
SetColor(index, color) => {
let mut color = ItemTheme::from(color);
std::mem::swap(&mut color, &mut model.clips()[index].write().unwrap().color);
Some(Self::SetColor(index, color.base))
},
})
}
}

View file

@ -1,26 +0,0 @@
use crate::*;
pub type ClipPool = Vec<Arc<RwLock<MidiClip>>>;
pub trait HasClips {
fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>;
fn clips_mut <'a> (&'a self) -> std::sync::RwLockWriteGuard<'a, ClipPool>;
fn add_clip (&self) -> (usize, Arc<RwLock<MidiClip>>) {
let clip = Arc::new(RwLock::new(MidiClip::new("Clip", true, 384, None, None)));
self.clips_mut().push(clip.clone());
(self.clips().len() - 1, clip)
}
}
#[macro_export] macro_rules! has_clips {
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? {
fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> {
$cb.read().unwrap()
}
fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> {
$cb.write().unwrap()
}
}
}
}

View file

@ -1,119 +0,0 @@
use crate::*;
#[derive(Debug)]
pub struct MidiPool {
pub visible: bool,
/// Collection of clips
pub clips: Arc<RwLock<Vec<Arc<RwLock<MidiClip>>>>>,
/// Selected clip
pub clip: AtomicUsize,
/// Mode switch
pub mode: Option<PoolMode>,
pub keys: InputMap<'static, Self, PoolCommand, TuiIn, SourceIter<'static>>,
//pub keys: SourceIter<'static>,
//pub keys_rename: SourceIter<'static>,
//pub keys_length: SourceIter<'static>,
//pub keys_file: SourceIter<'static>,
}
impl Default for MidiPool {
fn default () -> Self {
use PoolMode::*;
Self {
visible: true,
clips: Arc::from(RwLock::from(vec![])),
clip: 0.into(),
mode: None,
keys: InputMap::new(SourceIter(include_str!("../../../../config/keys_pool.edn")))
.layer_if(|pool: &Self|matches!(pool.mode, Some(Import(..))|Some(Export(..))),
SourceIter(include_str!("../../../../config/keys_pool_file.edn")))
.layer_if(|pool: &Self|matches!(pool.mode, Some(Rename(..))),
SourceIter(include_str!("../../../../config/keys_clip_rename.edn")))
.layer_if(|pool: &Self|matches!(pool.mode, Some(Length(..))),
SourceIter(include_str!("../../../../config/keys_clip_length.edn")))
}
}
}
has_clips!(|self: MidiPool|self.clips);
has_clip!(|self: MidiPool|self.clips().get(self.clip_index()).map(|c|c.clone()));
from!(|clip:&Arc<RwLock<MidiClip>>|MidiPool = {
let model = Self::default();
model.clips.write().unwrap().push(clip.clone());
model.clip.store(1, Relaxed);
model
});
impl MidiPool {
pub fn clip_index (&self) -> usize {
self.clip.load(Relaxed)
}
pub fn set_clip_index (&self, value: usize) {
self.clip.store(value, Relaxed);
}
pub fn mode (&self) -> &Option<PoolMode> {
&self.mode
}
pub fn mode_mut (&mut self) -> &mut Option<PoolMode> {
&mut self.mode
}
pub fn begin_clip_length (&mut self) {
let length = self.clips()[self.clip_index()].read().unwrap().length;
*self.mode_mut() = Some(PoolMode::Length(
self.clip_index(),
length,
ClipLengthFocus::Bar
));
}
pub fn begin_clip_rename (&mut self) {
let name = self.clips()[self.clip_index()].read().unwrap().name.clone();
*self.mode_mut() = Some(PoolMode::Rename(
self.clip_index(),
name
));
}
pub fn begin_import (&mut self) -> Usually<()> {
*self.mode_mut() = Some(PoolMode::Import(
self.clip_index(),
FileBrowser::new(None)?
));
Ok(())
}
pub fn begin_export (&mut self) -> Usually<()> {
*self.mode_mut() = Some(PoolMode::Export(
self.clip_index(),
FileBrowser::new(None)?
));
Ok(())
}
pub fn new_clip (&self) -> MidiClip {
MidiClip::new("Clip", true, 4 * PPQ, None, Some(ItemTheme::random()))
}
pub fn cloned_clip (&self) -> MidiClip {
let index = self.clip_index();
let mut clip = self.clips()[index].read().unwrap().duplicate();
clip.color = ItemTheme::random_near(clip.color, 0.25);
clip
}
pub fn add_new_clip (&self) -> (usize, Arc<RwLock<MidiClip>>) {
let clip = Arc::new(RwLock::new(self.new_clip()));
let index = {
let mut clips = self.clips.write().unwrap();
clips.push(clip.clone());
clips.len().saturating_sub(1)
};
self.clip.store(index, Relaxed);
(index, clip)
}
pub fn delete_clip (&mut self, clip: &MidiClip) -> bool {
let index = self.clips.read().unwrap().iter().position(|x|*x.read().unwrap()==*clip);
if let Some(index) = index {
self.clips.write().unwrap().remove(index);
return true
}
false
}
}

View file

@ -1,29 +0,0 @@
use crate::*;
pub struct PoolView<'a>(pub bool, pub &'a MidiPool);
content!(TuiOut: |self: PoolView<'a>| {
let Self(compact, model) = self;
let MidiPool { clips, .. } = self.1;
//let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||Tui::g(32).into());
let on_bg = |x|x;//Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, x));
let border = |x|x;//Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x);
let iter = | |model.clips().clone().into_iter();
let height = clips.read().unwrap().len() as u16;
Tui::bg(Color::Reset, Fixed::y(height, on_bg(border(Map::new(iter, move|clip: Arc<RwLock<MidiClip>>, i|{
let item_height = 1;
let item_offset = i as u16 * item_height;
let selected = i == model.clip_index();
let MidiClip { ref name, color, length, .. } = *clip.read().unwrap();
let bg = if selected { color.light.rgb } else { color.base.rgb };
let fg = color.lightest.rgb;
let name = if *compact { format!(" {i:>3}") } else { format!(" {i:>3} {name}") };
let length = if *compact { String::default() } else { format!("{length} ") };
Fixed::y(1, map_south(item_offset, item_height, Tui::bg(bg, lay!(
Fill::x(Align::w(Tui::fg(fg, Tui::bold(selected, name)))),
Fill::x(Align::e(Tui::fg(fg, Tui::bold(selected, length)))),
Fill::x(Align::w(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), ""))))),
Fill::x(Align::e(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), ""))))),
))))
})))))
});

View file

@ -1,31 +0,0 @@
use crate::*;
mod port_in; pub use self::port_in::*;
mod port_out; pub use self::port_out::*;
/// Update notes_in array
pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) {
match message {
MidiMessage::NoteOn { key, .. } => { keys[key.as_int() as usize] = true; }
MidiMessage::NoteOff { key, .. } => { keys[key.as_int() as usize] = false; },
_ => {}
}
}
/// Return boxed iterator of MIDI events
pub fn parse_midi_input <'a> (input: MidiIter<'a>) -> Box<dyn Iterator<Item=(usize, LiveEvent<'a>, &'a [u8])> + 'a> {
Box::new(input.map(|RawMidi { time, bytes }|(
time as usize,
LiveEvent::parse(bytes).unwrap(),
bytes
)))
}
/// Add "all notes off" to the start of a buffer.
pub fn all_notes_off (output: &mut [Vec<Vec<u8>>]) {
let mut buf = vec![];
let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() };
let evt = LiveEvent::Midi { channel: 0.into(), message: msg };
evt.write(&mut buf).unwrap();
output[0].push(buf);
}

View file

@ -1,107 +0,0 @@
use crate::*;
/// Trait for thing that may receive MIDI.
pub trait HasMidiIns {
fn midi_ins (&self) -> &Vec<JackMidiIn>;
fn midi_ins_mut (&mut self) -> &mut Vec<JackMidiIn>;
fn has_midi_ins (&self) -> bool {
!self.midi_ins().is_empty()
}
}
pub trait MidiRecordApi: HasClock + HasPlayClip + HasMidiIns {
fn notes_in (&self) -> &Arc<RwLock<[bool;128]>>;
fn recording (&self) -> bool;
fn recording_mut (&mut self) -> &mut bool;
fn toggle_record (&mut self) {
*self.recording_mut() = !self.recording();
}
fn monitoring (&self) -> bool;
fn monitoring_mut (&mut self) -> &mut bool;
fn toggle_monitor (&mut self) {
*self.monitoring_mut() = !self.monitoring();
}
fn overdub (&self) -> bool;
fn overdub_mut (&mut self) -> &mut bool;
fn toggle_overdub (&mut self) {
*self.overdub_mut() = !self.overdub();
}
fn monitor (&mut self, scope: &ProcessScope, midi_buf: &mut Vec<Vec<Vec<u8>>>) {
// For highlighting keys and note repeat
let notes_in = self.notes_in().clone();
let monitoring = self.monitoring();
for input in self.midi_ins_mut().iter() {
for (sample, event, bytes) in parse_midi_input(input.port().iter(scope)) {
if let LiveEvent::Midi { message, .. } = event {
if monitoring {
midi_buf[sample].push(bytes.to_vec());
}
// FIXME: don't lock on every event!
update_keys(&mut notes_in.write().unwrap(), &message);
}
}
}
}
fn record (&mut self, scope: &ProcessScope, midi_buf: &mut Vec<Vec<Vec<u8>>>) {
if self.monitoring() {
self.monitor(scope, midi_buf);
}
if !self.clock().is_rolling() {
return
}
if let Some((started, ref clip)) = self.play_clip().clone() {
self.record_clip(scope, started, clip, midi_buf);
}
if let Some((_start_at, _clip)) = &self.next_clip() {
self.record_next();
}
}
fn record_clip (
&mut self,
scope: &ProcessScope,
started: Moment,
clip: &Option<Arc<RwLock<MidiClip>>>,
_midi_buf: &mut Vec<Vec<Vec<u8>>>
) {
if let Some(clip) = clip {
let sample0 = scope.last_frame_time() as usize;
let start = started.sample.get() as usize;
let _recording = self.recording();
let timebase = self.clock().timebase().clone();
let quant = self.clock().quant.get();
let mut clip = clip.write().unwrap();
let length = clip.length;
for input in self.midi_ins_mut().iter() {
for (sample, event, _bytes) in parse_midi_input(input.port().iter(scope)) {
if let LiveEvent::Midi { message, .. } = event {
clip.record_event({
let sample = (sample0 + sample - start) as f64;
let pulse = timebase.samples_to_pulse(sample);
let quantized = (pulse / quant).round() * quant;
quantized as usize % length
}, message);
}
}
}
}
}
fn record_next (&mut self) {
// TODO switch to next clip and record into it
}
}

View file

@ -1,164 +0,0 @@
use crate::*;
/// Trait for thing that may output MIDI.
pub trait HasMidiOuts {
fn midi_outs (&self) -> &Vec<JackMidiOut>;
fn midi_outs_mut (&mut self) -> &mut Vec<JackMidiOut>;
fn has_midi_outs (&self) -> bool {
!self.midi_outs().is_empty()
}
/// Buffer for serializing a MIDI event. FIXME rename
fn midi_note (&mut self) -> &mut Vec<u8>;
}
pub trait MidiPlaybackApi: HasPlayClip + HasClock + HasMidiOuts {
fn notes_out (&self) -> &Arc<RwLock<[bool;128]>>;
/// Clear the section of the output buffer that we will be using,
/// emitting "all notes off" at start of buffer if requested.
fn clear (
&mut self, scope: &ProcessScope, out: &mut [Vec<Vec<u8>>], reset: bool
) {
let n_frames = (scope.n_frames() as usize).min(out.len());
for frame in &mut out[0..n_frames] {
frame.clear();
}
if reset {
all_notes_off(out);
}
}
/// Output notes from clip to MIDI output ports.
fn play (
&mut self, scope: &ProcessScope, note_buf: &mut Vec<u8>, out: &mut [Vec<Vec<u8>>]
) -> bool {
if !self.clock().is_rolling() {
return false
}
// If a clip is playing, write a chunk of MIDI events from it to the output buffer.
// If no clip is playing, prepare for switchover immediately.
self.play_clip().as_ref().map_or(true, |(started, clip)|{
self.play_chunk(scope, note_buf, out, started, clip)
})
}
/// Handle switchover from current to next playing clip.
fn switchover (
&mut self, scope: &ProcessScope, note_buf: &mut Vec<u8>, out: &mut [Vec<Vec<u8>>]
) {
if !self.clock().is_rolling() {
return
}
let sample0 = scope.last_frame_time() as usize;
//let samples = scope.n_frames() as usize;
if let Some((start_at, clip)) = &self.next_clip() {
let start = start_at.sample.get() as usize;
let sample = self.clock().started.read().unwrap()
.as_ref().unwrap().sample.get() as usize;
// If it's time to switch to the next clip:
if start <= sample0.saturating_sub(sample) {
// Samples elapsed since clip was supposed to start
let _skipped = sample0 - start;
// Switch over to enqueued clip
let started = Moment::from_sample(self.clock().timebase(), start as f64);
// Launch enqueued clip
*self.play_clip_mut() = Some((started, clip.clone()));
// Unset enqueuement (TODO: where to implement looping?)
*self.next_clip_mut() = None;
// Fill in remaining ticks of chunk from next clip.
self.play(scope, note_buf, out);
}
}
}
fn play_chunk (
&self,
scope: &ProcessScope,
note_buf: &mut Vec<u8>,
out: &mut [Vec<Vec<u8>>],
started: &Moment,
clip: &Option<Arc<RwLock<MidiClip>>>
) -> bool {
// First sample to populate. Greater than 0 means that the first
// pulse of the clip falls somewhere in the middle of the chunk.
let sample = (scope.last_frame_time() as usize).saturating_sub(
started.sample.get() as usize +
self.clock().started.read().unwrap().as_ref().unwrap().sample.get() as usize
);
// Iterator that emits sample (index into output buffer at which to write MIDI event)
// paired with pulse (index into clip from which to take the MIDI event) for each
// sample of the output buffer that corresponds to a MIDI pulse.
let pulses = self.clock().timebase().pulses_between_samples(sample, sample + scope.n_frames() as usize);
// Notes active during current chunk.
let notes = &mut self.notes_out().write().unwrap();
let length = clip.as_ref().map_or(0, |p|p.read().unwrap().length);
for (sample, pulse) in pulses {
// If a next clip is enqueued, and we're past the end of the current one,
// break the loop here (FIXME count pulse correctly)
let past_end = if clip.is_some() { pulse >= length } else { true };
if self.next_clip().is_some() && past_end {
return true
}
// If there's a currently playing clip, output notes from it to buffer:
if let Some(ref clip) = clip {
Self::play_pulse(clip, pulse, sample, note_buf, out, notes)
}
}
false
}
fn play_pulse (
clip: &RwLock<MidiClip>,
pulse: usize,
sample: usize,
note_buf: &mut Vec<u8>,
out: &mut [Vec<Vec<u8>>],
notes: &mut [bool;128]
) {
// Source clip from which the MIDI events will be taken.
let clip = clip.read().unwrap();
// Clip with zero length is not processed
if clip.length > 0 {
// Current pulse index in source clip
let pulse = pulse % clip.length;
// Output each MIDI event from clip at appropriate frames of output buffer:
for message in clip.notes[pulse].iter() {
// Clear output buffer for this MIDI event.
note_buf.clear();
// TODO: support MIDI channels other than CH1.
let channel = 0.into();
// Serialize MIDI event into message buffer.
LiveEvent::Midi { channel, message: *message }
.write(note_buf)
.unwrap();
// Append serialized message to output buffer.
out[sample].push(note_buf.clone());
// Update the list of currently held notes.
update_keys(&mut*notes, message);
}
}
}
/// Write a chunk of MIDI data from the output buffer to all assigned output ports.
fn write (&mut self, scope: &ProcessScope, out: &[Vec<Vec<u8>>]) {
let samples = scope.n_frames() as usize;
for port in self.midi_outs_mut().iter_mut() {
Self::write_port(&mut port.port_mut().writer(scope), samples, out)
}
}
/// Write a chunk of MIDI data from the output buffer to an output port.
fn write_port (writer: &mut MidiWriter, samples: usize, out: &[Vec<Vec<u8>>]) {
for (time, events) in out.iter().enumerate().take(samples) {
for bytes in events.iter() {
writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{
panic!("Failed to write MIDI data: {bytes:?}");
});
}
}
}
}

View file

@ -62,18 +62,3 @@ impl NoteRange for MidiRangeModel {
fn note_lo (&self) -> &AtomicUsize { &self.note_lo }
fn note_axis (&self) -> &AtomicUsize { &self.note_axis }
}
#[cfg(test)] #[test] fn test_midi_range () {
let model = MidiRangeModel::from((1, false));
let _ = model.time_len();
let _ = model.time_zoom();
let _ = model.time_lock();
let _ = model.time_start();
let _ = model.time_axis();
let _ = model.time_end();
let _ = model.note_lo();
let _ = model.note_axis();
let _ = model.note_hi();
}

View file

@ -1,5 +1,3 @@
mod clock; pub use self::clock::*;
mod time_moment; pub use self::time_moment::*;
mod time_note; pub use self::time_note::*;
mod time_perf; pub use self::time_perf::*;
@ -9,50 +7,3 @@ mod time_sample_rate; pub use self::time_sample_rate::*;
mod time_timebase; pub use self::time_timebase::*;
mod time_unit; pub use self::time_unit::*;
mod time_usec; pub use self::time_usec::*;
pub(crate) use ::tek_jack::{*, jack::{*, contrib::*}};
pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::*}};
pub(crate) use std::ops::{Add, Sub, Mul, Div, Rem};
pub(crate) use ::tengri::{input::*, dsl::*};
pub use ::atomic_float; pub(crate) use atomic_float::*;
/// Standard result type.
pub(crate) type Usually<T> = Result<T, Box<dyn std::error::Error>>;
/// Standard optional result type.
pub(crate) type Perhaps<T> = Result<Option<T>, Box<dyn std::error::Error>>;
pub trait Gettable<T> {
/// Returns current value
fn get (&self) -> T;
}
pub trait Mutable<T>: Gettable<T> {
/// Sets new value, returns old
fn set (&mut self, value: T) -> T;
}
pub trait InteriorMutable<T>: Gettable<T> {
/// Sets new value, returns old
fn set (&self, value: T) -> T;
}
impl Gettable<bool> for AtomicBool {
fn get (&self) -> bool { self.load(Relaxed) }
}
impl InteriorMutable<bool> for AtomicBool {
fn set (&self, value: bool) -> bool { self.swap(value, Relaxed) }
}
impl Gettable<usize> for AtomicUsize {
fn get (&self) -> usize { self.load(Relaxed) }
}
impl InteriorMutable<usize> for AtomicUsize {
fn set (&self, value: usize) -> usize { self.swap(value, Relaxed) }
}
#[cfg(test)] #[test] fn test_time () -> Usually<()> {
// TODO!
Ok(())
}

View file

@ -1,5 +1,6 @@
use crate::*;
use tengri::tui::PerfModel;
use ::jack::ProcessScope;
pub trait JackPerfModel {
fn update_from_jack_scope (&self, t0: Option<u64>, scope: &ProcessScope);