diff --git a/Cargo.lock b/Cargo.lock index 2e049b14..999db3cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1408,17 +1408,15 @@ dependencies = [ name = "tek" version = "0.2.0" dependencies = [ - "atomic_float", "backtrace", "clap", - "jack", "livi", - "midly", "once_cell", "palette", - "quanta", "rand", "symphonia", + "tek_jack", + "tek_midi", "tek_tui", "toml", "uuid", @@ -1438,6 +1436,24 @@ dependencies = [ name = "tek_input" version = "0.2.0" +[[package]] +name = "tek_jack" +version = "0.2.0" +dependencies = [ + "jack", +] + +[[package]] +name = "tek_midi" +version = "0.2.0" +dependencies = [ + "jack", + "midly", + "tek_jack", + "tek_time", + "tek_tui", +] + [[package]] name = "tek_output" version = "0.2.0" @@ -1445,6 +1461,16 @@ dependencies = [ "tek_edn", ] +[[package]] +name = "tek_time" +version = "0.2.0" +dependencies = [ + "atomic_float", + "quanta", + "tek_jack", + "tek_tui", +] + [[package]] name = "tek_tui" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index c1d320e5..f4e5c62a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,17 +4,15 @@ edition = "2021" version = "0.2.0" [dependencies] -tek_tui = { path = "./tui" } +tek_tui = { path = "./tui" } +tek_jack = { path = "./jack" } +tek_midi = { path = "./midi" } -atomic_float = "1.0.0" backtrace = "0.3.72" clap = { version = "4.5.4", features = [ "derive" ] } -jack = { path = "./rust-jack" } livi = "0.7.4" -midly = "0.5" once_cell = "1.19.0" palette = { version = "0.7.6", features = [ "random" ] } -quanta = "0.12.3" rand = "0.8.5" symphonia = { version = "0.5.4", features = [ "all" ] } toml = "0.8.12" diff --git a/Justfile b/Justfile index dcf4c382..07a23259 100644 --- a/Justfile +++ b/Justfile @@ -130,3 +130,6 @@ test: cd edn && cargo test && cd .. cd tui && cargo test && cd .. cargo test + +cloc: + for src in {edn,input,jack,midi,output,time,tui,.}; do echo $src; cloc $src/src; done diff --git a/bin/tek.rs b/bin/tek.rs index 07328f16..c0bd2fb1 100644 --- a/bin/tek.rs +++ b/bin/tek.rs @@ -1,12 +1,5 @@ -#[allow(unused_imports)] use std::sync::Arc; +use tek::*; #[allow(unused_imports)] use clap::{self, Parser, Subcommand, ValueEnum}; -#[allow(unused_imports)] use tek::{ - *, - jack::*, - tek_input::*, - tek_output::*, - tek_tui::{*, ratatui::prelude::Color} -}; #[derive(Debug, Parser)] #[command(version, about, long_about = None)] @@ -105,73 +98,131 @@ pub fn main () -> Usually<()> { let jack = JackConnection::new(name)?; let engine = Tui::new()?; Ok(match cli.mode { - Clock => - engine.run(&jack.activate_with(|jack|Ok( - crate::TransportTui::new(jack)? - ))?)?, - Sequencer { midi_from, midi_to, .. } => - engine.run(&jack.activate_with(|jack|Ok({ - let mut app = crate::Sequencer::new(jack)?; - let jack = jack.read().unwrap(); - let midi_in = jack.register_port("i", MidiIn::default())?; - let midi_out = jack.register_port("o", MidiOut::default())?; - connect_from(&jack, &midi_in, &midi_from)?; - connect_to(&jack, &midi_out, &midi_to)?; - app.player.midi_ins.push(midi_in); - app.player.midi_outs.push(midi_out); - app - }))?)?, - Sampler { midi_from, l_from, r_from, l_to, r_to, .. } => - engine.run(&jack.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: tek::Sampler::new( - jack, &"sampler", - &midi_from, - &[&l_from, &r_from], - &[&l_to, &r_to], - )?, - } - ))?)?, - Groovebox { midi_from, midi_to, l_from, r_from, l_to, r_to, .. } => - engine.run(&jack.activate_with(|jack|Ok({ - let app = tek::Groovebox::new( - jack, - &midi_from, &midi_to, + + Clock => engine.run(&jack.activate_with(|jack|Ok(crate::TransportTui { + clock: crate::Clock::from(jack), + jack: jack.clone() + }))?)?, + + Sequencer { + midi_from, + midi_to, .. + } => engine.run(&jack.activate_with(|jack|Ok({ + let clock = crate::Clock::from(jack); + let phrase = Arc::new(RwLock::new(crate::MidiClip::new( + "Clip", true, 4 * clock.timebase.ppq.get() as usize, + None, Some(ItemColor::random().into()) + ))); + let midi_in = jack.read().unwrap().register_port("i", MidiIn::default())?; + connect_from(&jack, &midi_in, &midi_from)?; + let midi_out = jack.read().unwrap().register_port("o", MidiOut::default())?; + connect_to(&jack, &midi_out, &midi_to)?; + crate::Sequencer { + _jack: jack.clone(), + pool: PoolModel::from(&phrase), + editor: crate::MidiEditor::from(&phrase), + player: crate::MidiPlayer::new(&clock, &phrase, &[midi_in], &[midi_out])?, + compact: true, + transport: true, + selectors: true, + size: Measure::new(), + midi_buf: vec![vec![];65536], + note_buf: vec![], + perf: PerfModel::default(), + status: true, + clock, + } + }))?)?, + + Sampler { + midi_from, l_from, r_from, l_to, r_to, .. + } => engine.run(&jack.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: tek::Sampler::new(jack, &"sampler", + &midi_from, &[&l_from, &r_from], - &[&l_to, &r_to], - )?; - if let Some(bpm) = cli.bpm { - app.clock().timebase.bpm.set(bpm); - } - if cli.sync_lead { - jack.read().unwrap().client().register_timebase_callback(false, |mut state|{ - app.clock().playhead.update_from_sample(state.position.frame() as f64); - state.position.bbt = Some(app.clock().bbt()); - state.position - })? - } else if cli.sync_follow { - jack.read().unwrap().client().register_timebase_callback(false, |state|{ - app.clock().playhead.update_from_sample(state.position.frame() as f64); - state.position - })? - } - app - }))?)?, - Arranger { scenes, tracks, track_width, midi_from, midi_to, .. } => - engine.run(&jack.activate_with(|jack|Ok({ - let mut app = crate::Arranger::try_from(jack)?; - app.color = ItemPalette::random(); - app.tracks_add(tracks, track_width, midi_from.as_slice(), midi_to.as_slice())?; - app.scenes_add(scenes)?; - app - }))?)?, + &[&l_to, &r_to], + )?, + } + ))?)?, + + Groovebox { + midi_from, midi_to, l_from, r_from, l_to, r_to, .. + } => engine.run(&jack.activate_with(|jack|Ok({ + let phrase = Arc::new(RwLock::new(MidiClip::new( + "Clip", true, 4 * player.clock.timebase.ppq.get() as usize, + None, Some(ItemColor::random().into()) + ))); + let mut player = crate::midi::MidiPlayer::new(jack, &"sequencer", Some(&phrase), + &midi_from, + &midi_to + )?; + player.play_phrase = Some((Moment::zero(&player.clock.timebase), Some(phrase.clone()))); + let sampler = crate::sampler::Sampler::new(jack, &"sampler", + midi_from, + &[l_from, r_from], + &[l_to, r_to ], + )?; + jack.read().unwrap().client().connect_ports( + &player.midi_outs[0], + &sampler.midi_in + )?; + let app = tek::Groovebox { + player, + sampler, + _jack: jack.clone(), + + pool: PoolModel::from(&phrase), + editor: MidiEditor::from(&phrase), + + compact: true, + status: true, + size: Measure::new(), + midi_buf: vec![vec![];65536], + note_buf: vec![], + perf: PerfModel::default(), + }; + + let app = tek::Groovebox::new( + jack, + &midi_from, &midi_to, + &[&l_from, &r_from], + &[&l_to, &r_to], + )?; + if let Some(bpm) = cli.bpm { + app.clock().timebase.bpm.set(bpm); + } + if cli.sync_lead { + jack.read().unwrap().client().register_timebase_callback(false, |mut state|{ + app.clock().playhead.update_from_sample(state.position.frame() as f64); + state.position.bbt = Some(app.clock().bbt()); + state.position + })? + } else if cli.sync_follow { + jack.read().unwrap().client().register_timebase_callback(false, |state|{ + app.clock().playhead.update_from_sample(state.position.frame() as f64); + state.position + })? + } + app + }))?)?, + + Arranger { + scenes, tracks, track_width, midi_from, midi_to, .. + } => engine.run(&jack.activate_with(|jack|Ok({ + let mut app = crate::Arranger::new(jack); + app.tracks_add(tracks, track_width, midi_from.as_slice(), midi_to.as_slice())?; + app.scenes_add(scenes)?; + app + }))?)?, + _ => todo!() }) } diff --git a/jack/Cargo.toml b/jack/Cargo.toml new file mode 100644 index 00000000..1c97b4dc --- /dev/null +++ b/jack/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "tek_jack" +edition = "2021" +version = "0.2.0" + +[dependencies] +jack = { path = "../rust-jack" } diff --git a/jack/src/from_jack.rs b/jack/src/from_jack.rs new file mode 100644 index 00000000..67e0843d --- /dev/null +++ b/jack/src/from_jack.rs @@ -0,0 +1,13 @@ +use crate::*; + +/// Implement [TryFrom<&Arc>>]: create app state from wrapped JACK handle. +#[macro_export] macro_rules! from_jack { + (|$jack:ident|$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)? $cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? TryFrom<&Arc>> for $Struct $(<$($L),*$($T),*>)? { + type Error = Box; + fn try_from ($jack: &Arc>) -> Usually { + Ok($cb) + } + } + }; +} diff --git a/jack/src/jack_audio.rs b/jack/src/jack_audio.rs new file mode 100644 index 00000000..626d8a5d --- /dev/null +++ b/jack/src/jack_audio.rs @@ -0,0 +1,26 @@ +use crate::*; + +/// Implement [Audio]: provide JACK callbacks. +#[macro_export] macro_rules! audio { + (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?,$c:ident,$s:ident|$cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? Audio for $Struct $(<$($L),*$($T),*>)? { + #[inline] fn process (&mut $self, $c: &Client, $s: &ProcessScope) -> Control { $cb } + } + } +} + +/// Trait for thing that has a JACK process callback. +pub trait Audio: Send + Sync { + fn process (&mut self, _: &Client, _: &ProcessScope) -> Control { + Control::Continue + } + fn callback ( + state: &Arc>, client: &Client, scope: &ProcessScope + ) -> Control where Self: Sized { + if let Ok(mut state) = state.write() { + state.process(client, scope) + } else { + Control::Quit + } + } +} diff --git a/jack/src/jack_connection.rs b/jack/src/jack_connection.rs new file mode 100644 index 00000000..cd9e64e3 --- /dev/null +++ b/jack/src/jack_connection.rs @@ -0,0 +1,132 @@ +use crate::*; + +/// This is a boxed realtime callback. +pub type BoxedAudioHandler = Box Control + Send>; + +/// This is the notification handler wrapper for a boxed realtime callback. +pub type DynamicAudioHandler = ClosureProcessHandler<(), BoxedAudioHandler>; + +/// This is a boxed [JackEvent] callback. +pub type BoxedJackEventHandler = Box; + +/// 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. + Inactive(Client), + /// During activation. + Activating, + /// After activation. Must not be dropped for JACK thread to persist. + Active(DynamicAsyncClient), +} + +impl From for Client { + fn from (jack: JackConnection) -> Self { + match jack { + JackConnection::Inactive(client) => client, + JackConnection::Activating => panic!("jack client still activating"), + JackConnection::Active(_) => panic!("jack client already activated"), + } + } +} + +impl JackConnection { + pub fn new (name: &str) -> Usually { + let (client, _) = Client::new(name, ClientOptions::NO_START_SERVER)?; + Ok(Self::Inactive(client)) + } + /// Return the internal [Client] handle that lets you call the JACK API. + pub fn client (&self) -> &Client { + match self { + Self::Inactive(ref client) => client, + Self::Activating => panic!("jack client has not finished activation"), + Self::Active(ref client) => client.as_client(), + } + } + /// 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. + fn activate ( + self, + mut cb: impl FnMut(&Arc>, &Client, &ProcessScope) -> Control + Send + 'static, + ) -> Usually>> + where + Self: Send + Sync + 'static + { + let client = Client::from(self); + let state = Arc::new(RwLock::new(Self::Activating)); + let event = Box::new(move|_|{/*TODO*/}) as Box; + let events = Notifications(event); + let frame = Box::new({let state = state.clone(); move|c: &_, s: &_|cb(&state, c, s)}); + let frames = ClosureProcessHandler::new(frame as BoxedAudioHandler); + *state.write().unwrap() = Self::Active(client.activate_async(events, frames)?); + Ok(state) + } + /// 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>> + { + // 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) + } + pub fn port_by_name (&self, name: &str) -> Option> { + self.client().port_by_name(name) + } + pub fn register_port (&self, name: &str, spec: PS) -> Usually> { + Ok(self.client().register_port(name, spec)?) + } +} diff --git a/jack/src/jack_event.rs b/jack/src/jack_event.rs new file mode 100644 index 00000000..e8dcaa45 --- /dev/null +++ b/jack/src/jack_event.rs @@ -0,0 +1,65 @@ +use crate::*; + +#[derive(Debug, Clone, PartialEq)] +/// Event enum for JACK events. +pub enum JackEvent { + ThreadInit, + Shutdown(ClientStatus, Arc), + Freewheel(bool), + SampleRate(Frames), + ClientRegistration(Arc, bool), + PortRegistration(PortId, bool), + PortRename(PortId, Arc, Arc), + PortsConnected(PortId, PortId, bool), + GraphReorder, + XRun, +} + +/// Generic notification handler that emits [JackEvent] +pub struct Notifications(pub T); + +impl NotificationHandler for Notifications { + fn thread_init(&self, _: &Client) { + self.0(JackEvent::ThreadInit); + } + + unsafe fn shutdown(&mut self, status: ClientStatus, reason: &str) { + self.0(JackEvent::Shutdown(status, reason.into())); + } + + fn freewheel(&mut self, _: &Client, enabled: bool) { + self.0(JackEvent::Freewheel(enabled)); + } + + fn sample_rate(&mut self, _: &Client, frames: Frames) -> Control { + self.0(JackEvent::SampleRate(frames)); + Control::Quit + } + + fn client_registration(&mut self, _: &Client, name: &str, reg: bool) { + self.0(JackEvent::ClientRegistration(name.into(), reg)); + } + + fn port_registration(&mut self, _: &Client, id: PortId, reg: bool) { + self.0(JackEvent::PortRegistration(id, reg)); + } + + fn port_rename(&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control { + self.0(JackEvent::PortRename(id, old.into(), new.into())); + Control::Continue + } + + fn ports_connected(&mut self, _: &Client, a: PortId, b: PortId, are: bool) { + self.0(JackEvent::PortsConnected(a, b, are)); + } + + fn graph_reorder(&mut self, _: &Client) -> Control { + self.0(JackEvent::GraphReorder); + Control::Continue + } + + fn xrun(&mut self, _: &Client) -> Control { + self.0(JackEvent::XRun); + Control::Continue + } +} diff --git a/jack/src/jack_port.rs b/jack/src/jack_port.rs new file mode 100644 index 00000000..c5e09bd9 --- /dev/null +++ b/jack/src/jack_port.rs @@ -0,0 +1,185 @@ +use crate::*; + +/// 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: 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: impl AsRef, connect: &[impl AsRef]) -> Usually> { + let jack = self.read().unwrap(); + 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(input) + } + fn midi_out (&self, name: impl AsRef, connect: &[impl AsRef]) -> Usually> { + let jack = self.read().unwrap(); + 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(output) + } + fn audio_in (&self, name: impl AsRef, connect: &[impl AsRef]) -> Usually> { + let jack = self.read().unwrap(); + 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(input) + } + fn audio_out (&self, name: impl AsRef, connect: &[impl AsRef]) -> Usually> { + let jack = self.read().unwrap(); + 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(output) + } +} + +///// Collection of JACK ports as [AudioIn]/[AudioOut]/[MidiIn]/[MidiOut]. +//#[derive(Default, Debug)] +//pub struct JackPorts { + //pub audio_ins: BTreeMap>, + //pub midi_ins: BTreeMap>, + //pub audio_outs: BTreeMap>, + //pub midi_outs: BTreeMap>, +//} + +///// Collection of JACK ports as [Unowned]. +//#[derive(Default, Debug)] +//pub struct UnownedJackPorts { + //pub audio_ins: BTreeMap>, + //pub midi_ins: BTreeMap>, + //pub audio_outs: BTreeMap>, + //pub midi_outs: BTreeMap>, +//} + +//impl JackPorts { + //pub fn clone_unowned(&self) -> UnownedJackPorts { + //let mut unowned = UnownedJackPorts::default(); + //for (name, port) in self.midi_ins.iter() { + //unowned.midi_ins.insert(name.clone(), port.clone_unowned()); + //} + //for (name, port) in self.midi_outs.iter() { + //unowned.midi_outs.insert(name.clone(), port.clone_unowned()); + //} + //for (name, port) in self.audio_ins.iter() { + //unowned.audio_ins.insert(name.clone(), port.clone_unowned()); + //} + //for (name, port) in self.audio_outs.iter() { + //unowned + //.audio_outs + //.insert(name.clone(), port.clone_unowned()); + //} + //unowned + //} +//} + +///// Implement the `Ports` trait. +//#[macro_export] +//macro_rules! ports { + //($T:ty $({ $(audio: { + //$(ins: |$ai_arg:ident|$ai_impl:expr,)? + //$(outs: |$ao_arg:ident|$ao_impl:expr,)? + //})? $(midi: { + //$(ins: |$mi_arg:ident|$mi_impl:expr,)? + //$(outs: |$mo_arg:ident|$mo_impl:expr,)? + //})?})?) => { + //impl Ports for $T {$( + //$( + //$(fn audio_ins <'a> (&'a self) -> Usually>> { + //let cb = |$ai_arg:&'a Self|$ai_impl; + //cb(self) + //})? + //)? + //$( + //$(fn audio_outs <'a> (&'a self) -> Usually>> { + //let cb = (|$ao_arg:&'a Self|$ao_impl); + //cb(self) + //})? + //)? + //)? $( + //$( + //$(fn midi_ins <'a> (&'a self) -> Usually>> { + //let cb = (|$mi_arg:&'a Self|$mi_impl); + //cb(self) + //})? + //)? + //$( + //$(fn midi_outs <'a> (&'a self) -> Usually>> { + //let cb = (|$mo_arg:&'a Self|$mo_impl); + //cb(self) + //})? + //)? + //)?} + //}; +//} + +/// Trait for things that may expose JACK ports. +pub trait Ports { + fn audio_ins(&self) -> Usually>> { + Ok(vec![]) + } + fn audio_outs(&self) -> Usually>> { + Ok(vec![]) + } + fn midi_ins(&self) -> Usually>> { + Ok(vec![]) + } + fn midi_outs(&self) -> Usually>> { + Ok(vec![]) + } +} + +fn register_ports( + client: &Client, + names: Vec, + spec: T, +) -> Usually>> { + names + .into_iter() + .try_fold(BTreeMap::new(), |mut ports, name| { + let port = client.register_port(&name, spec)?; + ports.insert(name, port); + Ok(ports) + }) +} + +fn query_ports(client: &Client, names: Vec) -> BTreeMap> { + names.into_iter().fold(BTreeMap::new(), |mut ports, name| { + let port = client.port_by_name(&name).unwrap(); + ports.insert(name, port); + ports + }) +} + diff --git a/jack/src/lib.rs b/jack/src/lib.rs new file mode 100644 index 00000000..d2fa3bd6 --- /dev/null +++ b/jack/src/lib.rs @@ -0,0 +1,222 @@ +pub(crate) use std::sync::{Arc, RwLock}; +pub(crate) use std::collections::BTreeMap; + +pub use ::jack; +pub(crate) use ::jack::{ + contrib::ClosureProcessHandler, NotificationHandler, + Client, AsyncClient, ClientOptions, ClientStatus, + ProcessScope, Control, CycleTimes, Frames, + Port, PortId, PortSpec, Unowned, MidiIn, MidiOut, AudioIn, AudioOut, + Transport, TransportState, MidiIter, MidiWriter, RawMidi, +}; + +mod from_jack; pub use self::from_jack::*; +mod jack_audio; pub use self::jack_audio::*; +mod jack_connection; pub use self::jack_connection::*; +mod jack_event; pub use self::jack_event::*; +mod jack_port; pub use self::jack_port::*; + +pub(crate) type Usually = Result>; + +//////////////////////////////////////////////////////////////////////////////////// + +///// A [AudioComponent] bound to a JACK client and a set of ports. +//pub struct JackDevice { + ///// The active JACK client of this device. + //pub client: DynamicAsyncClient, + ///// The device state, encapsulated for sharing between threads. + //pub state: Arc>>>, + ///// Unowned copies of the device's JACK ports, for connecting to the device. + ///// The "real" readable/writable `Port`s are owned by the `state`. + //pub ports: UnownedJackPorts, +//} + +//impl std::fmt::Debug for JackDevice { + //fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + //f.debug_struct("JackDevice") + //.field("ports", &self.ports) + //.finish() + //} +//} + +//impl Render for JackDevice { + //type Engine = E; + //fn min_size(&self, to: E::Size) -> Perhaps { + //self.state.read().unwrap().layout(to) + //} + //fn render(&self, to: &mut E::Output) -> Usually<()> { + //self.state.read().unwrap().render(to) + //} +//} + +//impl Handle for JackDevice { + //fn handle(&mut self, from: &E::Input) -> Perhaps { + //self.state.write().unwrap().handle(from) + //} +//} + +//impl Ports for JackDevice { + //fn audio_ins(&self) -> Usually>> { + //Ok(self.ports.audio_ins.values().collect()) + //} + //fn audio_outs(&self) -> Usually>> { + //Ok(self.ports.audio_outs.values().collect()) + //} + //fn midi_ins(&self) -> Usually>> { + //Ok(self.ports.midi_ins.values().collect()) + //} + //fn midi_outs(&self) -> Usually>> { + //Ok(self.ports.midi_outs.values().collect()) + //} +//} + +//impl JackDevice { + ///// Returns a locked mutex of the state's contents. + //pub fn state(&self) -> LockResult>>> { + //self.state.read() + //} + ///// Returns a locked mutex of the state's contents. + //pub fn state_mut(&self) -> LockResult>>> { + //self.state.write() + //} + //pub fn connect_midi_in(&self, index: usize, port: &Port) -> Usually<()> { + //Ok(self + //.client + //.as_client() + //.connect_ports(port, self.midi_ins()?[index])?) + //} + //pub fn connect_midi_out(&self, index: usize, port: &Port) -> Usually<()> { + //Ok(self + //.client + //.as_client() + //.connect_ports(self.midi_outs()?[index], port)?) + //} + //pub fn connect_audio_in(&self, index: usize, port: &Port) -> Usually<()> { + //Ok(self + //.client + //.as_client() + //.connect_ports(port, self.audio_ins()?[index])?) + //} + //pub fn connect_audio_out(&self, index: usize, port: &Port) -> Usually<()> { + //Ok(self + //.client + //.as_client() + //.connect_ports(self.audio_outs()?[index], port)?) + //} +//} + +///// `JackDevice` factory. Creates JACK `Client`s, performs port registration +///// and activation, and encapsulates a `AudioComponent` into a `JackDevice`. +//pub struct Jack { + //pub client: Client, + //pub midi_ins: Vec, + //pub audio_ins: Vec, + //pub midi_outs: Vec, + //pub audio_outs: Vec, +//} + +//impl Jack { + //pub fn new(name: &str) -> Usually { + //Ok(Self { + //midi_ins: vec![], + //audio_ins: vec![], + //midi_outs: vec![], + //audio_outs: vec![], + //client: Client::new(name, ClientOptions::NO_START_SERVER)?.0, + //}) + //} + //pub fn run<'a: 'static, D, E>( + //self, + //state: impl FnOnce(JackPorts) -> Box, + //) -> Usually> + //where + //D: AudioComponent + Sized + 'static, + //E: Engine + 'static, + //{ + //let owned_ports = JackPorts { + //audio_ins: register_ports(&self.client, self.audio_ins, AudioIn::default())?, + //audio_outs: register_ports(&self.client, self.audio_outs, AudioOut::default())?, + //midi_ins: register_ports(&self.client, self.midi_ins, MidiIn::default())?, + //midi_outs: register_ports(&self.client, self.midi_outs, MidiOut::default())?, + //}; + //let midi_outs = owned_ports + //.midi_outs + //.values() + //.map(|p| Ok(p.name()?)) + //.collect::>>()?; + //let midi_ins = owned_ports + //.midi_ins + //.values() + //.map(|p| Ok(p.name()?)) + //.collect::>>()?; + //let audio_outs = owned_ports + //.audio_outs + //.values() + //.map(|p| Ok(p.name()?)) + //.collect::>>()?; + //let audio_ins = owned_ports + //.audio_ins + //.values() + //.map(|p| Ok(p.name()?)) + //.collect::>>()?; + //let state = Arc::new(RwLock::new(state(owned_ports) as Box>)); + //let client = self.client.activate_async( + //Notifications(Box::new({ + //let _state = state.clone(); + //move |_event| { + //// FIXME: this deadlocks + ////state.lock().unwrap().handle(&event).unwrap(); + //} + //}) as Box), + //ClosureProcessHandler::new(Box::new({ + //let state = state.clone(); + //move |c: &Client, s: &ProcessScope| state.write().unwrap().process(c, s) + //}) as BoxedAudioHandler), + //)?; + //Ok(JackDevice { + //ports: UnownedJackPorts { + //audio_ins: query_ports(&client.as_client(), audio_ins), + //audio_outs: query_ports(&client.as_client(), audio_outs), + //midi_ins: query_ports(&client.as_client(), midi_ins), + //midi_outs: query_ports(&client.as_client(), midi_outs), + //}, + //client, + //state, + //}) + //} + //pub fn audio_in(mut self, name: &str) -> Self { + //self.audio_ins.push(name.to_string()); + //self + //} + //pub fn audio_out(mut self, name: &str) -> Self { + //self.audio_outs.push(name.to_string()); + //self + //} + //pub fn midi_in(mut self, name: &str) -> Self { + //self.midi_ins.push(name.to_string()); + //self + //} + //pub fn midi_out(mut self, name: &str) -> Self { + //self.midi_outs.push(name.to_string()); + //self + //} +//} + +///// A UI component that may be associated with a JACK client by the `Jack` factory. +//pub trait AudioComponent: Component + Audio { + ///// Perform type erasure for collecting heterogeneous devices. + //fn boxed(self) -> Box> + //where + //Self: Sized + 'static, + //{ + //Box::new(self) + //} +//} + +///// All things that implement the required traits can be treated as `AudioComponent`. +//impl + Audio> AudioComponent for W {} + +///////// + +/* +*/ diff --git a/midi/Cargo.toml b/midi/Cargo.toml new file mode 100644 index 00000000..f6b72964 --- /dev/null +++ b/midi/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "tek_midi" +edition = "2021" +version = "0.2.0" + +[dependencies] +tek_tui = { path = "../tui" } +tek_jack = { path = "../jack" } +tek_time = { path = "../time" } + +jack = { path = "../rust-jack" } +midly = "0.5" diff --git a/src/midi.rs b/midi/src/lib.rs similarity index 59% rename from src/midi.rs rename to midi/src/lib.rs index be478f11..125aafc3 100644 --- a/src/midi.rs +++ b/midi/src/lib.rs @@ -1,17 +1,18 @@ -use crate::*; +pub(crate) use ::tek_tui::{*, tek_input::*, tek_output::*, crossterm::event::KeyCode}; +pub(crate) use ::tek_jack::*; -pub(crate) mod midi_pool; pub(crate) use midi_pool::*; -pub(crate) mod midi_clip; pub(crate) use midi_clip::*; -pub(crate) mod midi_launch; pub(crate) use midi_launch::*; -pub(crate) mod midi_player; pub(crate) use midi_player::*; -pub(crate) mod midi_in; pub(crate) use midi_in::*; -pub(crate) mod midi_out; pub(crate) use midi_out::*; +mod midi_pool; pub(crate) use midi_pool::*; +mod midi_clip; pub(crate) use midi_clip::*; +mod midi_launch; pub(crate) use midi_launch::*; +mod midi_player; pub(crate) use midi_player::*; +mod midi_in; pub(crate) use midi_in::*; +mod midi_out; pub(crate) use midi_out::*; -pub(crate) mod midi_note; pub(crate) use midi_note::*; -pub(crate) mod midi_range; pub(crate) use midi_range::*; -pub(crate) mod midi_point; pub(crate) use midi_point::*; -pub(crate) mod midi_view; pub(crate) use midi_view::*; -pub(crate) mod midi_editor; pub(crate) use midi_editor::*; +mod midi_note; pub(crate) use midi_note::*; +mod midi_range; pub(crate) use midi_range::*; +mod midi_point; pub(crate) use midi_point::*; +mod midi_view; pub(crate) use midi_view::*; +mod midi_editor; pub(crate) use midi_editor::*; /// Add "all notes off" to the start of a buffer. pub fn all_notes_off (output: &mut [Vec>]) { @@ -39,3 +40,4 @@ pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) { _ => {} } } + diff --git a/src/midi/midi_clip.rs b/midi/src/midi_clip.rs similarity index 100% rename from src/midi/midi_clip.rs rename to midi/src/midi_clip.rs diff --git a/src/midi/midi_editor.rs b/midi/src/midi_editor.rs similarity index 100% rename from src/midi/midi_editor.rs rename to midi/src/midi_editor.rs diff --git a/src/midi/midi_in.rs b/midi/src/midi_in.rs similarity index 100% rename from src/midi/midi_in.rs rename to midi/src/midi_in.rs diff --git a/src/midi/midi_launch.rs b/midi/src/midi_launch.rs similarity index 100% rename from src/midi/midi_launch.rs rename to midi/src/midi_launch.rs diff --git a/src/midi/midi_note.rs b/midi/src/midi_note.rs similarity index 100% rename from src/midi/midi_note.rs rename to midi/src/midi_note.rs diff --git a/src/midi/midi_out.rs b/midi/src/midi_out.rs similarity index 100% rename from src/midi/midi_out.rs rename to midi/src/midi_out.rs diff --git a/src/midi/midi_player.rs b/midi/src/midi_player.rs similarity index 100% rename from src/midi/midi_player.rs rename to midi/src/midi_player.rs diff --git a/src/midi/midi_point.rs b/midi/src/midi_point.rs similarity index 100% rename from src/midi/midi_point.rs rename to midi/src/midi_point.rs diff --git a/src/midi/midi_pool.rs b/midi/src/midi_pool.rs similarity index 100% rename from src/midi/midi_pool.rs rename to midi/src/midi_pool.rs diff --git a/src/midi/midi_range.rs b/midi/src/midi_range.rs similarity index 100% rename from src/midi/midi_range.rs rename to midi/src/midi_range.rs diff --git a/src/midi/midi_view.rs b/midi/src/midi_view.rs similarity index 100% rename from src/midi/midi_view.rs rename to midi/src/midi_view.rs diff --git a/src/arranger.rs b/src/arranger.rs index 2f187ed5..493c6cdc 100644 --- a/src/arranger.rs +++ b/src/arranger.rs @@ -27,7 +27,35 @@ pub struct Arranger { pub perf: PerfModel, pub compact: bool, } +has_clock!(|self: Arranger|&self.clock); +has_phrases!(|self: Arranger|self.pool.phrases); +has_editor!(|self: Arranger|self.editor); +handle!(TuiIn: |self: Arranger, input|ArrangerCommand::execute_with_state(self, input.event())); impl Arranger { + pub fn new (jack: &Arc>) -> Self { + let clock = Clock::from(jack); + let phrase = Arc::new(RwLock::new(MidiClip::new( + "Clip", true, 4 * clock.timebase.ppq.get() as usize, + None, Some(ItemColor::random().into()) + ))); + Self { + clock, + pool: (&phrase).into(), + editor: (&phrase).into(), + selected: ArrangerSelection::Clip(0, 0), + scenes: vec![], + tracks: vec![], + color: ItemPalette::random(), + mode: ArrangerMode::V(1), + size: Measure::new(), + splits: [12, 20], + midi_buf: vec![vec![];65536], + note_buf: vec![], + perf: PerfModel::default(), + jack: jack.clone(), + compact: true, + } + } pub fn selected (&self) -> ArrangerSelection { self.selected } @@ -70,31 +98,3 @@ impl Arranger { } } } -from_jack!(|jack| Arranger { - let clock = Clock::from(jack); - let phrase = Arc::new(RwLock::new(MidiClip::new( - "Clip", true, 4 * clock.timebase.ppq.get() as usize, - None, Some(ItemColor::random().into()) - ))); - Self { - clock, - pool: (&phrase).into(), - editor: (&phrase).into(), - selected: ArrangerSelection::Clip(0, 0), - scenes: vec![], - tracks: vec![], - color: TuiTheme::bg().into(), - mode: ArrangerMode::V(1), - size: Measure::new(), - splits: [12, 20], - midi_buf: vec![vec![];65536], - note_buf: vec![], - perf: PerfModel::default(), - jack: jack.clone(), - compact: true, - } -}); -has_clock!(|self: Arranger|&self.clock); -has_phrases!(|self: Arranger|self.pool.phrases); -has_editor!(|self: Arranger|self.editor); -handle!(TuiIn: |self: Arranger, input|ArrangerCommand::execute_with_state(self, input.event())); diff --git a/src/groovebox.rs b/src/groovebox.rs index 133827c9..04ef661a 100644 --- a/src/groovebox.rs +++ b/src/groovebox.rs @@ -1,7 +1,6 @@ mod groovebox_audio; pub use self::groovebox_audio::*; mod groovebox_command; pub use self::groovebox_command::*; mod groovebox_tui; pub use self::groovebox_tui::*; -mod groovebox_edn; pub use self::groovebox_edn::*; use crate::*; use super::*; @@ -25,38 +24,48 @@ pub struct Groovebox { pub midi_buf: Vec>>, pub perf: PerfModel, } + has_clock!(|self: Groovebox|self.player.clock()); -impl Groovebox { - 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)?; - let phrase = Arc::new(RwLock::new(MidiClip::new( - "Clip", 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()))); - Ok(Self { - player, - sampler, - _jack: jack.clone(), - pool: crate::pool::PoolModel::from(&phrase), - editor: crate::midi::MidiEditor::from(&phrase), +impl EdnViewData for &Groovebox { + fn get_unit (&self, item: EdnItem<&str>) -> u16 { + use EdnItem::*; + match item.to_str() { + ":sample-h" => if self.compact { 0 } else { 5 }, + ":samples-w" => if self.compact { 4 } else { 11 }, + ":samples-y" => if self.compact { 1 } else { 0 }, + ":pool-w" => if self.compact { 5 } else { + let w = self.size.w(); + if w > 60 { 20 } else if w > 40 { 15 } else { 10 } + }, + _ => 0 + } + } + fn get_content <'a> (&'a self, item: EdnItem<&str>) -> RenderBox<'a, TuiOut> { + use EdnItem::*; + match item { + Nil => Box::new(()), + Sym(bol) => match bol { + ":input-meter-l" => Meter("L/", self.sampler.input_meter[0]).boxed(), + ":input-meter-r" => Box::new(Meter("R/", self.sampler.input_meter[1])), - compact: true, - status: true, - size: Measure::new(), - midi_buf: vec![vec![];65536], - note_buf: vec![], - perf: PerfModel::default(), - }) + ":transport" => Box::new(TransportView::new(true, &self.player.clock)), + ":clip-play" => Box::new(self.player.play_status()), + ":clip-next" => Box::new(self.player.next_status()), + ":clip-edit" => Box::new(self.editor.clip_status()), + ":edit-stat" => Box::new(self.editor.edit_status()), + ":pool-view" => Box::new(PoolView(self.compact, &self.pool)), + ":midi-view" => Box::new(&self.editor), + + ":sample-view" => Box::new(SampleViewer::from_sampler(&self.sampler, self.editor.note_point())), + ":sample-stat" => Box::new(SamplerStatus(&self.sampler, self.editor.note_point())), + ":samples-view" => Box::new(SampleList::new(self.compact, &self.sampler, &self.editor)), + + _ => panic!("unknown sym {bol:?}") + }, + Exp(items) => Box::new(EdnView::from_items(*self, items.as_slice())), + _ => panic!("no content for {item:?}") + } } } diff --git a/src/groovebox/groovebox_edn.rs b/src/groovebox/groovebox_edn.rs deleted file mode 100644 index 8f3c0b8e..00000000 --- a/src/groovebox/groovebox_edn.rs +++ /dev/null @@ -1,44 +0,0 @@ -use crate::*; - -impl EdnViewData for &Groovebox { - fn get_unit (&self, item: EdnItem<&str>) -> u16 { - use EdnItem::*; - match item.to_str() { - ":sample-h" => if self.compact { 0 } else { 5 }, - ":samples-w" => if self.compact { 4 } else { 11 }, - ":samples-y" => if self.compact { 1 } else { 0 }, - ":pool-w" => if self.compact { 5 } else { - let w = self.size.w(); - if w > 60 { 20 } else if w > 40 { 15 } else { 10 } - }, - _ => 0 - } - } - fn get_content <'a> (&'a self, item: EdnItem<&str>) -> RenderBox<'a, TuiOut> { - use EdnItem::*; - match item { - Nil => Box::new(()), - Sym(bol) => match bol { - ":input-meter-l" => Meter("L/", self.sampler.input_meter[0]).boxed(), - ":input-meter-r" => Box::new(Meter("R/", self.sampler.input_meter[1])), - - ":transport" => Box::new(TransportView::new(true, &self.player.clock)), - ":clip-play" => Box::new(self.player.play_status()), - ":clip-next" => Box::new(self.player.next_status()), - ":clip-edit" => Box::new(self.editor.clip_status()), - ":edit-stat" => Box::new(self.editor.edit_status()), - ":pool-view" => Box::new(PoolView(self.compact, &self.pool)), - ":midi-view" => Box::new(&self.editor), - - ":sample-view" => Box::new(SampleViewer::from_sampler(&self.sampler, self.editor.note_point())), - ":sample-stat" => Box::new(SamplerStatus(&self.sampler, self.editor.note_point())), - ":samples-view" => Box::new(SampleList::new(self.compact, &self.sampler, &self.editor)), - - _ => panic!("unknown sym {bol:?}") - }, - Exp(items) => Box::new(EdnView::from_items(*self, items.as_slice())), - _ => panic!("no content for {item:?}") - } - } -} - diff --git a/src/groovebox/groovebox_tui.rs b/src/groovebox/groovebox_tui.rs index da7788ca..feab9913 100644 --- a/src/groovebox/groovebox_tui.rs +++ b/src/groovebox/groovebox_tui.rs @@ -13,7 +13,7 @@ render!(TuiOut: (self: Groovebox) => self.size.of( Bsp::n(self.status_view(), Bsp::w(self.pool_view(), Fill::xy(Bsp::e(self.sampler_view(), &self.editor))))))))); -// this almost works: +// this almost does: //impl Content for Groovebox { //fn content (&self) -> impl Render { //self.size.of(EdnView::from_source(self, EDN)) diff --git a/src/jack.rs b/src/jack.rs deleted file mode 100644 index 4fe9a1fb..00000000 --- a/src/jack.rs +++ /dev/null @@ -1,624 +0,0 @@ -use crate::*; -pub use ::jack as libjack; -pub use ::jack::{ - contrib::ClosureProcessHandler, NotificationHandler, - Client, AsyncClient, ClientOptions, ClientStatus, - ProcessScope, Control, CycleTimes, Frames, - Port, PortId, PortSpec, Unowned, MidiIn, MidiOut, AudioIn, AudioOut, - Transport, TransportState, MidiIter, MidiWriter, RawMidi, -}; - -/// Implement [TryFrom<&Arc>>]: create app state from wrapped JACK handle. -#[macro_export] macro_rules! from_jack { - (|$jack:ident|$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)? $cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? TryFrom<&Arc>> for $Struct $(<$($L),*$($T),*>)? { - type Error = Box; - fn try_from ($jack: &Arc>) -> Usually { - Ok($cb) - } - } - }; -} - -/// Implement [Audio]: provide JACK callbacks. -#[macro_export] macro_rules! audio { - (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?,$c:ident,$s:ident|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? Audio for $Struct $(<$($L),*$($T),*>)? { - #[inline] fn process (&mut $self, $c: &Client, $s: &ProcessScope) -> Control { $cb } - } - } -} - -/// Trait for thing that has a JACK process callback. -pub trait Audio: Send + Sync { - fn process (&mut self, _: &Client, _: &ProcessScope) -> Control { - Control::Continue - } - fn callback ( - state: &Arc>, client: &Client, scope: &ProcessScope - ) -> Control where Self: Sized { - if let Ok(mut state) = state.write() { - state.process(client, scope) - } else { - Control::Quit - } - } -} - -/// This is a boxed realtime callback. -pub type BoxedAudioHandler = Box Control + Send>; - -/// This is the notification handler wrapper for a boxed realtime callback. -pub type DynamicAudioHandler = ClosureProcessHandler<(), BoxedAudioHandler>; - -/// This is a boxed [JackEvent] callback. -pub type BoxedJackEventHandler = Box; - -/// 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. - Inactive(Client), - /// During activation. - Activating, - /// After activation. Must not be dropped for JACK thread to persist. - Active(DynamicAsyncClient), -} - -from!(|jack: JackConnection|Client = match jack { - JackConnection::Inactive(client) => client, - JackConnection::Activating => panic!("jack client still activating"), - JackConnection::Active(_) => panic!("jack client already activated"), -}); - -impl JackConnection { - pub fn new (name: &str) -> Usually { - let (client, _) = Client::new(name, ClientOptions::NO_START_SERVER)?; - Ok(Self::Inactive(client)) - } - /// Return the internal [Client] handle that lets you call the JACK API. - pub fn client (&self) -> &Client { - match self { - Self::Inactive(ref client) => client, - Self::Activating => panic!("jack client has not finished activation"), - Self::Active(ref client) => client.as_client(), - } - } - /// 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. - fn activate ( - self, - mut cb: impl FnMut(&Arc>, &Client, &ProcessScope) -> Control + Send + 'static, - ) -> Usually>> - where - Self: Send + Sync + 'static - { - let client = Client::from(self); - let state = Arc::new(RwLock::new(Self::Activating)); - let event = Box::new(move|_|{/*TODO*/}) as Box; - let events = Notifications(event); - let frame = Box::new({let state = state.clone(); move|c: &_, s: &_|cb(&state, c, s)}); - let frames = ClosureProcessHandler::new(frame as BoxedAudioHandler); - *state.write().unwrap() = Self::Active(client.activate_async(events, frames)?); - Ok(state) - } - /// 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>> - { - // 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) - } - pub fn port_by_name (&self, name: &str) -> Option> { - self.client().port_by_name(name) - } - pub fn register_port (&self, name: &str, spec: PS) -> Usually> { - Ok(self.client().register_port(name, spec)?) - } -} - -/// 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: 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: impl AsRef, connect: &[impl AsRef]) -> Usually> { - let jack = self.read().unwrap(); - 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(input) - } - fn midi_out (&self, name: impl AsRef, connect: &[impl AsRef]) -> Usually> { - let jack = self.read().unwrap(); - 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(output) - } - fn audio_in (&self, name: impl AsRef, connect: &[impl AsRef]) -> Usually> { - let jack = self.read().unwrap(); - 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(input) - } - fn audio_out (&self, name: impl AsRef, connect: &[impl AsRef]) -> Usually> { - let jack = self.read().unwrap(); - 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(output) - } -} - -#[derive(Debug, Clone, PartialEq)] -/// Event enum for JACK events. -pub enum JackEvent { - ThreadInit, - Shutdown(ClientStatus, Arc), - Freewheel(bool), - SampleRate(Frames), - ClientRegistration(Arc, bool), - PortRegistration(PortId, bool), - PortRename(PortId, Arc, Arc), - PortsConnected(PortId, PortId, bool), - GraphReorder, - XRun, -} - -/// Generic notification handler that emits [JackEvent] -pub struct Notifications(pub T); - -impl NotificationHandler for Notifications { - fn thread_init(&self, _: &Client) { - self.0(JackEvent::ThreadInit); - } - - unsafe fn shutdown(&mut self, status: ClientStatus, reason: &str) { - self.0(JackEvent::Shutdown(status, reason.into())); - } - - fn freewheel(&mut self, _: &Client, enabled: bool) { - self.0(JackEvent::Freewheel(enabled)); - } - - fn sample_rate(&mut self, _: &Client, frames: Frames) -> Control { - self.0(JackEvent::SampleRate(frames)); - Control::Quit - } - - fn client_registration(&mut self, _: &Client, name: &str, reg: bool) { - self.0(JackEvent::ClientRegistration(name.into(), reg)); - } - - fn port_registration(&mut self, _: &Client, id: PortId, reg: bool) { - self.0(JackEvent::PortRegistration(id, reg)); - } - - fn port_rename(&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control { - self.0(JackEvent::PortRename(id, old.into(), new.into())); - Control::Continue - } - - fn ports_connected(&mut self, _: &Client, a: PortId, b: PortId, are: bool) { - self.0(JackEvent::PortsConnected(a, b, are)); - } - - fn graph_reorder(&mut self, _: &Client) -> Control { - self.0(JackEvent::GraphReorder); - Control::Continue - } - - fn xrun(&mut self, _: &Client) -> Control { - self.0(JackEvent::XRun); - Control::Continue - } -} - -//////////////////////////////////////////////////////////////////////////////////// - -///// A [AudioComponent] bound to a JACK client and a set of ports. -//pub struct JackDevice { - ///// The active JACK client of this device. - //pub client: DynamicAsyncClient, - ///// The device state, encapsulated for sharing between threads. - //pub state: Arc>>>, - ///// Unowned copies of the device's JACK ports, for connecting to the device. - ///// The "real" readable/writable `Port`s are owned by the `state`. - //pub ports: UnownedJackPorts, -//} - -//impl std::fmt::Debug for JackDevice { - //fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - //f.debug_struct("JackDevice") - //.field("ports", &self.ports) - //.finish() - //} -//} - -//impl Render for JackDevice { - //type Engine = E; - //fn min_size(&self, to: E::Size) -> Perhaps { - //self.state.read().unwrap().layout(to) - //} - //fn render(&self, to: &mut E::Output) -> Usually<()> { - //self.state.read().unwrap().render(to) - //} -//} - -//impl Handle for JackDevice { - //fn handle(&mut self, from: &E::Input) -> Perhaps { - //self.state.write().unwrap().handle(from) - //} -//} - -//impl Ports for JackDevice { - //fn audio_ins(&self) -> Usually>> { - //Ok(self.ports.audio_ins.values().collect()) - //} - //fn audio_outs(&self) -> Usually>> { - //Ok(self.ports.audio_outs.values().collect()) - //} - //fn midi_ins(&self) -> Usually>> { - //Ok(self.ports.midi_ins.values().collect()) - //} - //fn midi_outs(&self) -> Usually>> { - //Ok(self.ports.midi_outs.values().collect()) - //} -//} - -//impl JackDevice { - ///// Returns a locked mutex of the state's contents. - //pub fn state(&self) -> LockResult>>> { - //self.state.read() - //} - ///// Returns a locked mutex of the state's contents. - //pub fn state_mut(&self) -> LockResult>>> { - //self.state.write() - //} - //pub fn connect_midi_in(&self, index: usize, port: &Port) -> Usually<()> { - //Ok(self - //.client - //.as_client() - //.connect_ports(port, self.midi_ins()?[index])?) - //} - //pub fn connect_midi_out(&self, index: usize, port: &Port) -> Usually<()> { - //Ok(self - //.client - //.as_client() - //.connect_ports(self.midi_outs()?[index], port)?) - //} - //pub fn connect_audio_in(&self, index: usize, port: &Port) -> Usually<()> { - //Ok(self - //.client - //.as_client() - //.connect_ports(port, self.audio_ins()?[index])?) - //} - //pub fn connect_audio_out(&self, index: usize, port: &Port) -> Usually<()> { - //Ok(self - //.client - //.as_client() - //.connect_ports(self.audio_outs()?[index], port)?) - //} -//} - -///// Collection of JACK ports as [AudioIn]/[AudioOut]/[MidiIn]/[MidiOut]. -//#[derive(Default, Debug)] -//pub struct JackPorts { - //pub audio_ins: BTreeMap>, - //pub midi_ins: BTreeMap>, - //pub audio_outs: BTreeMap>, - //pub midi_outs: BTreeMap>, -//} - -///// Collection of JACK ports as [Unowned]. -//#[derive(Default, Debug)] -//pub struct UnownedJackPorts { - //pub audio_ins: BTreeMap>, - //pub midi_ins: BTreeMap>, - //pub audio_outs: BTreeMap>, - //pub midi_outs: BTreeMap>, -//} - -//impl JackPorts { - //pub fn clone_unowned(&self) -> UnownedJackPorts { - //let mut unowned = UnownedJackPorts::default(); - //for (name, port) in self.midi_ins.iter() { - //unowned.midi_ins.insert(name.clone(), port.clone_unowned()); - //} - //for (name, port) in self.midi_outs.iter() { - //unowned.midi_outs.insert(name.clone(), port.clone_unowned()); - //} - //for (name, port) in self.audio_ins.iter() { - //unowned.audio_ins.insert(name.clone(), port.clone_unowned()); - //} - //for (name, port) in self.audio_outs.iter() { - //unowned - //.audio_outs - //.insert(name.clone(), port.clone_unowned()); - //} - //unowned - //} -//} - -///// Implement the `Ports` trait. -//#[macro_export] -//macro_rules! ports { - //($T:ty $({ $(audio: { - //$(ins: |$ai_arg:ident|$ai_impl:expr,)? - //$(outs: |$ao_arg:ident|$ao_impl:expr,)? - //})? $(midi: { - //$(ins: |$mi_arg:ident|$mi_impl:expr,)? - //$(outs: |$mo_arg:ident|$mo_impl:expr,)? - //})?})?) => { - //impl Ports for $T {$( - //$( - //$(fn audio_ins <'a> (&'a self) -> Usually>> { - //let cb = |$ai_arg:&'a Self|$ai_impl; - //cb(self) - //})? - //)? - //$( - //$(fn audio_outs <'a> (&'a self) -> Usually>> { - //let cb = (|$ao_arg:&'a Self|$ao_impl); - //cb(self) - //})? - //)? - //)? $( - //$( - //$(fn midi_ins <'a> (&'a self) -> Usually>> { - //let cb = (|$mi_arg:&'a Self|$mi_impl); - //cb(self) - //})? - //)? - //$( - //$(fn midi_outs <'a> (&'a self) -> Usually>> { - //let cb = (|$mo_arg:&'a Self|$mo_impl); - //cb(self) - //})? - //)? - //)?} - //}; -//} - -///// `JackDevice` factory. Creates JACK `Client`s, performs port registration -///// and activation, and encapsulates a `AudioComponent` into a `JackDevice`. -//pub struct Jack { - //pub client: Client, - //pub midi_ins: Vec, - //pub audio_ins: Vec, - //pub midi_outs: Vec, - //pub audio_outs: Vec, -//} - -//impl Jack { - //pub fn new(name: &str) -> Usually { - //Ok(Self { - //midi_ins: vec![], - //audio_ins: vec![], - //midi_outs: vec![], - //audio_outs: vec![], - //client: Client::new(name, ClientOptions::NO_START_SERVER)?.0, - //}) - //} - //pub fn run<'a: 'static, D, E>( - //self, - //state: impl FnOnce(JackPorts) -> Box, - //) -> Usually> - //where - //D: AudioComponent + Sized + 'static, - //E: Engine + 'static, - //{ - //let owned_ports = JackPorts { - //audio_ins: register_ports(&self.client, self.audio_ins, AudioIn::default())?, - //audio_outs: register_ports(&self.client, self.audio_outs, AudioOut::default())?, - //midi_ins: register_ports(&self.client, self.midi_ins, MidiIn::default())?, - //midi_outs: register_ports(&self.client, self.midi_outs, MidiOut::default())?, - //}; - //let midi_outs = owned_ports - //.midi_outs - //.values() - //.map(|p| Ok(p.name()?)) - //.collect::>>()?; - //let midi_ins = owned_ports - //.midi_ins - //.values() - //.map(|p| Ok(p.name()?)) - //.collect::>>()?; - //let audio_outs = owned_ports - //.audio_outs - //.values() - //.map(|p| Ok(p.name()?)) - //.collect::>>()?; - //let audio_ins = owned_ports - //.audio_ins - //.values() - //.map(|p| Ok(p.name()?)) - //.collect::>>()?; - //let state = Arc::new(RwLock::new(state(owned_ports) as Box>)); - //let client = self.client.activate_async( - //Notifications(Box::new({ - //let _state = state.clone(); - //move |_event| { - //// FIXME: this deadlocks - ////state.lock().unwrap().handle(&event).unwrap(); - //} - //}) as Box), - //ClosureProcessHandler::new(Box::new({ - //let state = state.clone(); - //move |c: &Client, s: &ProcessScope| state.write().unwrap().process(c, s) - //}) as BoxedAudioHandler), - //)?; - //Ok(JackDevice { - //ports: UnownedJackPorts { - //audio_ins: query_ports(&client.as_client(), audio_ins), - //audio_outs: query_ports(&client.as_client(), audio_outs), - //midi_ins: query_ports(&client.as_client(), midi_ins), - //midi_outs: query_ports(&client.as_client(), midi_outs), - //}, - //client, - //state, - //}) - //} - //pub fn audio_in(mut self, name: &str) -> Self { - //self.audio_ins.push(name.to_string()); - //self - //} - //pub fn audio_out(mut self, name: &str) -> Self { - //self.audio_outs.push(name.to_string()); - //self - //} - //pub fn midi_in(mut self, name: &str) -> Self { - //self.midi_ins.push(name.to_string()); - //self - //} - //pub fn midi_out(mut self, name: &str) -> Self { - //self.midi_outs.push(name.to_string()); - //self - //} -//} - -///// A UI component that may be associated with a JACK client by the `Jack` factory. -//pub trait AudioComponent: Component + Audio { - ///// Perform type erasure for collecting heterogeneous devices. - //fn boxed(self) -> Box> - //where - //Self: Sized + 'static, - //{ - //Box::new(self) - //} -//} - -///// All things that implement the required traits can be treated as `AudioComponent`. -//impl + Audio> AudioComponent for W {} - -///////// - -/* - -/// Trait for things that may expose JACK ports. -pub trait Ports { - fn audio_ins(&self) -> Usually>> { - Ok(vec![]) - } - fn audio_outs(&self) -> Usually>> { - Ok(vec![]) - } - fn midi_ins(&self) -> Usually>> { - Ok(vec![]) - } - fn midi_outs(&self) -> Usually>> { - Ok(vec![]) - } -} - -fn register_ports( - client: &Client, - names: Vec, - spec: T, -) -> Usually>> { - names - .into_iter() - .try_fold(BTreeMap::new(), |mut ports, name| { - let port = client.register_port(&name, spec)?; - ports.insert(name, port); - Ok(ports) - }) -} - -fn query_ports(client: &Client, names: Vec) -> BTreeMap> { - names.into_iter().fold(BTreeMap::new(), |mut ports, name| { - let port = client.port_by_name(&name).unwrap(); - ports.insert(name, port); - ports - }) -} - -*/ diff --git a/src/lib.rs b/src/lib.rs index 9b445a0e..e9e8b380 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,6 +24,8 @@ pub(crate) use ::tek_tui::{ buffer::Cell, } }; +pub use ::tek_jack; +pub(crate) use ::tek_jack::{*, jack::{*, contrib::*}}; pub(crate) use std::cmp::{Ord, Eq, PartialEq}; pub(crate) use std::collections::BTreeMap; @@ -40,14 +42,10 @@ pub(crate) use std::thread::{spawn, JoinHandle}; pub(crate) use std::time::Duration; pub mod arranger; pub use self::arranger::*; -pub mod clock; pub use self::clock::*; -pub mod field; pub use self::field::*; pub mod file; pub use self::file::*; pub mod focus; pub use self::focus::*; pub mod groovebox; pub use self::groovebox::*; -pub mod jack; pub use self::jack::*; pub mod meter; pub use self::meter::*; -pub mod midi; pub use self::midi::*; pub mod mixer; pub use self::mixer::*; pub mod piano; pub use self::piano::*; pub mod plugin; pub use self::plugin::*; @@ -55,9 +53,6 @@ pub mod pool; pub use self::pool::*; pub mod sampler; pub use self::sampler::*; pub mod sequencer; pub use self::sequencer::*; -pub use ::atomic_float; -pub(crate) use atomic_float::*; - pub use ::midly::{self, num::u7}; pub(crate) use ::midly::{ Smf, @@ -73,37 +68,6 @@ testmod! { test } ($($name:ident)*) => { $(#[cfg(test)] mod $name;)* }; } -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) } -} - #[derive(Default)] pub struct BigBuffer { pub width: usize, diff --git a/src/sequencer.rs b/src/sequencer.rs index de785aec..87c53cca 100644 --- a/src/sequencer.rs +++ b/src/sequencer.rs @@ -6,7 +6,7 @@ use MidiEditCommand::*; use PhrasePoolCommand::*; /// Root view for standalone `tek_sequencer`. pub struct Sequencer { - _jack: Arc>, + pub _jack: Arc>, pub pool: PoolModel, pub editor: MidiEditor, @@ -23,32 +23,6 @@ pub struct Sequencer { pub midi_buf: Vec>>, pub perf: PerfModel, } -impl Sequencer { - pub fn new (jack: &Arc>) -> Usually { - let clock = Clock::from(jack); - let phrase = Arc::new(RwLock::new(MidiClip::new( - "Clip", true, 4 * clock.timebase.ppq.get() as usize, - None, Some(ItemColor::random().into()) - ))); - Ok(Self { - _jack: jack.clone(), - - pool: PoolModel::from(&phrase), - editor: MidiEditor::from(&phrase), - player: MidiPlayer::from((&clock, &phrase)), - - compact: true, - transport: true, - selectors: true, - size: Measure::new(), - midi_buf: vec![vec![];65536], - note_buf: vec![], - perf: PerfModel::default(), - status: true, - clock, - }) - } -} render!(TuiOut: (self: Sequencer) => self.size.of( Bsp::s(self.toolbar_view(), Bsp::n(self.selector_view(), diff --git a/time/Cargo.toml b/time/Cargo.toml new file mode 100644 index 00000000..e0f204e7 --- /dev/null +++ b/time/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "tek_time" +edition = "2021" +version = "0.2.0" + +[dependencies] +tek_tui = { path = "../tui" } +tek_jack = { path = "../jack" } +atomic_float = "1.0.0" +quanta = "0.12.3" +#jack = { path = "../rust-jack" } +#midly = "0.5" + diff --git a/src/clock/clock_tui.rs b/time/src/clock_tui.rs similarity index 96% rename from src/clock/clock_tui.rs rename to time/src/clock_tui.rs index 781c4cbb..c5ad06bd 100644 --- a/src/clock/clock_tui.rs +++ b/time/src/clock_tui.rs @@ -1,23 +1,25 @@ use crate::*; use ClockCommand::{Play, Pause, SetBpm, SetQuant, SetSync}; -use FocusCommand::{Next, Prev}; +//use FocusCommand::{Next, Prev}; use KeyCode::{Enter, Left, Right, Char}; /// Transport clock app. pub struct TransportTui { pub jack: Arc>, pub clock: Clock, } +handle!(TuiIn: |self: TransportTui, input|ClockCommand::execute_with_state(self, input.event())); +keymap!(TRANSPORT_KEYS = |state: TransportTui, input: Event| ClockCommand { + key(Char(' ')) => + if state.clock().is_stopped() { Play(None) } else { Pause(None) }, + shift(key(Char(' '))) => + if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) } +}); has_clock!(|self: TransportTui|&self.clock); audio!(|self: TransportTui, client, scope|ClockAudio(self).process(client, scope)); render!(TuiOut: (self: TransportTui) => TransportView { compact: false, clock: &self.clock }); -impl TransportTui { - pub fn new (jack: &Arc>) -> Usually { - Ok(Self { jack: jack.clone(), clock: Clock::from(jack) }) - } -} pub struct TransportView<'a> { pub compact: bool, pub clock: &'a Clock } impl<'a> TransportView<'a> { @@ -103,13 +105,6 @@ render!(TuiOut: (self: OutputStats) => Either(self.compact, Bsp::e(Tui::fg(TuiTheme::g(255), format!("{:.3}ms", self.latency)), " latency"), ))); -handle!(TuiIn: |self: TransportTui, input|ClockCommand::execute_with_state(self, input.event())); -keymap!(TRANSPORT_KEYS = |state: TransportTui, input: Event| ClockCommand { - key(Char(' ')) => - if state.clock().is_stopped() { Play(None) } else { Pause(None) }, - shift(key(Char(' '))) => - if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) } -}); // TODO: //keymap!(TRANSPORT_BPM_KEYS = |state: Clock, input: Event| ClockCommand { //key(Char(',')) => SetBpm(state.bpm().get() - 1.0), diff --git a/src/clock.rs b/time/src/lib.rs similarity index 89% rename from src/clock.rs rename to time/src/lib.rs index 70caf676..8156a18c 100644 --- a/src/clock.rs +++ b/time/src/lib.rs @@ -1,14 +1,24 @@ -use crate::*; +mod clock_tui; pub use self::clock_tui::*; +mod microsecond; pub use self::microsecond::*; +mod moment; pub use self::moment::*; +mod perf; pub use self::perf::*; +mod pulse; pub use self::pulse::*; +mod sample_count; pub use self::sample_count::*; +mod sample_rate; pub use self::sample_rate::*; +mod timebase; pub use self::timebase::*; +mod unit; pub use self::unit::*; -pub mod clock_tui; pub use self::clock_tui::*; -pub mod microsecond; pub(crate) use self::microsecond::*; -pub mod moment; pub(crate) use self::moment::*; -pub mod perf; pub(crate) use self::perf::*; -pub mod pulse; pub(crate) use self::pulse::*; -pub mod sample_count; pub(crate) use self::sample_count::*; -pub mod sample_rate; pub(crate) use self::sample_rate::*; -pub mod timebase; pub(crate) use self::timebase::*; -pub mod unit; pub(crate) use self::unit::*; +pub(crate) use ::tek_jack::{*, jack::{*, contrib::*}}; +pub(crate) use std::sync::{Arc, Mutex, RwLock, atomic::{AtomicUsize, Ordering::*}}; +pub(crate) use std::ops::{Add, Sub, Mul, Div, Rem}; +pub use ::atomic_float; pub(crate) use atomic_float::*; +pub(crate) use ::tek_tui::{ + *, + tek_input::*, + tek_output::*, + crossterm::event::KeyCode, + ratatui::prelude::* +}; pub trait HasClock: Send + Sync { fn clock (&self) -> &Clock; @@ -201,12 +211,12 @@ impl Clock { Ok(()) } - pub fn bbt (&self) -> ::jack::contrib::PositionBBT { + pub fn bbt (&self) -> 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; - ::jack::contrib::PositionBBT { + PositionBBT { bar: 1 + bar, beat: 1 + (pulse / ppq) % 4, tick: (pulse % ppq), diff --git a/src/clock/microsecond.rs b/time/src/microsecond.rs similarity index 100% rename from src/clock/microsecond.rs rename to time/src/microsecond.rs diff --git a/src/clock/moment.rs b/time/src/moment.rs similarity index 100% rename from src/clock/moment.rs rename to time/src/moment.rs diff --git a/src/clock/perf.rs b/time/src/perf.rs similarity index 100% rename from src/clock/perf.rs rename to time/src/perf.rs diff --git a/src/clock/pulse.rs b/time/src/pulse.rs similarity index 100% rename from src/clock/pulse.rs rename to time/src/pulse.rs diff --git a/src/clock/sample_count.rs b/time/src/sample_count.rs similarity index 100% rename from src/clock/sample_count.rs rename to time/src/sample_count.rs diff --git a/src/clock/sample_rate.rs b/time/src/sample_rate.rs similarity index 100% rename from src/clock/sample_rate.rs rename to time/src/sample_rate.rs diff --git a/src/clock/timebase.rs b/time/src/timebase.rs similarity index 100% rename from src/clock/timebase.rs rename to time/src/timebase.rs diff --git a/src/clock/unit.rs b/time/src/unit.rs similarity index 100% rename from src/clock/unit.rs rename to time/src/unit.rs diff --git a/tui/src/lib.rs b/tui/src/lib.rs index f21ec9a2..940a2d8e 100644 --- a/tui/src/lib.rs +++ b/tui/src/lib.rs @@ -17,8 +17,9 @@ mod tui_color; pub use self::tui_color::*; mod tui_style; pub use self::tui_style::*; mod tui_theme; pub use self::tui_theme::*; mod tui_border; pub use self::tui_border::*; +mod tui_field; pub use self::tui_field::*; -pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicBool, Ordering::*}}; +pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicUsize, AtomicBool, Ordering::*}}; pub(crate) use std::io::{stdout, Stdout}; pub(crate) use std::error::Error; @@ -38,6 +39,37 @@ pub 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) } +} + pub use ::better_panic; pub(crate) use better_panic::{Settings, Verbosity}; pub use ::palette; pub(crate) use ::palette::{*, convert::*, okhsl::*}; diff --git a/src/field.rs b/tui/src/tui_field.rs similarity index 100% rename from src/field.rs rename to tui/src/tui_field.rs