mix.volume: say what each track got and when a value was clamped (#1353)

* mix.volume: say what each track got and when a value was clamped

The fader stops at -144 and +12 dB, and mix.volume replied only with a track
count, so +15 dB played at +12 with nothing to show it. The reply now lists
each target's volume and sets clamped when a target or a group member was
held at a stop.

* mix.volume: list every requested track in the reply

A target moved as a group sibling of an earlier one was skipped before it was
added to volumes, so tracks [3, 6] in one group listed only track 3. Build
volumes after the loop from each requested track's final volume.
This commit is contained in:
Jonathan Cardoso authored and GitHub committed 2026-11-13 01:47:53 +01:00
1 parent d5d3846a78
commit 2aa342859a
2 files changed
+37 -3

No files matched your search

+15 -3
View File
@@ -244,6 +244,15 @@ fn volume(e: &mut Engine, p: &Value) -> Result<Value> {
return Err(bad("mix.volume", "`db` or `delta_db` required"));
}
let s = e.session_mut();
// The fader stops at -144 and +12 dB. The reply says when a value was held there: +15 dB used
// to play at +12 with nothing to show it.
let clamped = std::cell::Cell::new(false);
let fader = |db: f32| {
if !(-144.0..=12.0).contains(&db) {
clamped.set(true);
}
db.clamp(-144.0, 12.0)
};
// Tracks already moved as a group sibling of an earlier target must not move again.
let mut done: Vec<TrackId> = Vec::new();
for t in &tracks {
@@ -257,11 +266,14 @@ fn volume(e: &mut Engine, p: &Value) -> Result<Value> {
(None, Some(d)) => cur + d as f32,
_ => cur,
};
let new = if new.is_finite() { new.clamp(-144.0, 12.0) } else { cur };
let new = if new.is_finite() { fader(new) } else { cur };
let d = new - cur;
set_grouped(s, *t, "volume", |tr| tr.mixer.volume_db = if tr.id == *t { new } else { (tr.mixer.volume_db + d).clamp(-144.0, 12.0) });
set_grouped(s, *t, "volume", |tr| tr.mixer.volume_db = if tr.id == *t { new } else { fader(tr.mixer.volume_db + d) });
}
Ok(json!({"tracks": tracks.len()}))
// Every requested track at its final volume, also one that moved as a group sibling of an
// earlier target and was skipped above.
let volumes: Vec<Value> = tracks.iter().filter_map(|t| s.track(*t)).map(|tr| json!({"track": tr.id.0, "db": tr.mixer.volume_db})).collect();
Ok(json!({"tracks": tracks.len(), "volumes": volumes, "clamped": clamped.get()}))
}
fn pan(e: &mut Engine, p: &Value) -> Result<Value> {
+22
View File
@@ -306,6 +306,28 @@ fn volume_absolute_on_a_whole_group_keeps_offsets() {
assert_eq!(volumes(&e), [-10.0, -12.0]);
}
/// The fader stops at +12 dB; the reply says what each track got, and when a value was held at a stop.
#[test]
fn volume_held_at_the_fader_stop_is_reported() {
let mut e = grouped_demo();
let r = e.execute("mix.volume", &json!({"tracks": [3], "db": -6.0})).unwrap();
assert_eq!((r["volumes"].clone(), r["clamped"].clone()), (json!([{"track": 3, "db": -6.0}]), json!(false)));
let r = e.execute("mix.volume", &json!({"tracks": [3], "db": 15.0})).unwrap();
assert_eq!((r["volumes"].clone(), r["clamped"].clone()), (json!([{"track": 3, "db": 12.0}]), json!(true)));
assert_eq!(volumes(&e), [12.0, 12.0]);
// A group member held at the stop counts too.
let mut e = grouped_demo();
e.session_mut().track_mut(TrackId(6)).unwrap().mixer.volume_db = 6.0;
let r = e.execute("mix.volume", &json!({"tracks": [3], "db": 10.0})).unwrap();
assert_eq!(r["clamped"], json!(true));
assert_eq!(volumes(&e), [10.0, 12.0]);
// Both grouped targets are listed, each at the value it ended with.
let mut e = grouped_demo();
e.session_mut().track_mut(TrackId(6)).unwrap().mixer.volume_db = -2.0;
let r = e.execute("mix.volume", &json!({"tracks": [3, 6], "db": -10.0})).unwrap();
assert_eq!(r["volumes"], json!([{"track": 3, "db": -10.0}, {"track": 6, "db": -12.0}]));
}
#[test]
fn clear_with_unknown_param_keeps_lanes() {
let (mut e, t) = tone_session(&[(0, -6.0), (100, -3.0)]);