Skip to content

Commit

Permalink
Merge pull request #9 from mwylde/looper_levels
Browse files Browse the repository at this point in the history
Add visualization and control of looper output levels
  • Loading branch information
mwylde authored Jan 25, 2021
2 parents 7e12868 + 0a6467a commit 468fc12
Show file tree
Hide file tree
Showing 7 changed files with 217 additions and 65 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand Down
35 changes: 35 additions & 0 deletions loopers-common/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,12 @@ pub enum LooperCommand {

SetSpeed(LooperSpeed),

// [-1.0, 1.0]
SetPan(f32),

// [0.0, 1.0]
SetLevel(f32),

// Composite commands
RecordOverdubPlay,

Expand Down Expand Up @@ -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)),
Expand Down Expand Up @@ -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,
Expand All @@ -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<PathBuf>,
Expand Down
4 changes: 3 additions & 1 deletion loopers-common/src/gui_channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand All @@ -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,
}
Expand Down
58 changes: 55 additions & 3 deletions loopers-engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ pub struct Engine {
tmp_right: Vec<f64>,
output_left: Vec<f64>,
output_right: Vec<f64>,

looper_peaks: [[f32; 2]; 64],
}

#[allow(dead_code)]
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
&[
Expand Down Expand Up @@ -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] {
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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()
Expand Down
62 changes: 38 additions & 24 deletions loopers-engine/src/looper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +732,7 @@ pub enum ControlMessage {
Clear,
SetSpeed(LooperSpeed),
SetPan(f32),
SetLevel(f32),
SetParts(PartSet),
}

Expand Down Expand Up @@ -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,

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
));
}
}
Expand Down Expand Up @@ -1184,6 +1184,7 @@ impl LooperBackend {
mode,
speed: self.speed,
pan: self.pan,
level: self.level,
parts: self.parts,
offset: self.offset,
},
Expand Down Expand Up @@ -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,
};
Expand Down Expand Up @@ -1331,6 +1333,7 @@ pub struct Looper {
pub deleted: bool,
pub parts: PartSet,
pub pan: f32,
pub level: f32,

pub pan_law: PanLaw,

Expand All @@ -1351,6 +1354,7 @@ impl Looper {
parts,
LooperSpeed::One,
0.0,
1.0,
FrameTime(0),
vec![],
gui_output,
Expand All @@ -1362,6 +1366,7 @@ impl Looper {
parts: PartSet,
speed: LooperSpeed,
pan: f32,
level: f32,
offset: FrameTime,
samples: Vec<Sample>,
mut gui_sender: GuiSender,
Expand All @@ -1377,6 +1382,7 @@ impl Looper {
mode: LooperMode::Playing,
speed,
pan,
level,
parts,
offset,
};
Expand All @@ -1398,6 +1404,7 @@ impl Looper {
mode: LooperMode::Playing,
speed,
pan,
level,
parts,
deleted: false,
offset,
Expand Down Expand Up @@ -1425,6 +1432,7 @@ impl Looper {
mode: LooperMode::Playing,
parts,
pan,
level,
pan_law: PanLaw::Neg4_5,
deleted: false,
length_in_samples: length,
Expand Down Expand Up @@ -1467,6 +1475,7 @@ impl Looper {
state.parts,
state.speed,
state.pan,
state.level,
FrameTime(state.offset_samples),
samples,
gui_output,
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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();
Expand Down
Loading

0 comments on commit 468fc12

Please sign in to comment.