diff --git a/bin/cli_groovebox.rs b/bin/cli_groovebox.rs index ab516e84..facd66c9 100644 --- a/bin/cli_groovebox.rs +++ b/bin/cli_groovebox.rs @@ -37,15 +37,13 @@ pub struct GrooveboxCli { impl GrooveboxCli { fn run (&self) -> Usually<()> { Tui::run(JackConnection::new("tek_groovebox")?.activate_with(|jack|{ - let app = tek::GrooveboxTui::try_from(jack)?; - jack.read().unwrap().client().connect_ports(&app.player.midi_outs[0], &app.sampler.midi_in)?; - jack.connect_midi_from(&app.player.midi_ins[0], &self.midi_from)?; - jack.connect_midi_from(&app.sampler.midi_in, &self.midi_from)?; - jack.connect_midi_to(&app.player.midi_outs[0], &self.midi_to)?; - jack.connect_audio_from(&app.sampler.audio_ins[0], &self.l_from)?; - jack.connect_audio_from(&app.sampler.audio_ins[1], &self.r_from)?; - jack.connect_audio_to(&app.sampler.audio_outs[0], &self.l_to)?; - jack.connect_audio_to(&app.sampler.audio_outs[1], &self.r_to)?; + let app = tek::GrooveboxTui::new( + jack, + &self.midi_from.as_slice(), + &self.midi_to.as_slice(), + &[&self.l_from.as_slice(), &self.r_from.as_slice()], + &[&self.l_to.as_slice(), &self.r_to.as_slice()], + )?; if self.sync_lead { jack.read().unwrap().client().register_timebase_callback(false, |mut state|{ app.clock().playhead.update_from_sample(state.position.frame() as f64); diff --git a/bin/cli_sampler.rs b/bin/cli_sampler.rs index b2305de6..262e40c5 100644 --- a/bin/cli_sampler.rs +++ b/bin/cli_sampler.rs @@ -5,12 +5,41 @@ pub fn main () -> Usually<()> { SamplerCli::parse().run() } #[arg(short, long)] name: Option, /// Path to plugin #[arg(short, long)] path: Option, + /// MIDI outs to connect to MIDI input + #[arg(short='i', long)] + midi_from: Vec, + /// Audio outs to connect to left input + #[arg(short='l', long)] + l_from: Vec, + /// Audio outs to connect to right input + #[arg(short='r', long)] + r_from: Vec, + /// Audio ins to connect from left output + #[arg(short='L', long)] + l_to: Vec, + /// Audio ins to connect from right output + #[arg(short='R', long)] + r_to: Vec, } impl SamplerCli { fn run (&self) -> Usually<()> { - Tui::run(JackConnection::new("tek_sampler")?.activate_with(|x|{ - let sampler = tek::SamplerTui::try_from(x)?; - Ok(sampler) + Tui::run(JackConnection::new("tek_sampler")?.activate_with(|jack|{ + Ok(tek::SamplerTui { + cursor: (0, 0), + editing: None, + mode: None, + size: Measure::new(), + note_lo: 36.into(), + note_pt: 36.into(), + color: ItemPalette::from(Color::Rgb(64, 128, 32)), + state: Sampler::new( + jack, + &"sampler", + &self.midi_from.as_slice(), + &[&self.l_from.as_slice(), &self.r_from.as_slice()], + &[&self.l_to.as_slice(), &self.r_to.as_slice()], + )?, + }) })?)?; Ok(()) } diff --git a/src/groovebox.rs b/src/groovebox.rs index d3659008..ce802986 100644 --- a/src/groovebox.rs +++ b/src/groovebox.rs @@ -20,30 +20,50 @@ pub struct GrooveboxTui { pub midi_buf: Vec>>, pub perf: PerfModel, } +impl GrooveboxTui { + pub fn new ( + jack: &Arc>, + midi_from: &[impl AsRef], + midi_to: &[impl AsRef], + audio_from: &[&[impl AsRef];2], + audio_to: &[&[impl AsRef];2], + ) -> Usually { + let sampler = crate::sampler::Sampler::new( + jack, &"sampler", midi_from, audio_from, audio_to + )?; + let mut player = crate::midi::MidiPlayer::new( + jack, &"sequencer", &midi_from, &midi_to + )?; + jack.read().unwrap().client().connect_ports(&player.midi_outs[0], &sampler.midi_in)?; + //jack.connect_midi_from(&player.midi_ins[0], &midi_from)?; + //jack.connect_midi_from(&sampler.midi_in, &midi_from)?; + //jack.connect_midi_to(&player.midi_outs[0], &midi_to)?; + //jack.connect_audio_from(&sampler.audio_ins[0], &audio_from[0])?; + //jack.connect_audio_from(&sampler.audio_ins[1], &audio_from[1])?; + //jack.connect_audio_to(&sampler.audio_outs[0], &audio_to[0])?; + //jack.connect_audio_to(&sampler.audio_outs[1], &audio_to[1])?; -from_jack!(|jack|GrooveboxTui { - let mut player = MidiPlayer::new(jack, "sequencer")?; - let phrase = Arc::new(RwLock::new(MidiClip::new( - "New", true, 4 * player.clock.timebase.ppq.get() as usize, - None, Some(ItemColor::random().into()) - ))); - player.play_phrase = Some((Moment::zero(&player.clock.timebase), Some(phrase.clone()))); - let pool = crate::pool::PoolModel::from(&phrase); - let editor = crate::midi::MidiEditorModel::from(&phrase); - let sampler = crate::sampler::Sampler::new(jack, "sampler")?; - Self { - _jack: jack.clone(), - player, - pool, - editor, - sampler, - size: Measure::new(), - midi_buf: vec![vec![];65536], - note_buf: vec![], - perf: PerfModel::default(), - status: true, + let phrase = Arc::new(RwLock::new(MidiClip::new( + "New", true, 4 * player.clock.timebase.ppq.get() as usize, + None, Some(ItemColor::random().into()) + ))); + player.play_phrase = Some((Moment::zero(&player.clock.timebase), Some(phrase.clone()))); + let pool = crate::pool::PoolModel::from(&phrase); + let editor = crate::midi::MidiEditorModel::from(&phrase); + Ok(Self { + _jack: jack.clone(), + player, + pool, + editor, + sampler, + size: Measure::new(), + midi_buf: vec![vec![];65536], + note_buf: vec![], + perf: PerfModel::default(), + status: true, + }) } -}); +} has_clock!(|self: GrooveboxTui|self.player.clock()); audio!(|self: GrooveboxTui, client, scope|{ let t0 = self.perf.get_t0(); diff --git a/src/jack.rs b/src/jack.rs index 23850b1d..b0862784 100644 --- a/src/jack.rs +++ b/src/jack.rs @@ -45,13 +45,27 @@ pub trait Audio: Send + Sync { } } -pub type DynamicAsyncClient = AsyncClient; +/// This is a boxed realtime callback. +pub type BoxedAudioHandler = Box Control + Send>; -pub type DynamicAudioHandler = ClosureProcessHandler<(), BoxedAudioHandler>; +/// This is the notification handler wrapper for a boxed realtime callback. +pub type DynamicAudioHandler = ClosureProcessHandler<(), BoxedAudioHandler>; -pub type BoxedAudioHandler = Box Control + Send>; +/// This is a boxed [JackEvent] callback. +pub type BoxedJackEventHandler = Box; -/// Wraps [Client] or [DynamicAsyncClient] in place. +/// This is the notification handler wrapper for a boxed [JackEvent] callback. +pub type DynamicNotifications = Notifications; + +/// This is a running JACK [AsyncClient] with maximum type erasure. +/// It has one [Box] containing a function that handles [JackEvent]s, +/// and another [Box] containing a function that handles realtime IO, +/// and that's all it knows about them. +pub type DynamicAsyncClient = AsyncClient; + +/// This is a connection which may be `Inactive`, `Activating`, or `Active`. +/// In the `Active` and `Inactive` states, its `client` method returns a +/// [Client] which you can use to talk to the JACK API. #[derive(Debug)] pub enum JackConnection { /// Before activation. @@ -81,8 +95,11 @@ impl JackConnection { Self::Active(ref client) => client.as_client(), } } - /// Bind a process callback to a `JackConnection::Inactive`, - /// consuming it and returning a `JackConnection::Active`. + /// Activate a connection with an application. + /// + /// Consume a `JackConnection::Inactive`, + /// binding a process callback and + /// returning a `JackConnection::Active`. /// /// Needs work. Strange ownership situation between the callback /// and the host object. @@ -102,33 +119,48 @@ impl JackConnection { *state.write().unwrap() = Self::Active(client.activate_async(events, frames)?); Ok(state) } - /// Consume a `JackConnection::Inactive`, activate a client, - /// initialize an app around it with the `init` callback, - /// then return the result of it all. + /// Activate a connection with an application. + /// + /// * Wrap a [JackConnection::Inactive] into [Arc>]. + /// * Pass it to the `init` callback + /// * This allows user code to connect to JACK + /// * While user code retains clone of the + /// [Arc>] that is + /// passed to `init`, the audio engine is running. pub fn activate_with ( self, init: impl FnOnce(&Arc>)->Usually ) -> Usually>> { - let client = Arc::new(RwLock::new(self)); - let target = Arc::new(RwLock::new(init(&client)?)); - let event = Box::new(move|_|{/*TODO*/}) as Box; - let events = Notifications(event); - let frame = Box::new({ - let target = target.clone(); - move|c: &_, s: &_|if let Ok(mut target) = target.write() { - target.process(c, s) - } else { - Control::Quit - } - }); - let frames = ClosureProcessHandler::new(frame as BoxedAudioHandler); - let mut buffer = Self::Activating; - std::mem::swap(&mut*client.write().unwrap(), &mut buffer); - *client.write().unwrap() = Self::Active(Client::from(buffer).activate_async( - events, - frames, + // Wrap self for multiple ownership. + let connection = Arc::new(RwLock::new(self)); + // Run init callback. Return value is target. Target must retain clone of `connection`. + let target = Arc::new(RwLock::new(init(&connection)?)); + // Swap the `client` from the `JackConnection::Inactive` + // for a `JackConnection::Activating`. + let mut client = Self::Activating; + std::mem::swap(&mut*connection.write().unwrap(), &mut client); + // Replace the `JackConnection::Activating` with a + // `JackConnection::Active` wrapping the [AsyncClient] + // returned by the activation. + *connection.write().unwrap() = Self::Active(Client::from(client).activate_async( + // This is the misc notifications handler. It's a struct that wraps a [Box] + // which performs type erasure on a callback that takes [JackEvent], which is + // one of the available misc notifications. + Notifications(Box::new(move|_|{/*TODO*/}) as BoxedJackEventHandler), + // This is the main processing handler. It's a struct that wraps a [Box] + // which performs type erasure on a callback that takes [Client] and [ProcessScope] + // and passes them down to the `target`'s `process` callback, which in turn + // implements audio and MIDI input and output on a realtime basis. + ClosureProcessHandler::new(Box::new({ + let target = target.clone(); + move|c: &_, s: &_|if let Ok(mut target) = target.write() { + target.process(c, s) + } else { + Control::Quit + } + }) as BoxedAudioHandler), )?); Ok(target) } @@ -140,73 +172,69 @@ impl JackConnection { } } +/// This is a utility trait for things that may register or connect [Port]s. +/// It contains shorthand methods to this purpose. It's implemented for +/// `Arc>` for terse port registration in the +/// `init` callback of [JackClient::activate_with]. pub trait RegisterPort { - fn midi_in (&self, name: &str) -> Usually>; - fn midi_out (&self, name: &str) -> Usually>; - fn audio_in (&self, name: &str) -> Usually>; - fn audio_out (&self, name: &str) -> Usually>; - fn connect_midi_from (&self, my_input: &Port, ports: &[String]) -> Usually<()>; - fn connect_midi_to (&self, my_output: &Port, ports: &[String]) -> Usually<()>; - fn connect_audio_from (&self, my_input: &Port, ports: &[String]) -> Usually<()>; - fn connect_audio_to (&self, my_output: &Port, ports: &[String]) -> Usually<()>; + fn midi_in (&self, name: impl AsRef, connect: &[impl AsRef]) -> Usually>; + fn midi_out (&self, name: impl AsRef, connect: &[impl AsRef]) -> Usually>; + fn audio_in (&self, name: impl AsRef, connect: &[impl AsRef]) -> Usually>; + fn audio_out (&self, name: impl AsRef, connect: &[impl AsRef]) -> Usually>; } impl RegisterPort for Arc> { - fn midi_in (&self, name: &str) -> Usually> { - Ok(self.read().unwrap().client().register_port(name, MidiIn::default())?) - } - fn midi_out (&self, name: &str) -> Usually> { - Ok(self.read().unwrap().client().register_port(name, MidiOut::default())?) - } - fn audio_out (&self, name: &str) -> Usually> { - Ok(self.read().unwrap().client().register_port(name, AudioOut::default())?) - } - fn audio_in (&self, name: &str) -> Usually> { - Ok(self.read().unwrap().client().register_port(name, AudioIn::default())?) - } - fn connect_midi_from (&self, input: &Port, ports: &[String]) -> Usually<()> { + fn midi_in (&self, name: impl AsRef, connect: &[impl AsRef]) -> Usually> { let jack = self.read().unwrap(); - for port in ports.iter() { - if let Some(port) = jack.port_by_name(port).as_ref() { - jack.client().connect_ports(port, input)?; + let input = jack.client().register_port(name.as_ref(), MidiIn::default())?; + for port in connect.iter() { + let port = port.as_ref(); + if let Some(output) = jack.port_by_name(port).as_ref() { + jack.client().connect_ports(output, &input)?; } else { panic!("Missing MIDI output: {port}. Use jack_lsp to list all port names."); } } - Ok(()) + Ok(input) } - fn connect_midi_to (&self, output: &Port, ports: &[String]) -> Usually<()> { + fn midi_out (&self, name: impl AsRef, connect: &[impl AsRef]) -> Usually> { let jack = self.read().unwrap(); - for port in ports.iter() { - if let Some(port) = jack.port_by_name(port).as_ref() { - jack.client().connect_ports(output, port)?; + let output = jack.client().register_port(name.as_ref(), MidiOut::default())?; + for port in connect.iter() { + let port = port.as_ref(); + if let Some(input) = jack.port_by_name(port).as_ref() { + jack.client().connect_ports(&output, input)?; } else { panic!("Missing MIDI input: {port}. Use jack_lsp to list all port names."); } } - Ok(()) + Ok(output) } - fn connect_audio_from (&self, input: &Port, ports: &[String]) -> Usually<()> { + fn audio_in (&self, name: impl AsRef, connect: &[impl AsRef]) -> Usually> { let jack = self.read().unwrap(); - for port in ports.iter() { - if let Some(port) = jack.port_by_name(port).as_ref() { - jack.client().connect_ports(port, input)?; + let input = jack.client().register_port(name.as_ref(), AudioIn::default())?; + for port in connect.iter() { + let port = port.as_ref(); + if let Some(output) = jack.port_by_name(port).as_ref() { + jack.client().connect_ports(output, &input)?; } else { panic!("Missing MIDI output: {port}. Use jack_lsp to list all port names."); } } - Ok(()) + Ok(input) } - fn connect_audio_to (&self, output: &Port, ports: &[String]) -> Usually<()> { + fn audio_out (&self, name: impl AsRef, connect: &[impl AsRef]) -> Usually> { let jack = self.read().unwrap(); - for port in ports.iter() { - if let Some(port) = jack.port_by_name(port).as_ref() { - jack.client().connect_ports(output, port)?; + let output = jack.client().register_port(name.as_ref(), AudioOut::default())?; + for port in connect.iter() { + let port = port.as_ref(); + if let Some(input) = jack.port_by_name(port).as_ref() { + jack.client().connect_ports(&output, input)?; } else { panic!("Missing MIDI input: {port}. Use jack_lsp to list all port names."); } } - Ok(()) + Ok(output) } } @@ -225,10 +253,6 @@ pub enum JackEvent { XRun, } -/// Notification handler used by the [Jack] factory -/// when constructing [JackDevice]s. -pub type DynamicNotifications = Notifications>; - /// Generic notification handler that emits [JackEvent] pub struct Notifications(pub T); diff --git a/src/lib.rs b/src/lib.rs index 5f05cfaf..31bf316b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ pub mod time; pub(crate) use self::time::*; pub use self::time::HasClock; pub mod space; pub(crate) use self::space::*; +pub use self::space::Measure; pub mod tui; pub(crate) use self::tui::*; pub use tui::*; diff --git a/src/midi.rs b/src/midi.rs index f9c483aa..624235ce 100644 --- a/src/midi.rs +++ b/src/midi.rs @@ -127,7 +127,13 @@ pub struct MidiPlayer { pub note_buf: Vec, } impl MidiPlayer { - pub fn new (jack: &Arc>, name: &str) -> Usually { + pub fn new ( + jack: &Arc>, + name: impl AsRef, + midi_from: &[impl AsRef], + midi_to: &[impl AsRef], + ) -> Usually { + let name = name.as_ref(); Ok(Self { clock: ClockModel::from(jack), play_phrase: None, @@ -138,11 +144,11 @@ impl MidiPlayer { notes_in: RwLock::new([false;128]).into(), midi_ins: vec![ - jack.midi_in(&format!("M/{name}"))?, + jack.midi_in(&format!("M/{name}"), midi_from)?, ], midi_outs: vec![ - jack.midi_out(&format!("{name}/M"))?, + jack.midi_out(&format!("{name}/M"), midi_to)?, ], notes_out: RwLock::new([false;128]).into(), reset: true, diff --git a/src/sampler.rs b/src/sampler.rs index 363bba41..d11aa888 100644 --- a/src/sampler.rs +++ b/src/sampler.rs @@ -49,17 +49,24 @@ pub struct Sampler { pub output_gain: f32 } impl Sampler { - pub fn new (jack: &Arc>, name: &str) -> Usually { + pub fn new ( + jack: &Arc>, + name: impl AsRef, + midi_from: &[impl AsRef], + audio_from: &[&[impl AsRef];2], + audio_to: &[&[impl AsRef];2], + ) -> Usually { + let name = name.as_ref(); Ok(Self { - midi_in: jack.midi_in(&format!("M/{name}"))?, - audio_ins: vec![ - jack.audio_in(&format!("L/{name}"))?, - jack.audio_in(&format!("R/{name}"))? + midi_in: jack.midi_in(&format!("M/{name}"), midi_from)?, + audio_ins: vec![ + jack.audio_in(&format!("L/{name}"), audio_from[0])?, + jack.audio_in(&format!("R/{name}"), audio_from[1])? ], input_meter: vec![0.0;2], - audio_outs: vec![ - jack.audio_out(&format!("{name}/L"))?, - jack.audio_out(&format!("{name}/R"))?, + audio_outs: vec![ + jack.audio_out(&format!("{name}/L"), audio_to[0])?, + jack.audio_out(&format!("{name}/R"), audio_to[1])?, ], jack: jack.clone(), name: name.into(), diff --git a/src/sampler/sampler_tui.rs b/src/sampler/sampler_tui.rs index 1bd13a77..386910c6 100644 --- a/src/sampler/sampler_tui.rs +++ b/src/sampler/sampler_tui.rs @@ -11,7 +11,7 @@ pub struct SamplerTui { /// Lowest note displayed pub note_lo: AtomicUsize, pub note_pt: AtomicUsize, - color: ItemPalette + pub color: ItemPalette } impl SamplerTui { @@ -31,18 +31,18 @@ impl SamplerTui { } } -from_jack!(|jack|SamplerTui{ - Self { - cursor: (0, 0), - editing: None, - mode: None, - size: Measure::new(), - note_lo: 36.into(), - note_pt: 36.into(), - color: ItemPalette::from(Color::Rgb(64, 128, 32)), - state: Sampler::new(jack, "sampler")?, - } -}); +//from_jack!(|jack|SamplerTui{ + //Self { + //cursor: (0, 0), + //editing: None, + //mode: None, + //size: Measure::new(), + //note_lo: 36.into(), + //note_pt: 36.into(), + //color: ItemPalette::from(Color::Rgb(64, 128, 32)), + //state: Sampler::new(jack, &"sampler", &[], &[&[], &[]], &[&[], &[]])?, + //} +//}); render!(|self: SamplerTui|{ let keys_width = 5; diff --git a/src/space.rs b/src/space.rs index 5629081b..cfc5a0d2 100644 --- a/src/space.rs +++ b/src/space.rs @@ -15,6 +15,7 @@ pub(crate) mod fixed; pub(crate) use fixed::*; pub(crate) mod inset_outset; pub(crate) use inset_outset::*; pub(crate) mod layers; pub(crate) use layers::*; pub(crate) mod measure; pub(crate) use measure::*; +pub use self::measure::Measure; pub(crate) mod min_max; pub(crate) use min_max::*; pub(crate) mod push_pull; pub(crate) use push_pull::*; pub(crate) mod scroll;