From a22a793c3188329de88bfe2208eafc7e48a1caa5 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Thu, 1 May 2025 18:03:27 +0300 Subject: [PATCH] refactor into fewer crates, pt.2 --- Cargo.lock | 1 + crates/app/src/api.rs | 411 +++++++++++++++- crates/app/src/lib.rs | 10 +- crates/app/src/model.rs | 392 ++++++++++++++- crates/app/src/view.rs | 416 +++++++++++++++- crates/device/Cargo.toml | 6 +- crates/{engine => device}/src/clock.rs | 0 .../{engine => device}/src/clock/clock_api.rs | 6 +- .../src/clock/clock_model.rs | 8 +- crates/device/src/lib.rs | 38 ++ crates/device/src/plugin.rs | 289 ++++++++++- crates/device/src/plugin/plugin.rs | 275 ----------- crates/device/src/sampler.rs | 11 +- crates/device/src/sampler/sampler_view.rs | 6 +- .../midi/clip.rs => device/src/sequencer.rs} | 39 +- .../src/sequencer/seq_clip.rs} | 0 .../src/sequencer/seq_launch.rs} | 1 - crates/device/src/sequencer/seq_model.rs | 453 ++++++++++++++++++ .../src/sequencer/seq_view.rs} | 0 crates/engine/src/jack.rs | 27 +- crates/engine/src/jack/jack_client.rs | 2 +- crates/engine/src/jack/jack_event.rs | 1 + crates/engine/src/jack/jack_port.rs | 1 + crates/engine/src/lib.rs | 76 +++ crates/engine/src/midi.rs | 76 ++- crates/engine/src/midi/clip/clip_editor.rs | 244 ---------- crates/engine/src/midi/clip/clip_play.rs | 208 -------- crates/engine/src/midi/mode.rs | 18 - crates/engine/src/midi/mode/mode_browse.rs | 40 -- crates/engine/src/midi/mode/mode_length.rs | 133 ----- crates/engine/src/midi/mode/mode_rename.rs | 38 -- crates/engine/src/midi/piano.rs | 2 - crates/engine/src/midi/piano/piano_h.rs | 327 ------------- crates/engine/src/midi/piano/piano_v.rs | 34 -- crates/engine/src/midi/pool.rs | 4 - crates/engine/src/midi/pool/pool_api.rs | 178 ------- crates/engine/src/midi/pool/pool_clips.rs | 26 - crates/engine/src/midi/pool/pool_model.rs | 119 ----- crates/engine/src/midi/pool/pool_view.rs | 29 -- crates/engine/src/midi/port.rs | 31 -- crates/engine/src/midi/port/port_in.rs | 107 ----- crates/engine/src/midi/port/port_out.rs | 164 ------- crates/engine/src/{midi => }/note.rs | 0 .../engine/src/{midi => }/note/note_pitch.rs | 0 .../engine/src/{midi => }/note/note_point.rs | 0 .../engine/src/{midi => }/note/note_range.rs | 15 - crates/engine/src/time.rs | 49 -- crates/engine/src/time/time_perf.rs | 1 + 48 files changed, 2155 insertions(+), 2157 deletions(-) rename crates/{engine => device}/src/clock.rs (100%) rename crates/{engine => device}/src/clock/clock_api.rs (90%) rename crates/{engine => device}/src/clock/clock_model.rs (96%) delete mode 100644 crates/device/src/plugin/plugin.rs rename crates/{engine/src/midi/clip.rs => device/src/sequencer.rs} (53%) rename crates/{engine/src/midi/clip/clip_model.rs => device/src/sequencer/seq_clip.rs} (100%) rename crates/{engine/src/midi/clip/clip_launch.rs => device/src/sequencer/seq_launch.rs} (99%) create mode 100644 crates/device/src/sequencer/seq_model.rs rename crates/{engine/src/midi/clip/clip_view.rs => device/src/sequencer/seq_view.rs} (100%) create mode 100644 crates/engine/src/lib.rs delete mode 100644 crates/engine/src/midi/clip/clip_editor.rs delete mode 100644 crates/engine/src/midi/clip/clip_play.rs delete mode 100644 crates/engine/src/midi/mode.rs delete mode 100644 crates/engine/src/midi/mode/mode_browse.rs delete mode 100644 crates/engine/src/midi/mode/mode_length.rs delete mode 100644 crates/engine/src/midi/mode/mode_rename.rs delete mode 100644 crates/engine/src/midi/piano.rs delete mode 100644 crates/engine/src/midi/piano/piano_h.rs delete mode 100644 crates/engine/src/midi/piano/piano_v.rs delete mode 100644 crates/engine/src/midi/pool.rs delete mode 100644 crates/engine/src/midi/pool/pool_api.rs delete mode 100644 crates/engine/src/midi/pool/pool_clips.rs delete mode 100644 crates/engine/src/midi/pool/pool_model.rs delete mode 100644 crates/engine/src/midi/pool/pool_view.rs delete mode 100644 crates/engine/src/midi/port.rs delete mode 100644 crates/engine/src/midi/port/port_in.rs delete mode 100644 crates/engine/src/midi/port/port_out.rs rename crates/engine/src/{midi => }/note.rs (100%) rename crates/engine/src/{midi => }/note/note_pitch.rs (100%) rename crates/engine/src/{midi => }/note/note_point.rs (100%) rename crates/engine/src/{midi => }/note/note_range.rs (84%) diff --git a/Cargo.lock b/Cargo.lock index a5d8ba38..a07b0cb5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1540,6 +1540,7 @@ dependencies = [ "symphonia", "tek_engine", "tengri", + "uuid", "wavers", ] diff --git a/crates/app/src/api.rs b/crates/app/src/api.rs index c540066f..32b7af04 100644 --- a/crates/app/src/api.rs +++ b/crates/app/src/api.rs @@ -1,4 +1,5 @@ use crate::*; +use std::path::PathBuf; type MaybeClip = Option>>; @@ -89,7 +90,7 @@ impose!([app: Tek] (SceneCommand: ("add" [] Some(Self::Add)) - ("delete" [a: Option] Some(Self::Del(a.flatten().unwrap()))) + ("delete" [a: Option] 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 { 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, 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: |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), + 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] + 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 Command for PoolClipCommand { + fn execute (self, model: &mut T) -> Perhaps { + 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), +} + +atom_command!(ClipRenameCommand: |state: MidiPool| { + ("begin" [] Some(Self::Begin)) + ("cancel" [] Some(Self::Cancel)) + ("confirm" [] Some(Self::Confirm)) + ("set" [n: Arc] 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] 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>>), +} + +handle!(TuiIn: |self: MidiEditor, input|Ok(if let Some(command) = self.keys.command(self, input) { + command.execute(self)?; + Some(true) +} else { + None +})); + +impl Command for MidiEditCommand { + fn execute (self, state: &mut MidiEditor) -> Perhaps { + 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) + } +} + diff --git a/crates/app/src/lib.rs b/crates/app/src/lib.rs index 27b509c2..8ff103aa 100644 --- a/crates/app/src/lib.rs +++ b/crates/app/src/lib.rs @@ -19,11 +19,8 @@ pub type Usually = std::result::Result>; /// Standard optional result type. pub type Perhaps = std::result::Result, Box>; -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::*; diff --git a/crates/app/src/model.rs b/crates/app/src/model.rs index 0ed65783..c10792a4 100644 --- a/crates/app/src/model.rs +++ b/crates/app/src/model.rs @@ -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>>>>, + /// Selected clip + pub clip: AtomicUsize, + /// Mode switch + pub mode: Option, + + 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>|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 { + &self.mode + } + pub fn mode_mut (&mut self) -> &mut Option { + &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>) { + 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), + /// 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, +} + +impl ClipLength { + pub fn _new (pulses: usize, focus: Option) -> 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 { + format!("{}", self.bars()).into() + } + pub fn beats_string (&self) -> Arc { + format!("{}", self.beats()).into() + } + pub fn ticks_string (&self) -> Arc { + format!("{:>02}", self.ticks()).into() + } +} + +pub type ClipPool = Vec>>; + +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>) { + 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, + /// 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!(|self: MidiEditor|&self.size); + +content!(TuiOut: |self: MidiEditor| { + self.autoscroll(); + //self.autozoom(); + self.size.of(&self.mode) +}); + +from!(|clip: &Arc>|MidiEditor = { + let model = Self::from(Some(clip.clone())); + model.redraw(); + model +}); + +from!(|clip: Option>>|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 + '_ { + 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 + '_ { + 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>> { self.mode.clip() } + fn clip_mut (&mut self) -> &mut Option>> { self.mode.clip_mut() } + fn set_clip (&mut self, p: Option<&Arc>>) { self.mode.set_clip(p) } +} + +pub trait HasEditor { + fn editor (&self) -> &Option; + fn editor_mut (&mut self) -> &Option; + 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 { &$e0 } + fn editor_mut (&mut $self) -> &Option { &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 } + } + }; } diff --git a/crates/app/src/view.rs b/crates/app/src/view.rs index ec030508..5414c5bf 100644 --- a/crates/app/src/view.rs +++ b/crates/app/src/view.rs @@ -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>, 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>>, + /// Buffer where the whole clip is rerendered on change + pub buffer: Arc>, + /// Size of actual notes area + pub size: Measure, + /// 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>>) -> 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 { + (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 { + 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 { + 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 { + 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 + '_ { + 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!(|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>> { + &self.clip + } + fn clip_mut (&mut self) -> &mut Option>> { + &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>>) { + *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 for OctaveVertical { + fn content (&self) -> impl Render { + 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), "▟"), + ) + } +} diff --git a/crates/device/Cargo.toml b/crates/device/Cargo.toml index 9f94bfbb..669f1efc 100644 --- a/crates/device/Cargo.toml +++ b/crates/device/Cargo.toml @@ -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 = [] diff --git a/crates/engine/src/clock.rs b/crates/device/src/clock.rs similarity index 100% rename from crates/engine/src/clock.rs rename to crates/device/src/clock.rs diff --git a/crates/engine/src/clock/clock_api.rs b/crates/device/src/clock/clock_api.rs similarity index 90% rename from crates/engine/src/clock/clock_api.rs rename to crates/device/src/clock/clock_api.rs index a52fbccf..a2e5927f 100644 --- a/crates/engine/src/clock/clock_api.rs +++ b/crates/device/src/clock/clock_api.rs @@ -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")))) diff --git a/crates/engine/src/clock/clock_model.rs b/crates/device/src/clock/clock_model.rs similarity index 96% rename from crates/engine/src/clock/clock_model.rs rename to crates/device/src/clock/clock_model.rs index 21eb0fe3..f16e3558 100644 --- a/crates/engine/src/clock/clock_model.rs +++ b/crates/device/src/clock/clock_model.rs @@ -3,7 +3,7 @@ use crate::*; #[derive(Clone, Default)] pub struct Clock { /// JACK transport handle. - pub transport: Arc>, + pub transport: Arc>, /// Global temporal resolution (shared by [Moment] fields) pub timebase: Arc, /// 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), diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index e69de29b..2531f35a 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -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), +} diff --git a/crates/device/src/plugin.rs b/crates/device/src/plugin.rs index 40825854..f3f47d79 100644 --- a/crates/device/src/plugin.rs +++ b/crates/device/src/plugin.rs @@ -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, + pub path: Option>, + pub plugin: Option, + pub selected: usize, + pub mapping: bool, + pub midi_ins: Vec>, + pub midi_outs: Vec>, + pub audio_ins: Vec>, + pub audio_outs: Vec>, +} + +/// 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 { + 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>); +from!(|model: &Arc>| 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 { + //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 { + 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 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) +//}); diff --git a/crates/device/src/plugin/plugin.rs b/crates/device/src/plugin/plugin.rs deleted file mode 100644 index ddd58e65..00000000 --- a/crates/device/src/plugin/plugin.rs +++ /dev/null @@ -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, - pub path: Option>, - pub plugin: Option, - pub selected: usize, - pub mapping: bool, - pub midi_ins: Vec>, - pub midi_outs: Vec>, - pub audio_ins: Vec>, - pub audio_outs: Vec>, -} - -/// 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 { - 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>); -from!(|model: &Arc>| 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 { - //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 { - 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 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) -//}); diff --git a/crates/device/src/sampler.rs b/crates/device/src/sampler.rs index 8ddfc4b6..6423684b 100644 --- a/crates/device/src/sampler.rs +++ b/crates/device/src/sampler.rs @@ -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::*; diff --git a/crates/device/src/sampler/sampler_view.rs b/crates/device/src/sampler/sampler_view.rs index 9a50380b..880d670d 100644 --- a/crates/device/src/sampler/sampler_view.rs +++ b/crates/device/src/sampler/sampler_view.rs @@ -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 + 'a { let note_lo = editor.note_lo().load(Relaxed); let note_pt = editor.note_pos(); diff --git a/crates/engine/src/midi/clip.rs b/crates/device/src/sequencer.rs similarity index 53% rename from crates/engine/src/midi/clip.rs rename to crates/device/src/sequencer.rs index 95295621..8bccc2eb 100644 --- a/crates/engine/src/midi/clip.rs +++ b/crates/device/src/sequencer.rs @@ -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; - fn editor_mut (&mut self) -> &Option; - 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 { &$e0 } - fn editor_mut (&mut $self) -> &Option { &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(); diff --git a/crates/engine/src/midi/clip/clip_model.rs b/crates/device/src/sequencer/seq_clip.rs similarity index 100% rename from crates/engine/src/midi/clip/clip_model.rs rename to crates/device/src/sequencer/seq_clip.rs diff --git a/crates/engine/src/midi/clip/clip_launch.rs b/crates/device/src/sequencer/seq_launch.rs similarity index 99% rename from crates/engine/src/midi/clip/clip_launch.rs rename to crates/device/src/sequencer/seq_launch.rs index ee2f682e..48d30f1c 100644 --- a/crates/engine/src/midi/clip/clip_launch.rs +++ b/crates/device/src/sequencer/seq_launch.rs @@ -87,4 +87,3 @@ pub trait HasPlayClip: HasClock { FieldV(color, "Next:", format!("{} {}", time, name)) } } - diff --git a/crates/device/src/sequencer/seq_model.rs b/crates/device/src/sequencer/seq_model.rs new file mode 100644 index 00000000..810b97b3 --- /dev/null +++ b/crates/device/src/sequencer/seq_model.rs @@ -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>>)>, + /// Start time and next clip + pub next_clip: Option<(Moment, Option>>)>, + /// 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, + /// Play from current sequence to MIDI ports + pub midi_outs: Vec, + /// Notes currently held at input + pub notes_in: Arc>, + /// Notes currently held at output + pub notes_out: Arc>, + /// MIDI output buffer + pub note_buf: Vec, +} + +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, + jack: &Jack, + clock: Option<&Clock>, + clip: Option<&Arc>>, + midi_from: &[PortConnect], + midi_to: &[PortConnect], + ) -> Usually { + 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 { &self.midi_ins } + fn midi_ins_mut (&mut self) -> &mut Vec { &mut self.midi_ins } +} + +impl HasMidiOuts for MidiPlayer { + fn midi_outs (&self) -> &Vec { &self.midi_outs } + fn midi_outs_mut (&mut self) -> &mut Vec { &mut self.midi_outs } + fn midi_note (&mut self) -> &mut Vec { &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, + /// Note chunk buffer + pub &'a mut Vec>>, +); + +/// JACK process callback for a sequencer's clip player/recorder. +impl 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> { + &self.notes_in + } +} + +impl MidiPlaybackApi for MidiPlayer { + fn notes_out (&self) -> &Arc> { + &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>>)> { + &self.play_clip + } + fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { + &mut self.play_clip + } + fn next_clip (&self) -> &Option<(Moment, Option>>)> { + &self.next_clip + } + fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { + &mut self.next_clip + } +} + +pub trait MidiRecordApi: HasClock + HasPlayClip + HasMidiIns { + fn notes_in (&self) -> &Arc>; + + 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>>) { + // 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>>) { + 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>>, + _midi_buf: &mut Vec>> + ) { + 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>; + + /// 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>], 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, out: &mut [Vec>] + ) -> 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, out: &mut [Vec>] + ) { + 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, + out: &mut [Vec>], + started: &Moment, + clip: &Option>> + ) -> 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, + pulse: usize, + sample: usize, + note_buf: &mut Vec, + out: &mut [Vec>], + 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>]) { + 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>]) { + 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:?}"); + }); + } + } + } +} diff --git a/crates/engine/src/midi/clip/clip_view.rs b/crates/device/src/sequencer/seq_view.rs similarity index 100% rename from crates/engine/src/midi/clip/clip_view.rs rename to crates/device/src/sequencer/seq_view.rs diff --git a/crates/engine/src/jack.rs b/crates/engine/src/jack.rs index 149ce0d2..bcc64a04 100644 --- a/crates/engine/src/jack.rs +++ b/crates/engine/src/jack.rs @@ -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 = Result>; + +mod jack_client; pub use self::jack_client::*; +mod jack_event; pub use self::jack_event::*; +mod jack_port; pub use self::jack_port::*; + diff --git a/crates/engine/src/jack/jack_client.rs b/crates/engine/src/jack/jack_client.rs index ad89c7ae..5adb9a3e 100644 --- a/crates/engine/src/jack/jack_client.rs +++ b/crates/engine/src/jack/jack_client.rs @@ -1,5 +1,5 @@ use crate::*; -use ::jack::contrib::*; +use super::*; use self::JackState::*; /// Things that can provide a [jack::Client] reference. diff --git a/crates/engine/src/jack/jack_event.rs b/crates/engine/src/jack/jack_event.rs index 43571f69..9ba6ad0f 100644 --- a/crates/engine/src/jack/jack_event.rs +++ b/crates/engine/src/jack/jack_event.rs @@ -1,4 +1,5 @@ use crate::*; +use super::*; /// Event enum for JACK events. #[derive(Debug, Clone, PartialEq)] pub enum JackEvent { diff --git a/crates/engine/src/jack/jack_port.rs b/crates/engine/src/jack/jack_port.rs index 5c34b748..7347269d 100644 --- a/crates/engine/src/jack/jack_port.rs +++ b/crates/engine/src/jack/jack_port.rs @@ -1,4 +1,5 @@ use crate::*; +use super::*; macro_rules! impl_port { ($Name:ident : $Spec:ident -> $Pair:ident |$jack:ident, $name:ident|$port:expr) => { diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs new file mode 100644 index 00000000..da26acb2 --- /dev/null +++ b/crates/engine/src/lib.rs @@ -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 = std::result::Result>; + +/// Standard optional result type. +pub(crate) type Perhaps = std::result::Result, Box>; + +pub trait Gettable { + /// Returns current value + fn get (&self) -> T; +} + +pub trait Mutable: Gettable { + /// Sets new value, returns old + fn set (&mut self, value: T) -> T; +} + +pub trait InteriorMutable: Gettable { + /// Sets new value, returns old + fn set (&self, value: T) -> T; +} + +impl Gettable for AtomicBool { + fn get (&self) -> bool { self.load(Relaxed) } +} + +impl InteriorMutable for AtomicBool { + fn set (&self, value: bool) -> bool { self.swap(value, Relaxed) } +} + +impl Gettable for AtomicUsize { + fn get (&self) -> usize { self.load(Relaxed) } +} + +impl InteriorMutable 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(); +} diff --git a/crates/engine/src/midi.rs b/crates/engine/src/midi.rs index a617b9b3..b58425c8 100644 --- a/crates/engine/src/midi.rs +++ b/crates/engine/src/midi.rs @@ -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, &'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>]) { + 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; + + fn midi_ins_mut (&mut self) -> &mut Vec; + + 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; + + fn midi_outs_mut (&mut self) -> &mut Vec; + + 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; +} diff --git a/crates/engine/src/midi/clip/clip_editor.rs b/crates/engine/src/midi/clip/clip_editor.rs deleted file mode 100644 index efdd92d6..00000000 --- a/crates/engine/src/midi/clip/clip_editor.rs +++ /dev/null @@ -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, - /// 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!(|self: MidiEditor|&self.size); - -content!(TuiOut: |self: MidiEditor| { - self.autoscroll(); - //self.autozoom(); - self.size.of(&self.mode) -}); - -from!(|clip: &Arc>|MidiEditor = { - let model = Self::from(Some(clip.clone())); - model.redraw(); - model -}); - -from!(|clip: Option>>|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 + '_ { - 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 + '_ { - 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>> { self.mode.clip() } - fn clip_mut (&mut self) -> &mut Option>> { self.mode.clip_mut() } - fn set_clip (&mut self, p: Option<&Arc>>) { 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>>), -} - -handle!(TuiIn: |self: MidiEditor, input|Ok(if let Some(command) = self.keys.command(self, input) { - command.execute(self)?; - Some(true) -} else { - None -})); - -impl Command for MidiEditCommand { - fn execute (self, state: &mut MidiEditor) -> Perhaps { - 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) - } -} diff --git a/crates/engine/src/midi/clip/clip_play.rs b/crates/engine/src/midi/clip/clip_play.rs deleted file mode 100644 index b7df1fe9..00000000 --- a/crates/engine/src/midi/clip/clip_play.rs +++ /dev/null @@ -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>>)>, - /// Start time and next clip - pub next_clip: Option<(Moment, Option>>)>, - /// 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, - /// Play from current sequence to MIDI ports - pub midi_outs: Vec, - /// Notes currently held at input - pub notes_in: Arc>, - /// Notes currently held at output - pub notes_out: Arc>, - /// MIDI output buffer - pub note_buf: Vec, -} - -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, - jack: &Jack, - clock: Option<&Clock>, - clip: Option<&Arc>>, - midi_from: &[PortConnect], - midi_to: &[PortConnect], - ) -> Usually { - 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 { &self.midi_ins } - fn midi_ins_mut (&mut self) -> &mut Vec { &mut self.midi_ins } -} - -impl HasMidiOuts for MidiPlayer { - fn midi_outs (&self) -> &Vec { &self.midi_outs } - fn midi_outs_mut (&mut self) -> &mut Vec { &mut self.midi_outs } - fn midi_note (&mut self) -> &mut Vec { &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, - /// Note chunk buffer - pub &'a mut Vec>>, -); - -/// JACK process callback for a sequencer's clip player/recorder. -impl 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> { - &self.notes_in - } -} - -impl MidiPlaybackApi for MidiPlayer { - fn notes_out (&self) -> &Arc> { - &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>>)> { - &self.play_clip - } - fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { - &mut self.play_clip - } - fn next_clip (&self) -> &Option<(Moment, Option>>)> { - &self.next_clip - } - fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { - &mut self.next_clip - } -} diff --git a/crates/engine/src/midi/mode.rs b/crates/engine/src/midi/mode.rs deleted file mode 100644 index 6fd9c01c..00000000 --- a/crates/engine/src/midi/mode.rs +++ /dev/null @@ -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), - /// Editing the length of a pattern - Length(usize, usize, ClipLengthFocus), - /// Load clip from disk - Import(usize, FileBrowser), - /// Save clip to disk - Export(usize, FileBrowser), -} diff --git a/crates/engine/src/midi/mode/mode_browse.rs b/crates/engine/src/midi/mode/mode_browse.rs deleted file mode 100644 index dbf344aa..00000000 --- a/crates/engine/src/midi/mode/mode_browse.rs +++ /dev/null @@ -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] 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 -}); diff --git a/crates/engine/src/midi/mode/mode_length.rs b/crates/engine/src/midi/mode/mode_length.rs deleted file mode 100644 index 28944fd7..00000000 --- a/crates/engine/src/midi/mode/mode_length.rs +++ /dev/null @@ -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, -} - -impl ClipLength { - fn _new (pulses: usize, focus: Option) -> 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 { - format!("{}", self.bars()).into() - } - fn beats_string (&self) -> Arc { - format!("{}", self.beats()).into() - } - fn ticks_string (&self) -> Arc { - 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 -}); diff --git a/crates/engine/src/midi/mode/mode_rename.rs b/crates/engine/src/midi/mode/mode_rename.rs deleted file mode 100644 index 6e3d8844..00000000 --- a/crates/engine/src/midi/mode/mode_rename.rs +++ /dev/null @@ -1,38 +0,0 @@ -use crate::*; - -#[derive(Clone, Debug, PartialEq)] pub enum ClipRenameCommand { - Begin, - Cancel, - Confirm, - Set(Arc), -} - -atom_command!(ClipRenameCommand: |state: MidiPool| { - ("begin" [] Some(Self::Begin)) - ("cancel" [] Some(Self::Cancel)) - ("confirm" [] Some(Self::Confirm)) - ("set" [n: Arc] 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!() -}); diff --git a/crates/engine/src/midi/piano.rs b/crates/engine/src/midi/piano.rs deleted file mode 100644 index ea7b9152..00000000 --- a/crates/engine/src/midi/piano.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod piano_h; pub use self::piano_h::*; -mod piano_v; pub use self::piano_v::*; diff --git a/crates/engine/src/midi/piano/piano_h.rs b/crates/engine/src/midi/piano/piano_h.rs deleted file mode 100644 index d4650cf4..00000000 --- a/crates/engine/src/midi/piano/piano_h.rs +++ /dev/null @@ -1,327 +0,0 @@ -use crate::*; -use Color::*; - -/// A clip, rendered as a horizontal piano roll. -#[derive(Clone)] -pub struct PianoHorizontal { - pub clip: Option>>, - /// Buffer where the whole clip is rerendered on change - pub buffer: Arc>, - /// Size of actual notes area - pub size: Measure, - /// 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>>) -> 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 { - (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 { - 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 { - 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 { - 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 + '_ { - 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!(|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>> { - &self.clip - } - fn clip_mut (&mut self) -> &mut Option>> { - &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>>) { - *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!(), - } -} diff --git a/crates/engine/src/midi/piano/piano_v.rs b/crates/engine/src/midi/piano/piano_v.rs deleted file mode 100644 index 27bd1d5e..00000000 --- a/crates/engine/src/midi/piano/piano_v.rs +++ /dev/null @@ -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 for OctaveVertical { - fn content (&self) -> impl Render { - 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), "▟"), - ) - } -} diff --git a/crates/engine/src/midi/pool.rs b/crates/engine/src/midi/pool.rs deleted file mode 100644 index a9964d05..00000000 --- a/crates/engine/src/midi/pool.rs +++ /dev/null @@ -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::*; diff --git a/crates/engine/src/midi/pool/pool_api.rs b/crates/engine/src/midi/pool/pool_api.rs deleted file mode 100644 index cfe39287..00000000 --- a/crates/engine/src/midi/pool/pool_api.rs +++ /dev/null @@ -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, 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: |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), - 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] - 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 Command for PoolClipCommand { - fn execute (self, model: &mut T) -> Perhaps { - 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)) - }, - }) - } -} diff --git a/crates/engine/src/midi/pool/pool_clips.rs b/crates/engine/src/midi/pool/pool_clips.rs deleted file mode 100644 index 65a671aa..00000000 --- a/crates/engine/src/midi/pool/pool_clips.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::*; - -pub type ClipPool = Vec>>; - -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>) { - 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() - } - } - } -} diff --git a/crates/engine/src/midi/pool/pool_model.rs b/crates/engine/src/midi/pool/pool_model.rs deleted file mode 100644 index 9ff62d68..00000000 --- a/crates/engine/src/midi/pool/pool_model.rs +++ /dev/null @@ -1,119 +0,0 @@ -use crate::*; - -#[derive(Debug)] -pub struct MidiPool { - pub visible: bool, - /// Collection of clips - pub clips: Arc>>>>, - /// Selected clip - pub clip: AtomicUsize, - /// Mode switch - pub mode: Option, - - 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>|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 { - &self.mode - } - pub fn mode_mut (&mut self) -> &mut Option { - &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>) { - 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 - } -} diff --git a/crates/engine/src/midi/pool/pool_view.rs b/crates/engine/src/midi/pool/pool_view.rs deleted file mode 100644 index 9cfe8b10..00000000 --- a/crates/engine/src/midi/pool/pool_view.rs +++ /dev/null @@ -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>, 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), "◀"))))), - )))) - }))))) -}); diff --git a/crates/engine/src/midi/port.rs b/crates/engine/src/midi/port.rs deleted file mode 100644 index 25977123..00000000 --- a/crates/engine/src/midi/port.rs +++ /dev/null @@ -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, &'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>]) { - 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); -} diff --git a/crates/engine/src/midi/port/port_in.rs b/crates/engine/src/midi/port/port_in.rs deleted file mode 100644 index abbd1390..00000000 --- a/crates/engine/src/midi/port/port_in.rs +++ /dev/null @@ -1,107 +0,0 @@ -use crate::*; - -/// Trait for thing that may receive MIDI. -pub trait HasMidiIns { - fn midi_ins (&self) -> &Vec; - - fn midi_ins_mut (&mut self) -> &mut Vec; - - fn has_midi_ins (&self) -> bool { - !self.midi_ins().is_empty() - } -} - -pub trait MidiRecordApi: HasClock + HasPlayClip + HasMidiIns { - fn notes_in (&self) -> &Arc>; - - 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>>) { - // 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>>) { - 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>>, - _midi_buf: &mut Vec>> - ) { - 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 - } - -} diff --git a/crates/engine/src/midi/port/port_out.rs b/crates/engine/src/midi/port/port_out.rs deleted file mode 100644 index 159d11c6..00000000 --- a/crates/engine/src/midi/port/port_out.rs +++ /dev/null @@ -1,164 +0,0 @@ -use crate::*; - -/// Trait for thing that may output MIDI. -pub trait HasMidiOuts { - fn midi_outs (&self) -> &Vec; - - fn midi_outs_mut (&mut self) -> &mut Vec; - - 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; -} - -pub trait MidiPlaybackApi: HasPlayClip + HasClock + HasMidiOuts { - - fn notes_out (&self) -> &Arc>; - - /// 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>], 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, out: &mut [Vec>] - ) -> 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, out: &mut [Vec>] - ) { - 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, - out: &mut [Vec>], - started: &Moment, - clip: &Option>> - ) -> 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, - pulse: usize, - sample: usize, - note_buf: &mut Vec, - out: &mut [Vec>], - 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>]) { - 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>]) { - 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:?}"); - }); - } - } - } -} diff --git a/crates/engine/src/midi/note.rs b/crates/engine/src/note.rs similarity index 100% rename from crates/engine/src/midi/note.rs rename to crates/engine/src/note.rs diff --git a/crates/engine/src/midi/note/note_pitch.rs b/crates/engine/src/note/note_pitch.rs similarity index 100% rename from crates/engine/src/midi/note/note_pitch.rs rename to crates/engine/src/note/note_pitch.rs diff --git a/crates/engine/src/midi/note/note_point.rs b/crates/engine/src/note/note_point.rs similarity index 100% rename from crates/engine/src/midi/note/note_point.rs rename to crates/engine/src/note/note_point.rs diff --git a/crates/engine/src/midi/note/note_range.rs b/crates/engine/src/note/note_range.rs similarity index 84% rename from crates/engine/src/midi/note/note_range.rs rename to crates/engine/src/note/note_range.rs index 5ce55a73..308a4ae2 100644 --- a/crates/engine/src/midi/note/note_range.rs +++ b/crates/engine/src/note/note_range.rs @@ -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(); -} diff --git a/crates/engine/src/time.rs b/crates/engine/src/time.rs index 2ad81860..9d4e7459 100644 --- a/crates/engine/src/time.rs +++ b/crates/engine/src/time.rs @@ -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 = Result>; -/// Standard optional result type. -pub(crate) type Perhaps = Result, Box>; - -pub trait Gettable { - /// Returns current value - fn get (&self) -> T; -} - -pub trait Mutable: Gettable { - /// Sets new value, returns old - fn set (&mut self, value: T) -> T; -} - -pub trait InteriorMutable: Gettable { - /// Sets new value, returns old - fn set (&self, value: T) -> T; -} - -impl Gettable for AtomicBool { - fn get (&self) -> bool { self.load(Relaxed) } -} - -impl InteriorMutable for AtomicBool { - fn set (&self, value: bool) -> bool { self.swap(value, Relaxed) } -} - -impl Gettable for AtomicUsize { - fn get (&self) -> usize { self.load(Relaxed) } -} - -impl InteriorMutable for AtomicUsize { - fn set (&self, value: usize) -> usize { self.swap(value, Relaxed) } -} - -#[cfg(test)] #[test] fn test_time () -> Usually<()> { - // TODO! - Ok(()) -} diff --git a/crates/engine/src/time/time_perf.rs b/crates/engine/src/time/time_perf.rs index c435fdff..f14bcc66 100644 --- a/crates/engine/src/time/time_perf.rs +++ b/crates/engine/src/time/time_perf.rs @@ -1,5 +1,6 @@ use crate::*; use tengri::tui::PerfModel; +use ::jack::ProcessScope; pub trait JackPerfModel { fn update_from_jack_scope (&self, t0: Option, scope: &ProcessScope);