diff --git a/crates/model/src/mixer.rs b/crates/model/src/mixer.rs index e9a7867..001f2a1 100644 --- a/crates/model/src/mixer.rs +++ b/crates/model/src/mixer.rs @@ -425,7 +425,7 @@ pub enum Route { pub struct Insert { /// Registry id from `soundcraft-dsp` (e.g. `eq7`, `comp`). pub plugin: String, - #[serde(default)] + #[serde(default, deserialize_with = "finite_params")] pub params: BTreeMap, #[serde(default)] pub bypass: bool, @@ -577,6 +577,13 @@ pub(crate) fn finite_or(v: f32, d: f32) -> f32 { if v.is_finite() { v } else { d } } +/// Parameters saved as `null` (how JSON writes NaN and infinity) or beyond `f32` are dropped, so +/// they keep their defaults instead of failing the whole session. +fn finite_params<'de, D: serde::Deserializer<'de>>(d: D) -> Result, D::Error> { + let params: BTreeMap> = serde::Deserialize::deserialize(d)?; + Ok(params.into_iter().filter_map(|(k, v)| Some((k, v.map(|v| v as f32).filter(|v| v.is_finite())?))).collect()) +} + /// Fader taper used by Pro Tools-style faders: position 0..1 ↔ dB (-inf..+12). pub fn fader_pos_to_db(pos: f32) -> f32 { let p = finite_or(pos, 0.0).clamp(0.0, 1.0); @@ -624,6 +631,16 @@ mod tests { assert_eq!(bad.state_bytes(), None); } + #[test] + fn params_saved_as_null_or_out_of_range_are_dropped() { + let mut i = Insert::new("eq_7band"); + i.params.extend([("gain".to_string(), f32::NAN), ("q".to_string(), 0.7)]); + let back: Insert = serde_json::from_str(&serde_json::to_string(&i).unwrap()).unwrap(); + assert_eq!(back.params, BTreeMap::from([("q".to_string(), 0.7)])); + let big: Insert = serde_json::from_str(r#"{"plugin":"x","params":{"gain":1e39,"q":0.5}}"#).unwrap(); + assert_eq!(big.params, BTreeMap::from([("q".to_string(), 0.5)])); + } + #[test] fn fader_taper_round_trips() { for db in [-100.0f32, -60.0, -24.0, -10.0, 0.0, 6.0, 12.0] {