From 8cf42aff0b14ee8e294db1a648c74156b9f9177c Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 18 Dec 2024 12:46:42 +0100 Subject: [PATCH] extract edn; build out more groovebox --- crates/tek/src/api/sampler.rs | 119 +++------------------------ crates/tek/src/api/scene.rs | 31 ------- crates/tek/src/core.rs | 1 - crates/tek/src/core/edn.rs | 15 ---- crates/tek/src/core/space.rs | 8 +- crates/tek/src/edn.rs | 123 ++++++++++++++++++++++++++++ crates/tek/src/lib.rs | 3 + crates/tek/src/tui/app_groovebox.rs | 32 ++++++-- crates/tek/src/tui/app_sampler.rs | 102 +++++++++++++---------- crates/tek/src/tui/app_sequencer.rs | 2 +- 10 files changed, 231 insertions(+), 205 deletions(-) delete mode 100644 crates/tek/src/core/edn.rs create mode 100644 crates/tek/src/edn.rs diff --git a/crates/tek/src/api/sampler.rs b/crates/tek/src/api/sampler.rs index 07855dbd..c81e28cc 100644 --- a/crates/tek/src/api/sampler.rs +++ b/crates/tek/src/api/sampler.rs @@ -1,18 +1,5 @@ use crate::*; -pub struct SamplerAudio { - model: Arc> -} - -/// 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, -} - /// The sampler plugin plays sounds. #[derive(Debug)] pub struct Sampler { @@ -37,6 +24,15 @@ pub struct Sample { 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, +} + /// Load sample from WAV and assign to MIDI note. #[macro_export] macro_rules! sample { ($note:expr, $name:expr, $src:expr) => {{ @@ -49,66 +45,10 @@ pub struct Sample { } 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:?}") - }); - let midi_in = jack.read().unwrap().client().register_port("in", MidiIn::default())?; - Ok(Sampler { - jack: jack.clone(), - name: name.into(), - mapped: samples, - unmapped: Default::default(), - voices: Default::default(), - buffer: Default::default(), - midi_in: midi_in, - audio_outs: vec![], - output_gain: 0. - }) - } -} - -impl From<&Arc>> for SamplerAudio { - fn from (model: &Arc>) -> Self { - Self { model: model.clone() } - } -} - -audio!(|self: SamplerAudio, _client, scope|{ - self.process_midi_in(scope); - self.clear_output_buffer(); - self.process_audio_out(scope); - self.write_output_buffer(scope); - Control::Continue -}); - -impl SamplerAudio { /// Create [Voice]s from [Sample]s in response to MIDI input. pub fn process_midi_in (&mut self, scope: &ProcessScope) { - let Sampler { midi_in, mapped, voices, .. } = &*self.model.read().unwrap(); + let Sampler { midi_in, mapped, voices, .. } = self; for RawMidi { time, bytes } in midi_in.iter(scope) { if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() { if let MidiMessage::NoteOn { ref key, ref vel } = message { @@ -122,14 +62,14 @@ impl SamplerAudio { /// Zero the output buffer. pub fn clear_output_buffer (&mut self) { - for buffer in self.model.write().unwrap().buffer.iter_mut() { + for buffer in self.buffer.iter_mut() { buffer.fill(0.0); } } /// Mix all currently playing samples into the output. pub fn process_audio_out (&mut self, scope: &ProcessScope) { - let Sampler { ref mut buffer, voices, output_gain, .. } = &mut*self.model.write().unwrap(); + let Sampler { ref mut buffer, voices, output_gain, .. } = self; let channel_count = buffer.len(); voices.write().unwrap().retain_mut(|voice|{ for index in 0..scope.n_frames() as usize { @@ -151,7 +91,7 @@ impl SamplerAudio { /// Write output buffer to output ports. pub fn write_output_buffer (&mut self, scope: &ProcessScope) { - let Sampler { ref mut audio_outs, buffer, .. } = &mut*self.model.write().unwrap(); + let Sampler { ref mut audio_outs, buffer, .. } = self; for (i, port) in audio_outs.iter_mut().enumerate() { let buffer = &buffer[i]; for (i, value) in port.as_mut_slice(scope).iter_mut().enumerate() { @@ -162,7 +102,6 @@ impl SamplerAudio { } - impl Sample { pub fn new (name: &str, start: usize, end: usize, channels: Vec>) -> Self { Self { name: name.to_string(), start, end, channels, rate: None } @@ -175,38 +114,6 @@ impl Sample { 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![]; diff --git a/crates/tek/src/api/scene.rs b/crates/tek/src/api/scene.rs index 662569bc..2d464290 100644 --- a/crates/tek/src/api/scene.rs +++ b/crates/tek/src/api/scene.rs @@ -94,34 +94,3 @@ pub trait ArrangerSceneApi: Sized { } } - -//impl ArrangerScene { - - ////TODO - ////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(ArrangerScene { - ////name: Arc::new(name.unwrap_or("").to_string().into()), - ////color: ItemColor::random(), - ////clips, - ////}) - ////} -//} diff --git a/crates/tek/src/core.rs b/crates/tek/src/core.rs index a0e94939..caacfa57 100644 --- a/crates/tek/src/core.rs +++ b/crates/tek/src/core.rs @@ -1,7 +1,6 @@ pub(crate) mod audio; pub(crate) use audio::*; pub(crate) mod color; pub(crate) use color::*; pub(crate) mod command; pub(crate) use command::*; -pub(crate) mod edn; pub(crate) use edn::*; pub(crate) mod engine; pub(crate) use engine::*; pub(crate) mod focus; pub(crate) use focus::*; pub(crate) mod input; pub(crate) use input::*; diff --git a/crates/tek/src/core/edn.rs b/crates/tek/src/core/edn.rs deleted file mode 100644 index b2c50a53..00000000 --- a/crates/tek/src/core/edn.rs +++ /dev/null @@ -1,15 +0,0 @@ -pub use clojure_reader::edn::Edn; -//pub use clojure_reader::{edn::{read, Edn}, error::Error as EdnError}; - -/// EDN parsing helper. -#[macro_export] macro_rules! edn { - ($edn:ident { $($pat:pat => $expr:expr),* $(,)? }) => { - match $edn { $($pat => $expr),* } - }; - ($edn:ident in $args:ident { $($pat:pat => $expr:expr),* $(,)? }) => { - for $edn in $args { - edn!($edn { $($pat => $expr),* }) - } - }; -} - diff --git a/crates/tek/src/core/space.rs b/crates/tek/src/core/space.rs index 6942f437..490dcc60 100644 --- a/crates/tek/src/core/space.rs +++ b/crates/tek/src/core/space.rs @@ -76,8 +76,12 @@ pub trait Area: Copy { #[inline] fn lrtb (&self) -> [N;4] { [self.x(), self.x2(), self.y(), self.y2()] } #[inline] fn push_x (&self, x: N) -> [N;4] { [self.x() + x, self.y(), self.w(), self.h()] } #[inline] fn push_y (&self, y: N) -> [N;4] { [self.x(), self.y() + y, self.w(), self.h()] } - #[inline] fn shrink_x (&self, x: N) -> [N;4] { [self.x(), self.y(), self.w() - x, self.h()] } - #[inline] fn shrink_y (&self, y: N) -> [N;4] { [self.x(), self.y(), self.w(), self.h() - y] } + #[inline] fn shrink_x (&self, x: N) -> [N;4] { + [self.x(), self.y(), self.w().minus(x), self.h()] + } + #[inline] fn shrink_y (&self, y: N) -> [N;4] { + [self.x(), self.y(), self.w(), self.h().minus(y)] + } #[inline] fn set_w (&self, w: N) -> [N;4] { [self.x(), self.y(), w, self.h()] } #[inline] fn set_h (&self, h: N) -> [N;4] { [self.x(), self.y(), self.w(), h] } #[inline] fn clip_h (&self, h: N) -> [N;4] { diff --git a/crates/tek/src/edn.rs b/crates/tek/src/edn.rs new file mode 100644 index 00000000..f0c4d6ad --- /dev/null +++ b/crates/tek/src/edn.rs @@ -0,0 +1,123 @@ +use crate::*; +pub use clojure_reader::edn::Edn; +//pub use clojure_reader::{edn::{read, Edn}, error::Error as EdnError}; + +/// EDN parsing helper. +#[macro_export] macro_rules! edn { + ($edn:ident { $($pat:pat => $expr:expr),* $(,)? }) => { + match $edn { $($pat => $expr),* } + }; + ($edn:ident in $args:ident { $($pat:pat => $expr:expr),* $(,)? }) => { + for $edn in $args { + edn!($edn { $($pat => $expr),* }) + } + }; +} + +impl crate::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:?}") + }); + let midi_in = jack.read().unwrap().client().register_port("in", MidiIn::default())?; + Ok(Sampler { + jack: jack.clone(), + name: name.into(), + mapped: samples, + unmapped: Default::default(), + voices: Default::default(), + buffer: Default::default(), + midi_in: midi_in, + audio_outs: vec![], + output_gain: 0. + }) + } +} + +impl crate::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 + })))) + } +} + + +//impl ArrangerScene { + + ////TODO + ////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(ArrangerScene { + ////name: Arc::new(name.unwrap_or("").to_string().into()), + ////color: ItemColor::random(), + ////clips, + ////}) + ////} +//} diff --git a/crates/tek/src/lib.rs b/crates/tek/src/lib.rs index a6378d05..f27561eb 100644 --- a/crates/tek/src/lib.rs +++ b/crates/tek/src/lib.rs @@ -31,6 +31,9 @@ pub(crate) use api::*; pub mod tui; pub(crate) use tui::*; +pub mod edn; +pub(crate) use edn::*; + testmod! { test } pub(crate) use clap::{self, Parser}; diff --git a/crates/tek/src/tui/app_groovebox.rs b/crates/tek/src/tui/app_groovebox.rs index 84542517..654198f3 100644 --- a/crates/tek/src/tui/app_groovebox.rs +++ b/crates/tek/src/tui/app_groovebox.rs @@ -10,7 +10,8 @@ impl TryFrom<&Arc>> for GrooveboxTui { Ok(Self { sequencer, sampler: SamplerTui::try_from(jack)?, - split: 16 + split: 16, + focus: GrooveboxFocus::Sequencer, }) } } @@ -19,24 +20,39 @@ pub struct GrooveboxTui { pub sequencer: SequencerTui, pub sampler: SamplerTui, pub split: u16, + pub focus: GrooveboxFocus } +pub enum GrooveboxFocus { + Sequencer, + Sampler +} + +audio!(|self:GrooveboxTui,_client,_process|Control::Continue); + +render!(|self:GrooveboxTui|Bsp::n( + Fixed::h(2, SequencerStatusBar::from(&self.sequencer)), + Fill::h(Bsp::s(&self.sequencer, &self.sampler)), +)); + pub enum GrooveboxCommand { Sequencer(SequencerCommand), Sampler(SamplerCommand), } -render!(|self:GrooveboxTui|Bsp::n( - Fixed::h(2, SequencerStatusBar::from(&self.sequencer)), - Bsp::s(Fixed::h(self.split, &self.sequencer), &self.sampler), -)); -audio!(|self:GrooveboxTui,_client,_process|Control::Continue); handle!(|self:GrooveboxTui,input|GrooveboxCommand::execute_with_state(self, input)); + input_to_command!(GrooveboxCommand: |state:GrooveboxTui,input|match input.event() { // load sample - key_pat!(Char('l')) => GrooveboxCommand::Sampler(SamplerCommand::Import), - _ => GrooveboxCommand::Sequencer(SequencerCommand::input_to_command(&state.sequencer, input)?), + key_pat!(Char('l')) => GrooveboxCommand::Sampler(SamplerCommand::Import(FileBrowserCommand::Begin)), + _ => match state.focus { + GrooveboxFocus::Sequencer => + GrooveboxCommand::Sequencer(SequencerCommand::input_to_command(&state.sequencer, input)?), + GrooveboxFocus::Sampler => + GrooveboxCommand::Sampler(SamplerCommand::input_to_command(&state.sampler, input)?), + } }); + command!(|self:GrooveboxCommand,state:GrooveboxTui|match self { GrooveboxCommand::Sequencer(command) => command.execute(&mut state.sequencer)?.map(GrooveboxCommand::Sequencer), diff --git a/crates/tek/src/tui/app_sampler.rs b/crates/tek/src/tui/app_sampler.rs index 005a023b..d61db7aa 100644 --- a/crates/tek/src/tui/app_sampler.rs +++ b/crates/tek/src/tui/app_sampler.rs @@ -9,18 +9,32 @@ use symphonia::core::audio::SampleBuffer; use symphonia::default::get_codecs; pub enum SamplerCommand { - Import + Import(FileBrowserCommand), + SetName(String), + SetNote(u7, Arc>), + SetGain(f32), + NoteOn(u7, u7), + NoteOff(u7) } -input_to_command!(SamplerCommand: |state:SamplerTui,input|match input.event() { +input_to_command!(SamplerCommand:|state:SamplerTui,input|match state.mode { + Some(SamplerMode::Import(..)) => Self::Import(FileBrowserCommand::input_to_command(state, input)?), + _ => return None +}); +input_to_command!(FileBrowserCommand:|state:SamplerTui,input|match input { _ => return None }); command!(|self:SamplerCommand,state:SamplerTui|match self { - SamplerCommand::Import => { + SamplerCommand::Import(FileBrowserCommand::Begin) => { + let voices = &state.state.voices; + let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); + state.mode = Some(SamplerMode::Import(0, FileBrowser::new(None)?)); None }, _ => todo!() }); - +command!(|self:FileBrowserCommand,state:SamplerTui|match self { + _ => todo!() +}); impl TryFrom<&Arc>> for SamplerTui { type Error = Box; @@ -31,10 +45,9 @@ impl TryFrom<&Arc>> for SamplerTui { jack.read().unwrap().client().register_port("outR", AudioOut::default())?, ]; Ok(Self { - focus: SamplerFocus::_TODO, cursor: (0, 0), editing: None, - modal: Default::default(), + mode: None, state: Sampler { jack: jack.clone(), name: "Sampler".into(), @@ -51,47 +64,54 @@ impl TryFrom<&Arc>> for SamplerTui { } pub struct SamplerTui { - pub focus: SamplerFocus, pub state: Sampler, pub cursor: (usize, usize), pub editing: Option>>, - pub modal: Arc>>>, + pub mode: Option, } -/// Sections that may be focused -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub enum SamplerFocus { - _TODO +enum SamplerMode { + // Load sample from path + Import(usize, FileBrowser), } -audio!(|self: SamplerTui, _client, _scope|Control::Continue); -render!(|self: SamplerTui|{ - Fill::wh(lay!([ - - Fill::wh(render(|to|{ // border - let [x, y, w, h] = to.area(); - let green = Some(Style::default().fg(Color::Green)); - to.blit(&"🭚", x, y, green); - to.blit(&"🭥", x + w.saturating_sub(1), y, green); - to.blit(&"🬿", x, y + h.saturating_sub(1), green); - to.blit(&"🭊", x + w.saturating_sub(1), y + h.saturating_sub(1), green); - Ok(()) - })), - - col!([ - Tui::push_x(2, row!([ - Tui::bold(true, "Sampler"), "|Voices: ", - &format!("{}", self.state.voices.read().unwrap().len()), - ])), - Tui::either(self.state.unmapped.len() > 0, - col!((index, sample) in self.state.unmapped.iter().enumerate() => - { &format!("| Unmapped #{index}") }), "· No unmapped samples."), - Tui::either(self.state.mapped.len() > 0, - col!((index, sample) in self.state.unmapped.iter().enumerate() => - { &format!("| Mapped #{index}") }), "· No mapped samples."), - ]) - ])) +audio!(|self: SamplerTui, _client, scope|{ + self.state.process_midi_in(scope); + self.state.clear_output_buffer(); + self.state.process_audio_out(scope); + self.state.write_output_buffer(scope); + Control::Continue }); +render!(|self: SamplerTui|Tui::min_y(10, Fill::wh(lay!([ + + Fill::wh(render(|to|{ // border + let [x, y, w, h] = to.area(); + let green = Some(Style::default().fg(Color::Green)); + to.blit(&"🭚", x, y, green); + to.blit(&"🭥", x + w.saturating_sub(1), y, green); + to.blit(&"🬿", x, y + h.saturating_sub(1), green); + to.blit(&"🭊", x + w.saturating_sub(1), y + h.saturating_sub(1), green); + Ok(()) + })), + + col!(|add|{ + add(&Tui::push_x(2, row!([ + Tui::bold(true, "Sampler"), "|Voices: ", + &format!("{}", self.state.voices.read().unwrap().len()), + ])))?; + if let Some(SamplerMode::Import(_, browser)) = self.mode.as_ref() { + add(&browser) + } else { + add(&row!([ + " ", + col!(note in 35..=42 => { + &format!("{note:>3} ---------------- +0.0dB") + }), + ])) + } + }), + +])))); handle!(|self:SamplerTui,from|{ let cursor = &mut self.cursor; @@ -112,11 +132,11 @@ handle!(|self:SamplerTui,from|{ }, key_pat!(KeyCode::Char('a')) => { let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); - *self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); + self.mode = None;//Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); unmapped.push(sample); }, key_pat!(KeyCode::Char('r')) => if let Some(sample) = self.sample() { - *self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); + self.mode = None;//Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); }, key_pat!(KeyCode::Enter) => if let Some(sample) = self.sample() { self.editing = Some(sample.clone()); diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 0dcc3f0d..44e247f1 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -154,7 +154,7 @@ render!(|self: SequencerTui|{ PhraseSelector::play_phrase(&self.player), PhraseSelector::next_phrase(&self.player), ]), transport]); - with_size(with_status(col!([ toolbar, editor, ]))) + Tui::min_y(15, with_size(with_status(col!([ toolbar, editor, ])))) }); audio!(|self:SequencerTui, client, scope|{ // Start profiling cycle