Skip to main content

sound_to_patch/preset/vital/
codec.rs

1//! The `.vital` file: one compact JSON text.
2//!
3//! The model keeps the parsed tree with its key order, unknown keys and
4//! the structured parts as read. The reader checks only what the app needs
5//! to load a file: the stamp, `settings`, the three fields of
6//! `settings.sample` and both names of each modulation entry.
7//!
8//! A written file keeps every value bit for bit, but not the text of each
9//! number: the writer prints the shortest text that reads back to the same
10//! value. `1E5` becomes `100000.0`, and an integer of 1e23 becomes `1e+23`.
11//! A leading UTF-8 byte order mark is skipped and not written back.
12
13use std::any::Any;
14use std::collections::HashSet;
15use std::fmt;
16
17use serde::de::{self, Deserializer, IgnoredAny, MapAccess, Visitor};
18use serde_json::{Map, Number, Value};
19
20use crate::preset::format::{
21    DescribeOptions, Description, FormatError, Header, Node, PresetFormat, PresetModel, Route,
22    RoutePolarity, SHOWN_TEXT_CHARS, Status, Stored,
23};
24use crate::preset::registry::{ParamKind, ParamRegistry};
25use crate::preset::template::TemplateSource;
26use crate::preset::vital::{RETAIL_ONLY_KEYS, VitalTemplate, VitalVersion, registry, writers};
27use crate::synth::Synth;
28
29/// Top-level keys the app reads.
30const KNOWN_TOP: [&str; 12] = [
31    "synth_version",
32    "preset_name",
33    "author",
34    "comments",
35    "preset_style",
36    "macro1",
37    "macro2",
38    "macro3",
39    "macro4",
40    "settings",
41    "license",
42    "tuning",
43];
44
45/// The structured keys inside `settings`.
46const STRUCTURED: [&str; 4] = ["sample", "modulations", "wavetables", "lfos"];
47
48/// The style names the app offers. An empty style is allowed too.
49pub(crate) const STYLES: [&str; 9] = [
50    "Bass",
51    "Lead",
52    "Keys",
53    "Pad",
54    "Percussion",
55    "Sequence",
56    "Experiment",
57    "SFX",
58    "Template",
59];
60
61/// Modulation slots of a patch.
62pub(crate) const SLOTS: usize = 64;
63
64/// The numeric controls each modulation slot owns.
65const SLOT_CONTROLS: [&str; 5] = ["amount", "bipolar", "power", "stereo", "bypass"];
66
67/// The `.vital` format.
68///
69/// A read parses the whole JSON tree. Peak memory depends on the shape of
70/// the file: a file of long base64 texts takes about 3 times its size, and
71/// a file of short numbers up to about 38 times. Measured in a release
72/// build: 60 MB of `0,` items peaked at 2.28 GB, 60 MB in one text at
73/// 184 MB.
74#[derive(Debug)]
75pub struct VitalFormat {
76    max_bytes: usize,
77}
78
79impl VitalFormat {
80    /// A format whose reader refuses inputs larger than `max_bytes`.
81    #[must_use]
82    pub const fn new(max_bytes: usize) -> Self {
83        Self { max_bytes }
84    }
85
86    fn check_size(&self, bytes: &[u8]) -> Result<(), FormatError> {
87        if bytes.len() > self.max_bytes {
88            return Err(read_error(
89                format!(
90                    "the file has {} bytes, more than the limit of {}",
91                    bytes.len(),
92                    self.max_bytes
93                ),
94                "Check that the file is a Vital preset",
95            ));
96        }
97        Ok(())
98    }
99}
100
101fn read_error(cause: impl Into<String>, fix: impl Into<String>) -> FormatError {
102    FormatError::new("cannot read the .vital preset", cause, fix)
103}
104
105/// The depth `serde_json` stops at.
106const MAX_DEPTH: usize = 128;
107
108fn json_error(error: &serde_json::Error) -> FormatError {
109    // serde_json has no error kind for depth; its message is the only sign.
110    if error.to_string().starts_with("recursion limit exceeded") {
111        return read_error(
112            format!(
113                "the JSON nests deeper than {MAX_DEPTH} levels at line {} column {}",
114                error.line(),
115                error.column()
116            ),
117            "A Vital preset nests a few levels deep; pass a preset saved by Vital",
118        );
119    }
120    read_error(
121        format!("the text is not valid JSON: {error}"),
122        "Save the preset again from Vital, or fix the JSON at that position",
123    )
124}
125
126/// The bytes after a UTF-8 byte order mark, which some editors add.
127fn without_bom(bytes: &[u8]) -> &[u8] {
128    bytes.strip_prefix(b"\xEF\xBB\xBF").unwrap_or(bytes)
129}
130
131impl PresetFormat for VitalFormat {
132    fn synth(&self) -> Synth {
133        Synth::Vital
134    }
135
136    fn extension(&self) -> &'static str {
137        Synth::Vital.extension()
138    }
139
140    fn max_bytes(&self) -> usize {
141        self.max_bytes
142    }
143
144    fn read(&self, bytes: &[u8]) -> Result<Box<dyn PresetModel>, FormatError> {
145        self.check_size(bytes)?;
146        let value: Value =
147            serde_json::from_slice(without_bom(bytes)).map_err(|error| json_error(&error))?;
148        let Value::Object(root) = value else {
149            return Err(read_error(
150                "the top level is not a JSON object",
151                "Pass a .vital preset file",
152            ));
153        };
154        validate(&root)?;
155        Ok(Box::new(VitalModel { root }))
156    }
157
158    fn read_header(&self, bytes: &[u8]) -> Result<Header, FormatError> {
159        self.check_size(bytes)?;
160        let mut deserializer = serde_json::Deserializer::from_slice(without_bom(bytes));
161        let parts = deserializer
162            .deserialize_map(PartsVisitor)
163            .and_then(|parts| deserializer.end().map(|()| parts))
164            .map_err(|error| json_error(&error))?;
165        Ok(parts.header())
166    }
167
168    fn write(&self, model: &dyn PresetModel) -> Result<Vec<u8>, FormatError> {
169        let model = model.as_any().downcast_ref::<VitalModel>().ok_or_else(|| {
170            FormatError::new(
171                "cannot write the .vital preset",
172                "the model belongs to another synth",
173                "Write it with the format of its own synth",
174            )
175        })?;
176        // A model can lose a required key, as `reset("sample")` does; the
177        // app throws on such a file, so it is never written.
178        validate(&model.root)?;
179        serde_json::to_vec(&model.root).map_err(|error| {
180            FormatError::new(
181                "cannot write the .vital preset",
182                error.to_string(),
183                "Report this; a decoded model always encodes",
184            )
185        })
186    }
187
188    fn absent_means_default(&self, _key: &str) -> bool {
189        true
190    }
191
192    fn template(&self) -> &dyn TemplateSource {
193        &VitalTemplate
194    }
195
196    fn route_sources(&self) -> &'static [&'static str] {
197        &writers::ROUTE_SOURCES
198    }
199
200    fn default_polarity(&self, source: &str) -> RoutePolarity {
201        writers::default_polarity(source)
202    }
203
204    fn check_value(&self, _key: &str, _stored: f64) -> Result<(), FormatError> {
205        // The registry range covers every effect order code.
206        Ok(())
207    }
208
209    fn scan(&self, model: &dyn PresetModel, edited: &[&str]) -> Result<Vec<String>, FormatError> {
210        let model = model.as_any().downcast_ref::<VitalModel>().ok_or_else(|| {
211            FormatError::new(
212                "cannot check the .vital preset",
213                "the model belongs to another synth",
214                "Check it with the format of its own synth",
215            )
216        })?;
217        validate(&model.root)?;
218        writers::scan(model, edited)
219    }
220
221    fn factory_folder(&self) -> &'static [&'static str] {
222        &["Vital", "Factory"]
223    }
224}
225
226/// Checks what the app needs to load a file without throwing.
227fn validate(root: &Map<String, Value>) -> Result<(), FormatError> {
228    match root.get("synth_version") {
229        Some(Value::String(_)) => {}
230        Some(_) => {
231            return Err(read_error(
232                "`synth_version` is not a text",
233                "Write the build stamp as a text such as \"1.5.5\"",
234            ));
235        }
236        None => {
237            return Err(read_error(
238                "the file has no `synth_version`",
239                "Add the build stamp, such as \"synth_version\": \"1.0.6\"",
240            ));
241        }
242    }
243    let Some(settings) = root.get("settings") else {
244        return Err(read_error(
245            "the file has no `settings`",
246            "Save the preset again from Vital; every preset has a settings object",
247        ));
248    };
249    let Value::Object(settings) = settings else {
250        return Err(read_error(
251            "`settings` is not a JSON object",
252            "Save the preset again from Vital",
253        ));
254    };
255    // A text in a control fails the load, and a bool loads; a list, an
256    // object or null is no number either.
257    if let Ok(registry) = registry()
258        && let Some((key, value)) = settings.iter().find(|(key, value)| {
259            !(value.is_number() || value.is_boolean()) && registry.get(key).is_some()
260        })
261    {
262        return Err(read_error(
263            format!(
264                "the control `settings.{key}` holds {}, not a number",
265                json_kind(value)
266            ),
267            "Write a number, or save the preset again from Vital",
268        ));
269    }
270    let Some(Value::Object(sample)) = settings.get("sample") else {
271        return Err(read_error(
272            "`settings.sample` is missing or not an object",
273            "Add a sample object with `length`, `sample_rate` and `samples`, or start from the built-in template",
274        ));
275    };
276    for (field, is_ok) in [
277        ("length", sample.get("length").is_some_and(Value::is_number)),
278        (
279            "sample_rate",
280            sample.get("sample_rate").is_some_and(Value::is_number),
281        ),
282        (
283            "samples",
284            sample.get("samples").is_some_and(Value::is_string),
285        ),
286    ] {
287        if !is_ok {
288            return Err(read_error(
289                format!("`settings.sample.{field}` is missing or has the wrong type"),
290                "Vital throws on load without it; save the preset again from Vital",
291            ));
292        }
293    }
294    if let Some(modulations) = settings.get("modulations") {
295        let Value::Array(modulations) = modulations else {
296            return Err(read_error(
297                "`settings.modulations` is not a list",
298                "Write a list of {source, destination} objects",
299            ));
300        };
301        for (slot, entry) in modulations.iter().enumerate() {
302            for field in ["source", "destination"] {
303                if !entry.get(field).is_some_and(Value::is_string) {
304                    return Err(read_error(
305                        format!("`settings.modulations[{slot}].{field}` is missing or not a text"),
306                        "Vital throws on load without it; write an empty text for an unused slot",
307                    ));
308                }
309            }
310        }
311    }
312    Ok(())
313}
314
315/// The kind of a JSON value, for a message.
316fn json_kind(value: &Value) -> &'static str {
317    match value {
318        Value::Null => "null",
319        Value::Bool(_) => "a flag",
320        Value::Number(_) => "a number",
321        Value::String(_) => "a text",
322        Value::Array(_) => "a list",
323        Value::Object(_) => "an object",
324    }
325}
326
327/// The inputs of the header, read the same way from a model and from raw
328/// bytes.
329#[derive(Debug, Default)]
330struct HeaderParts {
331    version: Option<String>,
332    name: Option<String>,
333    author: Option<String>,
334    style: Option<String>,
335    top_keys: Vec<String>,
336    settings_keys: Vec<String>,
337}
338
339/// A JSON value as header text: a string as it is, anything else as JSON.
340fn text_of(value: &Value) -> String {
341    match value {
342        Value::String(text) => text.clone(),
343        other => other.to_string(),
344    }
345}
346
347impl HeaderParts {
348    fn from_root(root: &Map<String, Value>) -> Self {
349        let text = |key: &str| root.get(key).map(text_of);
350        Self {
351            version: text("synth_version"),
352            name: text("preset_name"),
353            author: text("author"),
354            style: text("preset_style"),
355            top_keys: root.keys().cloned().collect(),
356            settings_keys: match root.get("settings") {
357                Some(Value::Object(settings)) => settings.keys().cloned().collect(),
358                _ => Vec::new(),
359            },
360        }
361    }
362
363    fn header(&self) -> Header {
364        let mut header = Header::default();
365        let (stamp, status) = match &self.version {
366            None => ("(missing)".to_owned(), Status::Bad),
367            Some(text) => match text.parse::<VitalVersion>() {
368                Err(_) => (text.clone(), Status::Bad),
369                Ok(version) if version.major < 1 => (text.clone(), Status::Warn),
370                Ok(_) if self.settings_keys.iter().any(|key| key == "sub_octave") => {
371                    (text.clone(), Status::Warn)
372                }
373                Ok(_) => (text.clone(), Status::Ok),
374            },
375        };
376        header.push("build", stamp, Some(status));
377        let or_none = |value: &Option<String>| match value.as_deref() {
378            None | Some("") => "(none)".to_owned(),
379            Some(text) => text.to_owned(),
380        };
381        header.push("name", or_none(&self.name), None);
382        header.push("author", or_none(&self.author), None);
383        header.push("style", or_none(&self.style), None);
384        let known = registry().ok();
385        let controls = self
386            .settings_keys
387            .iter()
388            .filter(|key| known.is_some_and(|registry| registry.get(key).is_some()))
389            .count();
390        header.push("controls", controls.to_string(), None);
391        let unknown = unknown(known, &self.top_keys, &self.settings_keys);
392        let status = if unknown.is_empty() {
393            Status::Ok
394        } else {
395            Status::Warn
396        };
397        header.push("unknown keys", unknown.len().to_string(), Some(status));
398        let retail: Vec<&str> = RETAIL_ONLY_KEYS
399            .into_iter()
400            .filter(|key| self.settings_keys.iter().any(|have| have == key))
401            .collect();
402        if !retail.is_empty() {
403            header.push("retail-only keys", retail.join(", "), Some(Status::Warn));
404        }
405        header
406    }
407}
408
409/// Keys outside the registry and the known structure, as paths.
410fn unknown(
411    registry: Option<&crate::preset::registry::Registry>,
412    top_keys: &[String],
413    settings_keys: &[String],
414) -> Vec<String> {
415    let top = top_keys
416        .iter()
417        .filter(|key| !KNOWN_TOP.contains(&key.as_str()))
418        .cloned();
419    let settings = settings_keys
420        .iter()
421        .filter(|key| {
422            !STRUCTURED.contains(&key.as_str())
423                && registry.is_none_or(|registry| registry.get(key).is_none())
424        })
425        .map(|key| format!("settings.{key}"));
426    top.chain(settings).collect()
427}
428
429/// Reads the header parts and skips every value it does not need, so a
430/// large embedded sample is scanned once and never copied.
431struct PartsVisitor;
432
433impl<'de> Visitor<'de> for PartsVisitor {
434    type Value = HeaderParts;
435
436    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
437        f.write_str("a .vital preset object")
438    }
439
440    fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<HeaderParts, A::Error> {
441        let mut parts = HeaderParts::default();
442        let mut seen = HashSet::new();
443        while let Some(key) = map.next_key::<String>()? {
444            match key.as_str() {
445                "synth_version" => parts.version = Some(text_of(&map.next_value()?)),
446                "preset_name" => parts.name = Some(text_of(&map.next_value()?)),
447                "author" => parts.author = Some(text_of(&map.next_value()?)),
448                "preset_style" => parts.style = Some(text_of(&map.next_value()?)),
449                "settings" => parts.settings_keys = map.next_value::<SettingsKeys>()?.0,
450                _ => {
451                    map.next_value::<IgnoredAny>()?;
452                }
453            }
454            if seen.insert(key.clone()) {
455                parts.top_keys.push(key);
456            }
457        }
458        Ok(parts)
459    }
460}
461
462/// The keys of `settings`, with every value skipped.
463struct SettingsKeys(Vec<String>);
464
465impl<'de> de::Deserialize<'de> for SettingsKeys {
466    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
467        struct KeysVisitor;
468
469        impl<'de> Visitor<'de> for KeysVisitor {
470            type Value = SettingsKeys;
471
472            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
473                f.write_str("the settings object")
474            }
475
476            fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<SettingsKeys, A::Error> {
477                let mut keys: Vec<String> = Vec::new();
478                let mut seen = HashSet::new();
479                while let Some(key) = map.next_key::<String>()? {
480                    map.next_value::<IgnoredAny>()?;
481                    if seen.insert(key.clone()) {
482                        keys.push(key);
483                    }
484                }
485                Ok(SettingsKeys(keys))
486            }
487
488            fn visit_unit<E: de::Error>(self) -> Result<SettingsKeys, E> {
489                Ok(SettingsKeys(Vec::new()))
490            }
491        }
492
493        deserializer.deserialize_any(KeysVisitor)
494    }
495}
496
497/// A decoded `.vital` preset.
498///
499/// Duplicate keys in one object keep the first position and the last
500/// value, as the JSON reader does.
501#[derive(Clone, Debug, PartialEq)]
502pub struct VitalModel {
503    root: Map<String, Value>,
504}
505
506impl VitalModel {
507    /// The JSON tree as read, in file order.
508    #[must_use]
509    pub fn root(&self) -> &Map<String, Value> {
510        &self.root
511    }
512
513    fn settings(&self) -> Option<&Map<String, Value>> {
514        self.root.get("settings").and_then(Value::as_object)
515    }
516
517    fn settings_mut(&mut self) -> Result<&mut Map<String, Value>, FormatError> {
518        self.root
519            .get_mut("settings")
520            .and_then(Value::as_object_mut)
521            .ok_or_else(|| {
522                FormatError::new(
523                    "cannot edit the .vital preset",
524                    "the model has no `settings` object",
525                    "Read the preset again from a valid file",
526                )
527            })
528    }
529
530    pub(crate) fn settings_map_mut(&mut self) -> Result<&mut Map<String, Value>, FormatError> {
531        self.settings_mut()
532    }
533
534    pub(crate) fn set_top_text(&mut self, key: &str, text: &str) {
535        self.set_text(key, text);
536    }
537
538    fn set_text(&mut self, key: &str, text: &str) {
539        self.root
540            .insert(key.to_owned(), Value::String(text.to_owned()));
541    }
542
543    fn number(&self, key: &str) -> Option<f64> {
544        self.settings()?.get(key)?.as_f64()
545    }
546}
547
548fn stored_of(value: &Value) -> Option<Stored> {
549    match value {
550        Value::Number(number) => Some(match number.as_i64() {
551            Some(int) if !number.is_f64() => Stored::Int(int),
552            _ => Stored::Float(number.as_f64().unwrap_or(f64::NAN)),
553        }),
554        Value::Bool(flag) => Some(Stored::Flag(*flag)),
555        Value::String(text) => Some(Stored::Text(text.clone())),
556        Value::Null | Value::Array(_) | Value::Object(_) => None,
557    }
558}
559
560fn edit_error(cause: impl Into<String>, fix: impl Into<String>) -> FormatError {
561    FormatError::new("cannot edit the .vital preset", cause, fix)
562}
563
564/// Refuses a route without a source or a destination, and one into an
565/// on-off control: a switch, or the bipolar, stereo or bypass flag of a
566/// slot. Factory presets of Vital 1.0.x route into numbers, counts, menus
567/// such as `osc_1_transpose` and `lfo_1_tempo`, and the amount and power
568/// of other slots, and never into an on-off control.
569fn check_route(slot: usize, route: &Route) -> Result<(), FormatError> {
570    if route.source.is_empty() || route.destination.is_empty() {
571        return Err(edit_error(
572            format!(
573                "slot {}: a route needs a source and a destination",
574                slot + 1
575            ),
576            "Name both, or clear the slot",
577        ));
578    }
579    let target = registry()
580        .ok()
581        .and_then(|registry| registry.get(&route.destination));
582    let Some(spec) = target.filter(|spec| {
583        spec.kind == ParamKind::Flag
584            || (spec.key.starts_with("modulation_")
585                && ["_bipolar", "_stereo", "_bypass"]
586                    .iter()
587                    .any(|flag| spec.key.ends_with(flag)))
588    }) else {
589        return Ok(());
590    };
591    Err(edit_error(
592        format!(
593            "slot {}: `{}` is an on-off control and takes no modulation",
594            slot + 1,
595            spec.key
596        ),
597        "Route into a number, count or menu control, or into the amount or power of another slot",
598    ))
599}
600
601/// A JSON number for a finite control value.
602fn number_value(key: &str, value: &Stored) -> Result<Value, FormatError> {
603    match value {
604        Stored::Float(number) => Number::from_f64(*number).map(Value::Number).ok_or_else(|| {
605            edit_error(
606                format!("{key}: {number} is not a finite number"),
607                "Write a finite number; JSON cannot hold NaN or an infinity",
608            )
609        }),
610        Stored::Int(number) => Ok(Value::from(*number)),
611        Stored::Flag(_) | Stored::Text(_) => Err(edit_error(
612            format!("{key}: a control holds a number, not {value}"),
613            "Write a number; this writer puts only numbers into a control",
614        )),
615    }
616}
617
618impl PresetModel for VitalModel {
619    fn header(&self) -> Header {
620        HeaderParts::from_root(&self.root).header()
621    }
622
623    fn get(&self, key: &str) -> Option<Stored> {
624        self.settings()?.get(key).and_then(stored_of)
625    }
626
627    fn set(&mut self, key: &str, value: Stored) -> Result<(), FormatError> {
628        if STRUCTURED.contains(&key) {
629            return Err(edit_error(
630                format!("`{key}` is a structured part, not a control"),
631                "Use the writer for that part",
632            ));
633        }
634        let value = number_value(key, &value)?;
635        let settings = self.settings_mut()?;
636        // A retail-only part such as `custom_warps`, or any unknown key,
637        // can hold a structure; a number would replace it.
638        if let Some(have @ (Value::Array(_) | Value::Object(_))) = settings.get(key) {
639            return Err(edit_error(
640                format!("`{key}` holds {}, not a number", json_kind(have)),
641                "Leave the key out; an edit writes only numbers, and this one would replace a structure of the preset",
642            ));
643        }
644        settings.insert(key.to_owned(), value);
645        Ok(())
646    }
647
648    fn reset(&mut self, key: &str) {
649        if let Ok(settings) = self.settings_mut() {
650            settings.shift_remove(key);
651        }
652    }
653
654    fn routes(&self) -> Vec<Option<Route>> {
655        let entries = self
656            .settings()
657            .and_then(|settings| settings.get("modulations"))
658            .and_then(Value::as_array);
659        (0..SLOTS)
660            .map(|slot| {
661                let entry = entries?.get(slot)?;
662                let source = entry.get("source")?.as_str()?;
663                let destination = entry.get("destination")?.as_str()?;
664                if source.is_empty() || destination.is_empty() {
665                    return None;
666                }
667                let control = |name: &str| format!("modulation_{}_{name}", slot + 1);
668                let bipolar = self.number(&control("bipolar")).unwrap_or(0.0) >= 0.5;
669                let extras = ["power", "stereo", "bypass"]
670                    .into_iter()
671                    .filter_map(|name| {
672                        let key = control(name);
673                        self.get(&key).map(|value| (key, value))
674                    })
675                    .collect();
676                Some(Route {
677                    source: source.to_owned(),
678                    destination: destination.to_owned(),
679                    amount: self.number(&control("amount")).unwrap_or(0.0),
680                    polarity: if bipolar {
681                        RoutePolarity::Bipolar
682                    } else {
683                        RoutePolarity::Unipolar
684                    },
685                    extras,
686                })
687            })
688            .collect()
689    }
690
691    fn set_route(&mut self, slot: usize, route: Option<Route>) -> Result<(), FormatError> {
692        if slot >= SLOTS {
693            return Err(edit_error(
694                format!("modulation slot {} does not exist", slot + 1),
695                format!("Vital has {SLOTS} slots; use one from 1 to {SLOTS}"),
696            ));
697        }
698        let control = |name: &str| format!("modulation_{}_{name}", slot + 1);
699        // The same source and destination edit the slot in place: its
700        // controls and the extra fields of its entry, such as a drawn
701        // `line_mapping` curve, stay. Any other route starts from the
702        // defaults, so a new route never inherits a muted or curved slot.
703        let current = self.routes().get(slot).cloned().flatten();
704        let in_place = matches!((&current, &route), (Some(have), Some(new))
705            if have.source == new.source && have.destination == new.destination);
706        // Every slot control is written, so a cleared slot holds its
707        // defaults and the bipolar flag never relies on the GUI rule.
708        let mut numbers: Vec<(String, Value)> = SLOT_CONTROLS
709            .into_iter()
710            .map(|name| {
711                let key = control(name);
712                let default = registry()
713                    .ok()
714                    .and_then(|registry| registry.get(&key))
715                    .map_or(0.0, |spec| spec.default);
716                let kept = in_place.then(|| self.number(&key)).flatten();
717                (key, Value::from(kept.unwrap_or(default)))
718            })
719            .collect();
720        let mut put = |key: String, value: Value| {
721            if let Some(slot) = numbers.iter_mut().find(|(have, _)| *have == key) {
722                slot.1 = value;
723            }
724        };
725        let (source, destination) = match &route {
726            Some(route) => {
727                check_route(slot, route)?;
728                put(
729                    control("amount"),
730                    number_value(&control("amount"), &Stored::Float(route.amount))?,
731                );
732                let bipolar = Stored::Float(match route.polarity {
733                    RoutePolarity::Bipolar => 1.0,
734                    RoutePolarity::Unipolar => 0.0,
735                });
736                put(
737                    control("bipolar"),
738                    number_value(&control("bipolar"), &bipolar)?,
739                );
740                for (key, value) in &route.extras {
741                    if !["power", "stereo", "bypass"]
742                        .iter()
743                        .any(|name| *key == control(name))
744                    {
745                        return Err(edit_error(
746                            format!("slot {}: `{key}` is not a field of this slot", slot + 1),
747                            format!(
748                                "Use {}, {} or {}",
749                                control("power"),
750                                control("stereo"),
751                                control("bypass")
752                            ),
753                        ));
754                    }
755                    put(key.clone(), number_value(key, value)?);
756                }
757                (route.source.clone(), route.destination.clone())
758            }
759            None => (String::new(), String::new()),
760        };
761        let settings = self.settings_mut()?;
762        let modulations = settings
763            .entry("modulations")
764            .or_insert_with(|| Value::Array(Vec::new()));
765        let Value::Array(modulations) = modulations else {
766            return Err(edit_error(
767                "`settings.modulations` is not a list",
768                "Read the preset again from a valid file",
769            ));
770        };
771        while modulations.len() < SLOTS {
772            modulations.push(serde_json::json!({"source": "", "destination": ""}));
773        }
774        match (&mut modulations[slot], in_place) {
775            (Value::Object(entry), true) => {
776                entry.insert("source".to_owned(), Value::String(source));
777                entry.insert("destination".to_owned(), Value::String(destination));
778            }
779            (entry, _) => {
780                let mut fresh = Map::new();
781                fresh.insert("source".to_owned(), Value::String(source));
782                fresh.insert("destination".to_owned(), Value::String(destination));
783                *entry = Value::Object(fresh);
784            }
785        }
786        for (key, value) in numbers {
787            settings.insert(key, value);
788        }
789        Ok(())
790    }
791
792    fn set_name(&mut self, name: &str) -> Result<(), FormatError> {
793        self.set_text("preset_name", name);
794        Ok(())
795    }
796
797    fn set_author(&mut self, author: &str) -> Result<(), FormatError> {
798        self.set_text("author", author);
799        Ok(())
800    }
801
802    fn set_description(&mut self, description: &str) -> Result<(), FormatError> {
803        self.set_text("comments", description);
804        Ok(())
805    }
806
807    fn set_tags(&mut self, tags: &[String]) -> Result<(), FormatError> {
808        match tags {
809            [] => {
810                self.set_text("preset_style", "");
811                Ok(())
812            }
813            [style] if STYLES.contains(&style.as_str()) => {
814                self.set_text("preset_style", style);
815                Ok(())
816            }
817            _ => Err(edit_error(
818                format!("Vital takes one style, got {tags:?}"),
819                format!("Use one of: {}", STYLES.join(", ")),
820            )),
821        }
822    }
823
824    fn describe(&self, opts: &DescribeOptions) -> Description {
825        let registry = registry().ok();
826        let root = self
827            .root
828            .iter()
829            .map(|(key, value)| {
830                let node = match (key.as_str(), value) {
831                    ("settings", Value::Object(settings)) => Node::Map(
832                        settings
833                            .iter()
834                            .map(|(key, value)| {
835                                // A retail-only part such as `custom_warps`
836                                // is a structure; the header names it.
837                                let retail_part = RETAIL_ONLY_KEYS.contains(&key.as_str())
838                                    && (value.is_array() || value.is_object());
839                                let mark = if retail_part {
840                                    None
841                                } else {
842                                    control_mark(registry, key, value)
843                                };
844                                (key.clone(), marked(value, mark, opts))
845                            })
846                            .collect(),
847                    ),
848                    _ if !KNOWN_TOP.contains(&key.as_str()) => {
849                        marked(value, Some("unknown key".to_owned()), opts)
850                    }
851                    _ => describe_value(value, opts),
852                };
853                (key.clone(), node)
854            })
855            .collect();
856        Description {
857            root: Node::Map(root),
858        }
859    }
860
861    fn unknown_keys(&self) -> Vec<String> {
862        let parts = HeaderParts::from_root(&self.root);
863        unknown(registry().ok(), &parts.top_keys, &parts.settings_keys)
864    }
865
866    fn as_any(&self) -> &dyn Any {
867        self
868    }
869}
870
871/// The mark of a control value: wrong type, out of range, legacy or
872/// unknown.
873fn control_mark(
874    registry: Option<&crate::preset::registry::Registry>,
875    key: &str,
876    value: &Value,
877) -> Option<String> {
878    if STRUCTURED.contains(&key) {
879        return None;
880    }
881    let Some(spec) = registry.and_then(|registry| registry.get(key)) else {
882        return Some(if RETAIL_ONLY_KEYS.contains(&key) {
883            "retail-only key".to_owned()
884        } else {
885            "unknown key".to_owned()
886        });
887    };
888    // The reader refuses every other non-number in a control.
889    let Some(number) = value.as_f64() else {
890        return Some("a flag, not a number; Vital loads it".to_owned());
891    };
892    if !spec.accepts_saved(number) {
893        let whole = if spec.kind == ParamKind::Float {
894            ""
895        } else {
896            " whole numbers"
897        };
898        return Some(format!("out of range:{whole} {}", spec.range_text()));
899    }
900    spec.legacy.then(|| "legacy control".to_owned())
901}
902
903/// A value with a mark. A list or an object that carries a mark becomes
904/// one marked line: its compact JSON in a full view, its size otherwise.
905fn marked(value: &Value, mark: Option<String>, opts: &DescribeOptions) -> Node {
906    let text = match (value, opts.full) {
907        (Value::Array(_) | Value::Object(_), true) => {
908            serde_json::to_string(value).unwrap_or_default()
909        }
910        (Value::Array(items), false) => format!("list of {}", items.len()),
911        (Value::Object(map), false) => format!("object of {} keys", map.len()),
912        _ => match describe_value(value, opts) {
913            Node::Scalar { text, .. } => text,
914            node => return node,
915        },
916    };
917    match (mark, value) {
918        (None, Value::Array(_) | Value::Object(_)) => describe_value(value, opts),
919        (mark, _) => Node::Scalar { text, mark },
920    }
921}
922
923fn describe_value(value: &Value, opts: &DescribeOptions) -> Node {
924    match value {
925        Value::Object(map) => Node::Map(
926            map.iter()
927                .map(|(key, value)| (key.clone(), describe_value(value, opts)))
928                .collect(),
929        ),
930        Value::Array(items) => {
931            let shown = if opts.full { items.len() } else { opts.shown };
932            Node::List {
933                len: items.len(),
934                items: items
935                    .iter()
936                    .take(shown)
937                    .map(|item| describe_value(item, opts))
938                    .collect(),
939            }
940        }
941        Value::String(text) if !opts.full && text.chars().count() > SHOWN_TEXT_CHARS => {
942            Node::scalar(format!("<text of {} characters>", text.chars().count()))
943        }
944        other => Node::scalar(other.to_string()),
945    }
946}
947
948#[cfg(test)]
949mod tests {
950    use super::*;
951
952    const MINIMAL: &str = r#"{"synth_version":"1.0.6","settings":{"sample":{"length":1,"sample_rate":44100,"samples":"AAA="}}}"#;
953
954    fn read(text: &str) -> Result<Box<dyn PresetModel>, FormatError> {
955        VitalFormat::new(1 << 20).read(text.as_bytes())
956    }
957
958    fn model(text: &str) -> VitalModel {
959        read(text)
960            .unwrap()
961            .as_any()
962            .downcast_ref::<VitalModel>()
963            .unwrap()
964            .clone()
965    }
966
967    fn write(model: &dyn PresetModel) -> String {
968        String::from_utf8(VitalFormat::new(1 << 20).write(model).unwrap()).unwrap()
969    }
970
971    #[test]
972    fn a_control_that_holds_no_number_is_refused_by_name() {
973        for (value, kind) in [
974            (r#""loud""#, "a text"),
975            ("null", "null"),
976            ("[1,2]", "a list"),
977            (r#"{"x":1}"#, "an object"),
978        ] {
979            let text = format!(
980                r#"{{"synth_version":"1.0.6","settings":{{"filter_1_cutoff":{value},"sample":{{"length":1,"sample_rate":44100,"samples":"AAA="}}}}}}"#
981            );
982            let error = read(&text).unwrap_err().to_string();
983            assert_eq!(
984                error,
985                format!(
986                    "cannot read the .vital preset: the control `settings.filter_1_cutoff` holds {kind}, not a number. Write a number, or save the preset again from Vital"
987                )
988            );
989        }
990        // A bool loads in Vital, and an unknown key may hold anything.
991        for value in ["true", r#""loud""#] {
992            let key = if value == "true" {
993                "filter_1_cutoff"
994            } else {
995                "zz"
996            };
997            let text = format!(
998                r#"{{"synth_version":"1.0.6","settings":{{"{key}":{value},"sample":{{"length":1,"sample_rate":44100,"samples":"AAA="}}}}}}"#
999            );
1000            assert!(read(&text).is_ok(), "{text}");
1001        }
1002    }
1003
1004    #[test]
1005    fn a_number_never_replaces_a_structure() {
1006        let mut model = model(
1007            r#"{"synth_version":"1.5.5","settings":{"custom_warps":[[0,1]],"random_values":{"a":1},"zz":"text","sample":{"length":1,"sample_rate":44100,"samples":"AAA="}}}"#,
1008        );
1009        for (key, kind) in [("custom_warps", "a list"), ("random_values", "an object")] {
1010            let error = model.set(key, Stored::Float(2.0)).unwrap_err().to_string();
1011            assert!(
1012                error.contains(&format!("`{key}` holds {kind}, not a number")),
1013                "{error}"
1014            );
1015        }
1016        assert!(model.root["settings"]["custom_warps"].is_array());
1017        assert!(model.root["settings"]["random_values"].is_object());
1018        // A scalar of an unknown key is a value, and a raw edit replaces it.
1019        model.set("zz", Stored::Float(2.0)).unwrap();
1020        assert_eq!(model.get("zz"), Some(Stored::Float(2.0)));
1021    }
1022
1023    #[test]
1024    fn a_minimal_file_reads_and_writes_compact() {
1025        let model = model(MINIMAL);
1026        assert_eq!(write(&model), MINIMAL);
1027    }
1028
1029    #[test]
1030    fn each_missing_part_names_the_key_and_a_fix() {
1031        let cases = [
1032            (
1033                r#"{"settings":{"sample":{"length":1,"sample_rate":1,"samples":""}}}"#,
1034                "`synth_version`",
1035            ),
1036            (r#"{"synth_version":"1.0.6"}"#, "`settings`"),
1037            (
1038                r#"{"synth_version":"1.0.6","settings":{}}"#,
1039                "`settings.sample`",
1040            ),
1041            (
1042                r#"{"synth_version":"1.0.6","settings":{"sample":{"sample_rate":1,"samples":""}}}"#,
1043                "`settings.sample.length`",
1044            ),
1045            (
1046                r#"{"synth_version":"1.0.6","settings":{"sample":{"length":1,"sample_rate":1,"samples":""},"modulations":[{"destination":"x"}]}}"#,
1047                "`settings.modulations[0].source`",
1048            ),
1049            ("not json", "not valid JSON"),
1050            ("", "not valid JSON"),
1051            ("[]", "not a JSON object"),
1052            (
1053                r#"{"synth_version":1,"settings":{}}"#,
1054                "`synth_version` is not a text",
1055            ),
1056        ];
1057        for (text, needle) in cases {
1058            let message = read(text).unwrap_err().to_string();
1059            assert!(
1060                message.starts_with("cannot read the .vital preset: "),
1061                "{message}"
1062            );
1063            assert!(message.contains(needle), "{needle}: {message}");
1064        }
1065    }
1066
1067    #[test]
1068    fn deep_nesting_names_the_depth_limit() {
1069        for (open, close) in [("[", "]"), (r#"{"a":"#, "}")] {
1070            let deep = format!("{}1{}", open.repeat(100_000), close.repeat(100_000));
1071            let text = format!(
1072                r#"{{"synth_version":"1.0.6","settings":{{"x":{deep},"sample":{{"length":1,"sample_rate":44100,"samples":"AAA="}}}}}}"#
1073            );
1074            let error = read(&text).unwrap_err().to_string();
1075            assert!(error.contains("nests deeper than 128 levels"), "{error}");
1076            assert!(error.contains("pass a preset saved by Vital"), "{error}");
1077            // The header read skips values without recursing, so it ends
1078            // without a depth error.
1079            assert!(
1080                VitalFormat::new(1 << 20)
1081                    .read_header(text.as_bytes())
1082                    .is_ok()
1083            );
1084        }
1085    }
1086
1087    #[test]
1088    fn lists_and_objects_in_unknown_keys_are_marked() {
1089        let text = r#"{"synth_version":"1.0.6","zz_top":{"a":1},"settings":{"zz":[1,2],"zz_object":{"x":1},"sample":{"length":1,"sample_rate":44100,"samples":"AAA="}}}"#;
1090        let shown = model(text)
1091            .describe(&DescribeOptions::default())
1092            .to_string();
1093        for line in [
1094            "zz: list of 2  (unknown key)",
1095            "zz_object: object of 1 keys  (unknown key)",
1096            "zz_top: object of 1 keys  (unknown key)",
1097        ] {
1098            assert!(shown.contains(line), "{line}\n{shown}");
1099        }
1100        assert!(shown.contains("sample:\n"), "{shown}");
1101        let full = model(text)
1102            .describe(&DescribeOptions {
1103                full: true,
1104                shown: 0,
1105            })
1106            .to_string();
1107        assert!(full.contains("zz: [1,2]  (unknown key)"), "{full}");
1108    }
1109
1110    #[test]
1111    fn number_text_is_canonical_and_values_are_kept() {
1112        let text = MINIMAL.replace(
1113            r#""length":1"#,
1114            r#""length":1E5,"x":100000000000000000000000"#,
1115        );
1116        let written = write(read(&text).unwrap().as_ref());
1117        assert!(
1118            written.contains(r#""length":100000.0,"x":1e+23"#),
1119            "{written}"
1120        );
1121        assert_eq!(model(&written), model(&text));
1122    }
1123
1124    #[test]
1125    fn a_byte_order_mark_is_skipped() {
1126        let text = format!("\u{feff}{MINIMAL}");
1127        assert_eq!(write(read(&text).unwrap().as_ref()), MINIMAL);
1128        let header = VitalFormat::new(1 << 20)
1129            .read_header(text.as_bytes())
1130            .unwrap();
1131        assert_eq!(header.fields[0].value, "1.0.6");
1132    }
1133
1134    #[test]
1135    fn a_header_read_of_many_keys_is_linear() {
1136        let keys = (0..200_000)
1137            .map(|i| format!(r#""k{i}":0,"#))
1138            .collect::<Vec<_>>()
1139            .concat();
1140        let text = format!(
1141            r#"{{{keys}"synth_version":"1.0.6","settings":{{{keys}"sample":{{"length":1,"sample_rate":44100,"samples":"AAA="}}}}}}"#
1142        );
1143        let start = std::time::Instant::now();
1144        let header = VitalFormat::new(64 << 20)
1145            .read_header(text.as_bytes())
1146            .unwrap();
1147        // A quadratic scan of 200,000 keys took 15 s; a linear one takes
1148        // well under a second, even in a debug build.
1149        assert!(start.elapsed() < std::time::Duration::from_secs(5));
1150        let unknown = header
1151            .fields
1152            .iter()
1153            .find(|field| field.label == "unknown keys")
1154            .unwrap();
1155        assert_eq!(unknown.value, "400000");
1156    }
1157
1158    #[test]
1159    fn the_size_cap_applies_to_both_reads() {
1160        let format = VitalFormat::new(10);
1161        assert!(
1162            format
1163                .read(MINIMAL.as_bytes())
1164                .unwrap_err()
1165                .to_string()
1166                .contains("limit of 10")
1167        );
1168        assert!(format.read_header(MINIMAL.as_bytes()).is_err());
1169    }
1170
1171    #[test]
1172    fn a_declared_length_allocates_nothing() {
1173        // 2^28 float32 samples are 1 GiB. The reader keeps the sample as
1174        // text and never sizes a buffer from `length`.
1175        let text = r#"{"synth_version":"1.0.6","settings":{"sample":{"length":268435456,"sample_rate":44100,"samples":"AAA="}}}"#;
1176        assert_eq!(write(read(text).unwrap().as_ref()), text);
1177    }
1178
1179    #[test]
1180    fn hostile_values_read_as_written_or_fail() {
1181        let wrap = |settings: &str| {
1182            format!(
1183                r#"{{"synth_version":"1.0.6","settings":{{{settings}"sample":{{"length":1,"sample_rate":44100,"samples":"AAA="}}}}}}"#
1184            )
1185        };
1186        for bad in [
1187            wrap(r#""filter_1_cutoff":1e400,"#),
1188            wrap(r#""x":"\ud800","#),
1189            "[1]".to_owned(),
1190            "\"x\"".to_owned(),
1191        ] {
1192            assert!(read(&bad).is_err(), "{bad}");
1193        }
1194        let negative_zero = model(&wrap(r#""filter_1_cutoff":-0.0,"#));
1195        assert_eq!(
1196            negative_zero
1197                .get("filter_1_cutoff")
1198                .map(|value| match value {
1199                    Stored::Float(number) => number.to_bits(),
1200                    _ => 0,
1201                }),
1202            Some((-0.0_f64).to_bits())
1203        );
1204        let duplicate = model(&wrap(r#""filter_1_cutoff":1.0,"filter_1_cutoff":2.0,"#));
1205        assert_eq!(duplicate.get("filter_1_cutoff"), Some(Stored::Float(2.0)));
1206        let slots = (0..65)
1207            .map(|_| r#"{"source":"","destination":""}"#)
1208            .collect::<Vec<_>>()
1209            .join(",");
1210        let long = model(&wrap(&format!(r#""modulations":[{slots}],"#)));
1211        assert_eq!(write(&long), wrap(&format!(r#""modulations":[{slots}],"#)));
1212    }
1213
1214    #[test]
1215    fn unknown_keys_and_order_survive() {
1216        let text = r#"{"zeta":1,"synth_version":"1.5.5","settings":{"zz_new":2.5,"sample":{"length":1,"sample_rate":44100,"samples":"AAA="},"custom_warps":[],"filter_1_cutoff":60.0},"alpha":"x"}"#;
1217        let model = model(text);
1218        assert_eq!(write(&model), text);
1219        assert_eq!(
1220            model.unknown_keys(),
1221            ["zeta", "alpha", "settings.zz_new", "settings.custom_warps"]
1222        );
1223    }
1224
1225    #[test]
1226    fn set_accepts_finite_numbers_only() {
1227        let mut model = model(MINIMAL);
1228        let before = model.clone();
1229        assert!(model.set("filter_1_cutoff", Stored::Flag(true)).is_err());
1230        assert!(
1231            model
1232                .set("filter_1_cutoff", Stored::Float(f64::NAN))
1233                .is_err()
1234        );
1235        assert!(
1236            model
1237                .set("filter_1_cutoff", Stored::Text("60".into()))
1238                .is_err()
1239        );
1240        assert!(model.set("sample", Stored::Float(1.0)).is_err());
1241        assert_eq!(model, before);
1242        model.set("filter_1_cutoff", Stored::Float(72.5)).unwrap();
1243        model.set("osc_1_transpose", Stored::Int(-12)).unwrap();
1244        assert_eq!(model.get("filter_1_cutoff"), Some(Stored::Float(72.5)));
1245        assert_eq!(model.get("osc_1_transpose"), Some(Stored::Int(-12)));
1246        assert!(write(&model).ends_with(r#""filter_1_cutoff":72.5,"osc_1_transpose":-12}}"#));
1247        model.reset("filter_1_cutoff");
1248        assert_eq!(model.get("filter_1_cutoff"), None);
1249    }
1250
1251    #[test]
1252    fn get_keeps_absent_and_explicit_default_apart() {
1253        let model = model(
1254            r#"{"synth_version":"1.0.6","settings":{"filter_1_cutoff":60.0,"sample":{"length":1,"sample_rate":44100,"samples":"AAA="}}}"#,
1255        );
1256        assert_eq!(model.get("filter_1_cutoff"), Some(Stored::Float(60.0)));
1257        assert_eq!(model.get("filter_2_cutoff"), None);
1258    }
1259
1260    #[test]
1261    fn header_read_matches_the_model_header() {
1262        let text = r#"{"synth_version":"1.5.5","preset_name":"Bass","author":"a","preset_style":"Bass","extra":{"x":[1,2]},"settings":{"filter_1_cutoff":60.0,"custom_warps":[],"nope":1,"sample":{"length":1,"sample_rate":44100,"samples":"AAA="}}}"#;
1263        let header = VitalFormat::new(1 << 20)
1264            .read_header(text.as_bytes())
1265            .unwrap();
1266        assert_eq!(header, model(text).header());
1267        let fields: Vec<(&str, &str)> = header
1268            .fields
1269            .iter()
1270            .map(|field| (field.label.as_str(), field.value.as_str()))
1271            .collect();
1272        assert_eq!(
1273            fields,
1274            [
1275                ("build", "1.5.5"),
1276                ("name", "Bass"),
1277                ("author", "a"),
1278                ("style", "Bass"),
1279                ("controls", "1"),
1280                ("unknown keys", "3"),
1281                ("retail-only keys", "custom_warps"),
1282            ]
1283        );
1284    }
1285
1286    #[test]
1287    fn a_header_read_skips_a_large_sample() {
1288        let samples = "A".repeat(32 << 20);
1289        let text = format!(
1290            r#"{{"synth_version":"1.0.6","settings":{{"sample":{{"length":1,"sample_rate":44100,"samples":"{samples}"}},"modulations":[]}}}}"#
1291        );
1292        let header = VitalFormat::new(64 << 20)
1293            .read_header(text.as_bytes())
1294            .unwrap();
1295        assert_eq!(header.fields[0].value, "1.0.6");
1296        assert!(
1297            VitalFormat::new(64 << 20)
1298                .read_header(&text.as_bytes()[..100])
1299                .is_err()
1300        );
1301    }
1302
1303    #[test]
1304    fn old_stamps_and_sub_octave_warn() {
1305        let old = r#"{"synth_version":"0.9.0","settings":{"sample":{"length":1,"sample_rate":44100,"samples":"AAA="}}}"#;
1306        assert_eq!(model(old).header().fields[0].status, Some(Status::Warn));
1307        let sub = r#"{"synth_version":"1.0.6","settings":{"sub_octave":1,"sample":{"length":1,"sample_rate":44100,"samples":"AAA="}}}"#;
1308        assert_eq!(model(sub).header().fields[0].status, Some(Status::Warn));
1309        let bad = r#"{"synth_version":"1.5","settings":{"sample":{"length":1,"sample_rate":44100,"samples":"AAA="}}}"#;
1310        assert_eq!(model(bad).header().fields[0].status, Some(Status::Bad));
1311    }
1312
1313    #[test]
1314    fn describe_marks_odd_controls_and_elides() {
1315        let text = r#"{"synth_version":"1.0.6","settings":{"filter_1_cutoff":200.0,"filter_1_model":2.5,"osc_1_level":true,"sub_on":0.0,"zz":1,"sample":{"length":1,"sample_rate":44100,"samples":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"},"modulations":[{"source":"","destination":""},{"source":"","destination":""},{"source":"","destination":""},{"source":"","destination":""}]}}"#;
1316        let shown = model(text)
1317            .describe(&DescribeOptions::default())
1318            .to_string();
1319        assert!(
1320            shown.contains("filter_1_cutoff: 200.0  (out of range: 8.0 to 136.0)"),
1321            "{shown}"
1322        );
1323        assert!(
1324            shown.contains("filter_1_model: 2.5  (out of range: whole numbers 0.0 to 7.0)"),
1325            "{shown}"
1326        );
1327        assert!(
1328            shown.contains("osc_1_level: true  (a flag, not a number; Vital loads it)"),
1329            "{shown}"
1330        );
1331        assert!(shown.contains("sub_on: 0.0  (legacy control)"), "{shown}");
1332        assert!(shown.contains("zz: 1  (unknown key)"), "{shown}");
1333        assert!(
1334            shown.contains("samples: <text of 72 characters>"),
1335            "{shown}"
1336        );
1337        assert!(shown.contains("modulations: list of 4"), "{shown}");
1338        assert!(shown.contains("- ... 1 more"), "{shown}");
1339        let full = model(text)
1340            .describe(&DescribeOptions {
1341                full: true,
1342                shown: 0,
1343            })
1344            .to_string();
1345        assert!(!full.contains("more"), "{full}");
1346        assert!(full.contains("\"AAAA"), "{full}");
1347    }
1348
1349    #[test]
1350    fn routes_read_and_write_through_the_slot_controls() {
1351        let mut model = model(MINIMAL);
1352        assert!(model.routes().iter().all(Option::is_none));
1353        assert_eq!(model.routes().len(), 64);
1354        let route = Route {
1355            source: "lfo_1".into(),
1356            destination: "filter_1_cutoff".into(),
1357            amount: 0.25,
1358            polarity: RoutePolarity::Bipolar,
1359            extras: vec![("modulation_3_power".into(), Stored::Float(-2.0))],
1360        };
1361        model.set_route(2, Some(route.clone())).unwrap();
1362        let mut expected = route;
1363        expected.extras.extend([
1364            ("modulation_3_stereo".to_owned(), Stored::Float(0.0)),
1365            ("modulation_3_bypass".to_owned(), Stored::Float(0.0)),
1366        ]);
1367        assert_eq!(model.routes()[2], Some(expected));
1368        assert_eq!(model.get("modulation_3_amount"), Some(Stored::Float(0.25)));
1369        assert_eq!(model.get("modulation_3_bipolar"), Some(Stored::Float(1.0)));
1370        let written = write(&model);
1371        assert_eq!(written.matches("\"source\"").count(), 64);
1372        model.set_route(2, None).unwrap();
1373        assert!(model.routes()[2].is_none());
1374        assert_eq!(model.get("modulation_3_amount"), Some(Stored::Float(0.0)));
1375        assert_eq!(model.get("modulation_3_power"), Some(Stored::Float(0.0)));
1376        assert!(
1377            model
1378                .set_route(64, None)
1379                .unwrap_err()
1380                .to_string()
1381                .contains("64 slots")
1382        );
1383        let bad_extra = Route {
1384            source: "lfo_1".into(),
1385            destination: "filter_1_cutoff".into(),
1386            amount: 0.1,
1387            polarity: RoutePolarity::Unipolar,
1388            extras: vec![("modulation_9_power".into(), Stored::Float(1.0))],
1389        };
1390        assert!(model.set_route(0, Some(bad_extra)).is_err());
1391    }
1392
1393    #[test]
1394    fn a_model_without_a_required_key_does_not_write() {
1395        let mut model = model(MINIMAL);
1396        model.reset("sample");
1397        let error = VitalFormat::new(1 << 20)
1398            .write(&model)
1399            .unwrap_err()
1400            .to_string();
1401        assert!(error.contains("settings.sample"), "{error}");
1402    }
1403
1404    #[test]
1405    fn a_new_route_clears_the_old_slot_controls() {
1406        let mut model = model(MINIMAL);
1407        let muted = Route {
1408            source: "lfo_1".into(),
1409            destination: "filter_1_cutoff".into(),
1410            amount: 0.5,
1411            polarity: RoutePolarity::Unipolar,
1412            extras: vec![
1413                ("modulation_1_power".into(), Stored::Float(-2.0)),
1414                ("modulation_1_stereo".into(), Stored::Float(1.0)),
1415                ("modulation_1_bypass".into(), Stored::Float(1.0)),
1416            ],
1417        };
1418        model.set_route(0, Some(muted)).unwrap();
1419        let plain = Route {
1420            source: "env_2".into(),
1421            destination: "osc_1_level".into(),
1422            amount: 0.25,
1423            polarity: RoutePolarity::Unipolar,
1424            extras: Vec::new(),
1425        };
1426        model.set_route(0, Some(plain)).unwrap();
1427        for name in ["power", "stereo", "bypass"] {
1428            assert_eq!(
1429                model.get(&format!("modulation_1_{name}")),
1430                Some(Stored::Float(0.0)),
1431                "{name}"
1432            );
1433        }
1434    }
1435
1436    #[test]
1437    fn a_route_into_an_on_off_control_is_refused() {
1438        let mut model = model(MINIMAL);
1439        let route = |destination: &str| Route {
1440            source: "lfo_1".into(),
1441            destination: destination.into(),
1442            amount: 0.5,
1443            polarity: RoutePolarity::Unipolar,
1444            extras: Vec::new(),
1445        };
1446        for destination in ["osc_1_on", "modulation_3_bipolar", "modulation_3_bypass"] {
1447            let error = model
1448                .set_route(0, Some(route(destination)))
1449                .unwrap_err()
1450                .to_string();
1451            assert!(error.contains("is an on-off control"), "{error}");
1452            assert!(error.contains("number, count or menu control"), "{error}");
1453        }
1454        assert!(model.routes()[0].is_none());
1455        for (slot, destination) in [
1456            "lfo_1_tempo",
1457            "osc_1_transpose",
1458            "osc_1_unison_voices",
1459            "modulation_3_amount",
1460            "modulation_3_power",
1461        ]
1462        .into_iter()
1463        .enumerate()
1464        {
1465            model.set_route(slot, Some(route(destination))).unwrap();
1466            assert_eq!(
1467                model.routes()[slot]
1468                    .as_ref()
1469                    .map(|have| have.destination.as_str()),
1470                Some(destination)
1471            );
1472        }
1473    }
1474
1475    #[test]
1476    fn names_and_style_go_to_their_keys() {
1477        let mut model = model(MINIMAL);
1478        model.set_name("Wobble").unwrap();
1479        model.set_author("me").unwrap();
1480        model.set_description("from a stem").unwrap();
1481        model.set_tags(&["Bass".to_owned()]).unwrap();
1482        assert!(
1483            model
1484                .set_tags(&["Bass".to_owned(), "Lead".to_owned()])
1485                .is_err()
1486        );
1487        assert!(
1488            model
1489                .set_tags(&["Wobbly".to_owned()])
1490                .unwrap_err()
1491                .to_string()
1492                .contains("Bass, Lead")
1493        );
1494        let written = write(&model);
1495        assert!(written.ends_with(r#""preset_name":"Wobble","author":"me","comments":"from a stem","preset_style":"Bass"}"#), "{written}");
1496    }
1497
1498    #[test]
1499    fn integers_and_floats_keep_their_form() {
1500        let text = r#"{"synth_version":"1.0.6","settings":{"a":1,"b":1.0,"c":-0.0,"d":1e-7,"sample":{"length":1,"sample_rate":44100,"samples":"AAA="}}}"#;
1501        let model = model(text);
1502        assert_eq!(model.get("a"), Some(Stored::Int(1)));
1503        assert_eq!(model.get("b"), Some(Stored::Float(1.0)));
1504        assert_eq!(write(&model), text);
1505    }
1506
1507    #[test]
1508    fn duplicate_keys_keep_the_last_value() {
1509        let model = model(
1510            r#"{"synth_version":"1.0.6","settings":{"filter_1_cutoff":10.0,"sample":{"length":1,"sample_rate":44100,"samples":"AAA="},"filter_1_cutoff":20.0}}"#,
1511        );
1512        assert_eq!(model.get("filter_1_cutoff"), Some(Stored::Float(20.0)));
1513    }
1514}