Skip to main content

sound_to_patch/render/
state.rs

1//! The plug-in state of a preset, and the check of a state the plug-in
2//! saved back.
3//!
4//! Vital's state is the preset JSON as text with a trailing NUL, the form
5//! the plug-in saves itself. The builder forces the recipe's oversampling
6//! into the controls, because oversampling changes the sound.
7//!
8//! The builder also gives each state a tuning: the preset's own, or
9//! Vital's default. The plug-in keeps its current tuning when a state has
10//! none, so one host session would play the next preset in the last tuning.
11//!
12//! A host call reports success even when the plug-in kept its old sound,
13//! so a backend reads the state back and compares it with the sent one:
14//! the name, the tuning and every control. The plug-in keeps controls as
15//! 32-bit floats, so a number matches within one `f32` step. Structured
16//! parts such as the sample, the wavetables and the routes are not
17//! compared: Vital 1.0.7 re-encodes the sample and restamps the wavetables
18//! when it saves.
19
20use std::fmt;
21
22use serde_json::{Map, Value};
23
24use super::{MAX_KEY, PresetState, Recipe, RenderError};
25use crate::cast;
26use crate::preset::format::{FormatError, PresetFormat, Stored};
27use crate::preset::vital;
28use crate::synth::{Synth, SynthError};
29use crate::units::Bpm;
30
31/// The Vital control that sets oversampling.
32const OVERSAMPLING: &str = "oversampling";
33/// How many `{` bytes [`saved_preset`] tries as the start of the JSON.
34const MAX_STARTS: usize = 64;
35/// The top-level key of the tuning in a Vital state.
36const TUNING: &str = "tuning";
37/// The tempo control of a Vital preset, in beats per second despite its
38/// name.
39const TEMPO: &str = "beats_per_minute";
40/// The path of the tempo control in a [`Mismatch`].
41pub const TEMPO_PATH: &str = "settings.beats_per_minute";
42/// How many characters of a value a mismatch message shows.
43const SHOWN_CHARS: usize = 40;
44
45/// Builds the plug-in state of `preset`, a preset file of `synth`.
46///
47/// # Errors
48///
49/// Returns [`RenderError::Synth`] for a synth without a renderer and
50/// [`RenderError::Preset`] for a file that is not a valid preset.
51pub fn preset_state(
52    synth: Synth,
53    preset: &[u8],
54    recipe: &Recipe,
55) -> Result<PresetState, RenderError> {
56    match synth {
57        Synth::Vital => vital_state(preset, recipe),
58        Synth::Serum2 => Err(RenderError::Synth(SynthError::NotBuilt(synth))),
59    }
60}
61
62fn vital_state(preset: &[u8], recipe: &Recipe) -> Result<PresetState, RenderError> {
63    let mut model = vital::FORMAT.read(preset).map_err(RenderError::Preset)?;
64    model
65        .set(
66            OVERSAMPLING,
67            Stored::Float(recipe.vital_oversampling().stored()),
68        )
69        .map_err(RenderError::Preset)?;
70    let bytes = vital::FORMAT
71        .write(model.as_ref())
72        .map_err(RenderError::Preset)?;
73    let mut root: Map<String, Value> = serde_json::from_slice(&bytes).map_err(|source| {
74        RenderError::Internal(format!(
75            "the preset this program wrote does not parse ({source})"
76        ))
77    })?;
78    match root.get_mut(TUNING) {
79        Some(tuning) => {
80            check_tuning(tuning)?;
81            // The plug-in saves no empty mapping, so the read-back would
82            // miss it.
83            if let Some(tuning) = tuning.as_object_mut()
84                && tuning
85                    .get("mapping")
86                    .and_then(Value::as_array)
87                    .is_some_and(Vec::is_empty)
88            {
89                tuning.remove("mapping");
90            }
91        }
92        None => {
93            root.insert(TUNING.to_owned(), default_tuning());
94        }
95    }
96    let mut bytes = serde_json::to_vec(&root).map_err(|source| {
97        RenderError::Internal(format!("cannot encode the preset state ({source})"))
98    })?;
99    bytes.push(0);
100    Ok(PresetState::new(Synth::Vital, bytes))
101}
102
103/// Vital's default tuning as the plug-in saves it: twelve equal steps an
104/// octave, the scale starting at MIDI 60 (`Tuning::stateToJson`, source
105/// 1.0.6).
106fn default_tuning() -> Value {
107    serde_json::json!({
108        "scale_start_midi_note": 60,
109        "reference_midi_note": 0.0,
110        "tuning_name": "",
111        "mapping_name": "",
112        "default": true,
113        "scale": [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0],
114    })
115}
116
117/// Checks a preset's tuning against what `Tuning::jsonToState` reads. The
118/// plug-in answers a missing key or a wrong type with a dialog box, and an
119/// out-of-range note or mapping with a read out of bounds, so such a
120/// tuning never reaches it.
121fn check_tuning(tuning: &Value) -> Result<(), RenderError> {
122    let fail = |cause: String| {
123        Err(RenderError::Preset(FormatError::new(
124            "cannot render the .vital preset",
125            cause,
126            "Remove `tuning` to play in the default tuning, or load the tuning in Vital and save the preset again",
127        )))
128    };
129    let Value::Object(tuning) = tuning else {
130        return fail("`tuning` is not a JSON object".to_owned());
131    };
132    if tuning
133        .get("scale_start_midi_note")
134        .and_then(Value::as_u64)
135        .is_none_or(|note| note > u64::from(MAX_KEY))
136    {
137        return fail("`tuning.scale_start_midi_note` is not a MIDI note from 0 to 127".to_owned());
138    }
139    if !tuning
140        .get("reference_midi_note")
141        .is_some_and(Value::is_number)
142    {
143        return fail("`tuning.reference_midi_note` is missing or not a number".to_owned());
144    }
145    for key in ["tuning_name", "mapping_name"] {
146        if !tuning.get(key).is_some_and(Value::is_string) {
147            return fail(format!("`tuning.{key}` is missing or not a text"));
148        }
149    }
150    if tuning
151        .get("default")
152        .is_some_and(|value| !value.is_boolean())
153    {
154        return fail("`tuning.default` is not true or false".to_owned());
155    }
156    let Some(scale) = tuning
157        .get("scale")
158        .and_then(Value::as_array)
159        .filter(|scale| scale.iter().all(Value::is_number))
160    else {
161        return fail("`tuning.scale` is missing or not a list of numbers".to_owned());
162    };
163    if let Some(mapping) = tuning.get("mapping") {
164        let fits = mapping.as_array().is_some_and(|mapping| {
165            mapping.iter().all(|step| {
166                step.as_u64()
167                    .and_then(|step| usize::try_from(step).ok())
168                    .is_some_and(|step| step < scale.len())
169            })
170        });
171        if !fits {
172            return fail(
173                "`tuning.mapping` is not a list of positions in `tuning.scale`".to_owned(),
174            );
175        }
176    }
177    Ok(())
178}
179
180/// Finds the preset JSON inside a state: the first JSON object that holds
181/// `settings`. A state the plug-in saved wraps the JSON in binary headers
182/// and a trailer; a state the builder made is the JSON itself.
183#[must_use]
184pub fn saved_preset(state: &[u8]) -> Option<Map<String, Value>> {
185    state
186        .iter()
187        .enumerate()
188        .filter(|(_, byte)| **byte == b'{')
189        .take(MAX_STARTS)
190        .find_map(|(start, _)| {
191            let mut values = serde_json::Deserializer::from_slice(&state[start..]).into_iter();
192            match values.next() {
193                Some(Ok(Value::Object(root))) if root.contains_key("settings") => Some(root),
194                _ => None,
195            }
196        })
197}
198
199/// A part of the sent state that the saved state does not hold.
200#[derive(Clone, Debug, PartialEq)]
201pub struct Mismatch {
202    /// The path of the value, such as `settings.filter_1_cutoff`.
203    pub path: String,
204    /// The sent value. For the tempo control it is the value expected
205    /// from the render tempo, in beats per second.
206    pub sent: Value,
207    /// The saved value, or `None` when the key is missing.
208    pub saved: Option<Value>,
209}
210
211impl fmt::Display for Mismatch {
212    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213        match &self.saved {
214            Some(saved) => write!(
215                f,
216                "{} is {}, not {}",
217                self.path,
218                short(saved),
219                short(&self.sent)
220            ),
221            None => write!(f, "{} is missing", self.path),
222        }
223    }
224}
225
226/// A value as JSON, cut to [`SHOWN_CHARS`] characters.
227fn short(value: &Value) -> String {
228    let text = value.to_string();
229    match text.char_indices().nth(SHOWN_CHARS) {
230        Some((end, _)) => format!("{}... ({} characters)", &text[..end], text.chars().count()),
231        None => text,
232    }
233}
234
235/// Compares the preset JSON of a sent state with the one in a saved
236/// state: the name, the tuning and every control, a number under
237/// `settings`. Keys the plug-in adds on save are ignored.
238///
239/// The tempo control, `beats_per_minute` in beats per second, is compared
240/// with `bpm` instead of the sent value. A plug-in in a host sets it from
241/// the host's tempo on every block, so the saved value shows the tempo the
242/// render played at. The expected value is the one Vital computes,
243/// [`vital_beats_per_second`].
244///
245/// The comparison is Vital-shaped: the tempo key is a Vital key. A Serum 2
246/// read-back needs its own rule.
247///
248/// # Errors
249///
250/// Returns [`RenderError::Internal`] when the sent state, which this
251/// program built, holds no preset JSON, and [`RenderError::State`] when
252/// the saved state holds none.
253pub fn readback_mismatches(
254    sent: &PresetState,
255    saved: &[u8],
256    bpm: Bpm,
257) -> Result<Vec<Mismatch>, RenderError> {
258    let sent = saved_preset(sent.bytes())
259        .ok_or_else(|| RenderError::Internal("the sent state holds no preset JSON".to_owned()))?;
260    let saved = saved_preset(saved).ok_or_else(|| {
261        RenderError::State("the state the plug-in saved holds no preset JSON".to_owned())
262    })?;
263    let mut mismatches = Vec::new();
264    let mut check = |path: String, sent: &Value, saved: Option<&Value>| {
265        let same = match (sent, saved) {
266            (Value::Number(a), Some(Value::Number(b))) => a
267                .as_f64()
268                .zip(b.as_f64())
269                .is_some_and(|(a, b)| same_f32(a, b)),
270            (sent, Some(saved)) => sent == saved,
271            (_, None) => false,
272        };
273        if !same {
274            mismatches.push(Mismatch {
275                path,
276                sent: sent.clone(),
277                saved: saved.cloned(),
278            });
279        }
280    };
281    if let Some(name) = sent.get("preset_name") {
282        check("preset_name".to_owned(), name, saved.get("preset_name"));
283    }
284    let tuning = |root: &Map<String, Value>| root.get(TUNING).and_then(Value::as_object).cloned();
285    let saved_tuning = tuning(&saved).unwrap_or_default();
286    for (key, value) in tuning(&sent).unwrap_or_default() {
287        let path = format!("{TUNING}.{key}");
288        match (&value, saved_tuning.get(&key)) {
289            (Value::Array(list), Some(Value::Array(other))) if list.len() == other.len() => {
290                for (index, (value, other)) in list.iter().zip(other).enumerate() {
291                    check(format!("{path}[{index}]"), value, Some(other));
292                }
293            }
294            (value, other) => check(path, value, other),
295        }
296    }
297    let controls =
298        |root: &Map<String, Value>| root.get("settings").and_then(Value::as_object).cloned();
299    let saved_controls = controls(&saved).unwrap_or_default();
300    let sent_controls = controls(&sent).unwrap_or_default();
301    for (key, value) in &sent_controls {
302        if value.is_number() && key != TEMPO {
303            check(format!("settings.{key}"), value, saved_controls.get(key));
304        }
305    }
306    if sent_controls.contains_key(TEMPO) || saved_controls.contains_key(TEMPO) {
307        let beats_per_second = Value::from(vital_beats_per_second(bpm));
308        check(
309            format!("settings.{TEMPO}"),
310            &beats_per_second,
311            saved_controls.get(TEMPO),
312        );
313    }
314    Ok(mismatches)
315}
316
317/// The tempo Vital stores for a host tempo of `bpm`, in beats per second.
318///
319/// `SoundEngine::setBpm` divides by 60 in 32-bit floats. The shipped
320/// 1.0.7 plug-in computes it as `f32(bpm) * f32(1 / 60)`, a multiply by the
321/// reciprocal, which can lie one or two `f32` steps from the correctly
322/// rounded `bpm / 60` on a fractional tempo.
323#[must_use]
324pub fn vital_beats_per_second(bpm: Bpm) -> f64 {
325    f64::from(cast::f64_to_f32(bpm.get()) * cast::f64_to_f32(1.0 / 60.0))
326}
327
328/// Whether two numbers are the same `f32` or neighbours.
329fn same_f32(a: f64, b: f64) -> bool {
330    let (a, b) = (cast::f64_to_f32(a), cast::f64_to_f32(b));
331    let step = (a.next_up() - a).max(a - a.next_down());
332    (a - b).abs() <= step
333}
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338
339    const MINIMAL: &[u8] = include_bytes!("../../tests/fixtures/vital/minimal.vital");
340
341    /// The minimal preset with a name and two controls.
342    fn preset() -> Vec<u8> {
343        let mut preset: Value = serde_json::from_slice(MINIMAL).unwrap();
344        preset["preset_name"] = "Probe".into();
345        preset["settings"]["volume"] = 5473.0.into();
346        preset["settings"]["filter_1_cutoff"] = 60.1.into();
347        serde_json::to_vec(&preset).unwrap()
348    }
349
350    fn state() -> PresetState {
351        preset_state(Synth::Vital, &preset(), &Recipe::current()).unwrap()
352    }
353
354    #[test]
355    fn the_vital_state_is_the_preset_json_with_a_nul() {
356        let state = state();
357        assert_eq!(state.synth(), Synth::Vital);
358        assert_eq!(state.bytes().last(), Some(&0));
359        let json: Value =
360            serde_json::from_slice(&state.bytes()[..state.bytes().len() - 1]).unwrap();
361        assert!(json["settings"].is_object());
362    }
363
364    #[test]
365    fn the_state_forces_the_recipe_oversampling() {
366        let mut preset: Value = serde_json::from_slice(&preset()).unwrap();
367        preset["settings"][OVERSAMPLING] = 3.0.into();
368        let preset = serde_json::to_vec(&preset).unwrap();
369        let state = preset_state(Synth::Vital, &preset, &Recipe::current()).unwrap();
370        let settings = &saved_preset(state.bytes()).unwrap()["settings"];
371        assert_eq!(settings[OVERSAMPLING], 1.0);
372    }
373
374    #[test]
375    fn serum_2_and_a_broken_preset_are_refused() {
376        let recipe = Recipe::current();
377        assert!(matches!(
378            preset_state(Synth::Serum2, MINIMAL, &recipe),
379            Err(RenderError::Synth(SynthError::NotBuilt(Synth::Serum2)))
380        ));
381        assert!(matches!(
382            preset_state(Synth::Vital, b"{", &recipe),
383            Err(RenderError::Preset(_))
384        ));
385    }
386
387    /// A state wrapped the way the plug-in saves it: binary headers with a
388    /// stray `{`, the JSON with an added key, a NUL and a trailer.
389    fn wrapped(json: &Value) -> Vec<u8> {
390        let mut bytes = b"VstW\0\0\0\x08{\0\0\x01CcnK".to_vec();
391        bytes.extend_from_slice(&serde_json::to_vec(json).unwrap());
392        bytes.extend_from_slice(b"\0\0\0JUCEPrivateData");
393        bytes
394    }
395
396    #[test]
397    fn a_saved_state_matches_within_an_f32_step() {
398        let sent = state();
399        let mut saved = Value::Object(saved_preset(sent.bytes()).unwrap());
400        saved["added_on_save"] = true.into();
401        saved["settings"]["filter_1_cutoff"] = f64::from(cast::f64_to_f32(60.1)).into();
402        assert_eq!(
403            readback_mismatches(&sent, &wrapped(&saved), bpm()).unwrap(),
404            []
405        );
406    }
407
408    #[test]
409    fn a_changed_name_or_control_is_named() {
410        let sent = state();
411        let mut saved = Value::Object(saved_preset(sent.bytes()).unwrap());
412        saved["preset_name"] = "Other".into();
413        saved["settings"][OVERSAMPLING] = 0.0.into();
414        saved["settings"].as_object_mut().unwrap().remove("volume");
415        let paths: Vec<String> = readback_mismatches(&sent, &wrapped(&saved), bpm())
416            .unwrap()
417            .iter()
418            .map(|m| m.path.clone())
419            .collect();
420        assert!(paths.contains(&"preset_name".to_owned()), "{paths:?}");
421        assert!(
422            paths.contains(&"settings.oversampling".to_owned()),
423            "{paths:?}"
424        );
425    }
426
427    /// The minimal preset with `tuning` set to `tuning`.
428    fn tuned(tuning: Value) -> Result<PresetState, RenderError> {
429        let mut preset: Value = serde_json::from_slice(&preset()).unwrap();
430        preset[TUNING] = tuning;
431        preset_state(
432            Synth::Vital,
433            &serde_json::to_vec(&preset).unwrap(),
434            &Recipe::current(),
435        )
436    }
437
438    fn up_two() -> Value {
439        let mut tuning = default_tuning();
440        tuning["reference_midi_note"] = 2.into();
441        tuning["default"] = false.into();
442        tuning
443    }
444
445    #[test]
446    fn a_preset_without_a_tuning_gets_the_default_one() {
447        assert_eq!(
448            saved_preset(state().bytes()).unwrap()[TUNING],
449            default_tuning()
450        );
451    }
452
453    #[test]
454    fn a_preset_keeps_its_own_tuning() {
455        let mut tuning = up_two();
456        tuning["mapping"] = serde_json::json!([]);
457        let state = tuned(tuning).unwrap();
458        assert_eq!(saved_preset(state.bytes()).unwrap()[TUNING], up_two());
459    }
460
461    #[test]
462    fn a_tuning_the_plug_in_cannot_read_is_refused() {
463        let broken: [(&str, Value); 7] = [
464            ("scale_start_midi_note", 128.into()),
465            ("reference_midi_note", "2".into()),
466            ("tuning_name", Value::Null),
467            ("default", 1.into()),
468            ("scale", serde_json::json!([0, "1"])),
469            ("mapping", serde_json::json!([0, 13])),
470            ("mapping", serde_json::json!([-1])),
471        ];
472        for (key, value) in broken {
473            let mut tuning = default_tuning();
474            tuning[key] = value;
475            let error = tuned(tuning).unwrap_err();
476            assert!(matches!(error, RenderError::Preset(_)), "{key}: {error}");
477            assert!(
478                error.to_string().contains(&format!("`tuning.{key}`")),
479                "{error}"
480            );
481        }
482        assert!(matches!(tuned(12.into()), Err(RenderError::Preset(_))));
483    }
484
485    #[test]
486    fn a_tuning_the_plug_in_did_not_load_is_named() {
487        let sent = tuned(up_two()).unwrap();
488        let mut saved = Value::Object(saved_preset(sent.bytes()).unwrap());
489        saved[TUNING] = default_tuning();
490        let paths: Vec<String> = readback_mismatches(&sent, &wrapped(&saved), bpm())
491            .unwrap()
492            .iter()
493            .map(|m| m.path.clone())
494            .collect();
495        assert_eq!(paths, ["tuning.reference_midi_note", "tuning.default"]);
496    }
497
498    #[test]
499    fn a_long_value_is_cut_in_a_message() {
500        let mismatch = Mismatch {
501            path: "preset_name".to_owned(),
502            sent: "x".repeat(100).into(),
503            saved: Some("y".into()),
504        };
505        assert_eq!(
506            mismatch.to_string(),
507            format!(
508                "preset_name is \"y\", not \"{}... (102 characters)",
509                "x".repeat(39)
510            )
511        );
512    }
513
514    fn bpm() -> Bpm {
515        Bpm::new(120.0).unwrap()
516    }
517
518    /// Vital's tempo for `bpm`: `f32(bpm) * f32(1 / 60)`.
519    fn vital(bpm: f64) -> f64 {
520        f64::from(cast::f64_to_f32(bpm) * cast::f64_to_f32(1.0 / 60.0))
521    }
522
523    #[test]
524    fn the_saved_tempo_is_compared_with_the_render_tempo() {
525        let sent = state();
526        let mut saved = Value::Object(saved_preset(sent.bytes()).unwrap());
527        // Vital 1.0.7 read back 2.3333334922790527 at 140 BPM.
528        assert_eq!(vital(140.0), 2.333_333_492_279_052_7);
529        saved["settings"][TEMPO] = vital(140.0).into();
530        let at = |tempo: f64| {
531            readback_mismatches(&sent, &wrapped(&saved), Bpm::new(tempo).unwrap()).unwrap()
532        };
533        assert_eq!(at(140.0), []);
534        let mismatches = at(120.0);
535        assert_eq!(mismatches.len(), 1, "{mismatches:?}");
536        assert_eq!(
537            mismatches[0].to_string(),
538            "settings.beats_per_minute is 2.3333334922790527, not 2.0"
539        );
540        let mut sent_root = Value::Object(saved_preset(sent.bytes()).unwrap());
541        sent_root["settings"][TEMPO] = 2.0.into();
542        let with_tempo = PresetState::new(Synth::Vital, serde_json::to_vec(&sent_root).unwrap());
543        let mut lost = saved.clone();
544        lost["settings"].as_object_mut().unwrap().remove(TEMPO);
545        let mismatches =
546            readback_mismatches(&with_tempo, &wrapped(&lost), Bpm::new(140.0).unwrap()).unwrap();
547        assert_eq!(
548            mismatches[0].to_string(),
549            "settings.beats_per_minute is missing"
550        );
551    }
552
553    #[test]
554    fn a_fractional_tempo_matches_the_reciprocal_multiply() {
555        let sent = state();
556        let mut saved = Value::Object(saved_preset(sent.bytes()).unwrap());
557        saved["settings"][TEMPO] = vital(100.3).into();
558        // The correctly rounded 100.3 / 60 lies two f32 steps away.
559        assert!(!same_f32(vital(100.3), 100.3 / 60.0));
560        let mismatches =
561            readback_mismatches(&sent, &wrapped(&saved), Bpm::new(100.3).unwrap()).unwrap();
562        assert_eq!(mismatches, []);
563    }
564
565    #[test]
566    fn structured_parts_are_not_compared() {
567        let sent = state();
568        let mut saved = Value::Object(saved_preset(sent.bytes()).unwrap());
569        saved["settings"]["sample"]["samples"] = "AAAA".into();
570        assert_eq!(
571            readback_mismatches(&sent, &wrapped(&saved), bpm()).unwrap(),
572            []
573        );
574    }
575
576    #[test]
577    fn a_state_with_no_json_is_an_error() {
578        assert!(matches!(
579            readback_mismatches(&state(), b"VstW\0\0{", bpm()),
580            Err(RenderError::State(_))
581        ));
582    }
583
584    #[test]
585    fn a_sent_state_with_no_json_is_a_fault_of_this_program() {
586        let sent = PresetState::new(Synth::Vital, b"no preset".to_vec());
587        let message = readback_mismatches(&sent, &wrapped(&serde_json::json!({})), bpm())
588            .unwrap_err()
589            .to_string();
590        assert!(message.contains("fault of this program"), "{message}");
591        assert!(
592            !message.contains("Check that the preset loads"),
593            "{message}"
594        );
595    }
596
597    #[test]
598    fn neighbouring_f32_values_match_and_farther_ones_do_not() {
599        let x = 0.1_f32;
600        assert!(same_f32(f64::from(x), f64::from(x.next_up())));
601        assert!(!same_f32(f64::from(x), f64::from(x.next_up().next_up())));
602        assert!(same_f32(0.1, f64::from(x)));
603    }
604}