diff --git a/crates/engine/src/cmd/mix.rs b/crates/engine/src/cmd/mix.rs index 82517b4e..899c042e 100644 --- a/crates/engine/src/cmd/mix.rs +++ b/crates/engine/src/cmd/mix.rs @@ -244,6 +244,15 @@ fn volume(e: &mut Engine, p: &Value) -> Result { 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 = Vec::new(); for t in &tracks { @@ -257,11 +266,14 @@ fn volume(e: &mut Engine, p: &Value) -> Result { (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 = 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 { diff --git a/crates/engine/src/cmd/mix/tests.rs b/crates/engine/src/cmd/mix/tests.rs index e51c8153..1ced71dd 100644 --- a/crates/engine/src/cmd/mix/tests.rs +++ b/crates/engine/src/cmd/mix/tests.rs @@ -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)]);