diff --git a/README.md b/README.md index e9d5e51..0905567 100644 --- a/README.md +++ b/README.md @@ -223,7 +223,8 @@ Commands also differ in how they are affected by quantization: | Solo | Looper Targets | Immediate | Toggles the solo modifier on the selected loopers | | Delete | Looper Targets | Immediate | Deletes the selected loopers | | Clear | Looper Targets | Quantized | Clears all samples from the selected loopers | -| SetPan | Looper Targets, a pan value from -1 (fully left) to 1 (fully right) | Immediate | Sets the pan for the looper | +| SetPan | Looper Targets, a pan value from -1 (fully left) to 1 (fully right) | Immediate | Sets the pan for the looper | +| SetLevel | Looper Targets, a level value from 0 (silent) to 1 (full volume) | Immediate | Sets the output level for the looper | ① _RecordOverdubPlay is quantized from Record -> Overdub and Overdub -> Play, but queued from Play -> Overdub._ diff --git a/loopers-common/src/api.rs b/loopers-common/src/api.rs index 850207d..246b136 100644 --- a/loopers-common/src/api.rs +++ b/loopers-common/src/api.rs @@ -112,8 +112,12 @@ pub enum LooperCommand { SetSpeed(LooperSpeed), + // [-1.0, 1.0] SetPan(f32), + // [0.0, 1.0] + SetLevel(f32), + // Composite commands RecordOverdubPlay, @@ -178,8 +182,33 @@ impl LooperCommand { target, ) }) + }, + + "SetLevel" => { + let v = args.get(1).ok_or( + "SetLevel expects a target and a level value between 0 and 1".to_string(), + )?; + + let arg = if *v == "$data" { + None + } else { + let f = f32::from_str(v) + .map_err(|_| format!("Invalid value for SetLevel: '{}'", v))?; + if f < 0.0 || f > 1.0 { + return Err("Value for SetLevel must be between 0 and 1".to_string()); + } + Some(f) + }; + + Box::new(move |d| { + Looper( + SetLevel(arg.unwrap_or(d.data as f32 / 127.0)), + target, + ) + }) } + "1/2x" => Box::new(move |_| Looper(SetSpeed(LooperSpeed::Half), target)), "1x" => Box::new(move |_| Looper(SetSpeed(LooperSpeed::One), target)), "2x" => Box::new(move |_| Looper(SetSpeed(LooperSpeed::Double), target)), @@ -436,6 +465,10 @@ fn sync_mode_default() -> QuantizationMode { QuantizationMode::Measure } +fn level_default() -> f32 { + 1.0 +} + #[derive(Serialize, Deserialize, Clone, Debug)] pub struct SavedLooper { pub id: u32, @@ -444,6 +477,8 @@ pub struct SavedLooper { pub speed: LooperSpeed, #[serde(default)] pub pan: f32, + #[serde(default = "level_default")] + pub level: f32, #[serde(default)] pub parts: PartSet, pub samples: Vec, diff --git a/loopers-common/src/gui_channel.rs b/loopers-common/src/gui_channel.rs index 4b6d454..7cf940e 100644 --- a/loopers-common/src/gui_channel.rs +++ b/loopers-common/src/gui_channel.rs @@ -25,7 +25,8 @@ pub struct EngineStateSnapshot { pub looper_count: usize, pub part: Part, pub sync_mode: QuantizationMode, - pub input_levels: [f32; 2], + pub input_levels: [u8; 2], + pub looper_levels: [[u8; 2]; 64], pub metronome_volume: f32, } @@ -36,6 +37,7 @@ pub struct LooperState { pub mode: LooperMode, pub speed: LooperSpeed, pub pan: f32, + pub level: f32, pub parts: PartSet, pub offset: FrameTime, } diff --git a/loopers-engine/src/lib.rs b/loopers-engine/src/lib.rs index 836d1cd..1b7e637 100644 --- a/loopers-engine/src/lib.rs +++ b/loopers-engine/src/lib.rs @@ -73,6 +73,8 @@ pub struct Engine { tmp_right: Vec, output_left: Vec, output_right: Vec, + + looper_peaks: [[f32; 2]; 64], } #[allow(dead_code)] @@ -178,6 +180,8 @@ impl Engine { output_left: vec![0f64; 2048], output_right: vec![0f64; 2048], + + looper_peaks: [[0.0; 2]; 64], }; set_sample_rate(sample_rate); @@ -741,6 +745,7 @@ impl Engine { solo: bool, ) { if time.0 >= 0 { + let mut looper_index = 0; for looper in self.loopers.iter_mut() { if !looper.deleted { self.tmp_left.iter_mut().for_each(|i| *i = 0.0); @@ -773,6 +778,22 @@ impl Engine { .zip(&self.tmp_right[idx_range.clone()]) .for_each(|(a, b)| *a += *b); + // update our peaks + let mut peaks = [0f32; 2]; + for (i, vs) in [&self.tmp_left, &self.tmp_right].iter().enumerate() { + for v in *vs { + let v_abs = v.abs() as f32; + if v_abs > peaks[i] { + peaks[i] = v_abs; + } + } + } + + if let Some(p) = self.looper_peaks.get_mut(looper_index) { + *p = peaks; + } + looper_index += 1; + looper.process_input( time.0 as u64, &[ @@ -869,8 +890,8 @@ impl Engine { } } - fn compute_peaks(in_bufs: &[&[f32]]) -> [f32; 2] { - let mut peaks = [0f32; 2]; + fn compute_peaks(in_bufs: &[&[f32]]) -> [u8; 2] { + let mut peaks = [0u8; 2]; for c in 0..2 { let mut peak = 0f32; for v in in_bufs[c] { @@ -880,12 +901,36 @@ impl Engine { } } - peaks[c] = 20.0 * peak.log10(); + peaks[c] = Self::iec_scale(peak); } peaks } + fn iec_scale(amp: f32) -> u8 { + let db = 20.0 * amp.log10(); + + let d = if db < -70.0 { + 0.0 + } else if db < -60.0 { + db + 70.0 * 0.25 + } else if db < -50.0 { + db + 60.0 * 0.5 + 5.0 + } else if db < -40.0 { + db + 50.0 * 0.75 + 7.5 + } else if db < -30.0 { + db + 40.0 * 1.5 + 15.0 + } else if db < -20.0 { + db + 30.0 * 2.0 + 30.0 + } else if db < 0.0 { + db + 20.0 * 2.5 + 50.0 + } else { + 100.0 + }; + + d as u8 + } + // Step 1: Convert midi events to commands // Step 2: Handle commands // Step 3: Play current samples @@ -975,6 +1020,12 @@ impl Engine { out_r[i] = self.output_right[i] as f32; } + let mut peaks = [[0u8; 2]; 64]; + for (i, ps) in self.looper_peaks.iter().enumerate() { + peaks[i][0] = Self::iec_scale(ps[0]); + peaks[i][1] = Self::iec_scale(ps[1]); + } + // Update GUI self.gui_sender .send_update(GuiCommand::StateSnapshot(EngineStateSnapshot { @@ -986,6 +1037,7 @@ impl Engine { part: self.current_part, sync_mode: self.sync_mode, input_levels: Self::compute_peaks(&in_bufs), + looper_levels: peaks, metronome_volume: self .metronome .as_ref() diff --git a/loopers-engine/src/looper.rs b/loopers-engine/src/looper.rs index 560d49f..4eb8ca3 100644 --- a/loopers-engine/src/looper.rs +++ b/loopers-engine/src/looper.rs @@ -732,6 +732,7 @@ pub enum ControlMessage { Clear, SetSpeed(LooperSpeed), SetPan(f32), + SetLevel(f32), SetParts(PartSet), } @@ -859,6 +860,7 @@ pub struct LooperBackend { pub mode: LooperMode, pub speed: LooperSpeed, pub pan: f32, + pub level: f32, pub parts: PartSet, pub deleted: bool, @@ -912,6 +914,17 @@ impl LooperBackend { } } + fn current_state(&self) -> LooperState { + LooperState { + mode: self.mode, + speed: self.speed, + pan: self.pan, + level: self.level, + parts: self.parts, + offset: self.offset, + } + } + fn handle_msg(&mut self, msg: ControlMessage) -> bool /* continue */ { debug!("[{}] got control message: {:?}", self.id, msg); match msg { @@ -986,39 +999,26 @@ impl LooperBackend { self.speed = speed; self.gui_sender.send_update(GuiCommand::LooperStateChange( self.id, - LooperState { - mode: self.mode, - speed: self.speed, - pan: self.pan, - parts: self.parts, - offset: self.offset, - }, + self.current_state(), )); } ControlMessage::SetPan(pan) => { self.pan = pan; self.gui_sender.send_update(GuiCommand::LooperStateChange( self.id, - LooperState { - mode: self.mode, - speed: self.speed, - pan: self.pan, - parts: self.parts, - offset: self.offset, - }, + self.current_state(), + )); + } + ControlMessage::SetLevel(level) => { + self.level = level; + self.gui_sender.send_update(GuiCommand::LooperStateChange( + self.id, self.current_state() )); } ControlMessage::SetParts(parts) => { self.parts = parts; self.gui_sender.send_update(GuiCommand::LooperStateChange( - self.id, - LooperState { - mode: self.mode, - speed: self.speed, - pan: self.pan, - parts: self.parts, - offset: self.offset, - }, + self.id, self.current_state() )); } } @@ -1184,6 +1184,7 @@ impl LooperBackend { mode, speed: self.speed, pan: self.pan, + level: self.level, parts: self.parts, offset: self.offset, }, @@ -1300,6 +1301,7 @@ impl LooperBackend { parts: self.parts, speed: self.speed, pan: self.pan, + level: self.level, samples: Vec::with_capacity(self.samples.len()), offset_samples: self.offset.0, }; @@ -1331,6 +1333,7 @@ pub struct Looper { pub deleted: bool, pub parts: PartSet, pub pan: f32, + pub level: f32, pub pan_law: PanLaw, @@ -1351,6 +1354,7 @@ impl Looper { parts, LooperSpeed::One, 0.0, + 1.0, FrameTime(0), vec![], gui_output, @@ -1362,6 +1366,7 @@ impl Looper { parts: PartSet, speed: LooperSpeed, pan: f32, + level: f32, offset: FrameTime, samples: Vec, mut gui_sender: GuiSender, @@ -1377,6 +1382,7 @@ impl Looper { mode: LooperMode::Playing, speed, pan, + level, parts, offset, }; @@ -1398,6 +1404,7 @@ impl Looper { mode: LooperMode::Playing, speed, pan, + level, parts, deleted: false, offset, @@ -1425,6 +1432,7 @@ impl Looper { mode: LooperMode::Playing, parts, pan, + level, pan_law: PanLaw::Neg4_5, deleted: false, length_in_samples: length, @@ -1467,6 +1475,7 @@ impl Looper { state.parts, state.speed, state.pan, + state.level, FrameTime(state.offset_samples), samples, gui_output, @@ -1546,6 +1555,11 @@ impl Looper { self.send_to_backend(ControlMessage::SetPan(pan)); } + SetLevel(level) => { + self.level = level; + self.send_to_backend(ControlMessage::SetLevel(level)); + } + AddToPart(part) => { self.parts[part] = true; self.send_to_backend(ControlMessage::SetParts(self.parts)); @@ -1642,8 +1656,8 @@ impl Looper { && (self.mode == LooperMode::Playing || self.mode == LooperMode::Overdubbing)) { - outputs[0][out_idx] += l * pan_l as f64; - outputs[1][out_idx] += r * pan_r as f64; + outputs[0][out_idx] += l * pan_l as f64 * self.level as f64; + outputs[1][out_idx] += r * pan_r as f64 * self.level as f64; } } else if waiting > 0 && self.mode != LooperMode::Recording { backoff.spin(); diff --git a/loopers-gui/src/app.rs b/loopers-gui/src/app.rs index 7aef7cc..a688d15 100644 --- a/loopers-gui/src/app.rs +++ b/loopers-gui/src/app.rs @@ -1,4 +1,4 @@ -use crate::{skia::BACKGROUND_COLOR, AppData, Controller, GuiEvent, LooperData}; +use crate::{skia::BACKGROUND_COLOR, AppData, Controller, GuiEvent, LooperData, MouseEventType}; use crate::widgets::{ draw_circle_indicator, Button, ButtonState, ControlButton, ModalManager, PotWidget, @@ -23,6 +23,7 @@ use std::str::FromStr; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::{Duration, Instant}; +use loopers_common::clamp; const LOOP_ICON: &[u8] = include_bytes!("../resources/icons/loop.png"); const METRONOME_ICON: &[u8] = include_bytes!("../resources/icons/metronome.png"); @@ -489,7 +490,7 @@ impl BottomBarView { metronome_view: MetronomeView::new(), metronome_button: MetronomeButton::new(), time_view: TimeView::new(), - peak_view: PeakMeterView::new(), + peak_view: PeakMeterView::new(30), } } @@ -520,7 +521,9 @@ impl BottomBarView { let size = self.time_view.draw(h, data, canvas, controller, last_event); canvas.translate((size.width.round() + 20.0, 0.0)); - self.peak_view.draw(canvas, data, 160.0, h); + self.peak_view + .draw(canvas, data.engine_state.input_levels, None, 160.0, h, + |_| {}, last_event); canvas.restore(); } @@ -1001,41 +1004,21 @@ pub struct PeakMeterView { peaks: [(usize, Option); 2], levels: [usize; 2], image: Option<(Image, Instant)>, + last_mouse_value: Option, } impl PeakMeterView { - fn new() -> Self { + fn new(lines: usize) -> Self { Self { update_time: Duration::from_millis(80), - lines: 30, + lines, peaks: [(0, None), (0, None)], levels: [0, 0], image: None, + last_mouse_value: None, } } - fn iec_scale(db: f32) -> f32 { - let d = if db < -70.0 { - 0.0 - } else if db < -60.0 { - db + 70.0 * 0.25 - } else if db < -50.0 { - db + 60.0 * 0.5 + 5.0 - } else if db < -40.0 { - db + 50.0 * 0.75 + 7.5 - } else if db < -30.0 { - db + 40.0 * 1.5 + 15.0 - } else if db < -20.0 { - db + 30.0 * 2.0 + 30.0 - } else if db < 0.0 { - db + 20.0 * 2.5 + 50.0 - } else { - 100.0 - }; - - d / 100.0 - } - fn color(lines: usize, i: usize) -> Color { let p = i as f32 / lines as f32; if p < 0.8 { @@ -1094,7 +1077,8 @@ impl PeakMeterView { self.image = Some((surface.image_snapshot(), Instant::now())); } - fn draw(&mut self, canvas: &mut Canvas, data: &AppData, w: f32, h: f32) -> Size { + fn draw(&mut self, canvas: &mut Canvas, levels: [u8; 2], set_level: Option, + w: f32, h: f32, new_level: F, last_event: Option) -> Size { let mut paint = Paint::default(); paint.set_anti_alias(true); paint.set_stroke_width(1.5); @@ -1102,14 +1086,12 @@ impl PeakMeterView { let cur_time = Instant::now(); - for ((now, (peak, animation)), ref mut level) in data - .engine_state - .input_levels + for ((now, (peak, animation)), ref mut level) in levels .iter() .zip(self.peaks.iter_mut()) .zip(self.levels.iter_mut()) { - let v = (Self::iec_scale(*now) * self.lines as f32) as usize; + let v = (*now as f32 / 100.0 * self.lines as f32) as usize; // update our peaks (which are persisted for 1.2 seconds) if v > *peak { @@ -1153,6 +1135,49 @@ impl PeakMeterView { canvas.draw_path(&path, &paint); } + // if we have a level control, draw that over the vis + if let Some(level) = set_level { + let level = clamp(level, 0.0, 1.0); + let mut paint = Paint::default(); + paint.set_color(Color::WHITE); + paint.set_alpha_f(0.9); + paint.set_anti_alias(true); + paint.set_stroke_width(2.0); + paint.set_style(Style::Stroke); + + let mut path = Path::new(); + path.move_to((w * level, -5.0)); + path.line_to((w * level, h)); + canvas.draw_path(&path, &paint); + + // handle clicks + let bounds = Rect::from_size((w, h)); + if let Some(GuiEvent::MouseEvent(MouseEventType::MouseDown(MouseButton::Left), (x, y))) = last_event + { + let point = canvas + .total_matrix() + .invert() + .unwrap() + .map_point((x as f32, y as f32)); + + if bounds.contains(point) { + new_level(point.x / w); + self.last_mouse_value = Some(x as f32); + } + } else if let Some(GuiEvent::MouseEvent(MouseEventType::Moved, (x, _))) = last_event { + if let Some(p_x) = self.last_mouse_value { + let lv = clamp(level + (x as f32 - p_x) / w, 0.0, 1.0); + new_level(lv); + self.last_mouse_value = Some(x as f32); + } + } + + if let Some(GuiEvent::MouseEvent(MouseEventType::MouseUp(_), _)) = last_event { + self.last_mouse_value = None; + } + } + + Size::new(w, h) } } @@ -1531,6 +1556,7 @@ struct LooperView { active_button: ActiveButton, delete_button: DeleteButton, pan: PotWidget, + peak: PeakMeterView, } impl LooperView { @@ -1579,6 +1605,7 @@ impl LooperView { active_button: ActiveButton::new(), delete_button: DeleteButton::new(), pan: PotWidget::new(35.0, Color::WHITE), + peak: PeakMeterView::new(50), } } @@ -1735,6 +1762,13 @@ impl LooperView { }, last_event, ); + canvas.translate((0.0, 40.0)); + self.peak.draw(canvas, looper.levels, Some(looper.level), 70.0, 30.0, + |level| controller.send_command( + Command::Looper(LooperCommand::SetLevel(level), LooperTarget::Id(looper.id)), + "Failed to set level" + ), last_event); + canvas.restore(); // draw active button diff --git a/loopers-gui/src/lib.rs b/loopers-gui/src/lib.rs index be6c1d8..6f0223e 100644 --- a/loopers-gui/src/lib.rs +++ b/loopers-gui/src/lib.rs @@ -20,7 +20,7 @@ use loopers_common::gui_channel::{ }; use loopers_common::music::{MetricStructure, Tempo, TimeSignature}; use sdl2::mouse::MouseButton; -use std::collections::{HashMap, VecDeque}; +use std::collections::{BTreeMap, VecDeque}; use std::io::Write; use std::time::{Duration, Instant}; @@ -65,6 +65,8 @@ pub struct LooperData { parts: PartSet, speed: LooperSpeed, pan: f32, + level: f32, + levels: [u8; 2], waveform: Waveform, trigger: Option<(FrameTime, LooperCommand)>, } @@ -144,7 +146,7 @@ impl Controller { #[derive(Clone)] pub struct AppData { engine_state: EngineStateSnapshot, - loopers: HashMap, + loopers: BTreeMap, show_buttons: bool, messages: Log, global_triggers: Vec<( @@ -182,10 +184,11 @@ impl Gui { looper_count: 0, part: Part::A, sync_mode: QuantizationMode::Measure, - input_levels: [0.0, 0.0], + input_levels: [0, 0], + looper_levels: [[0; 2]; 64], metronome_volume: 1.0, }, - loopers: HashMap::new(), + loopers: BTreeMap::new(), show_buttons: SHOW_BUTTONS, messages: Log::new(), global_triggers: Vec::new(), @@ -213,6 +216,12 @@ impl Gui { self.state.engine_state = state; self.initialized = true; + for (i, (_, l)) in self.state.loopers.iter_mut().enumerate() { + if let Some(level) = self.state.engine_state.looper_levels.get(i) { + l.levels = *level; + } + } + // clear past triggers for l in self.state.loopers.values_mut() { if let Some((time, _)) = l.trigger { @@ -234,7 +243,9 @@ impl Gui { parts: state.parts, speed: state.speed, pan: state.pan, + level: state.level, waveform: [vec![], vec![]], + levels: [0; 2], trigger: None, }, ); @@ -251,7 +262,9 @@ impl Gui { parts: state.parts, speed: state.speed, pan: state.pan, + level: state.level, waveform: *waveform, + levels: [0; 2], trigger: None, }, ); @@ -271,6 +284,7 @@ impl Gui { l.parts = state.parts; l.speed = state.speed; l.pan = state.pan; + l.level = state.level; } else { warn!("Got looper state change for unknown looper {}", id); }