From df00fedfd6d774235d6086c10651254a73e1d3cf Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 28 Dec 2024 18:12:43 +0100 Subject: [PATCH] refactor sampler module --- crates/tek/src/groovebox.rs | 4 +- crates/tek/src/sampler.rs | 225 +++++++++++++--------- crates/tek/src/sampler/sampler_audio.rs | 104 ---------- crates/tek/src/sampler/sampler_control.rs | 96 --------- crates/tek/src/sampler/sampler_keys.rs | 15 -- crates/tek/src/sampler/sampler_tui.rs | 155 +++++++++++++++ 6 files changed, 293 insertions(+), 306 deletions(-) delete mode 100644 crates/tek/src/sampler/sampler_audio.rs delete mode 100644 crates/tek/src/sampler/sampler_control.rs delete mode 100644 crates/tek/src/sampler/sampler_keys.rs create mode 100644 crates/tek/src/sampler/sampler_tui.rs diff --git a/crates/tek/src/groovebox.rs b/crates/tek/src/groovebox.rs index 97e7e3c6..5152b397 100644 --- a/crates/tek/src/groovebox.rs +++ b/crates/tek/src/groovebox.rs @@ -110,9 +110,11 @@ render!(|self: GrooveboxSamples<'a>|{ bg = Color::Rgb(64,16,0); fg = Color::Rgb(224,64,32) } + } else if self.0.sampler.mapped[note].is_some() { + fg = TuiTheme::g(224); } Tui::bg(bg, if let Some(sample) = &self.0.sampler.mapped[note] { - Tui::fg(fg, format!("{note:3} ????? ")) + Tui::fg(fg, format!("{note:3} ?????? ")) } else { Tui::fg(fg, format!("{note:3} (none) ")) }) diff --git a/crates/tek/src/sampler.rs b/crates/tek/src/sampler.rs index 85434e82..b3d69db6 100644 --- a/crates/tek/src/sampler.rs +++ b/crates/tek/src/sampler.rs @@ -21,14 +21,9 @@ pub mod voice; pub(crate) use self::voice::*; pub use self::voice::Voice; -pub mod sampler_control; -pub(crate) use self::sampler_control::*; - -pub mod sampler_audio; -pub(crate) use self::sampler_audio::*; - -pub mod sampler_keys; -pub(crate) use self::sampler_keys::*; +pub mod sampler_tui; +pub(crate) use self::sampler_tui::*; +pub use self::sampler_tui::SamplerTui; pub mod sample_import; pub(crate) use self::sample_import::*; @@ -102,88 +97,138 @@ impl Sampler { } } } -pub struct SamplerTui { - pub state: Sampler, - pub cursor: (usize, usize), - pub editing: Option>>, - pub mode: Option, - /// Size of actual notes area - pub size: Measure, - /// Lowest note displayed - pub note_lo: AtomicUsize, - pub note_pt: AtomicUsize, - color: ItemPalette -} -impl SamplerTui { - /// Immutable reference to sample at cursor. - pub fn sample (&self) -> Option<&Arc>> { - for (i, sample) in self.state.mapped.iter().enumerate() { - if i == self.cursor.0 { - return sample.as_ref() - } - } - for (i, sample) in self.state.unmapped.iter().enumerate() { - if i + self.state.mapped.len() == self.cursor.0 { - return Some(sample) - } - } - None - } -} -from_jack!(|jack|SamplerTui{ - Self { - cursor: (0, 0), - editing: None, - mode: None, - size: Measure::new(), - note_lo: 36.into(), - note_pt: 36.into(), - color: ItemPalette::from(Color::Rgb(64, 128, 32)), - state: Sampler::new(jack, "sampler")?, - } -}); -render!(|self: SamplerTui|{ - let keys_width = 5; - let keys = move||SamplerKeys(self); - let fg = self.color.base.rgb; - let bg = self.color.darkest.rgb; - let border = Fill::wh(Outer(Style::default().fg(fg).bg(bg))); - let with_border = |x|lay!([border, Fill::wh(&x)]); - let with_size = |x|lay!([self.size, x]); - Tui::bg(bg, Fill::wh(with_border(Bsp::s( - Tui::fg(self.color.light.rgb, Tui::bold(true, "Sampler")), - with_size(Tui::shrink_y(1, Bsp::e( - Fixed::w(keys_width, keys()), - Fill::wh(render(|to: &mut TuiOutput|Ok({ - let x = to.area.x(); - let bg_base = self.color.darkest.rgb; - let bg_selected = self.color.darker.rgb; - let style_empty = Style::default().fg(self.color.base.rgb); - let style_full = Style::default().fg(self.color.lighter.rgb); - let note_hi = self.note_hi(); - let note_pt = self.note_point(); - for y in 0..self.size.h() { - let note = note_hi - y as usize; - let bg = if note == note_pt { bg_selected } else { bg_base }; - let style = Some(style_empty.bg(bg)); - to.blit(&" (no sample) ", x, to.area.y() + y as u16, style) - } - }))) - ))), - )))) -}); -impl NoteRange for SamplerTui { - fn note_lo (&self) -> &AtomicUsize { &self.note_lo } - fn note_axis (&self) -> &AtomicUsize { &self.size.y } -} -impl NotePoint for SamplerTui { - fn note_len (&self) -> usize {0/*TODO*/} - fn set_note_len (&self, x: usize) {} - fn note_point (&self) -> usize { self.note_pt.load(Relaxed) } - fn set_note_point (&self, x: usize) { self.note_pt.store(x, Relaxed); } + +pub enum SamplerCommand { + RecordBegin(u7), + RecordCancel, + RecordFinish, + SetSample(u7, Option>>), + SetGain(f32), + NoteOn(u7, u7), + NoteOff(u7), } -pub enum SamplerMode { - // Load sample from path - Import(usize, FileBrowser), +command!(|self: SamplerCommand, state: Sampler|match self { + Self::SetSample(index, sample) => { + let i = index.as_int() as usize; + let old = state.mapped[i].clone(); + state.mapped[i] = sample; + Some(Self::SetSample(index, old)) + }, + Self::RecordBegin(index) => { + state.begin_recording(index.as_int() as usize); + None + }, + Self::RecordCancel => { + state.cancel_recording(); + None + }, + Self::RecordFinish => { + state.finish_recording(); + None + }, + _ => todo!() +}); + +audio!(|self: SamplerTui, client, scope|{ + SamplerAudio(&mut self.state).process(client, scope) +}); + +pub struct SamplerAudio<'a>(pub &'a mut Sampler); + +audio!(|self: SamplerAudio<'a>, _client, scope|{ + self.0.process_midi_in(scope); + self.0.clear_output_buffer(); + self.0.process_audio_out(scope); + self.0.write_output_buffer(scope); + self.0.process_audio_in(scope); + Control::Continue +}); + +impl Sampler { + + pub fn process_audio_in (&mut self, scope: &ProcessScope) { + let Sampler { audio_ins, input_meter, recording, .. } = self; + if audio_ins.len() != input_meter.len() { + *input_meter = vec![0.0;audio_ins.len()]; + } + if let Some((_, sample)) = recording { + if sample.channels.len() != audio_ins.len() { + panic!("channel count mismatch"); + } + let iterator = audio_ins.iter().zip(input_meter).zip(sample.channels.iter_mut()); + let mut length = 0; + for ((input, meter), channel) in iterator { + let slice = input.as_slice(scope); + length = length.max(slice.len()); + let total: f32 = slice.iter().map(|x|x.abs()).sum(); + let count = slice.len() as f32; + *meter = 10. * (total / count).log10(); + channel.extend_from_slice(slice); + } + sample.end += length; + } else { + for (input, meter) in audio_ins.iter().zip(input_meter) { + let slice = input.as_slice(scope); + let total: f32 = slice.iter().map(|x|x.abs()).sum(); + let count = slice.len() as f32; + *meter = 10. * (total / count).log10(); + } + } + } + + /// 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; + for RawMidi { time, bytes } in midi_in.iter(scope) { + if let LiveEvent::Midi { + message: MidiMessage::NoteOn { ref key, ref vel }, .. + } = LiveEvent::parse(bytes).unwrap() { + if let Some(ref sample) = mapped[key.as_int() as usize] { + voices.write().unwrap().push(Sample::play(sample, time as usize, vel)); + } + } + } + } + + /// Zero the output buffer. + pub fn clear_output_buffer (&mut self) { + 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, .. } = self; + let channel_count = buffer.len(); + voices.write().unwrap().retain_mut(|voice|{ + for index in 0..scope.n_frames() as usize { + if let Some(frame) = voice.next() { + for (channel, sample) in frame.iter().enumerate() { + // Averaging mixer: + //self.buffer[channel % channel_count][index] = ( + //(self.buffer[channel % channel_count][index] + sample * self.output_gain) / 2.0 + //); + buffer[channel % channel_count][index] += sample * *output_gain; + } + } else { + return false + } + } + true + }); + } + + /// Write output buffer to output ports. + pub fn write_output_buffer (&mut self, scope: &ProcessScope) { + 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() { + *value = *buffer.get(i).unwrap_or(&0.0); + } + } + } + } diff --git a/crates/tek/src/sampler/sampler_audio.rs b/crates/tek/src/sampler/sampler_audio.rs deleted file mode 100644 index c7558455..00000000 --- a/crates/tek/src/sampler/sampler_audio.rs +++ /dev/null @@ -1,104 +0,0 @@ -use crate::*; - -audio!(|self: SamplerTui, client, scope|{ - SamplerAudio(&mut self.state).process(client, scope) -}); - -pub struct SamplerAudio<'a>(pub &'a mut Sampler); - -audio!(|self: SamplerAudio<'a>, _client, scope|{ - self.0.process_midi_in(scope); - self.0.clear_output_buffer(); - self.0.process_audio_out(scope); - self.0.write_output_buffer(scope); - self.0.process_audio_in(scope); - Control::Continue -}); - -impl Sampler { - - pub fn process_audio_in (&mut self, scope: &ProcessScope) { - let Sampler { audio_ins, input_meter, recording, .. } = self; - if audio_ins.len() != input_meter.len() { - *input_meter = vec![0.0;audio_ins.len()]; - } - if let Some((_, sample)) = recording { - if sample.channels.len() != audio_ins.len() { - panic!("channel count mismatch"); - } - let iterator = audio_ins.iter().zip(input_meter).zip(sample.channels.iter_mut()); - let mut length = 0; - for ((input, meter), channel) in iterator { - let slice = input.as_slice(scope); - length = length.max(slice.len()); - let total: f32 = slice.iter().map(|x|x.abs()).sum(); - let count = slice.len() as f32; - *meter = 10. * (total / count).log10(); - channel.extend_from_slice(slice); - } - sample.end += length; - } else { - for (input, meter) in audio_ins.iter().zip(input_meter) { - let slice = input.as_slice(scope); - let total: f32 = slice.iter().map(|x|x.abs()).sum(); - let count = slice.len() as f32; - *meter = 10. * (total / count).log10(); - } - } - } - - /// 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; - for RawMidi { time, bytes } in midi_in.iter(scope) { - if let LiveEvent::Midi { - message: MidiMessage::NoteOn { ref key, ref vel }, .. - } = LiveEvent::parse(bytes).unwrap() { - if let Some(ref sample) = mapped[key.as_int() as usize] { - voices.write().unwrap().push(Sample::play(sample, time as usize, vel)); - } - } - } - } - - /// Zero the output buffer. - pub fn clear_output_buffer (&mut self) { - 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, .. } = self; - let channel_count = buffer.len(); - voices.write().unwrap().retain_mut(|voice|{ - for index in 0..scope.n_frames() as usize { - if let Some(frame) = voice.next() { - for (channel, sample) in frame.iter().enumerate() { - // Averaging mixer: - //self.buffer[channel % channel_count][index] = ( - //(self.buffer[channel % channel_count][index] + sample * self.output_gain) / 2.0 - //); - buffer[channel % channel_count][index] += sample * *output_gain; - } - } else { - return false - } - } - true - }); - } - - /// Write output buffer to output ports. - pub fn write_output_buffer (&mut self, scope: &ProcessScope) { - 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() { - *value = *buffer.get(i).unwrap_or(&0.0); - } - } - } - -} diff --git a/crates/tek/src/sampler/sampler_control.rs b/crates/tek/src/sampler/sampler_control.rs deleted file mode 100644 index 6e490fef..00000000 --- a/crates/tek/src/sampler/sampler_control.rs +++ /dev/null @@ -1,96 +0,0 @@ -use crate::*; -use KeyCode::Char; - -handle!(|self: SamplerTui, input|SamplerTuiCommand::execute_with_state(self, input)); - -pub enum SamplerTuiCommand { - Import(FileBrowserCommand), - SelectNote(usize), - SelectField(usize), - Sample(SamplerCommand), -} - -pub enum SamplerCommand { - RecordBegin(u7), - RecordCancel, - RecordFinish, - SetSample(u7, Option>>), - SetGain(f32), - NoteOn(u7, u7), - NoteOff(u7), -} - -input_to_command!(SamplerTuiCommand: |state: SamplerTui, input|match state.mode { - Some(SamplerMode::Import(..)) => Self::Import( - FileBrowserCommand::input_to_command(state, input)? - ), - _ => match input.event() { - // load sample - key_pat!(Shift-Char('L')) => { - Self::Import(FileBrowserCommand::Begin) - }, - key_pat!(KeyCode::Up) => { - Self::SelectNote(state.note_point().overflowing_add(1).0.min(127)) - }, - key_pat!(KeyCode::Down) => { - Self::SelectNote(state.note_point().overflowing_sub(1).0.min(127)) - }, - _ => return None - } - //key_pat!(KeyCode::Char('p')) => if let Some(sample) = self.sample() { - //voices.write().unwrap().push(Sample::play(sample, 0, &100.into())); - //}, - //key_pat!(KeyCode::Char('a')) => { - //let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); - //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.mode = None;//Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); - //}, - //key_pat!(KeyCode::Enter) => if let Some(sample) = self.sample() { - //self.editing = Some(sample.clone()); - //}, - //_ => { - //return Ok(None) - //} - //} -}); - -command!(|self: SamplerTuiCommand, state: SamplerTui|match self { - Self::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 - }, - Self::SelectNote(index) => { - let old = state.note_point(); - state.set_note_point(index); - Some(Self::SelectNote(old)) - }, - Self::Sample(cmd) => cmd.execute(&mut state.state)?.map(Self::Sample), - _ => todo!() -}); - -command!(|self: SamplerCommand, state: Sampler|match self { - Self::SetSample(index, sample) => { - let i = index.as_int() as usize; - let old = state.mapped[i].clone(); - state.mapped[i] = sample; - Some(Self::SetSample(index, old)) - }, - Self::RecordBegin(index) => { - state.begin_recording(index.as_int() as usize); - None - }, - Self::RecordCancel => { - state.cancel_recording(); - None - }, - Self::RecordFinish => { - state.finish_recording(); - None - }, - _ => todo!() -}); diff --git a/crates/tek/src/sampler/sampler_keys.rs b/crates/tek/src/sampler/sampler_keys.rs deleted file mode 100644 index 92fad051..00000000 --- a/crates/tek/src/sampler/sampler_keys.rs +++ /dev/null @@ -1,15 +0,0 @@ -use crate::*; - -pub struct SamplerKeys<'a>(pub &'a SamplerTui); -has_color!(|self: SamplerKeys<'a>|self.0.color.base); -render!(|self: SamplerKeys<'a>|render(|to|Ok(render_keys_v(to, self)))); -impl<'a> NoteRange for SamplerKeys<'a> { - fn note_lo (&self) -> &AtomicUsize { &self.0.note_lo } - fn note_axis (&self) -> &AtomicUsize { &self.0.size.y } -} -impl<'a> NotePoint for SamplerKeys<'a> { - fn note_len (&self) -> usize {0/*TODO*/} - fn set_note_len (&self, x: usize) {} - fn note_point (&self) -> usize { self.0.note_point() } - fn set_note_point (&self, x: usize) { self.0.set_note_point(x); } -} diff --git a/crates/tek/src/sampler/sampler_tui.rs b/crates/tek/src/sampler/sampler_tui.rs new file mode 100644 index 00000000..1bd13a77 --- /dev/null +++ b/crates/tek/src/sampler/sampler_tui.rs @@ -0,0 +1,155 @@ +use crate::*; +use KeyCode::Char; + +pub struct SamplerTui { + pub state: Sampler, + pub cursor: (usize, usize), + pub editing: Option>>, + pub mode: Option, + /// Size of actual notes area + pub size: Measure, + /// Lowest note displayed + pub note_lo: AtomicUsize, + pub note_pt: AtomicUsize, + color: ItemPalette +} + +impl SamplerTui { + /// Immutable reference to sample at cursor. + pub fn sample (&self) -> Option<&Arc>> { + for (i, sample) in self.state.mapped.iter().enumerate() { + if i == self.cursor.0 { + return sample.as_ref() + } + } + for (i, sample) in self.state.unmapped.iter().enumerate() { + if i + self.state.mapped.len() == self.cursor.0 { + return Some(sample) + } + } + None + } +} + +from_jack!(|jack|SamplerTui{ + Self { + cursor: (0, 0), + editing: None, + mode: None, + size: Measure::new(), + note_lo: 36.into(), + note_pt: 36.into(), + color: ItemPalette::from(Color::Rgb(64, 128, 32)), + state: Sampler::new(jack, "sampler")?, + } +}); + +render!(|self: SamplerTui|{ + let keys_width = 5; + let keys = move||"";//SamplerKeys(self); + let fg = self.color.base.rgb; + let bg = self.color.darkest.rgb; + let border = Fill::wh(Outer(Style::default().fg(fg).bg(bg))); + let with_border = |x|lay!([border, Fill::wh(&x)]); + let with_size = |x|lay!([self.size, x]); + Tui::bg(bg, Fill::wh(with_border(Bsp::s( + Tui::fg(self.color.light.rgb, Tui::bold(true, "Sampler")), + with_size(Tui::shrink_y(1, Bsp::e( + Fixed::w(keys_width, keys()), + Fill::wh(render(|to: &mut TuiOutput|Ok({ + let x = to.area.x(); + let bg_base = self.color.darkest.rgb; + let bg_selected = self.color.darker.rgb; + let style_empty = Style::default().fg(self.color.base.rgb); + let style_full = Style::default().fg(self.color.lighter.rgb); + let note_hi = self.note_hi(); + let note_pt = self.note_point(); + for y in 0..self.size.h() { + let note = note_hi - y as usize; + let bg = if note == note_pt { bg_selected } else { bg_base }; + let style = Some(style_empty.bg(bg)); + to.blit(&" (no sample) ", x, to.area.y() + y as u16, style) + } + }))) + ))), + )))) +}); + +impl NoteRange for SamplerTui { + fn note_lo (&self) -> &AtomicUsize { &self.note_lo } + fn note_axis (&self) -> &AtomicUsize { &self.size.y } +} + +impl NotePoint for SamplerTui { + fn note_len (&self) -> usize {0/*TODO*/} + fn set_note_len (&self, x: usize) {} + fn note_point (&self) -> usize { self.note_pt.load(Relaxed) } + fn set_note_point (&self, x: usize) { self.note_pt.store(x, Relaxed); } +} + +pub enum SamplerMode { + // Load sample from path + Import(usize, FileBrowser), +} + +handle!(|self: SamplerTui, input|SamplerTuiCommand::execute_with_state(self, input)); + +pub enum SamplerTuiCommand { + Import(FileBrowserCommand), + SelectNote(usize), + SelectField(usize), + Sample(SamplerCommand), +} + +input_to_command!(SamplerTuiCommand: |state: SamplerTui, input|match state.mode { + Some(SamplerMode::Import(..)) => Self::Import( + FileBrowserCommand::input_to_command(state, input)? + ), + _ => match input.event() { + // load sample + key_pat!(Shift-Char('L')) => { + Self::Import(FileBrowserCommand::Begin) + }, + key_pat!(KeyCode::Up) => { + Self::SelectNote(state.note_point().overflowing_add(1).0.min(127)) + }, + key_pat!(KeyCode::Down) => { + Self::SelectNote(state.note_point().overflowing_sub(1).0.min(127)) + }, + _ => return None + } + //key_pat!(KeyCode::Char('p')) => if let Some(sample) = self.sample() { + //voices.write().unwrap().push(Sample::play(sample, 0, &100.into())); + //}, + //key_pat!(KeyCode::Char('a')) => { + //let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); + //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.mode = None;//Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); + //}, + //key_pat!(KeyCode::Enter) => if let Some(sample) = self.sample() { + //self.editing = Some(sample.clone()); + //}, + //_ => { + //return Ok(None) + //} + //} +}); + +command!(|self: SamplerTuiCommand, state: SamplerTui|match self { + Self::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 + }, + Self::SelectNote(index) => { + let old = state.note_point(); + state.set_note_point(index); + Some(Self::SelectNote(old)) + }, + Self::Sample(cmd) => cmd.execute(&mut state.state)?.map(Self::Sample), + _ => todo!() +});