From 8c37c95cc6ae2607d21962dbb00db992ebdb9f67 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sun, 10 Nov 2024 01:03:47 +0100 Subject: [PATCH] wip: refactor pt.4, reduce number of files --- Cargo.lock | 8 - Cargo.toml | 1 - crates/tek_api/src/api.rs | 0 crates/tek_api/src/api_cmd.rs | 77 --- crates/tek_api/src/api_edn.rs | 197 ------- crates/tek_api/src/api_jack.rs | 519 ++++++++++++++++++ crates/tek_api/src/arrange.rs | 65 +++ crates/tek_api/src/clock.rs | 36 ++ crates/tek_api/src/lib.rs | 303 +--------- crates/tek_api/src/mixer.rs | 17 + crates/tek_api/src/phrase.rs | 78 +++ crates/tek_api/src/plugin.rs | 190 +++++++ crates/tek_api/src/pool.rs | 17 + crates/tek_api/src/sample.rs | 72 +++ crates/tek_api/src/sampler.rs | 74 +++ crates/tek_api/src/scene.rs | 53 ++ .../src/sequencer.rs} | 49 +- crates/tek_api/src/track.rs | 86 +++ .../src/transport.rs} | 27 +- crates/tek_api/src/voice.rs | 0 crates/tek_cli/src/cli_arranger.rs | 32 +- crates/tek_cli/src/cli_sequencer.rs | 32 +- crates/tek_core/src/audio.rs | 508 ----------------- crates/tek_core/src/lib.rs | 2 +- crates/tek_snd/Cargo.toml | 8 - crates/tek_snd/README.md | 1 - crates/tek_snd/src/lib.rs | 10 - crates/tek_snd/src/snd_arranger.rs | 31 -- crates/tek_snd/src/snd_mixer.rs | 7 - crates/tek_snd/src/snd_plugin.rs | 59 -- crates/tek_snd/src/snd_sampler.rs | 11 - crates/tek_tui/src/arranger_cmd.rs | 101 ---- crates/tek_tui/src/arranger_tui.rs | 54 -- crates/tek_tui/src/lib.rs | 52 +- crates/tek_tui/src/mixer_tui.rs | 38 -- crates/tek_tui/src/plugin.rs | 33 -- crates/tek_tui/src/sampler_tui.rs | 98 ---- crates/tek_tui/src/sequencer.rs | 481 ---------------- crates/tek_tui/src/track_cli.rs | 6 - crates/tek_tui/src/transport.rs | 81 --- .../src/{arranger.rs => tui_arranger.rs} | 53 ++ ...rranger_tui_bar.rs => tui_arranger_bar.rs} | 0 ...rranger_tui_cmd.rs => tui_arranger_cmd.rs} | 110 ++++ ...rranger_tui_col.rs => tui_arranger_col.rs} | 0 ...rranger_tui_hor.rs => tui_arranger_hor.rs} | 0 ...rranger_tui_ver.rs => tui_arranger_ver.rs} | 0 crates/tek_tui/src/{mixer.rs => tui_mixer.rs} | 39 +- .../src/{mixer_cmd.rs => tui_mixer_cmd.rs} | 0 .../src/{plugin_tui.rs => tui_plugin.rs} | 32 ++ .../src/{plugin_cmd.rs => tui_plugin_cmd.rs} | 0 .../src/{plugin_lv2.rs => tui_plugin_lv2.rs} | 0 ...lugin_lv2_gui.rs => tui_plugin_lv2_gui.rs} | 18 - .../{plugin_vst2.rs => tui_plugin_vst2.rs} | 0 .../{plugin_vst3.rs => tui_plugin_vst3.rs} | 0 .../src/{sampler.rs => tui_sampler.rs} | 111 +++- .../{sampler_cmd.rs => tui_sampler_cmd.rs} | 0 .../{sequencer_tui.rs => tui_sequencer.rs} | 415 ++++++++++++++ ...{sequencer_cmd.rs => tui_sequencer_cmd.rs} | 0 .../{transport_tui.rs => tui_transport.rs} | 80 +++ ...{transport_cmd.rs => tui_transport_cmd.rs} | 0 60 files changed, 2185 insertions(+), 2187 deletions(-) create mode 100644 crates/tek_api/src/api.rs delete mode 100644 crates/tek_api/src/api_edn.rs create mode 100644 crates/tek_api/src/api_jack.rs create mode 100644 crates/tek_api/src/arrange.rs create mode 100644 crates/tek_api/src/clock.rs create mode 100644 crates/tek_api/src/mixer.rs create mode 100644 crates/tek_api/src/phrase.rs create mode 100644 crates/tek_api/src/plugin.rs create mode 100644 crates/tek_api/src/pool.rs create mode 100644 crates/tek_api/src/sample.rs create mode 100644 crates/tek_api/src/sampler.rs create mode 100644 crates/tek_api/src/scene.rs rename crates/{tek_snd/src/snd_sequencer.rs => tek_api/src/sequencer.rs} (88%) create mode 100644 crates/tek_api/src/track.rs rename crates/{tek_snd/src/snd_transport.rs => tek_api/src/transport.rs} (69%) create mode 100644 crates/tek_api/src/voice.rs delete mode 100644 crates/tek_snd/Cargo.toml delete mode 100644 crates/tek_snd/README.md delete mode 100644 crates/tek_snd/src/lib.rs delete mode 100644 crates/tek_snd/src/snd_arranger.rs delete mode 100644 crates/tek_snd/src/snd_mixer.rs delete mode 100644 crates/tek_snd/src/snd_plugin.rs delete mode 100644 crates/tek_snd/src/snd_sampler.rs delete mode 100644 crates/tek_tui/src/arranger_cmd.rs delete mode 100644 crates/tek_tui/src/arranger_tui.rs delete mode 100644 crates/tek_tui/src/mixer_tui.rs delete mode 100644 crates/tek_tui/src/plugin.rs delete mode 100644 crates/tek_tui/src/sampler_tui.rs delete mode 100644 crates/tek_tui/src/sequencer.rs delete mode 100644 crates/tek_tui/src/track_cli.rs delete mode 100644 crates/tek_tui/src/transport.rs rename crates/tek_tui/src/{arranger.rs => tui_arranger.rs} (94%) rename crates/tek_tui/src/{arranger_tui_bar.rs => tui_arranger_bar.rs} (100%) rename crates/tek_tui/src/{arranger_tui_cmd.rs => tui_arranger_cmd.rs} (67%) rename crates/tek_tui/src/{arranger_tui_col.rs => tui_arranger_col.rs} (100%) rename crates/tek_tui/src/{arranger_tui_hor.rs => tui_arranger_hor.rs} (100%) rename crates/tek_tui/src/{arranger_tui_ver.rs => tui_arranger_ver.rs} (100%) rename crates/tek_tui/src/{mixer.rs => tui_mixer.rs} (79%) rename crates/tek_tui/src/{mixer_cmd.rs => tui_mixer_cmd.rs} (100%) rename crates/tek_tui/src/{plugin_tui.rs => tui_plugin.rs} (72%) rename crates/tek_tui/src/{plugin_cmd.rs => tui_plugin_cmd.rs} (100%) rename crates/tek_tui/src/{plugin_lv2.rs => tui_plugin_lv2.rs} (100%) rename crates/tek_tui/src/{plugin_lv2_gui.rs => tui_plugin_lv2_gui.rs} (73%) rename crates/tek_tui/src/{plugin_vst2.rs => tui_plugin_vst2.rs} (100%) rename crates/tek_tui/src/{plugin_vst3.rs => tui_plugin_vst3.rs} (100%) rename crates/tek_tui/src/{sampler.rs => tui_sampler.rs} (79%) rename crates/tek_tui/src/{sampler_cmd.rs => tui_sampler_cmd.rs} (100%) rename crates/tek_tui/src/{sequencer_tui.rs => tui_sequencer.rs} (58%) rename crates/tek_tui/src/{sequencer_cmd.rs => tui_sequencer_cmd.rs} (100%) rename crates/tek_tui/src/{transport_tui.rs => tui_transport.rs} (54%) rename crates/tek_tui/src/{transport_cmd.rs => tui_transport_cmd.rs} (100%) diff --git a/Cargo.lock b/Cargo.lock index 696dee05..d347c250 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2697,14 +2697,6 @@ dependencies = [ "toml", ] -[[package]] -name = "tek_snd" -version = "0.1.0" -dependencies = [ - "tek_api", - "tek_core", -] - [[package]] name = "tek_tui" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 72911332..a4b7de76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,6 @@ resolver = "2" members = [ "crates/tek_core", - "crates/tek_snd", "crates/tek_api", "crates/tek_cli", "crates/tek_tui" diff --git a/crates/tek_api/src/api.rs b/crates/tek_api/src/api.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/tek_api/src/api_cmd.rs b/crates/tek_api/src/api_cmd.rs index 9bfd2c44..1ad131fe 100644 --- a/crates/tek_api/src/api_cmd.rs +++ b/crates/tek_api/src/api_cmd.rs @@ -1,68 +1,5 @@ use crate::*; -#[derive(Clone)] -pub enum ArrangerCommand { - Focus(FocusCommand), - Transport(TransportCommand), - Phrases(PhrasePoolCommand), - Editor(PhraseEditorCommand), - Arrangement(ArrangementCommand), - EditPhrase(Option>>), -} - -#[derive(Clone)] -pub enum ArrangementCommand { - New, - Load, - Save, - ToggleViewMode, - Delete, - Activate, - Increment, - Decrement, - ZoomIn, - ZoomOut, - Go(Direction), - Edit(Option>>), - Scene(SceneCommand), - Track(TrackCommand), - Clip(ClipCommand), -} - -#[derive(Clone)] -pub enum SceneCommand { - Next, - Prev, - Add, - Delete, - MoveForward, - MoveBack, - RandomColor, - SetSize(usize), - SetZoom(usize), -} - -#[derive(Clone)] -pub enum TrackCommand { - Next, - Prev, - Add, - Delete, - MoveForward, - MoveBack, - RandomColor, - SetSize(usize), - SetZoom(usize), -} - -#[derive(Clone)] -pub enum ClipCommand { - SetLoop(bool), - Get(usize, usize), - Put(usize, usize, Option>>), - Edit(Option>>), -} - #[derive(Clone, PartialEq)] pub enum SequencerCommand { Focus(FocusCommand), @@ -127,17 +64,3 @@ pub enum PhraseEditorCommand { TimeZoomSet(usize), Go(Direction), } - -#[derive(Copy, Clone, PartialEq)] -pub enum TransportCommand { - FocusNext, - FocusPrev, - Play(Option), - Pause(Option), - SeekUsec(f64), - SeekSample(f64), - SeekPulse(f64), - SetBpm(f64), - SetQuant(f64), - SetSync(f64), -} diff --git a/crates/tek_api/src/api_edn.rs b/crates/tek_api/src/api_edn.rs deleted file mode 100644 index f6b196e7..00000000 --- a/crates/tek_api/src/api_edn.rs +++ /dev/null @@ -1,197 +0,0 @@ -use crate::*; -use crate::midly::num::u7; - -impl Scene { - pub fn from_edn <'a, 'e> (args: &[Edn<'e>]) -> Usually { - let mut name = None; - let mut clips = vec![]; - edn!(edn in args { - Edn::Map(map) => { - let key = map.get(&Edn::Key(":name")); - if let Some(Edn::Str(n)) = key { - name = Some(*n); - } else { - panic!("unexpected key in scene '{name:?}': {key:?}") - } - }, - Edn::Symbol("_") => { - clips.push(None); - }, - Edn::Int(i) => { - clips.push(Some(*i as usize)); - }, - _ => panic!("unexpected in scene '{name:?}': {edn:?}") - }); - Ok(Scene { - name: Arc::new(name.unwrap_or("").to_string().into()), - color: ItemColor::random(), - clips, - }) - } -} - - -impl MixerTrack { - const SYM_NAME: &'static str = ":name"; - const SYM_GAIN: &'static str = ":gain"; - const SYM_SAMPLER: &'static str = "sampler"; - const SYM_LV2: &'static str = "lv2"; - pub fn from_edn <'a, 'e> (jack: &Arc>, args: &[Edn<'e>]) -> Usually { - let mut _gain = 0.0f64; - let mut track = MixerTrack { - name: String::new(), - ports: JackPorts::default(), - devices: vec![], - }; - #[allow(unused_mut)] - let mut devices: Vec = vec![]; - edn!(edn in args { - Edn::Map(map) => { - if let Some(Edn::Str(n)) = map.get(&Edn::Key(Self::SYM_NAME)) { - track.name = n.to_string(); - } - if let Some(Edn::Double(g)) = map.get(&Edn::Key(Self::SYM_GAIN)) { - _gain = f64::from(*g); - } - }, - Edn::List(args) => match args.get(0) { - // Add a sampler device to the track - Some(Edn::Symbol(Self::SYM_SAMPLER)) => { - devices.push(Sampler::from_edn(jack, &args[1..])?); - panic!( - "unsupported in track {}: {:?}; tek_mixer not compiled with feature \"sampler\"", - &track.name, - args.get(0).unwrap() - ) - }, - // Add a LV2 plugin to the track. - Some(Edn::Symbol(Self::SYM_LV2)) => { - devices.push(LV2Plugin::from_edn(jack, &args[1..])?); - panic!( - "unsupported in track {}: {:?}; tek_mixer not compiled with feature \"plugin\"", - &track.name, - args.get(0).unwrap() - ) - }, - None => - panic!("empty list track {}", &track.name), - _ => - panic!("unexpected in track {}: {:?}", &track.name, args.get(0).unwrap()) - }, - _ => {} - }); - for device in devices { - track.add_device(device); - } - Ok(track) - } -} - -impl LV2Plugin { - pub fn from_edn <'e> (jack: &Arc>, args: &[Edn<'e>]) -> Usually { - let mut name = String::new(); - let mut path = String::new(); - edn!(edn in args { - Edn::Map(map) => { - if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { - name = String::from(*n); - } - if let Some(Edn::Str(p)) = map.get(&Edn::Key(":path")) { - path = String::from(*p); - } - }, - _ => panic!("unexpected in lv2 '{name}'"), - }); - Plugin::new_lv2(jack, &name, &path) - } -} - -impl Sampler { - pub fn from_edn <'e> (jack: &Arc>, args: &[Edn<'e>]) -> Usually { - let mut name = String::new(); - let mut dir = String::new(); - let mut samples = BTreeMap::new(); - edn!(edn in args { - Edn::Map(map) => { - if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { - name = String::from(*n); - } - if let Some(Edn::Str(n)) = map.get(&Edn::Key(":dir")) { - dir = String::from(*n); - } - }, - Edn::List(args) => match args.get(0) { - Some(Edn::Symbol("sample")) => { - let (midi, sample) = Sample::from_edn(jack, &dir, &args[1..])?; - if let Some(midi) = midi { - samples.insert(midi, sample); - } else { - panic!("sample without midi binding: {}", sample.read().unwrap().name); - } - }, - _ => panic!("unexpected in sampler {name}: {args:?}") - }, - _ => panic!("unexpected in sampler {name}: {edn:?}") - }); - Ok(Sampler { - jack: jack.clone(), - name: name.into(), - mapped: samples, - unmapped: Default::default(), - voices: Default::default(), - ports: Default::default(), - buffer: Default::default(), - output_gain: 0. - }) - } -} - -impl Sample { - pub fn from_edn <'e> (jack: &Arc>, dir: &str, args: &[Edn<'e>]) -> Usually<(Option, Arc>)> { - let mut name = String::new(); - let mut file = String::new(); - let mut midi = None; - let mut start = 0usize; - edn!(edn in args { - Edn::Map(map) => { - if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { - name = String::from(*n); - } - if let Some(Edn::Str(f)) = map.get(&Edn::Key(":file")) { - file = String::from(*f); - } - if let Some(Edn::Int(i)) = map.get(&Edn::Key(":start")) { - start = *i as usize; - } - if let Some(Edn::Int(m)) = map.get(&Edn::Key(":midi")) { - midi = Some(u7::from(*m as u8)); - } - }, - _ => panic!("unexpected in sample {name}"), - }); - let (end, data) = Sample::read_data(&format!("{dir}/{file}"))?; - Ok((midi, Arc::new(RwLock::new(Self { - name: name.into(), - start, - end, - channels: data, - rate: None - })))) - } - - /// Read WAV from file - pub fn read_data (src: &str) -> Usually<(usize, Vec>)> { - let mut channels: Vec> = vec![]; - for channel in wavers::Wav::from_path(src)?.channels() { - channels.push(channel); - } - let mut end = 0; - let mut data: Vec> = vec![]; - for samples in channels.iter() { - let channel = Vec::from(samples.as_ref()); - end = end.max(channel.len()); - data.push(channel); - } - Ok((end, data)) - } -} diff --git a/crates/tek_api/src/api_jack.rs b/crates/tek_api/src/api_jack.rs new file mode 100644 index 00000000..ed8cd72b --- /dev/null +++ b/crates/tek_api/src/api_jack.rs @@ -0,0 +1,519 @@ +use crate::*; +use tek_core::jack::*; + +/// Trait for things that have 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 + } + } +} + +/// 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 wrap a JACK client. +pub trait AudioEngine { + fn activate ( + self, + process: impl FnMut(&Arc>, &Client, &ProcessScope) -> Control + Send + 'static + ) -> Usually>> where Self: Send + Sync + 'static; + fn client (&self) -> &Client; + fn transport (&self) -> Transport { + self.client().transport() + } + fn port_by_name (&self, name: &str) -> Option> { + self.client().port_by_name(name) + } + fn register_port (&self, name: &str, spec: PS) -> Usually> { + Ok(self.client().register_port(name, spec)?) + } + fn thread_init (&self, _: &Client) {} + unsafe fn shutdown (&mut self, status: ClientStatus, reason: &str) {} + fn freewheel (&mut self, _: &Client, enabled: bool) {} + fn client_registration (&mut self, _: &Client, name: &str, reg: bool) {} + fn port_registration (&mut self, _: &Client, id: PortId, reg: bool) {} + fn ports_connected (&mut self, _: &Client, a: PortId, b: PortId, are: bool) {} + fn sample_rate (&mut self, _: &Client, frames: Frames) -> Control { + Control::Continue + } + fn port_rename (&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control { + Control::Continue + } + fn graph_reorder (&mut self, _: &Client) -> Control { + Control::Continue + } + fn xrun (&mut self, _: &Client) -> Control { + Control::Continue + } +} + +/// Wraps [Client] or [DynamicAsyncClient] in place. +#[derive(Debug)] +pub enum JackClient { + /// Before activation. + Inactive(Client), + /// During activation. + Activating, + /// After activation. Must not be dropped for JACK thread to persist. + Active(DynamicAsyncClient), +} + +pub type DynamicAsyncClient = AsyncClient; + +pub type DynamicAudioHandler = contrib::ClosureProcessHandler<(), BoxedAudioHandler>; + +pub type BoxedAudioHandler = Box Control + Send>; + +impl JackClient { + pub fn new (name: &str) -> Usually { + let (client, _) = Client::new(name, ClientOptions::NO_START_SERVER)?; + Ok(Self::Inactive(client)) + } + 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 = contrib::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)?); + Ok(target) + } +} + +impl From for Client { + fn from (jack: JackClient) -> Client { + match jack { + JackClient::Inactive(client) => client, + JackClient::Activating => panic!("jack client still activating"), + JackClient::Active(_) => panic!("jack client already activated"), + } + } +} + +impl AudioEngine for JackClient { + 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(), + } + } + 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 = contrib::ClosureProcessHandler::new(frame as BoxedAudioHandler); + *state.write().unwrap() = Self::Active(client.activate_async(events, frames)?); + Ok(state) + } +} + +#[derive(Debug)] +/// Event enum for JACK events. +pub enum JackEvent { + ThreadInit, + Shutdown(ClientStatus, String), + Freewheel(bool), + SampleRate(Frames), + ClientRegistration(String, bool), + PortRegistration(PortId, bool), + PortRename(PortId, String, String), + PortsConnected(PortId, PortId, bool), + GraphReorder, + XRun, +} + +/// 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 + }) +} + +/// 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); + +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 Widget for JackDevice { + //type Engine = E; + //fn layout(&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), + //contrib::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 + //} +//} diff --git a/crates/tek_api/src/arrange.rs b/crates/tek_api/src/arrange.rs new file mode 100644 index 00000000..d9782351 --- /dev/null +++ b/crates/tek_api/src/arrange.rs @@ -0,0 +1,65 @@ +use scene::*; + +pub struct Arrangement { + /// JACK client handle (needs to not be dropped for standalone mode to work). + pub jack: Arc>, + /// Global timebase + pub clock: Arc, + /// Name of arranger + pub name: Arc>, + /// Collection of phrases. + pub phrases: Arc>>, + /// Collection of tracks. + pub tracks: Vec, + /// Collection of scenes. + pub scenes: Vec, +} + +impl Audio for Arrangement { + fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { + for track in self.tracks.iter_mut() { + track.player.process(client, scope); + } + Control::Continue + } +} + +#[derive(Clone)] +pub enum ArrangementCommand { + New, + Load, + Save, + ToggleViewMode, + Delete, + Activate, + Increment, + Decrement, + ZoomIn, + ZoomOut, + Go(Direction), + Edit(Option>>), + Scene(SceneCommand), + Track(TrackCommand), + Clip(ClipCommand), +} + +#[derive(Clone)] +pub enum ArrangementTrackCommand { + Next, + Prev, + Add, + Delete, + MoveForward, + MoveBack, + RandomColor, + SetSize(usize), + SetZoom(usize), +} + +#[derive(Clone)] +pub enum ArrangementClipCommand { + SetLoop(bool), + Get(usize, usize), + Put(usize, usize, Option>>), + Edit(Option>>), +} diff --git a/crates/tek_api/src/clock.rs b/crates/tek_api/src/clock.rs new file mode 100644 index 00000000..aa4bfe4b --- /dev/null +++ b/crates/tek_api/src/clock.rs @@ -0,0 +1,36 @@ +use crate::*; + +/// A timer with starting point, current time, and quantization +#[derive(Default, Debug)] +pub struct Clock { + /// Playback state + pub playing: RwLock>, + /// Global sample and usec at which playback started + pub started: RwLock>, + /// Current moment in time + pub current: Instant, + /// Note quantization factor + pub quant: Quantize, + /// Launch quantization factor + pub sync: LaunchSync, +} + +impl Clock { + #[inline] pub fn timebase (&self) -> &Arc { + &self.current.timebase + } + #[inline] pub fn pulse (&self) -> f64 { + self.current.pulse.get() + } + #[inline] pub fn quant (&self) -> f64 { + self.quant.get() + } + #[inline] pub fn sync (&self) -> f64 { + self.sync.get() + } + #[inline] pub fn next_launch_pulse (&self) -> usize { + let sync = self.sync.get() as usize; + let pulse = self.current.pulse.get() as usize; + if pulse % sync == 0 { pulse } else { (pulse / sync + 1) * sync } + } +} diff --git a/crates/tek_api/src/lib.rs b/crates/tek_api/src/lib.rs index 68d10c70..4be2cb82 100644 --- a/crates/tek_api/src/lib.rs +++ b/crates/tek_api/src/lib.rs @@ -1,292 +1,23 @@ pub(crate) use tek_core::*; -pub(crate) use tek_core::jack::{TransportState, Port, MidiIn, MidiOut}; -pub(crate) use tek_core::midly::{MidiMessage, num::u7}; +pub(crate) use tek_core::jack::*; +pub(crate) use tek_core::midly::{*, live::LiveEvent, num::u7}; pub(crate) use std::thread::JoinHandle; +pub(crate) use std::fmt::{Debug, Formatter, Error}; submod! { + clock + mixer + phrase + plugin + pool + sampler + sample + scene + sequencer + track + transport + voice + api_cmd - api_edn -} - -/// A timer with starting point, current time, and quantization -#[derive(Default, Debug)] -pub struct Clock { - /// Playback state - pub playing: RwLock>, - /// Global sample and usec at which playback started - pub started: RwLock>, - /// Current moment in time - pub current: Instant, - /// Note quantization factor - pub quant: Quantize, - /// Launch quantization factor - pub sync: LaunchSync, -} - -impl Clock { - #[inline] pub fn timebase (&self) -> &Arc { - &self.current.timebase - } - #[inline] pub fn pulse (&self) -> f64 { - self.current.pulse.get() - } - #[inline] pub fn quant (&self) -> f64 { - self.quant.get() - } - #[inline] pub fn sync (&self) -> f64 { - self.sync.get() - } - #[inline] pub fn next_launch_pulse (&self) -> usize { - let sync = self.sync.get() as usize; - let pulse = self.current.pulse.get() as usize; - if pulse % sync == 0 { pulse } else { (pulse / sync + 1) * sync } - } -} - -pub struct TransportToolbar { - pub jack: Arc>, - /// JACK transport handle. - pub transport: jack::Transport, - /// Current sample rate, tempo, and PPQ. - pub clock: Arc, - /// Enable metronome? - pub metronome: bool, -} - -pub struct Arrangement { - /// JACK client handle (needs to not be dropped for standalone mode to work). - pub jack: Arc>, - /// Global timebase - pub clock: Arc, - /// Name of arranger - pub name: Arc>, - /// Collection of phrases. - pub phrases: Arc>>, - /// Collection of tracks. - pub tracks: Vec, - /// Collection of scenes. - pub scenes: Vec, -} - -#[derive(Debug)] -pub struct Sequencer { - /// Name of track - pub name: Arc>, - /// Preferred width of track column - pub width: usize, - /// Identifying color of track - pub color: ItemColor, - /// MIDI player/recorder - pub player: PhrasePlayer, -} - -/// Phrase player. -#[derive(Debug)] -pub struct PhrasePlayer { - /// Global timebase - pub clock: Arc, - /// Start time and phrase being played - pub phrase: Option<(Instant, Option>>)>, - /// Start time and next phrase - pub next_phrase: Option<(Instant, 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_inputs: Vec>, - /// Play from current sequence to MIDI ports - pub midi_outputs: Vec>, - /// MIDI output buffer - pub midi_note: Vec, - /// MIDI output buffer - pub midi_chunk: Vec>>, - /// Notes currently held at input - pub notes_in: Arc>, - /// Notes currently held at output - pub notes_out: Arc>, -} - -#[derive(Default, Debug, Clone)] -pub struct Scene { - /// Name of scene - pub name: Arc>, - /// Clips in scene, one per track - pub clips: Vec>>>, - /// Identifying color of scene - pub color: ItemColor, -} - -/// A MIDI sequence. -#[derive(Debug, Clone, Default)] -pub struct Phrase { - pub uuid: uuid::Uuid, - /// Name of phrase - pub name: String, - /// Temporal resolution in pulses per quarter note - pub ppq: usize, - /// Length of phrase in pulses - pub length: usize, - /// Notes in phrase - pub notes: PhraseData, - /// Whether to loop the phrase or play it once - pub loop_on: bool, - /// Start of loop - pub loop_start: usize, - /// Length of loop - pub loop_length: usize, - /// All notes are displayed with minimum length - pub percussive: bool, - /// Identifying color of phrase - pub color: ItemColorTriplet, -} - -/// MIDI message structural -pub type PhraseData = Vec>; - -#[derive(Debug)] -pub struct Mixer { - /// JACK client handle (needs to not be dropped for standalone mode to work). - pub jack: Arc>, - pub name: String, - pub tracks: Vec, - pub selected_track: usize, - pub selected_column: usize, -} - -/// A mixer track. -#[derive(Debug)] -pub struct MixerTrack { - pub name: String, - /// Inputs and outputs of 1st and last device - pub ports: JackPorts, - /// Device chain - pub devices: Vec, -} - -/// The sampler plugin plays sounds. -#[derive(Debug)] -pub struct Sampler { - pub jack: Arc>, - pub name: String, - pub mapped: BTreeMap>>, - pub unmapped: Vec>>, - pub voices: Arc>>, - pub ports: JackPorts, - pub buffer: Vec>, - pub output_gain: f32 -} - -/// A sound sample. -#[derive(Default, Debug)] -pub struct Sample { - pub name: String, - pub start: usize, - pub end: usize, - pub channels: Vec>, - pub rate: Option, -} - -/// A currently playing instance of a sample. -#[derive(Default, Debug, Clone)] -pub struct Voice { - pub sample: Arc>, - pub after: usize, - pub position: usize, - pub velocity: f32, -} - -/// A plugin device. -#[derive(Debug)] -pub struct Plugin { - /// JACK client handle (needs to not be dropped for standalone mode to work). - pub jack: Arc>, - pub name: String, - pub path: Option, - pub plugin: Option, - pub selected: usize, - pub mapping: bool, - pub ports: JackPorts, -} -impl Plugin { - pub fn new_lv2 ( - jack: &Arc>, - name: &str, - path: &str, - ) -> Usually { - let plugin = LV2Plugin::new(path)?; - jack_from_lv2(name, &plugin.plugin)?.run(|ports|Box::new(Self { - jack: jack.clone(), - name: name.into(), - path: Some(String::from(path)), - plugin: Some(PluginKind::LV2(plugin)), - selected: 0, - mapping: false, - ports - })) - } -} - -/// Supported plugin formats. -#[derive(Default)] -pub enum PluginKind { - #[default] None, - LV2(LV2Plugin), - VST2 { instance: ::vst::host::PluginInstance }, - VST3, -} -impl std::fmt::Debug for PluginKind { - fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { - write!(f, "{}", match self { - Self::None => "(none)", - Self::LV2(_) => "LV2", - Self::VST2{..} => "VST2", - Self::VST3 => "VST3", - }) - } -} - -/// A LV2 plugin. -#[derive(Debug)] -pub struct LV2Plugin { - pub world: livi::World, - pub instance: livi::Instance, - pub plugin: livi::Plugin, - pub features: Arc, - pub port_list: Vec, - pub input_buffer: Vec, - pub ui_thread: Option>, -} - -impl LV2Plugin { - const INPUT_BUFFER: usize = 1024; - pub fn new (uri: &str) -> Usually { - let world = livi::World::with_load_bundle(&uri); - let features = world - .build_features(livi::FeaturesBuilder { - min_block_length: 1, - max_block_length: 65536, - }); - let plugin = world - .iter_plugins() - .nth(0) - .expect(&format!("plugin not found: {uri}")); - Ok(Self { - instance: unsafe { - plugin - .instantiate(features.clone(), 48000.0) - .expect(&format!("instantiate failed: {uri}")) - }, - port_list: plugin.ports().collect::>(), - input_buffer: Vec::with_capacity(Self::INPUT_BUFFER), - ui_thread: None, - world, - features, - plugin, - }) - } + api_jack } diff --git a/crates/tek_api/src/mixer.rs b/crates/tek_api/src/mixer.rs new file mode 100644 index 00000000..7f05914e --- /dev/null +++ b/crates/tek_api/src/mixer.rs @@ -0,0 +1,17 @@ +use crate::*; + +#[derive(Debug)] +pub struct Mixer { + /// JACK client handle (needs to not be dropped for standalone mode to work). + pub jack: Arc>, + pub name: String, + pub tracks: Vec, + pub selected_track: usize, + pub selected_column: usize, +} + +impl Audio for Mixer { + fn process (&mut self, _: &Client, _: &ProcessScope) -> Control { + Control::Continue + } +} diff --git a/crates/tek_api/src/phrase.rs b/crates/tek_api/src/phrase.rs new file mode 100644 index 00000000..7420eede --- /dev/null +++ b/crates/tek_api/src/phrase.rs @@ -0,0 +1,78 @@ +use crate::*; + +/// A MIDI sequence. +#[derive(Debug, Clone, Default)] +pub struct Phrase { + pub uuid: uuid::Uuid, + /// Name of phrase + pub name: String, + /// Temporal resolution in pulses per quarter note + pub ppq: usize, + /// Length of phrase in pulses + pub length: usize, + /// Notes in phrase + pub notes: PhraseData, + /// Whether to loop the phrase or play it once + pub loop_on: bool, + /// Start of loop + pub loop_start: usize, + /// Length of loop + pub loop_length: usize, + /// All notes are displayed with minimum length + pub percussive: bool, + /// Identifying color of phrase + pub color: ItemColorTriplet, +} + +/// MIDI message structural +pub type PhraseData = Vec>; + +impl Phrase { + pub fn new ( + name: impl AsRef, + loop_on: bool, + length: usize, + notes: Option, + color: Option, + ) -> Self { + Self { + uuid: uuid::Uuid::new_v4(), + name: name.as_ref().to_string(), + ppq: PPQ, + length, + notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]), + loop_on, + loop_start: 0, + loop_length: length, + percussive: true, + color: color.unwrap_or_else(ItemColorTriplet::random) + } + } + pub fn duplicate (&self) -> Self { + let mut clone = self.clone(); + clone.uuid = uuid::Uuid::new_v4(); + clone + } + pub fn toggle_loop (&mut self) { self.loop_on = !self.loop_on; } + pub fn record_event (&mut self, pulse: usize, message: MidiMessage) { + if pulse >= self.length { panic!("extend phrase first") } + self.notes[pulse].push(message); + } + /// Check if a range `start..end` contains MIDI Note On `k` + pub fn contains_note_on (&self, k: u7, start: usize, end: usize) -> bool { + //panic!("{:?} {start} {end}", &self); + for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() { + for event in events.iter() { + if let MidiMessage::NoteOn {key,..} = event { if *key == k { return true } } + } + } + return false + } +} +impl Default for Phrase { + fn default () -> Self { + Self::new("(empty)", false, 0, None, Some(ItemColor::from(Color::Rgb(0, 0, 0)).into())) + } +} +impl PartialEq for Phrase { fn eq (&self, other: &Self) -> bool { self.uuid == other.uuid } } +impl Eq for Phrase {} diff --git a/crates/tek_api/src/plugin.rs b/crates/tek_api/src/plugin.rs new file mode 100644 index 00000000..58efc0f7 --- /dev/null +++ b/crates/tek_api/src/plugin.rs @@ -0,0 +1,190 @@ +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: Arc>, + pub name: String, + pub path: Option, + pub plugin: Option, + pub selected: usize, + pub mapping: bool, +} +impl Plugin { + pub fn new_lv2 ( + jack: &Arc>, + name: &str, + path: &str, + ) -> Usually { + Ok(Self { + jack: jack.clone(), + name: name.into(), + path: Some(String::from(path)), + plugin: Some(PluginKind::LV2(LV2Plugin::new(path)?)), + selected: 0, + mapping: false, + }) + } + + //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) + //} +} + +/// 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<'_>) -> Result<(), Error> { + write!(f, "{}", match self { + Self::None => "(none)", + Self::LV2(_) => "LV2", + Self::VST2{..} => "VST2", + Self::VST3 => "VST3", + }) + } +} + +/// A LV2 plugin. +#[derive(Debug)] +pub struct LV2Plugin { + pub world: livi::World, + pub instance: livi::Instance, + pub plugin: livi::Plugin, + pub features: Arc, + pub port_list: Vec, + pub input_buffer: Vec, + pub ui_thread: Option>, +} + +impl LV2Plugin { + const INPUT_BUFFER: usize = 1024; + pub fn new (uri: &str) -> Usually { + let world = livi::World::with_load_bundle(&uri); + let features = world + .build_features(livi::FeaturesBuilder { + min_block_length: 1, + max_block_length: 65536, + }); + let plugin = world + .iter_plugins() + .nth(0) + .expect(&format!("plugin not found: {uri}")); + Ok(Self { + instance: unsafe { + plugin + .instantiate(features.clone(), 48000.0) + .expect(&format!("instantiate failed: {uri}")) + }, + port_list: plugin.ports().collect::>(), + input_buffer: Vec::with_capacity(Self::INPUT_BUFFER), + ui_thread: None, + world, + features, + plugin, + }) + } +} + +impl LV2Plugin { + pub fn from_edn <'e> (jack: &Arc>, args: &[Edn<'e>]) -> Usually { + let mut name = String::new(); + let mut path = String::new(); + edn!(edn in args { + Edn::Map(map) => { + if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { + name = String::from(*n); + } + if let Some(Edn::Str(p)) = map.get(&Edn::Key(":path")) { + path = String::from(*p); + } + }, + _ => panic!("unexpected in lv2 '{name}'"), + }); + Plugin::new_lv2(jack, &name, &path) + } +} + +impl Audio for LV2Plugin { + fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { + Control::Continue + } +} + +impl Audio for Plugin { + fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { + match self.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 self.ports.midi_ins.values() { + 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 self.ports.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( + self.ports.audio_ins.values().map(|o|o.as_slice(scope)) + ) + .with_audio_outputs( + self.ports.audio_outs.values_mut().map(|o|o.as_mut_slice(scope)) + ); + unsafe { + instance.run(scope.n_frames() as usize, ports).unwrap() + }; + }, + _ => {} + } + Control::Continue + } +} diff --git a/crates/tek_api/src/pool.rs b/crates/tek_api/src/pool.rs new file mode 100644 index 00000000..e6481341 --- /dev/null +++ b/crates/tek_api/src/pool.rs @@ -0,0 +1,17 @@ +use crate::*; + +/// Contains all phrases in a project +pub struct PhrasePool { + /// Scroll offset + pub scroll: usize, + /// Highlighted phrase + pub phrase: usize, + /// Phrases in the pool + pub phrases: Vec>>, + /// Mode switch + pub mode: Option, + /// Whether this widget is focused + pub focused: bool, + /// Whether this widget is entered + pub entered: bool, +} diff --git a/crates/tek_api/src/sample.rs b/crates/tek_api/src/sample.rs new file mode 100644 index 00000000..aa85676e --- /dev/null +++ b/crates/tek_api/src/sample.rs @@ -0,0 +1,72 @@ +use crate::*; + +/// A sound sample. +#[derive(Default, Debug)] +pub struct Sample { + pub name: String, + pub start: usize, + pub end: usize, + pub channels: Vec>, + pub rate: Option, +} + +impl Sample { + pub fn new (name: &str, start: usize, end: usize, channels: Vec>) -> Self { + Self { name: name.to_string(), start, end, channels, rate: None } + } + pub fn play (sample: &Arc>, after: usize, velocity: &u7) -> Voice { + Voice { + sample: sample.clone(), + after, + position: sample.read().unwrap().start, + velocity: velocity.as_int() as f32 / 127.0, + } + } + pub fn from_edn <'e> (jack: &Arc>, dir: &str, args: &[Edn<'e>]) -> Usually<(Option, Arc>)> { + let mut name = String::new(); + let mut file = String::new(); + let mut midi = None; + let mut start = 0usize; + edn!(edn in args { + Edn::Map(map) => { + if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { + name = String::from(*n); + } + if let Some(Edn::Str(f)) = map.get(&Edn::Key(":file")) { + file = String::from(*f); + } + if let Some(Edn::Int(i)) = map.get(&Edn::Key(":start")) { + start = *i as usize; + } + if let Some(Edn::Int(m)) = map.get(&Edn::Key(":midi")) { + midi = Some(u7::from(*m as u8)); + } + }, + _ => panic!("unexpected in sample {name}"), + }); + let (end, data) = Sample::read_data(&format!("{dir}/{file}"))?; + Ok((midi, Arc::new(RwLock::new(Self { + name: name.into(), + start, + end, + channels: data, + rate: None + })))) + } + + /// Read WAV from file + pub fn read_data (src: &str) -> Usually<(usize, Vec>)> { + let mut channels: Vec> = vec![]; + for channel in wavers::Wav::from_path(src)?.channels() { + channels.push(channel); + } + let mut end = 0; + let mut data: Vec> = vec![]; + for samples in channels.iter() { + let channel = Vec::from(samples.as_ref()); + end = end.max(channel.len()); + data.push(channel); + } + Ok((end, data)) + } +} diff --git a/crates/tek_api/src/sampler.rs b/crates/tek_api/src/sampler.rs new file mode 100644 index 00000000..14f1c024 --- /dev/null +++ b/crates/tek_api/src/sampler.rs @@ -0,0 +1,74 @@ +use crate::*; + +/// The sampler plugin plays sounds. +#[derive(Debug)] +pub struct Sampler { + pub jack: Arc>, + pub name: String, + pub mapped: BTreeMap>>, + pub unmapped: Vec>>, + pub voices: Arc>>, + pub midi_in: Port, + pub audio_outs: Vec>, + pub buffer: Vec>, + pub output_gain: f32 +} + +impl Audio for Sampler { + fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { + self.process_midi_in(scope); + self.clear_output_buffer(); + self.process_audio_out(scope); + self.write_output_buffer(scope); + Control::Continue + } +} + +impl Sampler { + pub fn from_edn <'e> (jack: &Arc>, args: &[Edn<'e>]) -> Usually { + let mut name = String::new(); + let mut dir = String::new(); + let mut samples = BTreeMap::new(); + edn!(edn in args { + Edn::Map(map) => { + if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { + name = String::from(*n); + } + if let Some(Edn::Str(n)) = map.get(&Edn::Key(":dir")) { + dir = String::from(*n); + } + }, + Edn::List(args) => match args.get(0) { + Some(Edn::Symbol("sample")) => { + let (midi, sample) = Sample::from_edn(jack, &dir, &args[1..])?; + if let Some(midi) = midi { + samples.insert(midi, sample); + } else { + panic!("sample without midi binding: {}", sample.read().unwrap().name); + } + }, + _ => panic!("unexpected in sampler {name}: {args:?}") + }, + _ => panic!("unexpected in sampler {name}: {edn:?}") + }); + Ok(Sampler { + jack: jack.clone(), + name: name.into(), + mapped: samples, + unmapped: Default::default(), + voices: Default::default(), + ports: Default::default(), + buffer: Default::default(), + output_gain: 0. + }) + } +} + +/// A currently playing instance of a sample. +#[derive(Default, Debug, Clone)] +pub struct Voice { + pub sample: Arc>, + pub after: usize, + pub position: usize, + pub velocity: f32, +} diff --git a/crates/tek_api/src/scene.rs b/crates/tek_api/src/scene.rs new file mode 100644 index 00000000..bed8289c --- /dev/null +++ b/crates/tek_api/src/scene.rs @@ -0,0 +1,53 @@ +use crate::*; + +#[derive(Default, Debug, Clone)] +pub struct Scene { + /// Name of scene + pub name: Arc>, + /// Clips in scene, one per track + pub clips: Vec>>>, + /// Identifying color of scene + pub color: ItemColor, +} + +impl Scene { + pub fn from_edn <'a, 'e> (args: &[Edn<'e>]) -> Usually { + let mut name = None; + let mut clips = vec![]; + edn!(edn in args { + Edn::Map(map) => { + let key = map.get(&Edn::Key(":name")); + if let Some(Edn::Str(n)) = key { + name = Some(*n); + } else { + panic!("unexpected key in scene '{name:?}': {key:?}") + } + }, + Edn::Symbol("_") => { + clips.push(None); + }, + Edn::Int(i) => { + clips.push(Some(*i as usize)); + }, + _ => panic!("unexpected in scene '{name:?}': {edn:?}") + }); + Ok(Scene { + name: Arc::new(name.unwrap_or("").to_string().into()), + color: ItemColor::random(), + clips, + }) + } +} + +#[derive(Clone)] +pub enum SceneCommand { + Next, + Prev, + Add, + Delete, + MoveForward, + MoveBack, + RandomColor, + SetSize(usize), + SetZoom(usize), +} diff --git a/crates/tek_snd/src/snd_sequencer.rs b/crates/tek_api/src/sequencer.rs similarity index 88% rename from crates/tek_snd/src/snd_sequencer.rs rename to crates/tek_api/src/sequencer.rs index 82368cf5..392b6927 100644 --- a/crates/tek_snd/src/snd_sequencer.rs +++ b/crates/tek_api/src/sequencer.rs @@ -1,18 +1,45 @@ use crate::*; -/// JACK process callback for sequencer app -impl Audio for Sequencer { - fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { - if let Some(ref transport) = self.transport { - transport.write().unwrap().process(client, scope); - } - self.player.process(client, scope); - Control::Continue - } +pub enum SequencerCommand {} + +#[derive(Debug)] +pub struct SequencerTrack { + /// Name of track + pub name: Arc>, + /// Preferred width of track column + pub width: usize, + /// Identifying color of track + pub color: ItemColor, + /// Global timebase + pub clock: Arc, + /// Start time and phrase being played + pub phrase: Option<(Instant, Option>>)>, + /// Start time and next phrase + pub next_phrase: Option<(Instant, 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_inputs: Vec>, + /// Play from current sequence to MIDI ports + pub midi_outputs: Vec>, + /// MIDI output buffer + pub midi_note: Vec, + /// MIDI output buffer + pub midi_chunk: Vec>>, + /// Notes currently held at input + pub notes_in: Arc>, + /// Notes currently held at output + pub notes_out: Arc>, } /// JACK process callback for a sequencer's phrase player/recorder. -impl Audio for PhrasePlayer { +impl Audio for SequencerTrack { fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { let has_midi_outputs = self.has_midi_outputs(); let has_midi_inputs = self.has_midi_inputs(); @@ -38,7 +65,7 @@ impl Audio for PhrasePlayer { } /// Methods used primarily by the process callback -impl PhrasePlayer { +impl SequencerTrack { fn is_rolling (&self) -> bool { *self.clock.playing.read().unwrap() == Some(TransportState::Rolling) } diff --git a/crates/tek_api/src/track.rs b/crates/tek_api/src/track.rs new file mode 100644 index 00000000..6b7f6b37 --- /dev/null +++ b/crates/tek_api/src/track.rs @@ -0,0 +1,86 @@ +use crate::*; + +pub enum MixerTrackCommand {} + +/// A mixer track. +#[derive(Debug)] +pub struct MixerTrack { + pub name: String, + /// Inputs of 1st device + pub audio_ins: Vec>, + /// Outputs of last device + pub audio_outs: Vec>, + /// Device chain + pub devices: Vec>, +} + +pub trait MixerTrackDevice: Audio + Debug { + fn boxed (self) -> Box where Self: Sized + 'static { + Box::new(self) + } +} + +impl MixerTrackDevice for Sampler {} + +impl MixerTrackDevice for LV2Plugin {} + +impl MixerTrack { + const SYM_NAME: &'static str = ":name"; + const SYM_GAIN: &'static str = ":gain"; + const SYM_SAMPLER: &'static str = "sampler"; + const SYM_LV2: &'static str = "lv2"; + pub fn from_edn <'a, 'e> (jack: &Arc>, args: &[Edn<'e>]) -> Usually { + let mut _gain = 0.0f64; + let mut track = MixerTrack { + name: String::new(), + audio_ins: vec![], + audio_outs: vec![], + devices: vec![], + }; + #[allow(unused_mut)] + let mut devices: Vec = vec![]; + edn!(edn in args { + Edn::Map(map) => { + if let Some(Edn::Str(n)) = map.get(&Edn::Key(Self::SYM_NAME)) { + track.name = n.to_string(); + } + if let Some(Edn::Double(g)) = map.get(&Edn::Key(Self::SYM_GAIN)) { + _gain = f64::from(*g); + } + }, + Edn::List(args) => match args.get(0) { + // Add a sampler device to the track + Some(Edn::Symbol(Self::SYM_SAMPLER)) => { + track.add_device( + Sampler::from_edn(jack, &args[1..])? + ); + panic!( + "unsupported in track {}: {:?}; tek_mixer not compiled with feature \"sampler\"", + &track.name, + args.get(0).unwrap() + ) + }, + // Add a LV2 plugin to the track. + Some(Edn::Symbol(Self::SYM_LV2)) => { + track.add_device( + LV2Plugin::from_edn(jack, &args[1..])? + ); + panic!( + "unsupported in track {}: {:?}; tek_mixer not compiled with feature \"plugin\"", + &track.name, + args.get(0).unwrap() + ) + }, + None => + panic!("empty list track {}", &track.name), + _ => + panic!("unexpected in track {}: {:?}", &track.name, args.get(0).unwrap()) + }, + _ => {} + }); + Ok(track) + } + pub fn add_device (&mut self, device: impl MixerTrackDevice) { + self.devices.push(device.boxed()) + } +} diff --git a/crates/tek_snd/src/snd_transport.rs b/crates/tek_api/src/transport.rs similarity index 69% rename from crates/tek_snd/src/snd_transport.rs rename to crates/tek_api/src/transport.rs index 61e95e50..5c8199f4 100644 --- a/crates/tek_snd/src/snd_transport.rs +++ b/crates/tek_api/src/transport.rs @@ -1,5 +1,30 @@ use crate::*; -impl Audio for TransportToolbar { + +#[derive(Copy, Clone, PartialEq)] +pub enum TransportCommand { + FocusNext, + FocusPrev, + Play(Option), + Pause(Option), + SeekUsec(f64), + SeekSample(f64), + SeekPulse(f64), + SetBpm(f64), + SetQuant(f64), + SetSync(f64), +} + +pub struct Transport { + pub jack: Arc>, + /// JACK transport handle. + pub transport: jack::Transport, + /// Current sample rate, tempo, and PPQ. + pub clock: Arc, + /// Enable metronome? + pub metronome: bool, +} + +impl Audio for Transport { fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { let times = scope.cycle_times().unwrap(); let CycleTimes { current_frames, current_usecs, next_usecs: _, period_usecs: _ } = times; diff --git a/crates/tek_api/src/voice.rs b/crates/tek_api/src/voice.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/tek_cli/src/cli_arranger.rs b/crates/tek_cli/src/cli_arranger.rs index 2ad4fee2..a50ab739 100644 --- a/crates/tek_cli/src/cli_arranger.rs +++ b/crates/tek_cli/src/cli_arranger.rs @@ -43,7 +43,7 @@ impl ArrangerCli { Some(scene_color_1.mix(scene_color_2, i as f32 / self.scenes as f32)) )?; } - Ok(Arranger::new( + Ok(ArrangerView::new( jack, self.transport.then_some(transport), arrangement, @@ -53,3 +53,33 @@ impl ArrangerCli { Ok(()) } } + +impl Audio for ArrangerView { + fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { + if let Some(ref transport) = self.transport { + transport.write().unwrap().process(client, scope); + } + let Arrangement { scenes, ref mut tracks, selected, .. } = &mut self.arrangement; + for track in tracks.iter_mut() { + track.player.process(client, scope); + } + if let ArrangementFocus::Clip(t, s) = selected { + if let Some(Some(Some(phrase))) = scenes.get(*s).map(|scene|scene.clips.get(*t)) { + if let Some(track) = tracks.get(*t) { + if let Some((ref started_at, Some(ref playing))) = track.player.phrase { + let phrase = phrase.read().unwrap(); + if *playing.read().unwrap() == *phrase { + let pulse = self.clock.current.pulse.get(); + let start = started_at.pulse.get(); + let now = (pulse - start) % phrase.length as f64; + self.editor.now.set(now); + return Control::Continue + } + } + } + } + } + self.editor.now.set(0.); + Control::Continue + } +} diff --git a/crates/tek_cli/src/cli_sequencer.rs b/crates/tek_cli/src/cli_sequencer.rs index 089ce1ff..36ce59e3 100644 --- a/crates/tek_cli/src/cli_sequencer.rs +++ b/crates/tek_cli/src/cli_sequencer.rs @@ -20,16 +20,6 @@ impl SequencerCli { fn run (&self) -> Usually<()> { Tui::run(JackClient::new("tek_sequencer")?.activate_with(|jack|{ let transport = TransportToolbar::new(jack, None); - let sequencer = Sequencer { - jack: jack.clone(), - focus_cursor: (1, 1), - entered: false, - phrases: Arc::new(RwLock::new(PhrasePool::new())), - editor: PhraseEditor::new(), - clock: transport.clock.clone(), - player: PhrasePlayer::new(jack, &transport.clock, "tek_sequencer")?, - transport: self.transport.then_some(Arc::new(RwLock::new(transport))), - }; if let Some(_) = self.name.as_ref() { // TODO: sequencer.name = Arc::new(RwLock::new(name.clone())); } @@ -41,8 +31,28 @@ impl SequencerCli { //phrase.write().unwrap().length = length; //} } - Ok(sequencer) + Ok(SequencerView { + jack: jack.clone(), + focus_cursor: (1, 1), + entered: false, + phrases: Arc::new(RwLock::new(PhrasePool::new())), + editor: PhraseEditor::new(), + clock: transport.clock.clone(), + player: PhrasePlayer::new(jack, &transport.clock, "tek_sequencer")?, + transport: self.transport.then_some(Arc::new(RwLock::new(transport))), + }) })?)?; Ok(()) } } + +/// JACK process callback for sequencer app +impl Audio for SequencerView { + fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { + if let Some(ref transport) = self.transport { + transport.write().unwrap().process(client, scope); + } + self.player.process(client, scope); + Control::Continue + } +} diff --git a/crates/tek_core/src/audio.rs b/crates/tek_core/src/audio.rs index 1d36fe07..073926af 100644 --- a/crates/tek_core/src/audio.rs +++ b/crates/tek_core/src/audio.rs @@ -1,510 +1,2 @@ use crate::*; use jack::*; -/// Trait for things that wrap a JACK client. -pub trait AudioEngine { - fn activate ( - self, - process: impl FnMut(&Arc>, &Client, &ProcessScope) -> Control + Send + 'static - ) -> Usually>> where Self: Send + Sync + 'static; - fn client (&self) -> &Client; - fn transport (&self) -> Transport { - self.client().transport() - } - fn port_by_name (&self, name: &str) -> Option> { - self.client().port_by_name(name) - } - fn register_port (&self, name: &str, spec: PS) -> Usually> { - Ok(self.client().register_port(name, spec)?) - } - fn thread_init (&self, _: &Client) {} - unsafe fn shutdown (&mut self, status: ClientStatus, reason: &str) {} - fn freewheel (&mut self, _: &Client, enabled: bool) {} - fn client_registration (&mut self, _: &Client, name: &str, reg: bool) {} - fn port_registration (&mut self, _: &Client, id: PortId, reg: bool) {} - fn ports_connected (&mut self, _: &Client, a: PortId, b: PortId, are: bool) {} - fn sample_rate (&mut self, _: &Client, frames: Frames) -> Control { - Control::Continue - } - fn port_rename (&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control { - Control::Continue - } - fn graph_reorder (&mut self, _: &Client) -> Control { - Control::Continue - } - fn xrun (&mut self, _: &Client) -> Control { - Control::Continue - } -} -/// Wraps [Client] or [DynamicAsyncClient] in place. -#[derive(Debug)] -pub enum JackClient { - /// Before activation. - Inactive(Client), - /// During activation. - Activating, - /// After activation. Must not be dropped for JACK thread to persist. - Active(DynamicAsyncClient), -} -pub type DynamicAsyncClient = AsyncClient; -pub type DynamicAudioHandler = contrib::ClosureProcessHandler<(), BoxedAudioHandler>; -pub type BoxedAudioHandler = Box Control + Send>; -impl JackClient { - pub fn new (name: &str) -> Usually { - let (client, _) = Client::new(name, ClientOptions::NO_START_SERVER)?; - Ok(Self::Inactive(client)) - } - 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 = contrib::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)?); - Ok(target) - } -} -impl From for Client { - fn from (jack: JackClient) -> Client { - match jack { - JackClient::Inactive(client) => client, - JackClient::Activating => panic!("jack client still activating"), - JackClient::Active(_) => panic!("jack client already activated"), - } - } -} -impl AudioEngine for JackClient { - 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(), - } - } - 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 = contrib::ClosureProcessHandler::new(frame as BoxedAudioHandler); - *state.write().unwrap() = Self::Active(client.activate_async(events, frames)?); - Ok(state) - } -} -/// Trait for things that have 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 - } - } -} - -/// 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![]) - } -} - -/// 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 {} - -/// `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), - contrib::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 - } -} - -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 - }) -} - -/// Notification handler used by the [Jack] factory -/// when constructing [JackDevice]s. -pub type DynamicNotifications = Notifications>; - -#[derive(Debug)] -/// Event enum for JACK events. -pub enum JackEvent { - ThreadInit, - Shutdown(ClientStatus, String), - Freewheel(bool), - SampleRate(Frames), - ClientRegistration(String, bool), - PortRegistration(PortId, bool), - PortRename(PortId, String, String), - 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 Widget for JackDevice { - type Engine = E; - fn layout(&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) - })? - )? - )?} - }; -} diff --git a/crates/tek_core/src/lib.rs b/crates/tek_core/src/lib.rs index 6a8bc071..b872a5e5 100644 --- a/crates/tek_core/src/lib.rs +++ b/crates/tek_core/src/lib.rs @@ -36,7 +36,7 @@ use std::fmt::{Debug, Display}; } submod! { - audio + //audio color command edn diff --git a/crates/tek_snd/Cargo.toml b/crates/tek_snd/Cargo.toml deleted file mode 100644 index 64634afe..00000000 --- a/crates/tek_snd/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "tek_snd" -edition = "2021" -version = "0.1.0" - -[dependencies] -tek_core = { path = "../tek_core" } -tek_api = { path = "../tek_api" } diff --git a/crates/tek_snd/README.md b/crates/tek_snd/README.md deleted file mode 100644 index 17da7482..00000000 --- a/crates/tek_snd/README.md +++ /dev/null @@ -1 +0,0 @@ -jack-based audio engine diff --git a/crates/tek_snd/src/lib.rs b/crates/tek_snd/src/lib.rs deleted file mode 100644 index 097bdbbd..00000000 --- a/crates/tek_snd/src/lib.rs +++ /dev/null @@ -1,10 +0,0 @@ -pub(crate) use tek_core::*; -submod! { - snd_arranger - snd_mixer - snd_plugin - snd_sampler - snd_sequencer - snd_transport -} - diff --git a/crates/tek_snd/src/snd_arranger.rs b/crates/tek_snd/src/snd_arranger.rs deleted file mode 100644 index 0186cf8c..00000000 --- a/crates/tek_snd/src/snd_arranger.rs +++ /dev/null @@ -1,31 +0,0 @@ -use crate::*; - -impl Audio for Arranger { - fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { - if let Some(ref transport) = self.transport { - transport.write().unwrap().process(client, scope); - } - let Arrangement { scenes, ref mut tracks, selected, .. } = &mut self.arrangement; - for track in tracks.iter_mut() { - track.player.process(client, scope); - } - if let ArrangementFocus::Clip(t, s) = selected { - if let Some(Some(Some(phrase))) = scenes.get(*s).map(|scene|scene.clips.get(*t)) { - if let Some(track) = tracks.get(*t) { - if let Some((ref started_at, Some(ref playing))) = track.player.phrase { - let phrase = phrase.read().unwrap(); - if *playing.read().unwrap() == *phrase { - let pulse = self.clock.current.pulse.get(); - let start = started_at.pulse.get(); - let now = (pulse - start) % phrase.length as f64; - self.editor.now.set(now); - return Control::Continue - } - } - } - } - } - self.editor.now.set(0.); - Control::Continue - } -} diff --git a/crates/tek_snd/src/snd_mixer.rs b/crates/tek_snd/src/snd_mixer.rs deleted file mode 100644 index b8593fb4..00000000 --- a/crates/tek_snd/src/snd_mixer.rs +++ /dev/null @@ -1,7 +0,0 @@ -use crate::*; - -impl Audio for Mixer { - fn process (&mut self, _: &Client, _: &ProcessScope) -> Control { - Control::Continue - } -} diff --git a/crates/tek_snd/src/snd_plugin.rs b/crates/tek_snd/src/snd_plugin.rs deleted file mode 100644 index 5ee03f29..00000000 --- a/crates/tek_snd/src/snd_plugin.rs +++ /dev/null @@ -1,59 +0,0 @@ -use crate::*; - -impl Audio for Plugin { - fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { - match self.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 self.ports.midi_ins.values() { - 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 self.ports.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( - self.ports.audio_ins.values().map(|o|o.as_slice(scope)) - ) - .with_audio_outputs( - self.ports.audio_outs.values_mut().map(|o|o.as_mut_slice(scope)) - ); - unsafe { - instance.run(scope.n_frames() as usize, ports).unwrap() - }; - }, - _ => {} - } - Control::Continue - } -} diff --git a/crates/tek_snd/src/snd_sampler.rs b/crates/tek_snd/src/snd_sampler.rs deleted file mode 100644 index 4ce221e6..00000000 --- a/crates/tek_snd/src/snd_sampler.rs +++ /dev/null @@ -1,11 +0,0 @@ -use crate::*; - -impl Audio for Sampler { - fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { - self.process_midi_in(scope); - self.clear_output_buffer(); - self.process_audio_out(scope); - self.write_output_buffer(scope); - Control::Continue - } -} diff --git a/crates/tek_tui/src/arranger_cmd.rs b/crates/tek_tui/src/arranger_cmd.rs deleted file mode 100644 index eaeaa9cc..00000000 --- a/crates/tek_tui/src/arranger_cmd.rs +++ /dev/null @@ -1,101 +0,0 @@ -use crate::*; - -#[derive(Clone)] -pub enum ArrangerCommand { - Focus(FocusCommand), - Transport(TransportCommand), - Phrases(PhrasePoolCommand), - Editor(PhraseEditorCommand), - Arrangement(ArrangementCommand), - EditPhrase(Option>>), -} -#[derive(Clone)] -pub enum ArrangementCommand { - New, - Load, - Save, - ToggleViewMode, - Delete, - Activate, - Increment, - Decrement, - ZoomIn, - ZoomOut, - MoveBack, - MoveForward, - RandomColor, - Put, - Get, - AddScene, - AddTrack, - ToggleLoop, - GoUp, - GoDown, - GoLeft, - GoRight, - Edit(Option>>), -} - -impl Command> for ArrangerCommand { - fn execute (self, state: &mut Arranger) -> Perhaps { - let undo = match self { - Self::Focus(cmd) => { - delegate(cmd, Self::Focus, state) - }, - Self::Phrases(cmd) => { - delegate(cmd, Self::Phrases, &mut*state.phrases.write().unwrap()) - }, - Self::Editor(cmd) => { - delegate(cmd, Self::Editor, &mut state.editor) - }, - Self::Arrangement(cmd) => { - delegate(cmd, Self::Arrangement, &mut state.arrangement) - }, - Self::Transport(cmd) => if let Some(ref transport) = state.transport { - delegate(cmd, Self::Transport, &mut*transport.write().unwrap()) - } else { - Ok(None) - }, - Self::EditPhrase(phrase) => { - state.editor.phrase = phrase.clone(); - state.focus(ArrangerFocus::PhraseEditor); - state.focus_enter(); - Ok(None) - } - }?; - state.show_phrase(); - state.update_status(); - return Ok(undo); - } -} -impl Command> for ArrangementCommand { - fn execute (self, state: &mut Arrangement) -> Perhaps { - use ArrangementCommand::*; - match self { - New => todo!(), - Load => todo!(), - Save => todo!(), - Edit(phrase) => { state.phrase = phrase.clone() }, - ToggleViewMode => { state.mode.to_next(); }, - Delete => { state.delete(); }, - Activate => { state.activate(); }, - Increment => { state.increment(); }, - Decrement => { state.decrement(); }, - ZoomIn => { state.zoom_in(); }, - ZoomOut => { state.zoom_out(); }, - MoveBack => { state.move_back(); }, - MoveForward => { state.move_forward(); }, - RandomColor => { state.randomize_color(); }, - Put => { state.phrase_put(); }, - Get => { state.phrase_get(); }, - AddScene => { state.scene_add(None, None)?; }, - AddTrack => { state.track_add(None, None)?; }, - ToggleLoop => { state.toggle_loop() }, - GoUp => { state.go_up() }, - GoDown => { state.go_down() }, - GoLeft => { state.go_left() }, - GoRight => { state.go_right() }, - }; - Ok(None) - } -} diff --git a/crates/tek_tui/src/arranger_tui.rs b/crates/tek_tui/src/arranger_tui.rs deleted file mode 100644 index 77f9e73c..00000000 --- a/crates/tek_tui/src/arranger_tui.rs +++ /dev/null @@ -1,54 +0,0 @@ -use crate::*; - -/// Layout for standalone arranger app. -impl Content for Arranger { - type Engine = Tui; - fn content (&self) -> impl Widget { - let focused = self.arrangement.focused; - let border_bg = Arranger::::border_bg(); - let border_fg = Arranger::::border_fg(focused); - let title_fg = Arranger::::title_fg(focused); - let border = Lozenge(Style::default().bg(border_bg).fg(border_fg)); - let entered = if self.arrangement.entered { "■" } else { " " }; - Split::down( - 1, - row!(menu in self.menu.menus.iter() => { - row!(" ", menu.title.as_str(), " ") - }), - Split::up( - 1, - widget(&self.status), - Split::up( - 1, - widget(&self.transport), - Split::down( - self.arrangement_split, - lay!( - widget(&self.arrangement).grow_y(1).border(border), - widget(&self.arrangement.size), - widget(&format!("[{}] Arrangement", entered)).fg(title_fg).push_x(1), - ), - Split::right( - self.phrases_split, - self.phrases.clone(), - widget(&self.editor), - ) - ) - ) - ) - ) - } -} - -impl Content for Arrangement { - type Engine = Tui; - fn content (&self) -> impl Widget { - Layers::new(move |add|{ - match self.mode { - ArrangementViewMode::Horizontal => { add(&HorizontalArranger(&self)) }, - ArrangementViewMode::Vertical(factor) => { add(&VerticalArranger(&self, factor)) }, - }?; - add(&self.size) - }) - } -} diff --git a/crates/tek_tui/src/lib.rs b/crates/tek_tui/src/lib.rs index 5b96d426..d8ff9389 100644 --- a/crates/tek_tui/src/lib.rs +++ b/crates/tek_tui/src/lib.rs @@ -10,36 +10,24 @@ pub(crate) use std::ffi::OsString; pub(crate) use std::fs::read_dir; submod! { - arranger - arranger_cmd - arranger_snd - arranger_tui - arranger_tui_bar - arranger_tui_cmd - arranger_tui_col - arranger_tui_hor - arranger_tui_ver - sequencer - sequencer_cmd - sequencer_snd - sequencer_tui - transport - transport_cmd - transport_snd - transport_tui - mixer - mixer_snd - mixer_cmd - mixer_tui - sampler - sampler_snd - sampler_cmd - plugin - plugin_snd - plugin_cmd - plugin_tui - plugin_lv2 - plugin_lv2_gui - plugin_vst2 - plugin_vst3 + tui_arranger + tui_arranger_bar + tui_arranger_cmd + tui_arranger_col + tui_arranger_hor + tui_arranger_ver + tui_sequencer + tui_sequencer_cmd + tui_transport + tui_transport_cmd + tui_mixer + tui_mixer_cmd + tui_sampler + tui_sampler_cmd + tui_plugin + tui_plugin_cmd + tui_plugin_lv2 + tui_plugin_lv2_gui + tui_plugin_vst2 + tui_plugin_vst3 } diff --git a/crates/tek_tui/src/mixer_tui.rs b/crates/tek_tui/src/mixer_tui.rs deleted file mode 100644 index fb38f458..00000000 --- a/crates/tek_tui/src/mixer_tui.rs +++ /dev/null @@ -1,38 +0,0 @@ -use crate::*; - -impl Content for Mixer { - type Engine = Tui; - fn content (&self) -> impl Widget { - Stack::right(|add| { - for channel in self.tracks.iter() { - add(channel)?; - } - Ok(()) - }) - } -} - -impl Content for Track { - type Engine = Tui; - fn content (&self) -> impl Widget { - TrackView { - chain: Some(&self), - direction: tek_core::Direction::Right, - focused: true, - entered: true, - //pub channels: u8, - //pub input_ports: Vec>, - //pub pre_gain_meter: f64, - //pub gain: f64, - //pub insert_ports: Vec>, - //pub return_ports: Vec>, - //pub post_gain_meter: f64, - //pub post_insert_meter: f64, - //pub level: f64, - //pub pan: f64, - //pub output_ports: Vec>, - //pub post_fader_meter: f64, - //pub route: String, - } - } -} diff --git a/crates/tek_tui/src/plugin.rs b/crates/tek_tui/src/plugin.rs deleted file mode 100644 index 9757c05d..00000000 --- a/crates/tek_tui/src/plugin.rs +++ /dev/null @@ -1,33 +0,0 @@ -use crate::*; - -/// A plugin device. -pub struct Plugin { - _engine: PhantomData, - /// JACK client handle (needs to not be dropped for standalone mode to work). - pub jack: Arc>, - pub name: String, - pub path: Option, - pub plugin: Option, - pub selected: usize, - pub mapping: bool, - pub ports: JackPorts, -} - -impl Plugin { - /// Create a plugin host device. - pub fn new ( - jack: &Arc>, - name: &str, - ) -> Usually { - Ok(Self { - _engine: Default::default(), - jack: jack.clone(), - name: name.into(), - path: None, - plugin: None, - selected: 0, - mapping: false, - ports: JackPorts::default() - }) - } -} diff --git a/crates/tek_tui/src/sampler_tui.rs b/crates/tek_tui/src/sampler_tui.rs deleted file mode 100644 index 0bf8fd9a..00000000 --- a/crates/tek_tui/src/sampler_tui.rs +++ /dev/null @@ -1,98 +0,0 @@ -use crate::*; - -impl Widget for Sampler { - type Engine = Tui; - fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> { - todo!() - } - fn render (&self, to: &mut TuiOutput) -> Usually<()> { - tui_render_sampler(self, to) - } -} - -pub fn tui_render_sampler (sampler: &Sampler, to: &mut TuiOutput) -> Usually<()> { - let [x, y, _, height] = to.area(); - let style = Style::default().gray(); - let title = format!(" {} ({})", sampler.name, sampler.voices.read().unwrap().len()); - to.blit(&title, x+1, y, Some(style.white().bold().not_dim())); - let mut width = title.len() + 2; - let mut y1 = 1; - let mut j = 0; - for (note, sample) in sampler.mapped.iter() - .map(|(note, sample)|(Some(note), sample)) - .chain(sampler.unmapped.iter().map(|sample|(None, sample))) - { - if y1 >= height { - break - } - let active = j == sampler.cursor.0; - width = width.max( - draw_sample(to, x, y + y1, note, &*sample.read().unwrap(), active)? - ); - y1 = y1 + 1; - j = j + 1; - } - let height = ((2 + y1) as u16).min(height); - //Ok(Some([x, y, (width as u16).min(to.area().w()), height])) - Ok(()) -} - -fn draw_sample ( - to: &mut TuiOutput, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool -) -> Usually { - let style = if focus { Style::default().green() } else { Style::default() }; - if focus { - to.blit(&"🬴", x+1, y, Some(style.bold())); - } - let label1 = format!("{:3} {:12}", - note.map(|n|n.to_string()).unwrap_or(String::default()), - sample.name); - let label2 = format!("{:>6} {:>6} +0.0", - sample.start, - sample.end); - to.blit(&label1, x+2, y, Some(style.bold())); - to.blit(&label2, x+3+label1.len()as u16, y, Some(style)); - Ok(label1.len() + label2.len() + 4) -} - -impl Widget for AddSampleModal { - type Engine = Tui; - fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> { - todo!() - //Align::Center(()).layout(to) - } - fn render (&self, to: &mut TuiOutput) -> Usually<()> { - todo!() - //let area = to.area(); - //to.make_dim(); - //let area = center_box( - //area, - //64.max(area.w().saturating_sub(8)), - //20.max(area.w().saturating_sub(8)), - //); - //to.fill_fg(area, Color::Reset); - //to.fill_bg(area, Nord::bg_lo(true, true)); - //to.fill_char(area, ' '); - //to.blit(&format!("{}", &self.dir.to_string_lossy()), area.x()+2, area.y()+1, Some(Style::default().bold()))?; - //to.blit(&"Select sample:", area.x()+2, area.y()+2, Some(Style::default().bold()))?; - //for (i, (is_dir, name)) in self.subdirs.iter() - //.map(|path|(true, path)) - //.chain(self.files.iter().map(|path|(false, path))) - //.enumerate() - //.skip(self.offset) - //{ - //if i >= area.h() as usize - 4 { - //break - //} - //let t = if is_dir { "" } else { "" }; - //let line = format!("{t} {}", name.to_string_lossy()); - //let line = &line[..line.len().min(area.w() as usize - 4)]; - //to.blit(&line, area.x() + 2, area.y() + 3 + i as u16, Some(if i == self.cursor { - //Style::default().green() - //} else { - //Style::default().white() - //}))?; - //} - //Lozenge(Style::default()).draw(to) - } -} diff --git a/crates/tek_tui/src/sequencer.rs b/crates/tek_tui/src/sequencer.rs deleted file mode 100644 index 97166ed3..00000000 --- a/crates/tek_tui/src/sequencer.rs +++ /dev/null @@ -1,481 +0,0 @@ -use crate::*; -use std::cmp::PartialEq; -/// MIDI message structural -pub type PhraseData = Vec>; -/// MIDI message serialized -pub type PhraseMessage = Vec; -/// Collection of serialized MIDI messages -pub type PhraseChunk = [Vec]; -/// Root level object for standalone `tek_sequencer` -pub struct Sequencer { - /// JACK client handle (needs to not be dropped for standalone mode to work). - pub jack: Arc>, - /// Controls the JACK transport. - pub transport: Option>>>, - /// Global timebase - pub clock: Arc, - /// Pool of all phrases available to the sequencer - pub phrases: Arc>>, - /// Phrase editor view - pub editor: PhraseEditor, - /// Phrase player - pub player: PhrasePlayer, - /// Which view is focused - pub focus_cursor: (usize, usize), - /// Whether the currently focused item is entered - pub entered: bool, -} -/// Sections in the sequencer app that may be focused -#[derive(Copy, Clone, PartialEq, Eq)] pub enum SequencerFocus { - /// The transport (toolbar) is focused - Transport, - /// The phrase list (pool) is focused - PhrasePool, - /// The phrase editor (sequencer) is focused - PhraseEditor, -} -/// Status bar for sequencer app -pub enum SequencerStatusBar { - Transport, - PhrasePool, - PhraseEditor, -} -/// Contains all phrases in a project -pub struct PhrasePool { - _engine: PhantomData, - /// Scroll offset - pub scroll: usize, - /// Highlighted phrase - pub phrase: usize, - /// Phrases in the pool - pub phrases: Vec>>, - /// Mode switch - pub mode: Option, - /// Whether this widget is focused - pub focused: bool, - /// Whether this widget is entered - pub entered: bool, -} -/// Modes for phrase pool -pub enum PhrasePoolMode { - /// Renaming a pattern - Rename(usize, String), - /// Editing the length of a pattern - Length(usize, usize, PhraseLengthFocus), -} -/// A MIDI sequence. -#[derive(Debug, Clone)] pub struct Phrase { - pub uuid: uuid::Uuid, - /// Name of phrase - pub name: String, - /// Temporal resolution in pulses per quarter note - pub ppq: usize, - /// Length of phrase in pulses - pub length: usize, - /// Notes in phrase - pub notes: PhraseData, - /// Whether to loop the phrase or play it once - pub loop_on: bool, - /// Start of loop - pub loop_start: usize, - /// Length of loop - pub loop_length: usize, - /// All notes are displayed with minimum length - pub percussive: bool, - /// Identifying color of phrase - pub color: ItemColorTriplet, -} -/// Contains state for viewing and editing a phrase -pub struct PhraseEditor { - _engine: PhantomData, - /// Phrase being played - pub phrase: Option>>, - /// Length of note that will be inserted, in pulses - pub note_len: usize, - /// The full piano keys are rendered to this buffer - pub keys: Buffer, - /// The full piano roll is rendered to this buffer - pub buffer: BigBuffer, - /// Cursor/scroll/zoom in pitch axis - pub note_axis: RwLock>, - /// Cursor/scroll/zoom in time axis - pub time_axis: RwLock>, - /// Whether this widget is focused - pub focused: bool, - /// Whether note enter mode is enabled - pub entered: bool, - /// Display mode - pub mode: bool, - /// Notes currently held at input - pub notes_in: Arc>, - /// Notes currently held at output - pub notes_out: Arc>, - /// Current position of global playhead - pub now: Arc, - /// Width of notes area at last render - pub width: AtomicUsize, - /// Height of notes area at last render - pub height: AtomicUsize, -} -/// Phrase player. -pub struct PhrasePlayer { - /// Global timebase - pub clock: Arc, - /// Start time and phrase being played - pub phrase: Option<(Instant, Option>>)>, - /// Start time and next phrase - pub next_phrase: Option<(Instant, 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_inputs: Vec>, - /// Play from current sequence to MIDI ports - pub midi_outputs: Vec>, - /// MIDI output buffer - pub midi_note: Vec, - /// MIDI output buffer - pub midi_chunk: Vec>>, - /// Notes currently held at input - pub notes_in: Arc>, - /// Notes currently held at output - pub notes_out: Arc>, -} -/// Displays and edits phrase length. -pub struct PhraseLength { - _engine: PhantomData, - /// Pulses per beat (quaver) - pub ppq: usize, - /// Beats per bar - pub bpb: usize, - /// Length of phrase in pulses - pub pulses: usize, - /// Selected subdivision - pub focus: Option, -} -/// Focused field of `PhraseLength` -#[derive(Copy, Clone)] pub enum PhraseLengthFocus { - /// Editing the number of bars - Bar, - /// Editing the number of beats - Beat, - /// Editing the number of ticks - Tick, -} -/// Focus layout of sequencer app -impl FocusGrid for Sequencer { - type Item = SequencerFocus; - fn cursor (&self) -> (usize, usize) { self.focus_cursor } - fn cursor_mut (&mut self) -> &mut (usize, usize) { &mut self.focus_cursor } - fn layout (&self) -> &[&[SequencerFocus]] { &[ - &[SequencerFocus::Transport], - &[SequencerFocus::PhrasePool, SequencerFocus::PhraseEditor], - ] } - fn focus_enter (&mut self) { self.entered = true } - fn focus_exit (&mut self) { self.entered = false } - fn entered (&self) -> Option { - if self.entered { Some(self.focused()) } else { None } - } - fn update_focus (&mut self) { - let focused = self.focused(); - if let Some(transport) = self.transport.as_ref() { - transport.write().unwrap().focused = focused == SequencerFocus::Transport - } - self.phrases.write().unwrap().focused = focused == SequencerFocus::PhrasePool; - self.editor.focused = focused == SequencerFocus::PhraseEditor; - } -} -impl PhrasePool { - pub fn new () -> Self { - Self { - _engine: Default::default(), - scroll: 0, - phrase: 0, - phrases: vec![Arc::new(RwLock::new(Phrase::default()))], - mode: None, - focused: false, - entered: false, - } - } - pub fn len (&self) -> usize { self.phrases.len() } - pub fn phrase (&self) -> &Arc> { &self.phrases[self.phrase] } - pub fn select_prev (&mut self) { self.phrase = self.index_before(self.phrase) } - pub fn select_next (&mut self) { self.phrase = self.index_after(self.phrase) } - pub fn index_before (&self, index: usize) -> usize { - index.overflowing_sub(1).0.min(self.len() - 1) - } - pub fn index_after (&self, index: usize) -> usize { - (index + 1) % self.len() - } - pub fn index_of (&self, phrase: &Phrase) -> Option { - for i in 0..self.phrases.len() { - if *self.phrases[i].read().unwrap() == *phrase { return Some(i) } - } - return None - } - fn new_phrase (name: Option<&str>, color: Option) -> Arc> { - Arc::new(RwLock::new(Phrase::new( - String::from(name.unwrap_or("(new)")), true, 4 * PPQ, None, color - ))) - } - pub fn delete_selected (&mut self) { - if self.phrase > 0 { - self.phrases.remove(self.phrase); - self.phrase = self.phrase.min(self.phrases.len().saturating_sub(1)); - } - } - pub fn append_new (&mut self, name: Option<&str>, color: Option) { - self.phrases.push(Self::new_phrase(name, color)); - self.phrase = self.phrases.len() - 1; - } - pub fn insert_new (&mut self, name: Option<&str>, color: Option) { - self.phrases.insert(self.phrase + 1, Self::new_phrase(name, color)); - self.phrase += 1; - } - pub fn insert_dup (&mut self) { - let mut phrase = self.phrases[self.phrase].read().unwrap().duplicate(); - phrase.color = ItemColorTriplet::random_near(phrase.color, 0.25); - self.phrases.insert(self.phrase + 1, Arc::new(RwLock::new(phrase))); - self.phrase += 1; - } - pub fn randomize_color (&mut self) { - let mut phrase = self.phrases[self.phrase].write().unwrap(); - phrase.color = ItemColorTriplet::random(); - } - pub fn begin_rename (&mut self) { - self.mode = Some(PhrasePoolMode::Rename( - self.phrase, - self.phrases[self.phrase].read().unwrap().name.clone() - )); - } - pub fn begin_length (&mut self) { - self.mode = Some(PhrasePoolMode::Length( - self.phrase, - self.phrases[self.phrase].read().unwrap().length, - PhraseLengthFocus::Bar - )); - } - pub fn move_up (&mut self) { - if self.phrase > 1 { - self.phrases.swap(self.phrase - 1, self.phrase); - self.phrase -= 1; - } - } - pub fn move_down (&mut self) { - if self.phrase < self.phrases.len().saturating_sub(1) { - self.phrases.swap(self.phrase + 1, self.phrase); - self.phrase += 1; - } - } -} -impl PhraseEditor { - pub fn new () -> Self { - Self { - _engine: Default::default(), - phrase: None, - note_len: 24, - notes_in: Arc::new(RwLock::new([false;128])), - notes_out: Arc::new(RwLock::new([false;128])), - keys: keys_vert(), - buffer: Default::default(), - focused: false, - entered: false, - mode: false, - now: Arc::new(0.into()), - width: 0.into(), - height: 0.into(), - note_axis: RwLock::new(FixedAxis { - start: 12, - point: Some(36), - clamp: Some(127) - }), - time_axis: RwLock::new(ScaledAxis { - start: 00, - point: Some(00), - clamp: Some(000), - scale: 24 - }), - } - } - pub fn note_cursor_inc (&self) { - let mut axis = self.note_axis.write().unwrap(); - axis.point_dec(1); - if let Some(point) = axis.point { if point < axis.start { axis.start = (point / 2) * 2; } } - } - pub fn note_cursor_dec (&self) { - let mut axis = self.note_axis.write().unwrap(); - axis.point_inc(1); - if let Some(point) = axis.point { if point > 73 { axis.point = Some(73); } } - } - pub fn note_page_up (&self) { - let mut axis = self.note_axis.write().unwrap(); - axis.start_dec(3); - axis.point_dec(3); - } - pub fn note_page_down (&self) { - let mut axis = self.note_axis.write().unwrap(); - axis.start_inc(3); - axis.point_inc(3); - } - pub fn note_scroll_inc (&self) { self.note_axis.write().unwrap().start_dec(1); } - pub fn note_scroll_dec (&self) { self.note_axis.write().unwrap().start_inc(1); } - pub fn note_length_inc (&mut self) { self.note_len = next_note_length(self.note_len) } - pub fn note_length_dec (&mut self) { self.note_len = prev_note_length(self.note_len) } - pub fn time_cursor_advance (&self) { - let point = self.time_axis.read().unwrap().point; - let length = self.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); - let forward = |time|(time + self.note_len) % length; - self.time_axis.write().unwrap().point = point.map(forward); - } - pub fn time_cursor_inc (&self) { - let scale = self.time_axis.read().unwrap().scale; - self.time_axis.write().unwrap().point_inc(scale); - } - pub fn time_cursor_dec (&self) { - let scale = self.time_axis.read().unwrap().scale; - self.time_axis.write().unwrap().point_dec(scale); - } - pub fn time_scroll_inc (&self) { - let scale = self.time_axis.read().unwrap().scale; - self.time_axis.write().unwrap().start_inc(scale); - } - pub fn time_scroll_dec (&self) { - let scale = self.time_axis.read().unwrap().scale; - self.time_axis.write().unwrap().start_dec(scale); - } - pub fn time_zoom_in (&self) { - let scale = self.time_axis.read().unwrap().scale; - self.time_axis.write().unwrap().scale = prev_note_length(scale) - } - pub fn time_zoom_out (&self) { - let scale = self.time_axis.read().unwrap().scale; - self.time_axis.write().unwrap().scale = next_note_length(scale) - } -} -impl Phrase { - pub fn new ( - name: impl AsRef, - loop_on: bool, - length: usize, - notes: Option, - color: Option, - ) -> Self { - Self { - uuid: uuid::Uuid::new_v4(), - name: name.as_ref().to_string(), - ppq: PPQ, - length, - notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]), - loop_on, - loop_start: 0, - loop_length: length, - percussive: true, - color: color.unwrap_or_else(ItemColorTriplet::random) - } - } - pub fn duplicate (&self) -> Self { - let mut clone = self.clone(); - clone.uuid = uuid::Uuid::new_v4(); - clone - } - pub fn toggle_loop (&mut self) { self.loop_on = !self.loop_on; } - pub fn record_event (&mut self, pulse: usize, message: MidiMessage) { - if pulse >= self.length { panic!("extend phrase first") } - self.notes[pulse].push(message); - } - /// Check if a range `start..end` contains MIDI Note On `k` - pub fn contains_note_on (&self, k: u7, start: usize, end: usize) -> bool { - //panic!("{:?} {start} {end}", &self); - for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() { - for event in events.iter() { - if let MidiMessage::NoteOn {key,..} = event { if *key == k { return true } } - } - } - return false - } -} -impl Default for Phrase { - fn default () -> Self { - Self::new("(empty)", false, 0, None, Some(ItemColor::from(Color::Rgb(0, 0, 0)).into())) - } -} -impl PartialEq for Phrase { fn eq (&self, other: &Self) -> bool { self.uuid == other.uuid } } -impl Eq for Phrase {} -impl PhrasePlayer { - pub fn new ( - jack: &Arc>, - clock: &Arc, - name: &str - ) -> Usually { - let jack = jack.read().unwrap(); - Ok(Self { - clock: clock.clone(), - phrase: None, - next_phrase: None, - notes_in: Arc::new(RwLock::new([false;128])), - notes_out: Arc::new(RwLock::new([false;128])), - monitoring: false, - recording: false, - overdub: true, - reset: true, - midi_note: Vec::with_capacity(8), - midi_chunk: vec![Vec::with_capacity(16);16384], - midi_outputs: vec![ - jack.client().register_port(format!("{name}_out0").as_str(), MidiOut::default())? - ], - midi_inputs: vec![ - jack.client().register_port(format!("{name}_in0").as_str(), MidiIn::default())? - ], - }) - } - pub fn toggle_monitor (&mut self) { self.monitoring = !self.monitoring; } - pub fn toggle_record (&mut self) { self.recording = !self.recording; } - pub fn toggle_overdub (&mut self) { self.overdub = !self.overdub; } - pub fn enqueue_next (&mut self, phrase: Option<&Arc>>) { - let start = self.clock.next_launch_pulse(); - self.next_phrase = Some(( - Instant::from_pulse(&self.clock.timebase(), start as f64), - phrase.map(|p|p.clone()) - )); - self.reset = true; - } - pub fn pulses_since_start (&self) -> Option { - if let Some((started, Some(_))) = self.phrase.as_ref() { - Some(self.clock.current.pulse.get() - started.pulse.get()) - } else { - None - } - } -} -impl PhraseLength { - pub fn new (pulses: usize, focus: Option) -> Self { - Self { _engine: Default::default(), ppq: PPQ, bpb: 4, pulses, focus } - } - pub fn bars (&self) -> usize { self.pulses / (self.bpb * self.ppq) } - pub fn beats (&self) -> usize { (self.pulses % (self.bpb * self.ppq)) / self.ppq } - pub fn ticks (&self) -> usize { self.pulses % self.ppq } - pub fn bars_string (&self) -> String { format!("{}", self.bars()) } - pub fn beats_string (&self) -> String { format!("{}", self.beats()) } - pub fn ticks_string (&self) -> String { format!("{:>02}", self.ticks()) } -} -impl PhraseLengthFocus { - pub fn next (&mut self) { - *self = match self { - Self::Bar => Self::Beat, - Self::Beat => Self::Tick, - Self::Tick => Self::Bar, - } - } - pub fn prev (&mut self) { - *self = match self { - Self::Bar => Self::Tick, - Self::Beat => Self::Bar, - Self::Tick => Self::Beat, - } - } -} diff --git a/crates/tek_tui/src/track_cli.rs b/crates/tek_tui/src/track_cli.rs deleted file mode 100644 index 6448e447..00000000 --- a/crates/tek_tui/src/track_cli.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! Multi-track mixer -include!("lib.rs"); -pub fn main () -> Usually<()> { - Tui::run(Arc::new(RwLock::new(crate::Track::new("")?)))?; - Ok(()) -} diff --git a/crates/tek_tui/src/transport.rs b/crates/tek_tui/src/transport.rs deleted file mode 100644 index 3bfc3e38..00000000 --- a/crates/tek_tui/src/transport.rs +++ /dev/null @@ -1,81 +0,0 @@ -use crate::*; -/// Stores and displays time-related state. -#[derive(Debug)] -pub struct TransportView { - _engine: PhantomData, - state: TransportToolbar, - focused: bool, - focus: TransportFocus, -} -/// Which item of the transport toolbar is focused -#[derive(Clone, Copy, PartialEq)] -pub enum TransportFocus { - Bpm, - Sync, - PlayPause, - Clock, - Quant, -} -impl TransportView { - pub fn new (jack: &Arc>, clock: Option<&Arc>) -> Self { - Self { - _engine: Default::default(), - focused: false, - focus: TransportFocus::PlayPause, - state: TransportToolbar { - metronome: false, - transport: jack.read().unwrap().transport(), - jack: jack.clone(), - clock: match clock { - Some(clock) => clock.clone(), - None => { - let timebase = Arc::new(Timebase::default()); - Arc::new(TransportTime { - playing: Some(TransportState::Stopped).into(), - quant: 24.into(), - sync: (timebase.ppq.get() * 4.).into(), - current: Instant::default(), - started: None.into(), - }) - } - }, - } - } - } - pub fn toggle_play (&mut self) -> Usually<()> { - let playing = self.clock.playing.read().unwrap().expect("1st sample has not been processed yet"); - let playing = match playing { - TransportState::Stopped => { - self.transport.start()?; - Some(TransportState::Starting) - }, - _ => { - self.transport.stop()?; - self.transport.locate(0)?; - Some(TransportState::Stopped) - }, - }; - *self.clock.playing.write().unwrap() = playing; - Ok(()) - } -} -impl TransportFocus { - pub fn next (&mut self) { - *self = match self { - Self::PlayPause => Self::Bpm, - Self::Bpm => Self::Quant, - Self::Quant => Self::Sync, - Self::Sync => Self::Clock, - Self::Clock => Self::PlayPause, - } - } - pub fn prev (&mut self) { - *self = match self { - Self::PlayPause => Self::Clock, - Self::Bpm => Self::PlayPause, - Self::Quant => Self::Bpm, - Self::Sync => Self::Quant, - Self::Clock => Self::Sync, - } - } -} diff --git a/crates/tek_tui/src/arranger.rs b/crates/tek_tui/src/tui_arranger.rs similarity index 94% rename from crates/tek_tui/src/arranger.rs rename to crates/tek_tui/src/tui_arranger.rs index 53f8bd5c..31715f23 100644 --- a/crates/tek_tui/src/arranger.rs +++ b/crates/tek_tui/src/tui_arranger.rs @@ -811,3 +811,56 @@ impl Scene { match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None } } } + +/// Layout for standalone arranger app. +impl Content for Arranger { + type Engine = Tui; + fn content (&self) -> impl Widget { + let focused = self.arrangement.focused; + let border_bg = Arranger::::border_bg(); + let border_fg = Arranger::::border_fg(focused); + let title_fg = Arranger::::title_fg(focused); + let border = Lozenge(Style::default().bg(border_bg).fg(border_fg)); + let entered = if self.arrangement.entered { "■" } else { " " }; + Split::down( + 1, + row!(menu in self.menu.menus.iter() => { + row!(" ", menu.title.as_str(), " ") + }), + Split::up( + 1, + widget(&self.status), + Split::up( + 1, + widget(&self.transport), + Split::down( + self.arrangement_split, + lay!( + widget(&self.arrangement).grow_y(1).border(border), + widget(&self.arrangement.size), + widget(&format!("[{}] Arrangement", entered)).fg(title_fg).push_x(1), + ), + Split::right( + self.phrases_split, + self.phrases.clone(), + widget(&self.editor), + ) + ) + ) + ) + ) + } +} + +impl Content for Arrangement { + type Engine = Tui; + fn content (&self) -> impl Widget { + Layers::new(move |add|{ + match self.mode { + ArrangementViewMode::Horizontal => { add(&HorizontalArranger(&self)) }, + ArrangementViewMode::Vertical(factor) => { add(&VerticalArranger(&self, factor)) }, + }?; + add(&self.size) + }) + } +} diff --git a/crates/tek_tui/src/arranger_tui_bar.rs b/crates/tek_tui/src/tui_arranger_bar.rs similarity index 100% rename from crates/tek_tui/src/arranger_tui_bar.rs rename to crates/tek_tui/src/tui_arranger_bar.rs diff --git a/crates/tek_tui/src/arranger_tui_cmd.rs b/crates/tek_tui/src/tui_arranger_cmd.rs similarity index 67% rename from crates/tek_tui/src/arranger_tui_cmd.rs rename to crates/tek_tui/src/tui_arranger_cmd.rs index 563f2778..5298d1c7 100644 --- a/crates/tek_tui/src/arranger_tui_cmd.rs +++ b/crates/tek_tui/src/tui_arranger_cmd.rs @@ -1,5 +1,15 @@ use crate::*; +#[derive(Clone)] +pub enum ArrangerCommand { + Focus(FocusCommand), + Transport(TransportCommand), + Phrases(PhrasePoolCommand), + Editor(PhraseEditorCommand), + Arrangement(ArrangementCommand), + EditPhrase(Option>>), +} + /// Handle top-level events in standalone arranger. impl Handle for Arranger { fn handle (&mut self, i: &TuiInput) -> Perhaps { @@ -159,3 +169,103 @@ impl InputToCommand> for ArrangementCommand { //Ok(Some(true)) //} //} + +#[derive(Clone)] +pub enum ArrangerCommand { + Focus(FocusCommand), + Transport(TransportCommand), + Phrases(PhrasePoolCommand), + Editor(PhraseEditorCommand), + Arrangement(ArrangementCommand), + EditPhrase(Option>>), +} +#[derive(Clone)] +pub enum ArrangementCommand { + New, + Load, + Save, + ToggleViewMode, + Delete, + Activate, + Increment, + Decrement, + ZoomIn, + ZoomOut, + MoveBack, + MoveForward, + RandomColor, + Put, + Get, + AddScene, + AddTrack, + ToggleLoop, + GoUp, + GoDown, + GoLeft, + GoRight, + Edit(Option>>), +} + +impl Command> for ArrangerCommand { + fn execute (self, state: &mut Arranger) -> Perhaps { + let undo = match self { + Self::Focus(cmd) => { + delegate(cmd, Self::Focus, state) + }, + Self::Phrases(cmd) => { + delegate(cmd, Self::Phrases, &mut*state.phrases.write().unwrap()) + }, + Self::Editor(cmd) => { + delegate(cmd, Self::Editor, &mut state.editor) + }, + Self::Arrangement(cmd) => { + delegate(cmd, Self::Arrangement, &mut state.arrangement) + }, + Self::Transport(cmd) => if let Some(ref transport) = state.transport { + delegate(cmd, Self::Transport, &mut*transport.write().unwrap()) + } else { + Ok(None) + }, + Self::EditPhrase(phrase) => { + state.editor.phrase = phrase.clone(); + state.focus(ArrangerFocus::PhraseEditor); + state.focus_enter(); + Ok(None) + } + }?; + state.show_phrase(); + state.update_status(); + return Ok(undo); + } +} +impl Command> for ArrangementCommand { + fn execute (self, state: &mut Arrangement) -> Perhaps { + use ArrangementCommand::*; + match self { + New => todo!(), + Load => todo!(), + Save => todo!(), + Edit(phrase) => { state.phrase = phrase.clone() }, + ToggleViewMode => { state.mode.to_next(); }, + Delete => { state.delete(); }, + Activate => { state.activate(); }, + Increment => { state.increment(); }, + Decrement => { state.decrement(); }, + ZoomIn => { state.zoom_in(); }, + ZoomOut => { state.zoom_out(); }, + MoveBack => { state.move_back(); }, + MoveForward => { state.move_forward(); }, + RandomColor => { state.randomize_color(); }, + Put => { state.phrase_put(); }, + Get => { state.phrase_get(); }, + AddScene => { state.scene_add(None, None)?; }, + AddTrack => { state.track_add(None, None)?; }, + ToggleLoop => { state.toggle_loop() }, + GoUp => { state.go_up() }, + GoDown => { state.go_down() }, + GoLeft => { state.go_left() }, + GoRight => { state.go_right() }, + }; + Ok(None) + } +} diff --git a/crates/tek_tui/src/arranger_tui_col.rs b/crates/tek_tui/src/tui_arranger_col.rs similarity index 100% rename from crates/tek_tui/src/arranger_tui_col.rs rename to crates/tek_tui/src/tui_arranger_col.rs diff --git a/crates/tek_tui/src/arranger_tui_hor.rs b/crates/tek_tui/src/tui_arranger_hor.rs similarity index 100% rename from crates/tek_tui/src/arranger_tui_hor.rs rename to crates/tek_tui/src/tui_arranger_hor.rs diff --git a/crates/tek_tui/src/arranger_tui_ver.rs b/crates/tek_tui/src/tui_arranger_ver.rs similarity index 100% rename from crates/tek_tui/src/arranger_tui_ver.rs rename to crates/tek_tui/src/tui_arranger_ver.rs diff --git a/crates/tek_tui/src/mixer.rs b/crates/tek_tui/src/tui_mixer.rs similarity index 79% rename from crates/tek_tui/src/mixer.rs rename to crates/tek_tui/src/tui_mixer.rs index 11b80376..c2b49b5b 100644 --- a/crates/tek_tui/src/mixer.rs +++ b/crates/tek_tui/src/tui_mixer.rs @@ -88,8 +88,6 @@ impl Track { //} } - - pub struct TrackView<'a, E: Engine> { pub chain: Option<&'a Track>, pub direction: Direction, @@ -131,3 +129,40 @@ impl<'a> Widget for TrackView<'a, Tui> { //} } } + +impl Content for Mixer { + type Engine = Tui; + fn content (&self) -> impl Widget { + Stack::right(|add| { + for channel in self.tracks.iter() { + add(channel)?; + } + Ok(()) + }) + } +} + +impl Content for Track { + type Engine = Tui; + fn content (&self) -> impl Widget { + TrackView { + chain: Some(&self), + direction: tek_core::Direction::Right, + focused: true, + entered: true, + //pub channels: u8, + //pub input_ports: Vec>, + //pub pre_gain_meter: f64, + //pub gain: f64, + //pub insert_ports: Vec>, + //pub return_ports: Vec>, + //pub post_gain_meter: f64, + //pub post_insert_meter: f64, + //pub level: f64, + //pub pan: f64, + //pub output_ports: Vec>, + //pub post_fader_meter: f64, + //pub route: String, + } + } +} diff --git a/crates/tek_tui/src/mixer_cmd.rs b/crates/tek_tui/src/tui_mixer_cmd.rs similarity index 100% rename from crates/tek_tui/src/mixer_cmd.rs rename to crates/tek_tui/src/tui_mixer_cmd.rs diff --git a/crates/tek_tui/src/plugin_tui.rs b/crates/tek_tui/src/tui_plugin.rs similarity index 72% rename from crates/tek_tui/src/plugin_tui.rs rename to crates/tek_tui/src/tui_plugin.rs index da9defc5..c42c1c85 100644 --- a/crates/tek_tui/src/plugin_tui.rs +++ b/crates/tek_tui/src/tui_plugin.rs @@ -1,4 +1,36 @@ use crate::*; + +/// A plugin device. +pub struct Plugin { + _engine: PhantomData, + /// JACK client handle (needs to not be dropped for standalone mode to work). + pub jack: Arc>, + pub name: String, + pub path: Option, + pub plugin: Option, + pub selected: usize, + pub mapping: bool, + pub ports: JackPorts, +} + +impl Plugin { + /// Create a plugin host device. + pub fn new ( + jack: &Arc>, + name: &str, + ) -> Usually { + Ok(Self { + _engine: Default::default(), + jack: jack.clone(), + name: name.into(), + path: None, + plugin: None, + selected: 0, + mapping: false, + ports: JackPorts::default() + }) + } +} impl Widget for Plugin { type Engine = Tui; fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> { diff --git a/crates/tek_tui/src/plugin_cmd.rs b/crates/tek_tui/src/tui_plugin_cmd.rs similarity index 100% rename from crates/tek_tui/src/plugin_cmd.rs rename to crates/tek_tui/src/tui_plugin_cmd.rs diff --git a/crates/tek_tui/src/plugin_lv2.rs b/crates/tek_tui/src/tui_plugin_lv2.rs similarity index 100% rename from crates/tek_tui/src/plugin_lv2.rs rename to crates/tek_tui/src/tui_plugin_lv2.rs diff --git a/crates/tek_tui/src/plugin_lv2_gui.rs b/crates/tek_tui/src/tui_plugin_lv2_gui.rs similarity index 73% rename from crates/tek_tui/src/plugin_lv2_gui.rs rename to crates/tek_tui/src/tui_plugin_lv2_gui.rs index f096c048..a296eee5 100644 --- a/crates/tek_tui/src/plugin_lv2_gui.rs +++ b/crates/tek_tui/src/tui_plugin_lv2_gui.rs @@ -56,21 +56,3 @@ impl ApplicationHandler for LV2PluginUI { fn lv2_ui_instantiate (kind: &str) { //let host = Suil } - -pub 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) -} diff --git a/crates/tek_tui/src/plugin_vst2.rs b/crates/tek_tui/src/tui_plugin_vst2.rs similarity index 100% rename from crates/tek_tui/src/plugin_vst2.rs rename to crates/tek_tui/src/tui_plugin_vst2.rs diff --git a/crates/tek_tui/src/plugin_vst3.rs b/crates/tek_tui/src/tui_plugin_vst3.rs similarity index 100% rename from crates/tek_tui/src/plugin_vst3.rs rename to crates/tek_tui/src/tui_plugin_vst3.rs diff --git a/crates/tek_tui/src/sampler.rs b/crates/tek_tui/src/tui_sampler.rs similarity index 79% rename from crates/tek_tui/src/sampler.rs rename to crates/tek_tui/src/tui_sampler.rs index c2f46c0e..44c263e4 100644 --- a/crates/tek_tui/src/sampler.rs +++ b/crates/tek_tui/src/tui_sampler.rs @@ -117,20 +117,6 @@ pub struct Sample { pub rate: Option, } -impl Sample { - pub fn new (name: &str, start: usize, end: usize, channels: Vec>) -> Self { - Self { name: name.to_string(), start, end, channels, rate: None } - } - pub fn play (sample: &Arc>, after: usize, velocity: &u7) -> Voice { - Voice { - sample: sample.clone(), - after, - position: sample.read().unwrap().start, - velocity: velocity.as_int() as f32 / 127.0, - } - } -} - /// Load sample from WAV and assign to MIDI note. #[macro_export] macro_rules! sample { @@ -412,3 +398,100 @@ impl Iterator for Voice { None } } + +impl Widget for Sampler { + type Engine = Tui; + fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> { + todo!() + } + fn render (&self, to: &mut TuiOutput) -> Usually<()> { + tui_render_sampler(self, to) + } +} + +pub fn tui_render_sampler (sampler: &Sampler, to: &mut TuiOutput) -> Usually<()> { + let [x, y, _, height] = to.area(); + let style = Style::default().gray(); + let title = format!(" {} ({})", sampler.name, sampler.voices.read().unwrap().len()); + to.blit(&title, x+1, y, Some(style.white().bold().not_dim())); + let mut width = title.len() + 2; + let mut y1 = 1; + let mut j = 0; + for (note, sample) in sampler.mapped.iter() + .map(|(note, sample)|(Some(note), sample)) + .chain(sampler.unmapped.iter().map(|sample|(None, sample))) + { + if y1 >= height { + break + } + let active = j == sampler.cursor.0; + width = width.max( + draw_sample(to, x, y + y1, note, &*sample.read().unwrap(), active)? + ); + y1 = y1 + 1; + j = j + 1; + } + let height = ((2 + y1) as u16).min(height); + //Ok(Some([x, y, (width as u16).min(to.area().w()), height])) + Ok(()) +} + +fn draw_sample ( + to: &mut TuiOutput, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool +) -> Usually { + let style = if focus { Style::default().green() } else { Style::default() }; + if focus { + to.blit(&"🬴", x+1, y, Some(style.bold())); + } + let label1 = format!("{:3} {:12}", + note.map(|n|n.to_string()).unwrap_or(String::default()), + sample.name); + let label2 = format!("{:>6} {:>6} +0.0", + sample.start, + sample.end); + to.blit(&label1, x+2, y, Some(style.bold())); + to.blit(&label2, x+3+label1.len()as u16, y, Some(style)); + Ok(label1.len() + label2.len() + 4) +} + +impl Widget for AddSampleModal { + type Engine = Tui; + fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> { + todo!() + //Align::Center(()).layout(to) + } + fn render (&self, to: &mut TuiOutput) -> Usually<()> { + todo!() + //let area = to.area(); + //to.make_dim(); + //let area = center_box( + //area, + //64.max(area.w().saturating_sub(8)), + //20.max(area.w().saturating_sub(8)), + //); + //to.fill_fg(area, Color::Reset); + //to.fill_bg(area, Nord::bg_lo(true, true)); + //to.fill_char(area, ' '); + //to.blit(&format!("{}", &self.dir.to_string_lossy()), area.x()+2, area.y()+1, Some(Style::default().bold()))?; + //to.blit(&"Select sample:", area.x()+2, area.y()+2, Some(Style::default().bold()))?; + //for (i, (is_dir, name)) in self.subdirs.iter() + //.map(|path|(true, path)) + //.chain(self.files.iter().map(|path|(false, path))) + //.enumerate() + //.skip(self.offset) + //{ + //if i >= area.h() as usize - 4 { + //break + //} + //let t = if is_dir { "" } else { "" }; + //let line = format!("{t} {}", name.to_string_lossy()); + //let line = &line[..line.len().min(area.w() as usize - 4)]; + //to.blit(&line, area.x() + 2, area.y() + 3 + i as u16, Some(if i == self.cursor { + //Style::default().green() + //} else { + //Style::default().white() + //}))?; + //} + //Lozenge(Style::default()).draw(to) + } +} diff --git a/crates/tek_tui/src/sampler_cmd.rs b/crates/tek_tui/src/tui_sampler_cmd.rs similarity index 100% rename from crates/tek_tui/src/sampler_cmd.rs rename to crates/tek_tui/src/tui_sampler_cmd.rs diff --git a/crates/tek_tui/src/sequencer_tui.rs b/crates/tek_tui/src/tui_sequencer.rs similarity index 58% rename from crates/tek_tui/src/sequencer_tui.rs rename to crates/tek_tui/src/tui_sequencer.rs index 73815b4c..1e21cb76 100644 --- a/crates/tek_tui/src/sequencer_tui.rs +++ b/crates/tek_tui/src/tui_sequencer.rs @@ -1,4 +1,419 @@ use crate::*; +use std::cmp::PartialEq; +/// MIDI message structural +pub type PhraseData = Vec>; +/// MIDI message serialized +pub type PhraseMessage = Vec; +/// Collection of serialized MIDI messages +pub type PhraseChunk = [Vec]; +/// Root level object for standalone `tek_sequencer` +pub struct Sequencer { + /// JACK client handle (needs to not be dropped for standalone mode to work). + pub jack: Arc>, + /// Controls the JACK transport. + pub transport: Option>>>, + /// Global timebase + pub clock: Arc, + /// Pool of all phrases available to the sequencer + pub phrases: Arc>>, + /// Phrase editor view + pub editor: PhraseEditor, + /// Phrase player + pub player: PhrasePlayer, + /// Which view is focused + pub focus_cursor: (usize, usize), + /// Whether the currently focused item is entered + pub entered: bool, +} +/// Sections in the sequencer app that may be focused +#[derive(Copy, Clone, PartialEq, Eq)] pub enum SequencerFocus { + /// The transport (toolbar) is focused + Transport, + /// The phrase list (pool) is focused + PhrasePool, + /// The phrase editor (sequencer) is focused + PhraseEditor, +} +/// Status bar for sequencer app +pub enum SequencerStatusBar { + Transport, + PhrasePool, + PhraseEditor, +} +/// Modes for phrase pool +pub enum PhrasePoolMode { + /// Renaming a pattern + Rename(usize, String), + /// Editing the length of a pattern + Length(usize, usize, PhraseLengthFocus), +} +/// A MIDI sequence. +#[derive(Debug, Clone)] pub struct Phrase { + pub uuid: uuid::Uuid, + /// Name of phrase + pub name: String, + /// Temporal resolution in pulses per quarter note + pub ppq: usize, + /// Length of phrase in pulses + pub length: usize, + /// Notes in phrase + pub notes: PhraseData, + /// Whether to loop the phrase or play it once + pub loop_on: bool, + /// Start of loop + pub loop_start: usize, + /// Length of loop + pub loop_length: usize, + /// All notes are displayed with minimum length + pub percussive: bool, + /// Identifying color of phrase + pub color: ItemColorTriplet, +} +/// Contains state for viewing and editing a phrase +pub struct PhraseEditor { + _engine: PhantomData, + /// Phrase being played + pub phrase: Option>>, + /// Length of note that will be inserted, in pulses + pub note_len: usize, + /// The full piano keys are rendered to this buffer + pub keys: Buffer, + /// The full piano roll is rendered to this buffer + pub buffer: BigBuffer, + /// Cursor/scroll/zoom in pitch axis + pub note_axis: RwLock>, + /// Cursor/scroll/zoom in time axis + pub time_axis: RwLock>, + /// Whether this widget is focused + pub focused: bool, + /// Whether note enter mode is enabled + pub entered: bool, + /// Display mode + pub mode: bool, + /// Notes currently held at input + pub notes_in: Arc>, + /// Notes currently held at output + pub notes_out: Arc>, + /// Current position of global playhead + pub now: Arc, + /// Width of notes area at last render + pub width: AtomicUsize, + /// Height of notes area at last render + pub height: AtomicUsize, +} +/// Phrase player. +pub struct PhrasePlayer { + /// Global timebase + pub clock: Arc, + /// Start time and phrase being played + pub phrase: Option<(Instant, Option>>)>, + /// Start time and next phrase + pub next_phrase: Option<(Instant, 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_inputs: Vec>, + /// Play from current sequence to MIDI ports + pub midi_outputs: Vec>, + /// MIDI output buffer + pub midi_note: Vec, + /// MIDI output buffer + pub midi_chunk: Vec>>, + /// Notes currently held at input + pub notes_in: Arc>, + /// Notes currently held at output + pub notes_out: Arc>, +} +/// Displays and edits phrase length. +pub struct PhraseLength { + _engine: PhantomData, + /// Pulses per beat (quaver) + pub ppq: usize, + /// Beats per bar + pub bpb: usize, + /// Length of phrase in pulses + pub pulses: usize, + /// Selected subdivision + pub focus: Option, +} +/// Focused field of `PhraseLength` +#[derive(Copy, Clone)] pub enum PhraseLengthFocus { + /// Editing the number of bars + Bar, + /// Editing the number of beats + Beat, + /// Editing the number of ticks + Tick, +} +/// Focus layout of sequencer app +impl FocusGrid for Sequencer { + type Item = SequencerFocus; + fn cursor (&self) -> (usize, usize) { self.focus_cursor } + fn cursor_mut (&mut self) -> &mut (usize, usize) { &mut self.focus_cursor } + fn layout (&self) -> &[&[SequencerFocus]] { &[ + &[SequencerFocus::Transport], + &[SequencerFocus::PhrasePool, SequencerFocus::PhraseEditor], + ] } + fn focus_enter (&mut self) { self.entered = true } + fn focus_exit (&mut self) { self.entered = false } + fn entered (&self) -> Option { + if self.entered { Some(self.focused()) } else { None } + } + fn update_focus (&mut self) { + let focused = self.focused(); + if let Some(transport) = self.transport.as_ref() { + transport.write().unwrap().focused = focused == SequencerFocus::Transport + } + self.phrases.write().unwrap().focused = focused == SequencerFocus::PhrasePool; + self.editor.focused = focused == SequencerFocus::PhraseEditor; + } +} +impl PhrasePool { + pub fn new () -> Self { + Self { + _engine: Default::default(), + scroll: 0, + phrase: 0, + phrases: vec![Arc::new(RwLock::new(Phrase::default()))], + mode: None, + focused: false, + entered: false, + } + } + pub fn len (&self) -> usize { self.phrases.len() } + pub fn phrase (&self) -> &Arc> { &self.phrases[self.phrase] } + pub fn select_prev (&mut self) { self.phrase = self.index_before(self.phrase) } + pub fn select_next (&mut self) { self.phrase = self.index_after(self.phrase) } + pub fn index_before (&self, index: usize) -> usize { + index.overflowing_sub(1).0.min(self.len() - 1) + } + pub fn index_after (&self, index: usize) -> usize { + (index + 1) % self.len() + } + pub fn index_of (&self, phrase: &Phrase) -> Option { + for i in 0..self.phrases.len() { + if *self.phrases[i].read().unwrap() == *phrase { return Some(i) } + } + return None + } + fn new_phrase (name: Option<&str>, color: Option) -> Arc> { + Arc::new(RwLock::new(Phrase::new( + String::from(name.unwrap_or("(new)")), true, 4 * PPQ, None, color + ))) + } + pub fn delete_selected (&mut self) { + if self.phrase > 0 { + self.phrases.remove(self.phrase); + self.phrase = self.phrase.min(self.phrases.len().saturating_sub(1)); + } + } + pub fn append_new (&mut self, name: Option<&str>, color: Option) { + self.phrases.push(Self::new_phrase(name, color)); + self.phrase = self.phrases.len() - 1; + } + pub fn insert_new (&mut self, name: Option<&str>, color: Option) { + self.phrases.insert(self.phrase + 1, Self::new_phrase(name, color)); + self.phrase += 1; + } + pub fn insert_dup (&mut self) { + let mut phrase = self.phrases[self.phrase].read().unwrap().duplicate(); + phrase.color = ItemColorTriplet::random_near(phrase.color, 0.25); + self.phrases.insert(self.phrase + 1, Arc::new(RwLock::new(phrase))); + self.phrase += 1; + } + pub fn randomize_color (&mut self) { + let mut phrase = self.phrases[self.phrase].write().unwrap(); + phrase.color = ItemColorTriplet::random(); + } + pub fn begin_rename (&mut self) { + self.mode = Some(PhrasePoolMode::Rename( + self.phrase, + self.phrases[self.phrase].read().unwrap().name.clone() + )); + } + pub fn begin_length (&mut self) { + self.mode = Some(PhrasePoolMode::Length( + self.phrase, + self.phrases[self.phrase].read().unwrap().length, + PhraseLengthFocus::Bar + )); + } + pub fn move_up (&mut self) { + if self.phrase > 1 { + self.phrases.swap(self.phrase - 1, self.phrase); + self.phrase -= 1; + } + } + pub fn move_down (&mut self) { + if self.phrase < self.phrases.len().saturating_sub(1) { + self.phrases.swap(self.phrase + 1, self.phrase); + self.phrase += 1; + } + } +} +impl PhraseEditor { + pub fn new () -> Self { + Self { + _engine: Default::default(), + phrase: None, + note_len: 24, + notes_in: Arc::new(RwLock::new([false;128])), + notes_out: Arc::new(RwLock::new([false;128])), + keys: keys_vert(), + buffer: Default::default(), + focused: false, + entered: false, + mode: false, + now: Arc::new(0.into()), + width: 0.into(), + height: 0.into(), + note_axis: RwLock::new(FixedAxis { + start: 12, + point: Some(36), + clamp: Some(127) + }), + time_axis: RwLock::new(ScaledAxis { + start: 00, + point: Some(00), + clamp: Some(000), + scale: 24 + }), + } + } + pub fn note_cursor_inc (&self) { + let mut axis = self.note_axis.write().unwrap(); + axis.point_dec(1); + if let Some(point) = axis.point { if point < axis.start { axis.start = (point / 2) * 2; } } + } + pub fn note_cursor_dec (&self) { + let mut axis = self.note_axis.write().unwrap(); + axis.point_inc(1); + if let Some(point) = axis.point { if point > 73 { axis.point = Some(73); } } + } + pub fn note_page_up (&self) { + let mut axis = self.note_axis.write().unwrap(); + axis.start_dec(3); + axis.point_dec(3); + } + pub fn note_page_down (&self) { + let mut axis = self.note_axis.write().unwrap(); + axis.start_inc(3); + axis.point_inc(3); + } + pub fn note_scroll_inc (&self) { self.note_axis.write().unwrap().start_dec(1); } + pub fn note_scroll_dec (&self) { self.note_axis.write().unwrap().start_inc(1); } + pub fn note_length_inc (&mut self) { self.note_len = next_note_length(self.note_len) } + pub fn note_length_dec (&mut self) { self.note_len = prev_note_length(self.note_len) } + pub fn time_cursor_advance (&self) { + let point = self.time_axis.read().unwrap().point; + let length = self.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); + let forward = |time|(time + self.note_len) % length; + self.time_axis.write().unwrap().point = point.map(forward); + } + pub fn time_cursor_inc (&self) { + let scale = self.time_axis.read().unwrap().scale; + self.time_axis.write().unwrap().point_inc(scale); + } + pub fn time_cursor_dec (&self) { + let scale = self.time_axis.read().unwrap().scale; + self.time_axis.write().unwrap().point_dec(scale); + } + pub fn time_scroll_inc (&self) { + let scale = self.time_axis.read().unwrap().scale; + self.time_axis.write().unwrap().start_inc(scale); + } + pub fn time_scroll_dec (&self) { + let scale = self.time_axis.read().unwrap().scale; + self.time_axis.write().unwrap().start_dec(scale); + } + pub fn time_zoom_in (&self) { + let scale = self.time_axis.read().unwrap().scale; + self.time_axis.write().unwrap().scale = prev_note_length(scale) + } + pub fn time_zoom_out (&self) { + let scale = self.time_axis.read().unwrap().scale; + self.time_axis.write().unwrap().scale = next_note_length(scale) + } +} +impl PhrasePlayer { + pub fn new ( + jack: &Arc>, + clock: &Arc, + name: &str + ) -> Usually { + let jack = jack.read().unwrap(); + Ok(Self { + clock: clock.clone(), + phrase: None, + next_phrase: None, + notes_in: Arc::new(RwLock::new([false;128])), + notes_out: Arc::new(RwLock::new([false;128])), + monitoring: false, + recording: false, + overdub: true, + reset: true, + midi_note: Vec::with_capacity(8), + midi_chunk: vec![Vec::with_capacity(16);16384], + midi_outputs: vec![ + jack.client().register_port(format!("{name}_out0").as_str(), MidiOut::default())? + ], + midi_inputs: vec![ + jack.client().register_port(format!("{name}_in0").as_str(), MidiIn::default())? + ], + }) + } + pub fn toggle_monitor (&mut self) { self.monitoring = !self.monitoring; } + pub fn toggle_record (&mut self) { self.recording = !self.recording; } + pub fn toggle_overdub (&mut self) { self.overdub = !self.overdub; } + pub fn enqueue_next (&mut self, phrase: Option<&Arc>>) { + let start = self.clock.next_launch_pulse(); + self.next_phrase = Some(( + Instant::from_pulse(&self.clock.timebase(), start as f64), + phrase.map(|p|p.clone()) + )); + self.reset = true; + } + pub fn pulses_since_start (&self) -> Option { + if let Some((started, Some(_))) = self.phrase.as_ref() { + Some(self.clock.current.pulse.get() - started.pulse.get()) + } else { + None + } + } +} +impl PhraseLength { + pub fn new (pulses: usize, focus: Option) -> Self { + Self { _engine: Default::default(), ppq: PPQ, bpb: 4, pulses, focus } + } + pub fn bars (&self) -> usize { self.pulses / (self.bpb * self.ppq) } + pub fn beats (&self) -> usize { (self.pulses % (self.bpb * self.ppq)) / self.ppq } + pub fn ticks (&self) -> usize { self.pulses % self.ppq } + pub fn bars_string (&self) -> String { format!("{}", self.bars()) } + pub fn beats_string (&self) -> String { format!("{}", self.beats()) } + pub fn ticks_string (&self) -> String { format!("{:>02}", self.ticks()) } +} +impl PhraseLengthFocus { + pub fn next (&mut self) { + *self = match self { + Self::Bar => Self::Beat, + Self::Beat => Self::Tick, + Self::Tick => Self::Bar, + } + } + pub fn prev (&mut self) { + *self = match self { + Self::Bar => Self::Tick, + Self::Beat => Self::Bar, + Self::Tick => Self::Beat, + } + } +} impl Content for Sequencer { type Engine = Tui; fn content (&self) -> impl Widget { diff --git a/crates/tek_tui/src/sequencer_cmd.rs b/crates/tek_tui/src/tui_sequencer_cmd.rs similarity index 100% rename from crates/tek_tui/src/sequencer_cmd.rs rename to crates/tek_tui/src/tui_sequencer_cmd.rs diff --git a/crates/tek_tui/src/transport_tui.rs b/crates/tek_tui/src/tui_transport.rs similarity index 54% rename from crates/tek_tui/src/transport_tui.rs rename to crates/tek_tui/src/tui_transport.rs index 14490769..4c5fed04 100644 --- a/crates/tek_tui/src/transport_tui.rs +++ b/crates/tek_tui/src/tui_transport.rs @@ -1,4 +1,84 @@ use crate::*; +/// Stores and displays time-related state. +#[derive(Debug)] +pub struct TransportView { + _engine: PhantomData, + state: TransportToolbar, + focused: bool, + focus: TransportFocus, +} +/// Which item of the transport toolbar is focused +#[derive(Clone, Copy, PartialEq)] +pub enum TransportFocus { + Bpm, + Sync, + PlayPause, + Clock, + Quant, +} +impl TransportView { + pub fn new (jack: &Arc>, clock: Option<&Arc>) -> Self { + Self { + _engine: Default::default(), + focused: false, + focus: TransportFocus::PlayPause, + state: TransportToolbar { + metronome: false, + transport: jack.read().unwrap().transport(), + jack: jack.clone(), + clock: match clock { + Some(clock) => clock.clone(), + None => { + let timebase = Arc::new(Timebase::default()); + Arc::new(TransportTime { + playing: Some(TransportState::Stopped).into(), + quant: 24.into(), + sync: (timebase.ppq.get() * 4.).into(), + current: Instant::default(), + started: None.into(), + }) + } + }, + } + } + } + pub fn toggle_play (&mut self) -> Usually<()> { + let playing = self.clock.playing.read().unwrap().expect("1st sample has not been processed yet"); + let playing = match playing { + TransportState::Stopped => { + self.transport.start()?; + Some(TransportState::Starting) + }, + _ => { + self.transport.stop()?; + self.transport.locate(0)?; + Some(TransportState::Stopped) + }, + }; + *self.clock.playing.write().unwrap() = playing; + Ok(()) + } +} +impl TransportFocus { + pub fn next (&mut self) { + *self = match self { + Self::PlayPause => Self::Bpm, + Self::Bpm => Self::Quant, + Self::Quant => Self::Sync, + Self::Sync => Self::Clock, + Self::Clock => Self::PlayPause, + } + } + pub fn prev (&mut self) { + *self = match self { + Self::PlayPause => Self::Clock, + Self::Bpm => Self::PlayPause, + Self::Quant => Self::Bpm, + Self::Sync => Self::Quant, + Self::Clock => Self::Sync, + } + } +} impl Content for TransportToolbar { type Engine = Tui; fn content (&self) -> impl Widget { diff --git a/crates/tek_tui/src/transport_cmd.rs b/crates/tek_tui/src/tui_transport_cmd.rs similarity index 100% rename from crates/tek_tui/src/transport_cmd.rs rename to crates/tek_tui/src/tui_transport_cmd.rs