groovebox: display input meters!

This commit is contained in:
🪞👃🪞 2025-05-11 00:25:04 +03:00
parent e5752ea4b0
commit 7690549bdc
12 changed files with 239 additions and 117 deletions

View file

@ -8,8 +8,9 @@
(bsp/n (fixed/y 1 :view-status) (bsp/n (fixed/y 1 :view-status)
(bsp/n (fixed/y 5 :view-sample-viewer) (bsp/n (fixed/y 5 :view-sample-viewer)
(bsp/w (fixed/x :w-sidebar :view-pool) (bsp/w (fixed/x :w-sidebar :view-pool)
(bsp/e :view-samples-keys (bsp/e :view-meters-input
(fill/y :view-editor)))))))) (bsp/e :view-samples-keys
(fill/y :view-editor)))))))))
(keys (keys
(layer-if :focus-pool-import "./keys_pool_file.edn") (layer-if :focus-pool-import "./keys_pool_file.edn")

View file

@ -56,6 +56,9 @@ impl App {
))) )))
)) ))
} }
pub fn view_meters_input (&self) -> impl Content<TuiOut> + use<'_> {
self.sampler().map(|s|s.view_meters_input())
}
} }
impl App { impl App {

View file

@ -20,8 +20,9 @@ default = [ "clock", "editor", "sequencer", "sampler", "lv2" ]
clock = [] clock = []
editor = [] editor = []
meter = [] meter = []
mixer = []
sequencer = [ "clock", "uuid" ] sequencer = [ "clock", "uuid" ]
sampler = [ "meter", "symphonia", "wavers" ] sampler = [ "meter", "mixer", "symphonia", "wavers" ]
lv2 = [ "livi", "winit" ] lv2 = [ "livi", "winit" ]
vst2 = [] vst2 = []
vst3 = [] vst3 = []

View file

@ -31,7 +31,7 @@ audio!(|self: DeviceAudio<'a>, client, scope|{
use Device::*; use Device::*;
match self.0 { match self.0 {
#[cfg(feature = "sampler")] #[cfg(feature = "sampler")]
Sampler(sampler) => SamplerAudio(sampler).process(client, scope), Sampler(sampler) => sampler.process(client, scope),
#[cfg(feature = "lv2")] #[cfg(feature = "lv2")]
Lv2(lv2) => lv2.process(client, scope), Lv2(lv2) => lv2.process(client, scope),

View file

@ -35,6 +35,9 @@ pub use self::device::*;
#[cfg(feature = "meter")] mod meter; #[cfg(feature = "meter")] mod meter;
#[cfg(feature = "meter")] pub use self::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")] mod lv2;
#[cfg(feature = "lv2")] pub use self::lv2::*; #[cfg(feature = "lv2")] pub use self::lv2::*;

View file

@ -1,16 +1,36 @@
use crate::*; use crate::*;
#[derive(Debug, Default, Clone)] #[derive(Debug, Default)]
pub struct RMSMeter(f32); pub enum MeteringMode {
#[default]
impl RMSMeter { Rms,
pub fn set (&mut self, samples: &[u32]) { Log10,
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();
}
} }
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 [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
}

View file

@ -0,0 +1,41 @@
#[derive(Debug, Default)]
pub enum MixingMode {
#[default]
Summing,
Average,
}
pub fn mix_summing <const N: usize> (
buffer: &mut [Vec<f32>], 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 <const N: usize> (
buffer: &mut [Vec<f32>], 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
}

View file

@ -9,7 +9,7 @@ impl Sampler {
//todo!(); //todo!();
//} //}
///// Immutable reference to sample at cursor. ///// Immutable reference to sample at cursor.
//fn sample_selected (&self) -> MaybeSample { //fn sample_selected (&self) -> Option<Arc<RwLock<Sample>>> {
//for (i, sample) in self.mapped.iter().enumerate() { //for (i, sample) in self.mapped.iter().enumerate() {
//if i == self.cursor().0 { //if i == self.cursor().0 {
//return sample.as_ref() //return sample.as_ref()
@ -88,7 +88,7 @@ impl SamplerCommand {
//Self::Select(state.set_note_pos(i)) //Self::Select(state.set_note_pos(i))
//} //}
///// Assign sample to pitch ///// Assign sample to pitch
//fn set (&self, pitch: u7, sample: MaybeSample) -> Option<Self> { //fn set (&self, pitch: u7, sample: Option<Arc<RwLock<Sample>>>) -> Option<Self> {
//let i = pitch.as_int() as usize; //let i = pitch.as_int() as usize;
//let old = self.mapped[i].clone(); //let old = self.mapped[i].clone();
//self.mapped[i] = sample; //self.mapped[i] = sample;
@ -106,7 +106,7 @@ impl SamplerCommand {
//fn note_off (&self, state: &mut Sampler, pitch: u7) -> Option<Self> { //fn note_off (&self, state: &mut Sampler, pitch: u7) -> Option<Self> {
//todo!() //todo!()
//} //}
//fn set_sample (&self, state: &mut Sampler, pitch: u7, s: MaybeSample) -> Option<Self> { //fn set_sample (&self, state: &mut Sampler, pitch: u7, s: Option<Arc<RwLock<Sample>>>) -> Option<Self> {
//Some(Self::SetSample(p, state.set_sample(p, s))) //Some(Self::SetSample(p, state.set_sample(p, s)))
//} //}
//fn import (&self, state: &mut Sampler, c: FileBrowserCommand) -> Option<Self> { //fn import (&self, state: &mut Sampler, c: FileBrowserCommand) -> Option<Self> {
@ -131,7 +131,7 @@ impl SamplerCommand {
////(SetGain [p: u7, gain: f32] cmd_todo!("\n\rtodo: {self:?}")) ////(SetGain [p: u7, gain: f32] cmd_todo!("\n\rtodo: {self:?}"))
////(NoteOn [p: u7, velocity: u7] cmd_todo!("\n\rtodo: {self:?}")) ////(NoteOn [p: u7, velocity: u7] cmd_todo!("\n\rtodo: {self:?}"))
////(NoteOff [p: 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<Arc<RwLock<Sample>>>] Some(Self::SetSample(p, state.set_sample(p, s))))
////(Import [c: FileBrowserCommand] match c { ////(Import [c: FileBrowserCommand] match c {
////FileBrowserCommand::Begin => { ////FileBrowserCommand::Begin => {
//////let voices = &state.state.voices; //////let voices = &state.state.voices;
@ -154,7 +154,7 @@ impl SamplerCommand {
////Some(Self::RecordCancel)) ////Some(Self::RecordCancel))
////("record/finish" [] ////("record/finish" []
////Some(Self::RecordFinish)) ////Some(Self::RecordFinish))
////("set/sample" [i: u7, s: MaybeSample] ////("set/sample" [i: u7, s: Option<Arc<RwLock<Sample>>>]
////Some(Self::SetSample(i.expect("no index"), s.expect("no sampler")))) ////Some(Self::SetSample(i.expect("no index"), s.expect("no sampler"))))
////("set/start" [i: u7, s: usize] ////("set/start" [i: u7, s: usize]
////Some(Self::SetStart(i.expect("no index"), s.expect("no start")))) ////Some(Self::SetStart(i.expect("no index"), s.expect("no start"))))

View file

@ -1,80 +1,102 @@
use crate::*; use crate::*;
pub struct SamplerAudio<'a>(pub &'a mut Sampler); audio!(|self: Sampler, _client, scope|{
self.process_midi_in(scope);
audio!(|self: SamplerAudio<'a>, _client, scope|{ self.process_audio_out(scope);
self.0.process_midi_in(scope); self.process_audio_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 Control::Continue
}); });
impl Sampler { impl Sampler {
pub fn process_audio_in (&mut self, scope: &ProcessScope) { pub fn process_audio_in (&mut self, scope: &ProcessScope) {
let Sampler { audio_ins, input_meter, recording, .. } = self; self.reset_input_meters();
if audio_ins.len() != input_meter.len() { if self.recording.is_some() {
*input_meter = vec![0.0;audio_ins.len()]; self.record_into(scope);
}
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;
} else { } else {
for (input, meter) in audio_ins.iter().zip(input_meter) { self.update_input_meters(scope);
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();
}
} }
} }
/// Zero the output buffer. /// Make sure that input meter count corresponds to input channel count
pub fn clear_output_buffer (&mut self) { fn reset_input_meters (&mut self) {
for buffer in self.buffer.iter_mut() { let channels = self.audio_ins.len();
buffer.fill(0.0); 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. /// Mix all currently playing samples into the output.
pub fn process_audio_out (&mut self, scope: &ProcessScope) { 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(); let channel_count = buffer.len();
voices.write().unwrap().retain_mut(|voice|{ match mixing_mode {
for index in 0..scope.n_frames() as usize { MixingMode::Summing => voices.write().unwrap().retain_mut(|voice|{
if let Some(frame) = voice.next() { mix_summing(buffer.as_mut_slice(), *output_gain, frames, ||voice.next())
for (channel, sample) in frame.iter().enumerate() { }),
// Averaging mixer: MixingMode::Average => voices.write().unwrap().retain_mut(|voice|{
//self.buffer[channel % channel_count][index] = ( mix_average(buffer.as_mut_slice(), *output_gain, frames, ||voice.next())
//(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. /// 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; let Sampler { ref mut audio_outs, buffer, .. } = self;
for (i, port) in audio_outs.iter_mut().enumerate() { for (i, port) in audio_outs.iter_mut().enumerate() {
let buffer = &buffer[i]; let buffer = &buffer[i];

View file

@ -1,6 +1,7 @@
use crate::*; use crate::*;
impl Sample { impl Sample {
/// Read WAV from file /// Read WAV from file
pub fn read_data (src: &str) -> Usually<(usize, Vec<Vec<f32>>)> { pub fn read_data (src: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
let mut channels: Vec<wavers::Samples<f32>> = vec![]; let mut channels: Vec<wavers::Samples<f32>> = vec![];
@ -16,6 +17,7 @@ impl Sample {
} }
Ok((end, data)) Ok((end, data))
} }
pub fn from_file (path: &PathBuf) -> Usually<Self> { pub fn from_file (path: &PathBuf) -> Usually<Self> {
let name = path.file_name().unwrap().to_string_lossy().into(); let name = path.file_name().unwrap().to_string_lossy().into();
let mut sample = Self { name, ..Default::default() }; 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()); sample.end = sample.channels.iter().fold(0, |l, c|l + c.len());
Ok(sample) Ok(sample)
} }
fn decode_packet ( fn decode_packet (
&mut self, decoder: &mut Box<dyn Decoder>, packet: Packet &mut self, decoder: &mut Box<dyn Decoder>, packet: Packet
) -> Usually<()> { ) -> Usually<()> {
@ -84,4 +87,5 @@ impl Sample {
} }
Ok(()) Ok(())
} }
} }

View file

@ -1,55 +1,76 @@
use crate::*; use crate::*;
pub type MaybeSample = Option<Arc<RwLock<Sample>>>;
/// The sampler device plays sounds in response to MIDI notes. /// The sampler device plays sounds in response to MIDI notes.
#[derive(Debug)] #[derive(Debug)]
pub struct Sampler { pub struct Sampler {
pub name: String, /// Name of sampler.
pub mapped: [MaybeSample;128], pub name: String,
pub recording: Option<(usize, Arc<RwLock<Sample>>)>, /// Device color.
pub unmapped: Vec<Arc<RwLock<Sample>>>, pub color: ItemTheme,
pub voices: Arc<RwLock<Vec<Voice>>>, /// Audio input ports. Samples get recorded here.
pub midi_in: Option<JackMidiIn>, pub audio_ins: Vec<JackAudioIn>,
pub audio_ins: Vec<JackAudioIn>, /// Audio input meters.
pub input_meter: Vec<f32>, pub input_meters: Vec<f32>,
pub audio_outs: Vec<JackAudioOut>, /// Sample currently being recorded.
pub buffer: Vec<Vec<f32>>, pub recording: Option<(usize, Arc<RwLock<Sample>>)>,
pub output_gain: f32, /// Recording buffer.
pub editing: MaybeSample, pub buffer: Vec<Vec<f32>>,
pub mode: Option<SamplerMode>, /// Samples mapped to MIDI notes.
/// Size of actual notes area pub mapped: [Option<Arc<RwLock<Sample>>>;128],
pub size: Measure<TuiOut>, /// Samples that are not mapped to MIDI notes.
/// Lowest note displayed pub unmapped: Vec<Arc<RwLock<Sample>>>,
pub note_lo: AtomicUsize, /// Sample currently being edited.
/// Selected note pub editing: Option<Arc<RwLock<Sample>>>,
pub note_pt: AtomicUsize, /// MIDI input port. Triggers sample playback.
/// Selected note as row/col pub midi_in: Option<JackMidiIn>,
pub cursor: (AtomicUsize, AtomicUsize), /// Collection of currently playing instances of samples.
pub color: ItemTheme pub voices: Arc<RwLock<Vec<Voice>>>,
/// Audio output ports. Voices get played here.
pub audio_outs: Vec<JackAudioOut>,
/// Audio output meters.
pub output_meters: Vec<f32>,
/// 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<SamplerMode>,
/// Size of rendered sampler.
pub size: Measure<TuiOut>,
/// 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 { impl Default for Sampler {
fn default () -> Self { fn default () -> Self {
Self { Self {
midi_in: None, midi_in: None,
audio_ins: vec![], audio_ins: vec![],
input_meter: vec![0.0;2], input_meters: vec![0.0;2],
audio_outs: vec![], output_meters: vec![0.0;2],
name: "tek_sampler".to_string(), audio_outs: vec![],
mapped: [const { None };128], name: "tek_sampler".to_string(),
unmapped: vec![], mapped: [const { None };128],
voices: Arc::new(RwLock::new(vec![])), unmapped: vec![],
buffer: vec![vec![0.0;16384];2], voices: Arc::new(RwLock::new(vec![])),
output_gain: 1., buffer: vec![vec![0.0;16384];2],
recording: None, output_gain: 1.,
mode: None, recording: None,
editing: None, mode: None,
size: Default::default(), editing: None,
note_lo: 0.into(), size: Default::default(),
note_pt: 0.into(), note_lo: 0.into(),
cursor: (0.into(), 0.into()), note_pt: 0.into(),
color: Default::default(), cursor: (0.into(), 0.into()),
color: Default::default(),
mixing_mode: Default::default(),
metering_mode: Default::default(),
} }
} }
} }

View file

@ -102,6 +102,12 @@ impl Sampler {
pub fn status (&self, index: usize) -> impl Content<TuiOut> { pub fn status (&self, index: usize) -> impl Content<TuiOut> {
draw_status(self.mapped[index].as_ref()) draw_status(self.mapped[index].as_ref())
} }
pub fn view_meters_input (&self) -> impl Content<TuiOut> + 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<Arc<RwLock<Sample>>>) -> String { fn draw_list_item (sample: &Option<Arc<RwLock<Sample>>>) -> String {