From 54e3761d620cbdc810e2bb2dddc5692829993df3 Mon Sep 17 00:00:00 2001 From: grovebit <337789029+grovebit@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:57:18 +0100 Subject: [PATCH] Open sessions whose plugin parameters were saved as null JSON has no NaN or infinity, so serde writes them as null, and loading null into a plugin parameter failed the whole session with "not a SoundCraft session: invalid type: null, expected f32". One plugin reporting a non-finite value, or a value like 1e39 that overflows f32 in an edited file (it loads as infinity), made the session and every later save and autosave of it unopenable. Insert and instrument parameters that are null or overflow f32 are now dropped on load, so they keep the plugin's default and the rest of the session opens. --- crates/model/src/mixer.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) 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] {