diff --git a/config/config_groovebox.edn b/config/config_groovebox.edn index 59bc032a..05f22924 100644 --- a/config/config_groovebox.edn +++ b/config/config_groovebox.edn @@ -8,8 +8,9 @@ (bsp/n (fixed/y 1 :view-status) (bsp/n (fixed/y 5 :view-sample-viewer) (bsp/w (fixed/x :w-sidebar :view-pool) - (bsp/e :view-samples-keys - (fill/y :view-editor)))))))) + (bsp/e :view-meters-input + (bsp/e :view-samples-keys + (fill/y :view-editor))))))))) (keys (layer-if :focus-pool-import "./keys_pool_file.edn") diff --git a/crates/app/src/view.rs b/crates/app/src/view.rs index 9333d10a..e81cb73d 100644 --- a/crates/app/src/view.rs +++ b/crates/app/src/view.rs @@ -56,6 +56,9 @@ impl App { ))) )) } + pub fn view_meters_input (&self) -> impl Content + use<'_> { + self.sampler().map(|s|s.view_meters_input()) + } } impl App { diff --git a/crates/device/Cargo.toml b/crates/device/Cargo.toml index 0f8b2230..a6529854 100644 --- a/crates/device/Cargo.toml +++ b/crates/device/Cargo.toml @@ -20,8 +20,9 @@ default = [ "clock", "editor", "sequencer", "sampler", "lv2" ] clock = [] editor = [] meter = [] +mixer = [] sequencer = [ "clock", "uuid" ] -sampler = [ "meter", "symphonia", "wavers" ] +sampler = [ "meter", "mixer", "symphonia", "wavers" ] lv2 = [ "livi", "winit" ] vst2 = [] vst3 = [] diff --git a/crates/device/src/device.rs b/crates/device/src/device.rs index ee6a404f..70da6c28 100644 --- a/crates/device/src/device.rs +++ b/crates/device/src/device.rs @@ -31,7 +31,7 @@ audio!(|self: DeviceAudio<'a>, client, scope|{ use Device::*; match self.0 { #[cfg(feature = "sampler")] - Sampler(sampler) => SamplerAudio(sampler).process(client, scope), + Sampler(sampler) => sampler.process(client, scope), #[cfg(feature = "lv2")] Lv2(lv2) => lv2.process(client, scope), diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index f15f3a7a..bb773305 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -35,6 +35,9 @@ pub use self::device::*; #[cfg(feature = "meter")] mod meter; #[cfg(feature = "meter")] pub use self::meter::*; +#[cfg(feature = "mixer")] mod mixer; +#[cfg(feature = "mixer")] pub use self::mixer::*; + #[cfg(feature = "lv2")] mod lv2; #[cfg(feature = "lv2")] pub use self::lv2::*; diff --git a/crates/device/src/meter.rs b/crates/device/src/meter.rs index 74dba443..81416bd0 100644 --- a/crates/device/src/meter.rs +++ b/crates/device/src/meter.rs @@ -1,16 +1,36 @@ use crate::*; -#[derive(Debug, Default, Clone)] -pub struct RMSMeter(f32); - -impl RMSMeter { - pub fn set (&mut self, samples: &[u32]) { - let sum: usize = samples.iter().map(|s|*s as usize).reduce(|sum, sample|sum + sample) - .unwrap_or(0); - self.0 = (sum as f32 / samples.len() as f32).sqrt(); - } +#[derive(Debug, Default)] +pub enum MeteringMode { + #[default] + Rms, + Log10, } -render!(TuiOut: |self: RMSMeter, to| { +#[derive(Debug, Default, Clone)] +pub struct Meter(pub f32); + +render!(TuiOut: |self: Meter, to| { let [x, y, w, h] = to.area(); + let signal = 100.0 - f32::max(0.0, f32::min(100.0, self.0.abs())); + let v = (signal * h as f32 / 100.0).ceil() as u16; + let y2 = y + h; + //to.blit(&format!("\r{v} {} {signal}", self.0), x * 20, y, None); + for y in y..(y + v) { + for x in x..(x + w) { + to.blit(&"▌", x, y2 - y, Some(Style::default().green())); + } + } }); + +pub fn to_rms (samples: &[u32]) -> f32 { + let sum: usize = samples.iter() + .map(|s|*s as usize) + .reduce(|sum, sample|sum + sample) + .unwrap_or(0); + (sum as f32 / samples.len() as f32).sqrt() +} + +pub fn to_log10 (samples: &[u32]) -> f32 { + 0.0 +} diff --git a/crates/device/src/mixer.rs b/crates/device/src/mixer.rs new file mode 100644 index 00000000..d6c34432 --- /dev/null +++ b/crates/device/src/mixer.rs @@ -0,0 +1,41 @@ +#[derive(Debug, Default)] +pub enum MixingMode { + #[default] + Summing, + Average, +} + +pub fn mix_summing ( + buffer: &mut [Vec], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>, +) -> bool { + let channels = buffer.len(); + for index in 0..frames { + if let Some(frame) = next() { + for (channel, sample) in frame.iter().enumerate() { + let channel = channel % channels; + buffer[channel][index] += sample * gain; + } + } else { + return false + } + } + true +} + +pub fn mix_average ( + buffer: &mut [Vec], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>, +) -> bool { + let channels = buffer.len(); + for index in 0..frames { + if let Some(frame) = next() { + for (channel, sample) in frame.iter().enumerate() { + let channel = channel % channels; + let value = buffer[channel][index]; + buffer[channel][index] = (value + sample * gain) / 2.0; + } + } else { + return false + } + } + true +} diff --git a/crates/device/src/sampler/sampler_api.rs b/crates/device/src/sampler/sampler_api.rs index e374f88e..fd120976 100644 --- a/crates/device/src/sampler/sampler_api.rs +++ b/crates/device/src/sampler/sampler_api.rs @@ -9,7 +9,7 @@ impl Sampler { //todo!(); //} ///// Immutable reference to sample at cursor. - //fn sample_selected (&self) -> MaybeSample { + //fn sample_selected (&self) -> Option>> { //for (i, sample) in self.mapped.iter().enumerate() { //if i == self.cursor().0 { //return sample.as_ref() @@ -88,7 +88,7 @@ impl SamplerCommand { //Self::Select(state.set_note_pos(i)) //} ///// Assign sample to pitch - //fn set (&self, pitch: u7, sample: MaybeSample) -> Option { + //fn set (&self, pitch: u7, sample: Option>>) -> Option { //let i = pitch.as_int() as usize; //let old = self.mapped[i].clone(); //self.mapped[i] = sample; @@ -106,7 +106,7 @@ impl SamplerCommand { //fn note_off (&self, state: &mut Sampler, pitch: u7) -> Option { //todo!() //} - //fn set_sample (&self, state: &mut Sampler, pitch: u7, s: MaybeSample) -> Option { + //fn set_sample (&self, state: &mut Sampler, pitch: u7, s: Option>>) -> Option { //Some(Self::SetSample(p, state.set_sample(p, s))) //} //fn import (&self, state: &mut Sampler, c: FileBrowserCommand) -> Option { @@ -131,7 +131,7 @@ impl SamplerCommand { ////(SetGain [p: u7, gain: f32] cmd_todo!("\n\rtodo: {self:?}")) ////(NoteOn [p: u7, velocity: u7] cmd_todo!("\n\rtodo: {self:?}")) ////(NoteOff [p: u7] cmd_todo!("\n\rtodo: {self:?}")) - ////(SetSample [p: u7, s: MaybeSample] Some(Self::SetSample(p, state.set_sample(p, s)))) + ////(SetSample [p: u7, s: Option>>] Some(Self::SetSample(p, state.set_sample(p, s)))) ////(Import [c: FileBrowserCommand] match c { ////FileBrowserCommand::Begin => { //////let voices = &state.state.voices; @@ -154,7 +154,7 @@ impl SamplerCommand { ////Some(Self::RecordCancel)) ////("record/finish" [] ////Some(Self::RecordFinish)) - ////("set/sample" [i: u7, s: MaybeSample] + ////("set/sample" [i: u7, s: Option>>] ////Some(Self::SetSample(i.expect("no index"), s.expect("no sampler")))) ////("set/start" [i: u7, s: usize] ////Some(Self::SetStart(i.expect("no index"), s.expect("no start")))) diff --git a/crates/device/src/sampler/sampler_audio.rs b/crates/device/src/sampler/sampler_audio.rs index 4cea762c..cfbe7b6e 100644 --- a/crates/device/src/sampler/sampler_audio.rs +++ b/crates/device/src/sampler/sampler_audio.rs @@ -1,80 +1,102 @@ use crate::*; -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); +audio!(|self: Sampler, _client, scope|{ + self.process_midi_in(scope); + self.process_audio_out(scope); + self.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 { - let mut sample = sample.write().unwrap(); - 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.port().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; + self.reset_input_meters(); + if self.recording.is_some() { + self.record_into(scope); } else { - for (input, meter) in audio_ins.iter().zip(input_meter) { - let slice = input.port().as_slice(scope); - let total: f32 = slice.iter().map(|x|x.abs()).sum(); - let count = slice.len() as f32; - *meter = 10. * (total / count).log10(); - } + self.update_input_meters(scope); } } - /// Zero the output buffer. - pub fn clear_output_buffer (&mut self) { - for buffer in self.buffer.iter_mut() { - buffer.fill(0.0); + /// Make sure that input meter count corresponds to input channel count + fn reset_input_meters (&mut self) { + let channels = self.audio_ins.len(); + if self.input_meters.len() != channels { + self.input_meters = vec![f32::MIN;channels]; + } + } + + /// Record from inputs to sample + fn record_into (&mut self, scope: &ProcessScope) { + let mut sample = self.recording + .as_mut().expect("no recording sample").1 + .write().unwrap(); + if sample.channels.len() != self.audio_ins.len() { + panic!("channel count mismatch"); + } + let samples_with_meters = self.audio_ins.iter() + .zip(self.input_meters.iter_mut()) + .zip(sample.channels.iter_mut()); + let mut length = 0; + for ((input, meter), channel) in samples_with_meters { + let slice = input.port().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; + } + + /// Update input meters + fn update_input_meters (&mut self, scope: &ProcessScope) { + for (input, meter) in self.audio_ins.iter().zip(self.input_meters.iter_mut()) { + let slice = input.port().as_slice(scope); + let total: f32 = slice.iter().map(|x|x.abs()).sum(); + let count = slice.len() as f32; + *meter = 10. * (total / count).log10(); + } + } + + /// Make sure that output meter count corresponds to input channel count + fn reset_output_meters (&mut self) { + let channels = self.audio_outs.len(); + if self.output_meters.len() != channels { + self.output_meters = vec![f32::MIN;channels]; } } /// 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; + self.clear_output_buffer(); + self.populate_output_buffer(scope.n_frames() as usize); + self.write_output_buffer(scope); + } + + /// Zero the output buffer. + fn clear_output_buffer (&mut self) { + for buffer in self.buffer.iter_mut() { + buffer.fill(0.0); + } + } + + /// Write playing voices to output buffer + fn populate_output_buffer (&mut self, frames: usize) { + let Sampler { ref mut buffer, voices, output_gain, mixing_mode, .. } = 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 - }); + match mixing_mode { + MixingMode::Summing => voices.write().unwrap().retain_mut(|voice|{ + mix_summing(buffer.as_mut_slice(), *output_gain, frames, ||voice.next()) + }), + MixingMode::Average => voices.write().unwrap().retain_mut(|voice|{ + mix_average(buffer.as_mut_slice(), *output_gain, frames, ||voice.next()) + }), + } } /// Write output buffer to output ports. - pub fn write_output_buffer (&mut self, scope: &ProcessScope) { + 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]; diff --git a/crates/device/src/sampler/sampler_data.rs b/crates/device/src/sampler/sampler_data.rs index 8c3b3bda..b4834174 100644 --- a/crates/device/src/sampler/sampler_data.rs +++ b/crates/device/src/sampler/sampler_data.rs @@ -1,6 +1,7 @@ use crate::*; impl Sample { + /// Read WAV from file pub fn read_data (src: &str) -> Usually<(usize, Vec>)> { let mut channels: Vec> = vec![]; @@ -16,6 +17,7 @@ impl Sample { } Ok((end, data)) } + pub fn from_file (path: &PathBuf) -> Usually { let name = path.file_name().unwrap().to_string_lossy().into(); let mut sample = Self { name, ..Default::default() }; @@ -49,6 +51,7 @@ impl Sample { sample.end = sample.channels.iter().fold(0, |l, c|l + c.len()); Ok(sample) } + fn decode_packet ( &mut self, decoder: &mut Box, packet: Packet ) -> Usually<()> { @@ -84,4 +87,5 @@ impl Sample { } Ok(()) } + } diff --git a/crates/device/src/sampler/sampler_model.rs b/crates/device/src/sampler/sampler_model.rs index 2a55e143..ad429a9d 100644 --- a/crates/device/src/sampler/sampler_model.rs +++ b/crates/device/src/sampler/sampler_model.rs @@ -1,55 +1,76 @@ use crate::*; -pub type MaybeSample = Option>>; - /// The sampler device plays sounds in response to MIDI notes. #[derive(Debug)] pub struct Sampler { - pub name: String, - pub mapped: [MaybeSample;128], - pub recording: Option<(usize, Arc>)>, - pub unmapped: Vec>>, - pub voices: Arc>>, - pub midi_in: Option, - pub audio_ins: Vec, - pub input_meter: Vec, - pub audio_outs: Vec, - pub buffer: Vec>, - pub output_gain: f32, - pub editing: MaybeSample, - pub mode: Option, - /// Size of actual notes area - pub size: Measure, - /// Lowest note displayed - pub note_lo: AtomicUsize, - /// Selected note - pub note_pt: AtomicUsize, - /// Selected note as row/col - pub cursor: (AtomicUsize, AtomicUsize), - pub color: ItemTheme + /// Name of sampler. + pub name: String, + /// Device color. + pub color: ItemTheme, + /// Audio input ports. Samples get recorded here. + pub audio_ins: Vec, + /// Audio input meters. + pub input_meters: Vec, + /// Sample currently being recorded. + pub recording: Option<(usize, Arc>)>, + /// Recording buffer. + pub buffer: Vec>, + /// Samples mapped to MIDI notes. + pub mapped: [Option>>;128], + /// Samples that are not mapped to MIDI notes. + pub unmapped: Vec>>, + /// Sample currently being edited. + pub editing: Option>>, + /// MIDI input port. Triggers sample playback. + pub midi_in: Option, + /// Collection of currently playing instances of samples. + pub voices: Arc>>, + /// Audio output ports. Voices get played here. + pub audio_outs: Vec, + /// Audio output meters. + pub output_meters: Vec, + /// How to mix the voices. + pub mixing_mode: MixingMode, + /// How to meter the inputs and outputs. + pub metering_mode: MeteringMode, + /// Fixed gain applied to all output. + pub output_gain: f32, + /// Currently active modal, if any. + pub mode: Option, + /// Size of rendered sampler. + pub size: Measure, + /// Lowest note displayed. + pub note_lo: AtomicUsize, + /// Currently selected note. + pub note_pt: AtomicUsize, + /// Selected note as row/col. + pub cursor: (AtomicUsize, AtomicUsize), } impl Default for Sampler { fn default () -> Self { Self { - midi_in: None, - audio_ins: vec![], - input_meter: vec![0.0;2], - audio_outs: vec![], - name: "tek_sampler".to_string(), - mapped: [const { None };128], - unmapped: vec![], - voices: Arc::new(RwLock::new(vec![])), - buffer: vec![vec![0.0;16384];2], - output_gain: 1., - recording: None, - mode: None, - editing: None, - size: Default::default(), - note_lo: 0.into(), - note_pt: 0.into(), - cursor: (0.into(), 0.into()), - color: Default::default(), + midi_in: None, + audio_ins: vec![], + input_meters: vec![0.0;2], + output_meters: vec![0.0;2], + audio_outs: vec![], + name: "tek_sampler".to_string(), + mapped: [const { None };128], + unmapped: vec![], + voices: Arc::new(RwLock::new(vec![])), + buffer: vec![vec![0.0;16384];2], + output_gain: 1., + recording: None, + mode: None, + editing: None, + size: Default::default(), + note_lo: 0.into(), + note_pt: 0.into(), + cursor: (0.into(), 0.into()), + color: Default::default(), + mixing_mode: Default::default(), + metering_mode: Default::default(), } } } diff --git a/crates/device/src/sampler/sampler_view.rs b/crates/device/src/sampler/sampler_view.rs index d984b3a4..8ceef605 100644 --- a/crates/device/src/sampler/sampler_view.rs +++ b/crates/device/src/sampler/sampler_view.rs @@ -102,6 +102,12 @@ impl Sampler { pub fn status (&self, index: usize) -> impl Content { draw_status(self.mapped[index].as_ref()) } + + pub fn view_meters_input (&self) -> impl Content + use<'_> { + Tui::bg(Black, Fixed::x(2, Map::east(1, ||self.input_meters.iter(), |value, _index|{ + Fill::y(Meter(*value)) + }))) + } } fn draw_list_item (sample: &Option>>) -> String {