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/engine/src/clock.rs b/crates/app/src/clock.rs similarity index 100% rename from crates/engine/src/clock.rs rename to crates/app/src/clock.rs diff --git a/crates/engine/src/clock/clock_api.rs b/crates/app/src/clock/clock_api.rs similarity index 100% rename from crates/engine/src/clock/clock_api.rs rename to crates/app/src/clock/clock_api.rs diff --git a/crates/engine/src/clock/clock_model.rs b/crates/app/src/clock/clock_model.rs similarity index 100% rename from crates/engine/src/clock/clock_model.rs rename to crates/app/src/clock/clock_model.rs diff --git a/crates/app/src/editor.rs b/crates/app/src/editor.rs new file mode 100644 index 00000000..62c7b812 --- /dev/null +++ b/crates/app/src/editor.rs @@ -0,0 +1,3 @@ +mod editor_api; +mod editor_model; +mod editor_view; diff --git a/crates/app/src/editor/editor_api.rs b/crates/app/src/editor/editor_api.rs new file mode 100644 index 00000000..1ac85118 --- /dev/null +++ b/crates/app/src/editor/editor_api.rs @@ -0,0 +1,97 @@ +use crate::*; + +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/engine/src/midi/clip/clip_editor.rs b/crates/app/src/editor/editor_model.rs similarity index 59% rename from crates/engine/src/midi/clip/clip_editor.rs rename to crates/app/src/editor/editor_model.rs index efdd92d6..befec6be 100644 --- a/crates/engine/src/midi/clip/clip_editor.rs +++ b/crates/app/src/editor/editor_model.rs @@ -51,39 +51,6 @@ from!(|clip: Option>>|MidiEditor = { 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 @@ -181,64 +148,3 @@ impl MidiViewer for MidiEditor { 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/piano/piano_h.rs b/crates/app/src/editor/editor_view.rs similarity index 92% rename from crates/engine/src/midi/piano/piano_h.rs rename to crates/app/src/editor/editor_view.rs index d4650cf4..67c9dc18 100644 --- a/crates/engine/src/midi/piano/piano_h.rs +++ b/crates/app/src/editor/editor_view.rs @@ -325,3 +325,39 @@ fn to_key (note: usize) -> &'static str { _ => 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/app/src/lib.rs b/crates/app/src/lib.rs index 27b509c2..4ff7b835 100644 --- a/crates/app/src/lib.rs +++ b/crates/app/src/lib.rs @@ -41,6 +41,9 @@ mod audio; pub use self::audio::*; mod model; pub use self::model::*; mod view; pub use self::view::*; +mod pool; +mod editor; + #[cfg(test)] #[test] fn test_model () { let mut tek = Tek::default(); let _ = tek.clip(); diff --git a/crates/app/src/model.rs b/crates/app/src/model.rs index 0ed65783..ab1e0fb9 100644 --- a/crates/app/src/model.rs +++ b/crates/app/src/model.rs @@ -787,11 +787,3 @@ impl HasTracks for Tek { fn tracks (&self) -> &Vec { &self.tracks } fn tracks_mut (&mut self) -> &mut Vec { &mut self.tracks } } - -#[derive(Debug)] -pub enum Device { - Sequencer(MidiPlayer), - Sampler(Sampler), - #[cfg(feature="host")] - Plugin(Plugin), -} diff --git a/crates/engine/src/midi/pool.rs b/crates/app/src/pool.rs similarity index 100% rename from crates/engine/src/midi/pool.rs rename to crates/app/src/pool.rs diff --git a/crates/engine/src/midi/pool/pool_api.rs b/crates/app/src/pool/pool_api.rs similarity index 100% rename from crates/engine/src/midi/pool/pool_api.rs rename to crates/app/src/pool/pool_api.rs diff --git a/crates/engine/src/midi/pool/pool_clips.rs b/crates/app/src/pool/pool_clips.rs similarity index 100% rename from crates/engine/src/midi/pool/pool_clips.rs rename to crates/app/src/pool/pool_clips.rs diff --git a/crates/engine/src/midi/mode.rs b/crates/app/src/pool/pool_mode.rs similarity index 99% rename from crates/engine/src/midi/mode.rs rename to crates/app/src/pool/pool_mode.rs index 6fd9c01c..0b3bd49f 100644 --- a/crates/engine/src/midi/mode.rs +++ b/crates/app/src/pool/pool_mode.rs @@ -16,3 +16,4 @@ pub enum PoolMode { /// Save clip to disk Export(usize, FileBrowser), } + diff --git a/crates/engine/src/midi/mode/mode_browse.rs b/crates/app/src/pool/pool_mode/mode_browse.rs similarity index 100% rename from crates/engine/src/midi/mode/mode_browse.rs rename to crates/app/src/pool/pool_mode/mode_browse.rs diff --git a/crates/engine/src/midi/mode/mode_length.rs b/crates/app/src/pool/pool_mode/mode_length.rs similarity index 100% rename from crates/engine/src/midi/mode/mode_length.rs rename to crates/app/src/pool/pool_mode/mode_length.rs diff --git a/crates/engine/src/midi/mode/mode_rename.rs b/crates/app/src/pool/pool_mode/mode_rename.rs similarity index 100% rename from crates/engine/src/midi/mode/mode_rename.rs rename to crates/app/src/pool/pool_mode/mode_rename.rs diff --git a/crates/engine/src/midi/pool/pool_model.rs b/crates/app/src/pool/pool_model.rs similarity index 100% rename from crates/engine/src/midi/pool/pool_model.rs rename to crates/app/src/pool/pool_model.rs diff --git a/crates/engine/src/midi/pool/pool_view.rs b/crates/app/src/pool/pool_view.rs similarity index 100% rename from crates/engine/src/midi/pool/pool_view.rs rename to crates/app/src/pool/pool_view.rs diff --git a/crates/device/Cargo.toml b/crates/device/Cargo.toml index 9f94bfbb..ce041180 100644 --- a/crates/device/Cargo.toml +++ b/crates/device/Cargo.toml @@ -6,6 +6,7 @@ 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 } @@ -14,4 +15,4 @@ wavers = { workspace = true, optional = true } default = [ "sequencer", "sampler" ] lv2 = [ "livi" ] sampler = [ "symphonia", "wavers" ] -sequencer = [] +sequencer = [ "uuid" ] diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index e69de29b..4bae13d6 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -0,0 +1,35 @@ +#![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 = "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/engine/src/midi/clip.rs b/crates/device/src/sequencer.rs similarity index 91% rename from crates/engine/src/midi/clip.rs rename to crates/device/src/sequencer.rs index 95295621..eeec08e9 100644 --- a/crates/engine/src/midi/clip.rs +++ b/crates/device/src/sequencer.rs @@ -1,8 +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::*; + +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::*; pub trait HasEditor { fn editor (&self) -> &Option; 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..004db566 --- /dev/null +++ b/crates/device/src/sequencer/seq_model.rs @@ -0,0 +1,452 @@ +//! 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 + } +} + +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..390d4cb7 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::*; +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..a052ac7b --- /dev/null +++ b/crates/engine/src/lib.rs @@ -0,0 +1,61 @@ +#![feature(type_alias_impl_trait)] + +mod jack; pub use self::jack::*; +mod time; pub use self::time::*; +mod note; pub use self::note::*; +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(()) +} diff --git a/crates/engine/src/midi.rs b/crates/engine/src/midi.rs index a617b9b3..fa9fa4f2 100644 --- a/crates/engine/src/midi.rs +++ b/crates/engine/src/midi.rs @@ -1,21 +1,60 @@ -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(crate) use ::midly::{ + 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_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/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_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/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 100% rename from crates/engine/src/midi/note/note_range.rs rename to crates/engine/src/note/note_range.rs 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);