From fe6ffea5df28be39779ce595051e1ad8e9de1df6 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Thu, 4 Jul 2024 15:32:41 +0300 Subject: [PATCH] refactor: jack proto-lib --- src/core.rs | 65 ++++--- src/core/jack.rs | 271 ---------------------------- src/core/midi.rs | 46 +++-- src/jack.rs | 65 +++++++ src/jack/device.rs | 29 +++ src/jack/event.rs | 64 +++++++ src/jack/factory.rs | 78 ++++++++ src/{core/port.rs => jack/ports.rs} | 23 +-- src/jack/process.rs | 30 +++ src/main.rs | 172 +++++++++--------- src/model/track.rs | 28 ++- src/view/sequencer.rs | 37 +++- 12 files changed, 467 insertions(+), 441 deletions(-) delete mode 100644 src/core/jack.rs create mode 100644 src/jack.rs create mode 100644 src/jack/device.rs create mode 100644 src/jack/event.rs create mode 100644 src/jack/factory.rs rename src/{core/port.rs => jack/ports.rs} (85%) create mode 100644 src/jack/process.rs diff --git a/src/core.rs b/src/core.rs index 7a64b0b4..e5b40260 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,30 +1,27 @@ -pub use std::error::Error; -pub use std::io::{stdout, Stdout, Write}; -pub use std::thread::{spawn, JoinHandle}; -pub use std::time::Duration; -pub use std::collections::BTreeMap; -pub use std::sync::{Arc, Mutex, MutexGuard}; -pub use std::sync::atomic::{Ordering, AtomicBool, AtomicUsize}; -pub use std::sync::mpsc::{channel, Sender, Receiver}; +/// Define and reexport submodules. +#[macro_export] macro_rules! submod { + ($($name:ident)*) => { $(mod $name; pub use self::$name::*;)* }; +} -pub use microxdg::XdgApp; -pub use ratatui::prelude::*; -pub use midly::{MidiMessage, live::LiveEvent, num::u7}; -pub use crossterm::{ExecutableCommand, QueueableCommand}; -pub use crossterm::event::{Event, KeyEvent, KeyCode, KeyModifiers}; - -macro_rules! submod { ($($name:ident)*) => { $(mod $name; pub use self::$name::*;)* }; } - -submod!( handle jack keymap midi port render run time ); +submod!( handle keymap midi render run time ); +/// Standard result type. pub type Usually = Result>; -pub trait Component: Render + Handle {} +/// A UI component. +pub trait Component: Render + Handle { + /// Perform type erasure for collecting heterogeneous components. + fn boxed (self) -> Box where Self: Sized + 'static { + Box::new(self) + } +} +/// Anything that implements `Render` + `Handle` can be used as a UI component. impl Component for T {} -/// A UI component that may have presence on the JACK grap. +/// A UI component that may be associated with a JACK client by the `Jack` factory. pub trait Device: Render + Handle + Process + Send + Sync { + /// Perform type erasure for collecting heterogeneous devices. fn boxed (self) -> Box where Self: Sized + 'static { Box::new(self) } @@ -34,12 +31,24 @@ pub trait Device: Render + Handle + Process + Send + Sync { impl Device for T {} // Reexport macros: -pub use crate::{ - render, - handle, - process, - phrase, - keymap, - key, - ports -}; +pub use crate::{submod, render, handle, process, phrase, keymap, key, ports}; + +// Reexport JACK proto-lib: +pub use crate::jack::*; + +// Various stdlib dependencies: +pub use std::error::Error; +pub use std::io::{stdout, Stdout, Write}; +pub use std::thread::{spawn, JoinHandle}; +pub use std::time::Duration; +pub use std::collections::BTreeMap; +pub use std::sync::{Arc, Mutex, MutexGuard}; +pub use std::sync::atomic::{Ordering, AtomicBool, AtomicUsize}; +pub use std::sync::mpsc::{channel, Sender, Receiver}; + +// Various non-stdlib dependencies: +pub use microxdg::XdgApp; +pub use ratatui::prelude::*; +pub use midly::{MidiMessage, live::LiveEvent, num::u7}; +pub use crossterm::{ExecutableCommand, QueueableCommand}; +pub use crossterm::event::{Event, KeyEvent, KeyCode, KeyModifiers}; diff --git a/src/core/jack.rs b/src/core/jack.rs deleted file mode 100644 index 8459264d..00000000 --- a/src/core/jack.rs +++ /dev/null @@ -1,271 +0,0 @@ -use crate::core::*; - -pub struct Jack { - pub client: Client, - pub ports: JackPorts, -} - -pub struct JackDevice { - pub client: DynamicAsyncClient, - pub state: Arc>>, - pub ports: UnownedJackPorts, -} - -pub enum JackClient { - Active(DynamicAsyncClient), - Inactive(Client), -} - -ports!(JackDevice { - audio: { - ins: |s|Ok(s.ports.audio_ins.values().collect()), - outs: |s|Ok(s.ports.audio_outs.values().collect()), - } - midi: { - ins: |s|Ok(s.ports.midi_ins.values().collect()), - outs: |s|Ok(s.ports.midi_outs.values().collect()), - } -}); - -impl Jack { - pub fn new (name: &str) -> Usually { - let (client, _) = Client::new(name, ClientOptions::NO_START_SERVER)?; - Ok(Self { client, ports: JackPorts::default() }) - } - pub fn run ( - mut self, state: impl FnOnce(JackPorts)->Box - ) - -> Usually - { - let mut owned_ports = JackPorts::default(); - std::mem::swap(&mut self.ports, &mut owned_ports); - let unowned_ports = owned_ports.clone_unowned(&self.client); - let state = Arc::new(Mutex::new(state(owned_ports) as Box)); - let client = self.client.activate_async( - Notifications(Box::new({ - let _state = state.clone(); - move|_event|{ - // FIXME: this deadlocks - //state.lock().unwrap().handle(&event).unwrap(); - } - }) as Box), - ClosureProcessHandler::new(Box::new({ - let state = state.clone(); - move|c: &Client, s: &ProcessScope|{ - state.lock().unwrap().process(c, s) - } - }) as BoxedProcessHandler) - )?; - Ok(JackDevice { client, state, ports: unowned_ports }) - } - pub fn ports_from_lv2 (self, plugin: &::livi::Plugin) -> Usually { - let counts = plugin.port_counts(); - let mut jack = self; - for i in 0..counts.atom_sequence_inputs { - jack = jack.register_midi_in(&format!("midi-in-{i}"))? - } - for i in 0..counts.atom_sequence_outputs { - jack = jack.register_midi_out(&format!("midi-out-{i}"))?; - } - for i in 0..counts.audio_inputs { - jack = jack.register_audio_in(&format!("audio-in-{i}"))? - } - for i in 0..counts.audio_outputs { - jack = jack.register_audio_out(&format!("audio-out-{i}"))?; - } - Ok(jack) - } - pub fn register_midi_out (mut self, name: &str) -> Usually { - let port = self.client.register_port(name, MidiOut::default())?; - self.ports.midi_outs.insert(name.to_string(), port); - Ok(self) - } - pub fn register_midi_in (mut self, name: &str) -> Usually { - let port = self.client.register_port(name, MidiIn::default())?; - self.ports.midi_ins.insert(name.to_string(), port); - Ok(self) - } - pub fn register_audio_out (mut self, name: &str) -> Usually { - let port = self.client.register_port(name, AudioOut::default())?; - self.ports.audio_outs.insert(name.to_string(), port); - Ok(self) - } - pub fn register_audio_in (mut self, name: &str) -> Usually { - let port = self.client.register_port(name, AudioIn::default())?; - self.ports.audio_ins.insert(name.to_string(), port); - Ok(self) - } -} - -pub fn jack_run (name: &str, app: &Arc>) -> Usually - where T: Handle + Process + Send + 'static -{ - let options = ClientOptions::NO_START_SERVER; - let (client, _status) = Client::new(name, options)?; - Ok(client.activate_async( - Notifications(Box::new({ - let _app = app.clone(); - move|_event|{ - // FIXME: this deadlocks - //app.lock().unwrap().handle(&event).unwrap(); - } - }) as Box), - ClosureProcessHandler::new(Box::new({ - let app = app.clone(); - move|c: &Client, s: &ProcessScope|{ - app.lock().unwrap().process(c, s) - //Control::Continue - } - }) as BoxedProcessHandler) - )?) -} - -pub trait Process { - fn process (&mut self, _: &Client, _: &ProcessScope) -> Control { - Control::Continue - } -} - -#[macro_export] macro_rules! process { - ($T:ty) => { - impl Process for $T {} - }; - ($T:ty |$self:ident, $c:ident, $s:ident|$block:tt) => { - impl Process for $T { - fn process (&mut $self, $c: &Client, $s: &ProcessScope) -> Control { - $block - } - } - }; - ($T:ty = $process:path) => { - impl Process for $T { - fn process (&mut self, c: &Client, s: &ProcessScope) -> Control { - $process(self, c, s) - } - } - } -} - -pub type DynamicAsyncClient = - AsyncClient; - -pub type DynamicNotifications = - Notifications>; - -pub type DynamicProcessHandler = - ClosureProcessHandler; - -pub type BoxedProcessHandler = - Box Control + Send>; - -pub use ::jack::{ - AsyncClient, - AudioIn, - AudioOut, - Client, - ClientOptions, - ClientStatus, - ClosureProcessHandler, - Control, - Frames, - MidiIn, - MidiOut, - NotificationHandler, - Port, - PortFlags, - PortId, - PortSpec, - ProcessHandler, - ProcessScope, - RawMidi, - Transport, - TransportState, - TransportStatePosition, - Unowned -}; - -#[derive(Debug)] -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, -} - -pub struct Notifications(pub T); - -impl NotificationHandler for Notifications { - fn thread_init (&self, _: &Client) { - self.0(AppEvent::Jack(JackEvent::ThreadInit)); - } - - fn shutdown (&mut self, status: ClientStatus, reason: &str) { - self.0(AppEvent::Jack(JackEvent::Shutdown(status, reason.into()))); - } - - fn freewheel (&mut self, _: &Client, enabled: bool) { - self.0(AppEvent::Jack(JackEvent::Freewheel(enabled))); - } - - fn sample_rate (&mut self, _: &Client, frames: Frames) -> Control { - self.0(AppEvent::Jack(JackEvent::SampleRate(frames))); - Control::Quit - } - - fn client_registration (&mut self, _: &Client, name: &str, reg: bool) { - self.0(AppEvent::Jack(JackEvent::ClientRegistration(name.into(), reg))); - } - - fn port_registration (&mut self, _: &Client, id: PortId, reg: bool) { - self.0(AppEvent::Jack(JackEvent::PortRegistration(id, reg))); - } - - fn port_rename (&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control { - self.0(AppEvent::Jack(JackEvent::PortRename(id, old.into(), new.into()))); - Control::Continue - } - - fn ports_connected (&mut self, _: &Client, a: PortId, b: PortId, are: bool) { - self.0(AppEvent::Jack(JackEvent::PortsConnected(a, b, are))); - } - - fn graph_reorder (&mut self, _: &Client) -> Control { - self.0(AppEvent::Jack(JackEvent::GraphReorder)); - Control::Continue - } - - fn xrun (&mut self, _: &Client) -> Control { - self.0(AppEvent::Jack(JackEvent::XRun)); - Control::Continue - } -} - -/// Add "all notes off" to the start of a buffer. -pub fn all_notes_off (output: &mut MIDIChunk) { - output[0] = Some(vec![]); - if let Some(Some(frame)) = output.get_mut(0) { - let mut buf = vec![]; - let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() }; - let evt = LiveEvent::Midi { channel: 0.into(), message: msg }; - evt.write(&mut buf).unwrap(); - frame.push(buf); - } -} - -/// Write to JACK port from output buffer (containing notes from sequence and/or monitor) -pub fn write_output (writer: &mut ::jack::MidiWriter, output: &mut MIDIChunk, frames: usize) { - for time in 0..frames { - if let Some(Some(frame)) = output.get_mut(time ) { - for event in frame.iter() { - writer.write(&::jack::RawMidi { time: time as u32, bytes: &event }) - .expect(&format!("{event:?}")); - } - } - } -} diff --git a/src/core/midi.rs b/src/core/midi.rs index 88f5ed4a..2832a5c9 100644 --- a/src/core/midi.rs +++ b/src/core/midi.rs @@ -9,28 +9,26 @@ pub type MIDIMessage = pub type MIDIChunk = [Option>]; -pub const KEY_WHITE: Style = Style { - fg: Some(Color::Gray), - bg: None, - underline_color: None, - add_modifier: ::ratatui::style::Modifier::empty(), - sub_modifier: ::ratatui::style::Modifier::empty(), -}; - -pub const KEY_BLACK: Style = Style { - fg: Some(Color::Black), - bg: None, - underline_color: None, - add_modifier: ::ratatui::style::Modifier::empty(), - sub_modifier: ::ratatui::style::Modifier::empty(), -}; - -pub const KEY_STYLE: [Style;12] = [ - KEY_WHITE, KEY_BLACK, KEY_WHITE, KEY_BLACK, KEY_WHITE, - KEY_WHITE, KEY_BLACK, KEY_WHITE, KEY_BLACK, KEY_WHITE, KEY_BLACK, KEY_WHITE, -]; - -pub const KEYS_VERTICAL: [&'static str; 6] = [ - "▄", "▄", "█", "▀", "▀", "▀", -]; +/// Add "all notes off" to the start of a buffer. +pub fn all_notes_off (output: &mut MIDIChunk) { + output[0] = Some(vec![]); + if let Some(Some(frame)) = output.get_mut(0) { + let mut buf = vec![]; + let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() }; + let evt = LiveEvent::Midi { channel: 0.into(), message: msg }; + evt.write(&mut buf).unwrap(); + frame.push(buf); + } +} +/// Write to JACK port from output buffer (containing notes from sequence and/or monitor) +pub fn write_output (writer: &mut ::jack::MidiWriter, output: &mut MIDIChunk, frames: usize) { + for time in 0..frames { + if let Some(Some(frame)) = output.get_mut(time ) { + for event in frame.iter() { + writer.write(&::jack::RawMidi { time: time as u32, bytes: &event }) + .expect(&format!("{event:?}")); + } + } + } +} diff --git a/src/jack.rs b/src/jack.rs new file mode 100644 index 00000000..51fc6420 --- /dev/null +++ b/src/jack.rs @@ -0,0 +1,65 @@ +use crate::core::*; + +submod!( device event factory ports process ); + +pub type DynamicAsyncClient = + AsyncClient; + +pub type DynamicNotifications = + Notifications>; + +pub type DynamicProcessHandler = + ClosureProcessHandler; + +pub type BoxedProcessHandler = + Box Control + Send>; + +pub use ::_jack::{ + AsyncClient, + AudioIn, + AudioOut, + Client, + ClientOptions, + ClientStatus, + ClosureProcessHandler, + Control, + Frames, + MidiIn, + MidiOut, + NotificationHandler, + Port, + PortFlags, + PortId, + PortSpec, + ProcessHandler, + ProcessScope, + RawMidi, + Transport, + TransportState, + TransportStatePosition, + Unowned +}; + +/// Just run thing with JACK. Returns the activated client. +pub fn jack_run (name: &str, app: &Arc>) -> Usually + where T: Handle + Process + Send + 'static +{ + let options = ClientOptions::NO_START_SERVER; + let (client, _status) = Client::new(name, options)?; + Ok(client.activate_async( + Notifications(Box::new({ + let _app = app.clone(); + move|_event|{ + // FIXME: this deadlocks + //app.lock().unwrap().handle(&event).unwrap(); + } + }) as Box), + ClosureProcessHandler::new(Box::new({ + let app = app.clone(); + move|c: &Client, s: &ProcessScope|{ + app.lock().unwrap().process(c, s) + //Control::Continue + } + }) as BoxedProcessHandler) + )?) +} diff --git a/src/jack/device.rs b/src/jack/device.rs new file mode 100644 index 00000000..3ad0eaf9 --- /dev/null +++ b/src/jack/device.rs @@ -0,0 +1,29 @@ +use super::*; + +/// A Device 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, +} +ports!(JackDevice { + audio: { + ins: |s|Ok(s.ports.audio_ins.values().collect()), + outs: |s|Ok(s.ports.audio_outs.values().collect()), + } + midi: { + ins: |s|Ok(s.ports.midi_ins.values().collect()), + outs: |s|Ok(s.ports.midi_outs.values().collect()), + } +}); +impl JackDevice { + /// Returns a locked mutex of the state's contents. + pub fn state (&self) -> MutexGuard> { + self.state.lock().unwrap() + } +} + diff --git a/src/jack/event.rs b/src/jack/event.rs new file mode 100644 index 00000000..fc0585dc --- /dev/null +++ b/src/jack/event.rs @@ -0,0 +1,64 @@ +use super::*; + +#[derive(Debug)] +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, +} + +pub struct Notifications(pub T); + +impl NotificationHandler for Notifications { + fn thread_init (&self, _: &Client) { + self.0(AppEvent::Jack(JackEvent::ThreadInit)); + } + + fn shutdown (&mut self, status: ClientStatus, reason: &str) { + self.0(AppEvent::Jack(JackEvent::Shutdown(status, reason.into()))); + } + + fn freewheel (&mut self, _: &Client, enabled: bool) { + self.0(AppEvent::Jack(JackEvent::Freewheel(enabled))); + } + + fn sample_rate (&mut self, _: &Client, frames: Frames) -> Control { + self.0(AppEvent::Jack(JackEvent::SampleRate(frames))); + Control::Quit + } + + fn client_registration (&mut self, _: &Client, name: &str, reg: bool) { + self.0(AppEvent::Jack(JackEvent::ClientRegistration(name.into(), reg))); + } + + fn port_registration (&mut self, _: &Client, id: PortId, reg: bool) { + self.0(AppEvent::Jack(JackEvent::PortRegistration(id, reg))); + } + + fn port_rename (&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control { + self.0(AppEvent::Jack(JackEvent::PortRename(id, old.into(), new.into()))); + Control::Continue + } + + fn ports_connected (&mut self, _: &Client, a: PortId, b: PortId, are: bool) { + self.0(AppEvent::Jack(JackEvent::PortsConnected(a, b, are))); + } + + fn graph_reorder (&mut self, _: &Client) -> Control { + self.0(AppEvent::Jack(JackEvent::GraphReorder)); + Control::Continue + } + + fn xrun (&mut self, _: &Client) -> Control { + self.0(AppEvent::Jack(JackEvent::XRun)); + Control::Continue + } +} + diff --git a/src/jack/factory.rs b/src/jack/factory.rs new file mode 100644 index 00000000..ed2cfd99 --- /dev/null +++ b/src/jack/factory.rs @@ -0,0 +1,78 @@ +use super::*; + +/// `JackDevice` factory. Creates JACK `Client`s, performs port registration +/// and activation, and encapsulates a `Device` into a `JackDevice`. +pub struct Jack { + pub client: Client, + pub ports: JackPorts, +} +impl Jack { + pub fn new (name: &str) -> Usually { + let (client, _) = Client::new(name, ClientOptions::NO_START_SERVER)?; + Ok(Self { client, ports: JackPorts::default() }) + } + pub fn run ( + mut self, state: impl FnOnce(JackPorts)->Box + ) + -> Usually + { + let mut owned_ports = JackPorts::default(); + std::mem::swap(&mut self.ports, &mut owned_ports); + let unowned_ports = owned_ports.clone_unowned(&self.client); + let state = Arc::new(Mutex::new(state(owned_ports) as Box)); + let client = self.client.activate_async( + Notifications(Box::new({ + let _state = state.clone(); + move|_event|{ + // FIXME: this deadlocks + //state.lock().unwrap().handle(&event).unwrap(); + } + }) as Box), + ClosureProcessHandler::new(Box::new({ + let state = state.clone(); + move|c: &Client, s: &ProcessScope|{ + state.lock().unwrap().process(c, s) + } + }) as BoxedProcessHandler) + )?; + Ok(JackDevice { client, state, ports: unowned_ports }) + } + pub fn ports_from_lv2 (self, plugin: &::livi::Plugin) -> Usually { + let counts = plugin.port_counts(); + let mut jack = self; + for i in 0..counts.atom_sequence_inputs { + jack = jack.register_midi_in(&format!("midi-in-{i}"))? + } + for i in 0..counts.atom_sequence_outputs { + jack = jack.register_midi_out(&format!("midi-out-{i}"))?; + } + for i in 0..counts.audio_inputs { + jack = jack.register_audio_in(&format!("audio-in-{i}"))? + } + for i in 0..counts.audio_outputs { + jack = jack.register_audio_out(&format!("audio-out-{i}"))?; + } + Ok(jack) + } + pub fn register_midi_out (mut self, name: &str) -> Usually { + let port = self.client.register_port(name, MidiOut::default())?; + self.ports.midi_outs.insert(name.to_string(), port); + Ok(self) + } + pub fn register_midi_in (mut self, name: &str) -> Usually { + let port = self.client.register_port(name, MidiIn::default())?; + self.ports.midi_ins.insert(name.to_string(), port); + Ok(self) + } + pub fn register_audio_out (mut self, name: &str) -> Usually { + let port = self.client.register_port(name, AudioOut::default())?; + self.ports.audio_outs.insert(name.to_string(), port); + Ok(self) + } + pub fn register_audio_in (mut self, name: &str) -> Usually { + let port = self.client.register_port(name, AudioIn::default())?; + self.ports.audio_ins.insert(name.to_string(), port); + Ok(self) + } +} + diff --git a/src/core/port.rs b/src/jack/ports.rs similarity index 85% rename from src/core/port.rs rename to src/jack/ports.rs index 2c99228d..a1817431 100644 --- a/src/core/port.rs +++ b/src/jack/ports.rs @@ -1,4 +1,4 @@ -use crate::core::*; +use super::*; #[derive(Default)] pub struct JackPorts { @@ -95,25 +95,4 @@ pub trait Ports { )? )?} }; - -} - -pub struct DevicePort { - pub name: String, - pub port: Port, - pub connect: Vec, -} - -impl DevicePort { - pub fn new (client: &Client, name: &str, connect: &[&str]) -> Usually { - let mut connects = vec![]; - for port in connect.iter() { - connects.push(port.to_string()); - } - Ok(Self { - name: name.to_string(), - port: client.register_port(name, T::default())?, - connect: connects, - }) - } } diff --git a/src/jack/process.rs b/src/jack/process.rs new file mode 100644 index 00000000..e4d54e2a --- /dev/null +++ b/src/jack/process.rs @@ -0,0 +1,30 @@ +use super::*; + +/// Trait for things that have a JACK process callback. +pub trait Process { + fn process (&mut self, _: &Client, _: &ProcessScope) -> Control { + Control::Continue + } +} + +/// Define the JACK process callback associated with a struct. +#[macro_export] macro_rules! process { + ($T:ty) => { + impl Process for $T {} + }; + ($T:ty |$self:ident, $c:ident, $s:ident|$block:tt) => { + impl Process for $T { + fn process (&mut $self, $c: &Client, $s: &ProcessScope) -> Control { + $block + } + } + }; + ($T:ty = $process:path) => { + impl Process for $T { + fn process (&mut self, c: &Client, s: &ProcessScope) -> Control { + $process(self, c, s) + } + } + } +} + diff --git a/src/main.rs b/src/main.rs index ae9f793d..4a11d43c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ #![allow(macro_expanded_macro_exports_accessed_by_absolute_paths)] extern crate clap; -extern crate jack; +extern crate jack as _jack; extern crate crossterm; pub mod cli; @@ -12,6 +12,7 @@ pub mod control; pub mod core; pub mod model; pub mod view; +pub mod jack; use crate::{core::*, model::*}; @@ -35,48 +36,6 @@ pub fn main () -> Usually<()> { let client = jack.as_client(); let timebase = &state.timebase; let ppq = timebase.ppq() as usize; - state.tracks = vec![ - - Track::new("Drums", &jack.as_client(), Some(vec![ - Phrase::new("4 kicks", ppq * 4, Some(phrase! { - 00 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, - 04 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, - 08 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, - 12 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, - })), - ]), Some(vec![ - Sampler::new("Sampler", Some(BTreeMap::from([ - sample!(36, "Kick", "/home/user/Lab/Music/pak/kik.wav"), - sample!(40, "Snare", "/home/user/Lab/Music/pak/sna.wav"), - sample!(44, "Hihat", "/home/user/Lab/Music/pak/chh.wav"), - ])))?, - Plugin::lv2( - "Panagement", - "file:///home/user/.lv2/Auburn Sounds Panagement 2.lv2" - )?, - ]),)?, - - Track::new("Bass", &jack.as_client(), Some(vec![ - Phrase::new("Offbeat", ppq * 4, Some(phrase! { - 00 * ppq/4 => MidiMessage::NoteOff { key: 40.into(), vel: 100.into() }, - 02 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() }, - 04 * ppq/4 => MidiMessage::NoteOff { key: 40.into(), vel: 100.into() }, - 06 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() }, - 08 * ppq/4 => MidiMessage::NoteOff { key: 40.into(), vel: 100.into() }, - 10 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() }, - 12 * ppq/4 => MidiMessage::NoteOff { key: 40.into(), vel: 100.into() }, - 14 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() }, - })), - ]), Some(vec![ - Plugin::lv2("Odin2", "file:///home/user/.lv2/Odin2.lv2")?, - ]))?, - - ]; - //for track in state.tracks.iter() { - //if let Some(port) = track.midi_ins()?.get(0) { - //client.connect_ports(&track.midi_out, port)?; - //} - //} state.tracks[0].sequence = Some(0); state.tracks[1].sequence = Some(0); state.track_cursor = 1; @@ -87,6 +46,51 @@ pub fn main () -> Usually<()> { state.transport = Some(client.transport()); state.playing = Some(TransportState::Stopped); state.jack = Some(jack); + + let drums = state.add_track(Some("Drums"))?; + drums.add_device(Sampler::new("Sampler", Some(BTreeMap::from([ + sample!(36, "Kick", "/home/user/Lab/Music/pak/kik.wav"), + sample!(40, "Snare", "/home/user/Lab/Music/pak/sna.wav"), + sample!(44, "Hihat", "/home/user/Lab/Music/pak/chh.wav"), + ])))?); + drums.add_device(Plugin::lv2( + "Panagement", + "file:///home/user/.lv2/Auburn Sounds Panagement 2.lv2" + )?); + + drums.add_phrase("4 kicks", ppq * 4, Some(phrase! { + 00 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, + 04 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, + 08 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, + 12 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, + })); + drums.add_phrase("D-Beat", ppq * 4, Some(phrase! { + 00 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, + 04 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() }, + 08 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, + 10 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, + 12 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() }, + })); + drums.add_phrase("Garage", ppq * 4, Some(phrase! { + 00 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, + 04 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() }, + 11 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, + 12 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() }, + })); + + let bass = state.add_track(Some("Bass"))?; + bass.add_device(Plugin::lv2("Odin2", "file:///home/user/.lv2/Odin2.lv2")?); + bass.add_phrase("Offbeat", ppq * 4, Some(phrase! { + 00 * ppq/4 => MidiMessage::NoteOff { key: 40.into(), vel: 100.into() }, + 02 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() }, + 04 * ppq/4 => MidiMessage::NoteOff { key: 40.into(), vel: 100.into() }, + 06 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() }, + 08 * ppq/4 => MidiMessage::NoteOff { key: 40.into(), vel: 100.into() }, + 10 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() }, + 12 * ppq/4 => MidiMessage::NoteOff { key: 40.into(), vel: 100.into() }, + 14 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() }, + })); + Ok(()) })) } @@ -205,6 +209,47 @@ impl App { }; Ok(()) } + pub fn add_scene (&mut self, name: Option<&str>) -> Usually<&mut Scene> { + let name = name.ok_or_else(||format!("Scene {}", self.scenes.len() + 1))?; + self.scenes.push(Scene::new(&name, vec![])); + self.scene_cursor = self.scenes.len(); + Ok(&mut self.scenes[self.scene_cursor - 1]) + } + pub fn add_track (&mut self, name: Option<&str>) -> Usually<&mut Track> { + let name = name.ok_or_else(||format!("Track {}", self.tracks.len() + 1))?; + self.tracks.push(Track::new(&name, self.jack.as_ref().unwrap().as_client(), None, None)?); + self.track_cursor = self.tracks.len(); + Ok(&mut self.tracks[self.track_cursor - 1]) + } + pub fn track (&self) -> Option<(usize, &Track)> { + match self.track_cursor { 0 => None, _ => { + let id = self.track_cursor as usize - 1; + self.tracks.get(id).map(|t|(id, t)) + } } + } + pub fn track_mut (&mut self) -> Option<(usize, &mut Track)> { + match self.track_cursor { 0 => None, _ => { + let id = self.track_cursor as usize - 1; + self.tracks.get_mut(id).map(|t|(id, t)) + } } + } + pub fn scene (&self) -> Option<(usize, &Scene)> { + match self.scene_cursor { 0 => None, _ => { + let id = self.scene_cursor as usize - 1; + self.scenes.get(id).map(|t|(id, t)) + } } + } + pub fn scene_mut (&mut self) -> Option<(usize, &mut Scene)> { + match self.scene_cursor { 0 => None, _ => { + let id = self.scene_cursor as usize - 1; + self.scenes.get_mut(id).map(|t|(id, t)) + } } + } + pub fn phrase_id (&self) -> Option { + let (track_id, _) = self.track()?; + let (_, scene) = self.scene()?; + *scene.clips.get(track_id)? + } pub fn connect_tracks (&self) -> Usually<()> { //let (client, _status) = Client::new( //&format!("{}-init", &self.name), ClientOptions::NO_START_SERVER @@ -233,45 +278,4 @@ impl App { //} Ok(()) } - pub fn add_scene (&mut self, name: Option<&str>) -> Usually<&mut Scene> { - let name = name.ok_or_else(||format!("Scene {}", self.scenes.len() + 1))?; - self.scenes.push(Scene::new(&name, vec![])); - self.scene_cursor = self.scenes.len(); - Ok(&mut self.scenes[self.scene_cursor - 1]) - } - pub fn add_track (&mut self, name: Option<&str>) -> Usually<&mut Track> { - let name = name.ok_or_else(||format!("Track {}", self.tracks.len() + 1))?; - self.tracks.push(Track::new(&name, self.jack.as_ref().unwrap().as_client(), None, None)?); - self.track_cursor = self.tracks.len(); - Ok(&mut self.tracks[self.track_cursor - 1]) - } - pub fn track (&self) -> Option<(usize, &Track)> { - match self.track_cursor { 0 => None, _ => { - let id = self.track_cursor as usize - 1; - self.tracks.get(id).map(|t|(id, t)) - } } - } - pub fn track_mut (&mut self) -> Option<(usize, &mut Track)> { - match self.track_cursor { 0 => None, _ => { - let id = self.track_cursor as usize - 1; - self.tracks.get_mut(id).map(|t|(id, t)) - } } - } - pub fn scene (&self) -> Option<(usize, &Scene)> { - match self.scene_cursor { 0 => None, _ => { - let id = self.scene_cursor as usize - 1; - self.scenes.get(id).map(|t|(id, t)) - } } - } - pub fn scene_mut (&mut self) -> Option<(usize, &mut Scene)> { - match self.scene_cursor { 0 => None, _ => { - let id = self.scene_cursor as usize - 1; - self.scenes.get_mut(id).map(|t|(id, t)) - } } - } - pub fn phrase_id (&self) -> Option { - let (track_id, _) = self.track()?; - let (_, scene) = self.scene()?; - *scene.clips.get(track_id)? - } } diff --git a/src/model/track.rs b/src/model/track.rs index 35881b9c..10909b0e 100644 --- a/src/model/track.rs +++ b/src/model/track.rs @@ -38,22 +38,29 @@ impl Track { recording: false, overdub: true, sequence: None, - phrases: phrases.unwrap_or_else(||vec![]), - devices: devices.unwrap_or_else(||vec![]), + phrases: phrases.unwrap_or_else(||Vec::with_capacity(16)), + devices: devices.unwrap_or_else(||Vec::with_capacity(16)), device: 0, }) } - pub fn device (&self, i: usize) -> Option>> { + pub fn add_device ( + &mut self, device: JackDevice + ) -> &mut JackDevice { + self.devices.push(device); + let index = self.devices.len() - 1; + &mut self.devices[index] + } + pub fn get_device (&self, i: usize) -> Option>> { self.devices.get(i).map(|d|d.state.lock().unwrap()) } - pub fn active_device (&self) -> Option>> { - self.device(self.device) + pub fn device (&self) -> Option>> { + self.get_device(self.device) } pub fn first_device (&self) -> Option>> { - self.device(0) + self.get_device(0) } pub fn last_device (&self) -> Option>> { - self.device(self.devices.len().saturating_sub(1)) + self.get_device(self.devices.len().saturating_sub(1)) } pub fn phrase (&self) -> Option<&Phrase> { if let Some(phrase) = self.sequence { @@ -69,6 +76,13 @@ impl Track { None } } + pub fn add_phrase ( + &mut self, name: &str, length: usize, data: Option + ) -> &mut Phrase { + self.phrases.push(Phrase::new(name, length, data)); + let index = self.phrases.len() - 1; + &mut self.phrases[index] + } } ports!(Track { diff --git a/src/view/sequencer.rs b/src/view/sequencer.rs index aa9e9e6c..fcb5769b 100644 --- a/src/view/sequencer.rs +++ b/src/view/sequencer.rs @@ -4,9 +4,11 @@ use crate::{core::*,model::*,view::*}; pub enum SequencerMode { Tiny, Compact, Horizontal, Vertical, } pub struct SequencerView<'a> { - pub focused: bool, - pub phrase: Option<&'a Phrase>, - pub ppq: usize, + pub focused: bool, + /// Displayed phrase + pub phrase: Option<&'a Phrase>, + /// Resolution of MIDI sequence + pub ppq: usize, /// Range of notes to display pub note_start: usize, /// Position of cursor within note range @@ -17,9 +19,8 @@ pub struct SequencerView<'a> { pub time_start: usize, /// Position of cursor within time range pub time_cursor: usize, - /// Current time - pub now: usize + pub now: usize } impl<'a> Render for SequencerView<'a> { @@ -97,6 +98,10 @@ mod horizontal { } } + pub const KEYS_VERTICAL: [&'static str; 6] = [ + "▄", "▄", "█", "▀", "▀", "▀", + ]; + pub fn keys (buf: &mut Buffer, area: Rect, note0: usize, _notes: &[bool]) -> Usually { @@ -187,6 +192,7 @@ mod horizontal { } } + //pub fn footer ( //buf: &mut Buffer, //area: Rect, @@ -293,6 +299,27 @@ mod horizontal { //buf.set_string(x, y, c, Style::default()); //} +//pub const KEY_WHITE: Style = Style { + //fg: Some(Color::Gray), + //bg: None, + //underline_color: None, + //add_modifier: ::ratatui::style::Modifier::empty(), + //sub_modifier: ::ratatui::style::Modifier::empty(), +//}; + +//pub const KEY_BLACK: Style = Style { + //fg: Some(Color::Black), + //bg: None, + //underline_color: None, + //add_modifier: ::ratatui::style::Modifier::empty(), + //sub_modifier: ::ratatui::style::Modifier::empty(), +//}; + +//pub const KEY_STYLE: [Style;12] = [ + //KEY_WHITE, KEY_BLACK, KEY_WHITE, KEY_BLACK, KEY_WHITE, + //KEY_WHITE, KEY_BLACK, KEY_WHITE, KEY_BLACK, KEY_WHITE, KEY_BLACK, KEY_WHITE, +//]; + //pub fn keys (s: &Sequencer, buf: &mut Buffer, area: Rect, beat: usize) { //let ppq = s.timebase.ppq() as usize; //let Rect { x, y, .. } = area;