diff --git a/crates/tek/src/api.rs b/crates/tek/src/api.rs index c4047c4c..88951c7a 100644 --- a/crates/tek/src/api.rs +++ b/crates/tek/src/api.rs @@ -1,9 +1,10 @@ use crate::*; -mod phrase; pub(crate) use phrase::*; -mod jack; pub(crate) use self::jack::*; -mod clip; pub(crate) use clip::*; -mod clock; pub(crate) use clock::*; -mod player; pub(crate) use player::*; -mod scene; pub(crate) use scene::*; -mod track; pub(crate) use track::*; +mod phrase; pub(crate) use phrase::*; +mod jack; pub(crate) use self::jack::*; +mod clip; pub(crate) use clip::*; +mod clock; pub(crate) use clock::*; +mod player; pub(crate) use player::*; +mod scene; pub(crate) use scene::*; +mod track; pub(crate) use track::*; +mod sampler; pub(crate) use sampler::*; diff --git a/crates/tek/src/api/_todo_api_sampler_sample.rs b/crates/tek/src/api/_todo_api_sampler_sample.rs deleted file mode 100644 index aa85676e..00000000 --- a/crates/tek/src/api/_todo_api_sampler_sample.rs +++ /dev/null @@ -1,72 +0,0 @@ -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/src/api/_todo_api_sampler_voice.rs b/crates/tek/src/api/_todo_api_sampler_voice.rs deleted file mode 100644 index 1dd3ba4a..00000000 --- a/crates/tek/src/api/_todo_api_sampler_voice.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::*; - -/// 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, -} - -impl Iterator for Voice { - type Item = [f32;2]; - fn next (&mut self) -> Option { - if self.after > 0 { - self.after = self.after - 1; - return Some([0.0, 0.0]) - } - let sample = self.sample.read().unwrap(); - if self.position < sample.end { - let position = self.position; - self.position = self.position + 1; - return sample.channels[0].get(position).map(|_amplitude|[ - sample.channels[0][position] * self.velocity, - sample.channels[0][position] * self.velocity, - ]) - } - None - } -} diff --git a/crates/tek/src/api/_todo_api_sampler.rs b/crates/tek/src/api/sampler.rs similarity index 57% rename from crates/tek/src/api/_todo_api_sampler.rs rename to crates/tek/src/api/sampler.rs index 2976c08a..260fbf53 100644 --- a/crates/tek/src/api/_todo_api_sampler.rs +++ b/crates/tek/src/api/sampler.rs @@ -1,5 +1,18 @@ 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 { @@ -14,6 +27,27 @@ pub struct Sampler { 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, +} + +/// Load sample from WAV and assign to MIDI note. +#[macro_export] macro_rules! sample { + ($note:expr, $name:expr, $src:expr) => {{ + let (end, data) = read_sample_data($src)?; + ( + u7::from_int_lossy($note).into(), + Sample::new($name, 0, end, data).into() + ) + }}; +} + impl Sampler { pub fn from_edn <'e> (jack: &Arc>, args: &[Edn<'e>]) -> Usually { let mut name = String::new(); @@ -56,10 +90,6 @@ impl Sampler { } } -pub struct SamplerAudio { - model: Arc> -} - impl From<&Arc>> for SamplerAudio { fn from (model: &Arc>) -> Self { Self { model: model.clone() } @@ -133,3 +163,85 @@ 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 } + } + 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)) + } +} + +impl Iterator for Voice { + type Item = [f32;2]; + fn next (&mut self) -> Option { + if self.after > 0 { + self.after = self.after - 1; + return Some([0.0, 0.0]) + } + let sample = self.sample.read().unwrap(); + if self.position < sample.end { + let position = self.position; + self.position = self.position + 1; + return sample.channels[0].get(position).map(|_amplitude|[ + sample.channels[0][position] * self.velocity, + sample.channels[0][position] * self.velocity, + ]) + } + None + } +} diff --git a/crates/tek/src/tui.rs b/crates/tek/src/tui.rs index f1072b7d..e037b5ad 100644 --- a/crates/tek/src/tui.rs +++ b/crates/tek/src/tui.rs @@ -9,6 +9,7 @@ mod engine_output; pub(crate) use engine_output::*; mod app_transport; pub(crate) use app_transport::*; mod app_sequencer; pub(crate) use app_sequencer::*; +mod app_sampler; pub(crate) use app_sampler::*; mod app_groovebox; pub(crate) use app_groovebox::*; mod app_arranger; pub(crate) use app_arranger::*; diff --git a/crates/tek/src/tui/_todo_tui_sampler_cmd.rs b/crates/tek/src/tui/_todo_tui_sampler_cmd.rs deleted file mode 100644 index a1b9e16a..00000000 --- a/crates/tek/src/tui/_todo_tui_sampler_cmd.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::*; -impl Handle for Sampler { - fn handle (&mut self, from: &TuiInput) -> Perhaps { - match from.event() { - key!(KeyCode::Up) => { - self.cursor.0 = if self.cursor.0 == 0 { - self.mapped.len() + self.unmapped.len() - 1 - } else { - self.cursor.0 - 1 - }; - Ok(Some(true)) - }, - key!(KeyCode::Down) => { - self.cursor.0 = (self.cursor.0 + 1) % (self.mapped.len() + self.unmapped.len()); - Ok(Some(true)) - }, - key!(KeyCode::Char('p')) => { - if let Some(sample) = self.sample() { - self.voices.write().unwrap().push(Sample::play(sample, 0, &100.into())); - } - Ok(Some(true)) - }, - key!(KeyCode::Char('a')) => { - let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); - *self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &self.voices)?)); - self.unmapped.push(sample); - Ok(Some(true)) - }, - key!(KeyCode::Char('r')) => { - if let Some(sample) = self.sample() { - *self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &self.voices)?)); - } - Ok(Some(true)) - }, - key!(KeyCode::Enter) => { - if let Some(sample) = self.sample() { - self.editing = Some(sample.clone()); - } - Ok(Some(true)) - } - _ => Ok(None) - } - } -} -impl Handle for AddSampleModal { - fn handle (&mut self, from: &TuiInput) -> Perhaps { - if from.handle_keymap(self, KEYMAP_ADD_SAMPLE)? { - return Ok(Some(true)) - } - Ok(Some(true)) - } -} diff --git a/crates/tek/src/tui/app_groovebox.rs b/crates/tek/src/tui/app_groovebox.rs index 5f19bf4a..efd4f1fb 100644 --- a/crates/tek/src/tui/app_groovebox.rs +++ b/crates/tek/src/tui/app_groovebox.rs @@ -1,2 +1,32 @@ use crate::*; use super::*; + +impl TryFrom<&Arc>> for GrooveboxTui { + type Error = Box; + fn try_from (jack: &Arc>) -> Usually { + Ok(Self { + sequencer: SequencerTui::try_from(jack)?, + sampler: SamplerTui::try_from(jack)?, + focus: GrooveboxFocus::Sampler, + }) + } +} + +struct GrooveboxTui { + pub sequencer: SequencerTui, + pub sampler: SamplerTui, + pub focus: GrooveboxFocus, +} + +/// Sections that may be focused +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum GrooveboxFocus { + /// The transport (toolbar) is focused + Transport(TransportFocus), + /// The phrase list (pool) is focused + PhraseList, + /// The phrase editor (sequencer) is focused + PhraseEditor, + /// The sample player is focused + Sampler +} diff --git a/crates/tek/src/tui/_todo_tui_sampler.rs b/crates/tek/src/tui/app_sampler.rs similarity index 70% rename from crates/tek/src/tui/_todo_tui_sampler.rs rename to crates/tek/src/tui/app_sampler.rs index 791144dc..f733577b 100644 --- a/crates/tek/src/tui/_todo_tui_sampler.rs +++ b/crates/tek/src/tui/app_sampler.rs @@ -1,79 +1,5 @@ use crate::*; - -/// The sampler plugin plays sounds. -pub struct SamplerView { - _engine: PhantomData, - pub state: Sampler, - pub cursor: (usize, usize), - pub editing: Option>>, - pub buffer: Vec>, - pub modal: Arc>>>, -} - -impl SamplerView { - pub fn new ( - jack: &Arc>, - name: &str, - mapped: Option>>> - ) -> Usually> { - Jack::new(name)? - .midi_in("midi") - .audio_in("recL") - .audio_in("recR") - .audio_out("outL") - .audio_out("outR") - .run(|ports|Box::new(Self { - _engine: Default::default(), - jack: jack.clone(), - name: name.into(), - cursor: (0, 0), - editing: None, - mapped: mapped.unwrap_or_else(||BTreeMap::new()), - unmapped: vec![], - voices: Arc::new(RwLock::new(vec![])), - ports, - buffer: vec![vec![0.0;16384];2], - output_gain: 0.5, - modal: Default::default() - })) - } - /// Immutable reference to sample at cursor. - pub fn sample (&self) -> Option<&Arc>> { - for (i, sample) in self.mapped.values().enumerate() { - if i == self.cursor.0 { - return Some(sample) - } - } - for (i, sample) in self.unmapped.iter().enumerate() { - if i + self.mapped.len() == self.cursor.0 { - return Some(sample) - } - } - None - } -} - -/// A sound sample. -#[derive(Default, Debug)] -pub struct Sample { - pub name: String, - pub start: usize, - pub end: usize, - pub channels: Vec>, - pub rate: Option, -} - -/// Load sample from WAV and assign to MIDI note. -#[macro_export] macro_rules! sample { - ($note:expr, $name:expr, $src:expr) => {{ - let (end, data) = read_sample_data($src)?; - ( - u7::from_int_lossy($note).into(), - Sample::new($name, 0, end, data).into() - ) - }}; -} - +use super::*; use std::fs::File; use symphonia::core::codecs::CODEC_TYPE_NULL; use symphonia::core::errors::Error; @@ -82,6 +8,92 @@ use symphonia::core::probe::Hint; use symphonia::core::audio::SampleBuffer; use symphonia::default::get_codecs; +impl TryFrom<&Arc>> for SamplerTui { + type Error = Box; + fn try_from (jack: &Arc>) -> Usually { + let midi_in = jack.read().unwrap().client().register_port("in", MidiIn::default())?; + let audio_outs = vec![ + jack.read().unwrap().client().register_port("outL", AudioOut::default())?, + jack.read().unwrap().client().register_port("outR", AudioOut::default())?, + ]; + Ok(Self { + focus: SamplerFocus::_TODO, + cursor: (0, 0), + editing: None, + modal: Default::default(), + state: Sampler { + jack: jack.clone(), + name: "Sampler".into(), + mapped: BTreeMap::new(), + unmapped: vec![], + voices: Arc::new(RwLock::new(vec![])), + buffer: vec![vec![0.0;16384];2], + output_gain: 0.5, + midi_in, + audio_outs, + }, + }) + } +} + +pub struct SamplerTui { + pub focus: SamplerFocus, + pub state: Sampler, + pub cursor: (usize, usize), + pub editing: Option>>, + pub modal: Arc>>>, +} + +/// Sections that may be focused +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum SamplerFocus { + _TODO +} + +render!(|self: SamplerTui|render(|to|{ + let [x, y, _, height] = to.area(); + let style = Style::default().gray(); + let title = format!(" {} ({})", self.state.name, self.state.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 self.state.mapped.iter() + .map(|(note, sample)|(Some(note), sample)) + .chain(self.state.unmapped.iter().map(|sample|(None, sample))) + { + if y1 >= height { + break + } + let active = j == self.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(()) +})); + +impl SamplerTui { + /// Immutable reference to sample at cursor. + pub fn sample (&self) -> Option<&Arc>> { + for (i, sample) in self.state.mapped.values().enumerate() { + if i == self.cursor.0 { + return Some(sample) + } + } + for (i, sample) in self.state.unmapped.iter().enumerate() { + if i + self.state.mapped.len() == self.cursor.0 { + return Some(sample) + } + } + None + } +} + pub struct AddSampleModal { exited: bool, dir: PathBuf, @@ -204,33 +216,49 @@ impl AddSampleModal { } } -pub const KEYMAP_ADD_SAMPLE: &'static [KeyBinding] = keymap!(AddSampleModal { - [Esc, NONE, "sampler/add/close", "close help dialog", |modal: &mut AddSampleModal|{ - modal.exit(); - Ok(true) - }], - [Up, NONE, "sampler/add/prev", "select previous entry", |modal: &mut AddSampleModal|{ - modal.prev(); - Ok(true) - }], - [Down, NONE, "sampler/add/next", "select next entry", |modal: &mut AddSampleModal|{ - modal.next(); - Ok(true) - }], - [Enter, NONE, "sampler/add/enter", "activate selected entry", |modal: &mut AddSampleModal|{ - if modal.pick()? { - modal.exit(); +fn read_sample_data (_: &str) -> Usually<(usize, Vec>)> { + todo!(); +} + +impl Handle for SamplerTui { + fn handle (&mut self, from: &TuiInput) -> Perhaps { + let cursor = &mut self.cursor; + let unmapped = &mut self.state.unmapped; + let mapped = &self.state.mapped; + let voices = &self.state.voices; + match from.event() { + key!(KeyCode::Up) => cursor.0 = if cursor.0 == 0 { + mapped.len() + unmapped.len() - 1 + } else { + cursor.0 - 1 + }, + key!(KeyCode::Down) => { + cursor.0 = (cursor.0 + 1) % (mapped.len() + unmapped.len()); + }, + key!(KeyCode::Char('p')) => if let Some(sample) = self.sample() { + voices.write().unwrap().push(Sample::play(sample, 0, &100.into())); + }, + key!(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)?)); + unmapped.push(sample); + }, + key!(KeyCode::Char('r')) => if let Some(sample) = self.sample() { + *self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); + }, + key!(KeyCode::Enter) => if let Some(sample) = self.sample() { + self.editing = Some(sample.clone()); + }, + _ => { + return Ok(None) + } } - Ok(true) - }], - [Char('p'), NONE, "sampler/add/preview", "preview selected entry", |modal: &mut AddSampleModal|{ - modal.try_preview()?; - Ok(true) - }] -}); + Ok(Some(true)) + } +} fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { - let (mut subdirs, mut files) = read_dir(dir)? + let (mut subdirs, mut files) = std::fs::read_dir(dir)? .fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{ let entry = entry.expect("failed to read drectory entry"); let meta = entry.metadata().expect("failed to read entry metadata"); @@ -316,42 +344,6 @@ impl Sample { } } -impl Render for SamplerView { - fn min_size (&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: &SamplerView, 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 { @@ -410,3 +402,37 @@ impl Render for AddSampleModal { //Lozenge(Style::default()).draw(to) } } + +//impl Handle for AddSampleModal { + //fn handle (&mut self, from: &TuiInput) -> Perhaps { + //if from.handle_keymap(self, KEYMAP_ADD_SAMPLE)? { + //return Ok(Some(true)) + //} + //Ok(Some(true)) + //} +//} + +//pub const KEYMAP_ADD_SAMPLE: &'static [KeyBinding] = keymap!(AddSampleModal { + //[Esc, NONE, "sampler/add/close", "close help dialog", |modal: &mut AddSampleModal|{ + //modal.exit(); + //Ok(true) + //}], + //[Up, NONE, "sampler/add/prev", "select previous entry", |modal: &mut AddSampleModal|{ + //modal.prev(); + //Ok(true) + //}], + //[Down, NONE, "sampler/add/next", "select next entry", |modal: &mut AddSampleModal|{ + //modal.next(); + //Ok(true) + //}], + //[Enter, NONE, "sampler/add/enter", "activate selected entry", |modal: &mut AddSampleModal|{ + //if modal.pick()? { + //modal.exit(); + //} + //Ok(true) + //}], + //[Char('p'), NONE, "sampler/add/preview", "preview selected entry", |modal: &mut AddSampleModal|{ + //modal.try_preview()?; + //Ok(true) + //}] +//}); diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 0dcf3c32..13ee7904 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -64,6 +64,17 @@ pub struct SequencerTui { pub perf: PerfModel, } +/// Sections in the sequencer app that may be focused +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum SequencerFocus { + /// The transport (toolbar) is focused + Transport(TransportFocus), + /// The phrase list (pool) is focused + PhraseList, + /// The phrase editor (sequencer) is focused + PhraseEditor, +} + impl Audio for SequencerTui { fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { // Start profiling cycle @@ -180,17 +191,6 @@ impl HasFocus for SequencerTui { } } -/// Sections in the sequencer app that may be focused -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub enum SequencerFocus { - /// The transport (toolbar) is focused - Transport(TransportFocus), - /// The phrase list (pool) is focused - PhraseList, - /// The phrase editor (sequencer) is focused - PhraseEditor, -} - impl Into> for SequencerFocus { fn into (self) -> Option { if let Self::Transport(transport) = self {