Skip to main content

sound_to_patch/preset/vital/
template.rs

1//! The Vital template: a built-in default that our code makes, or a preset
2//! the user saved from the app and passes with `--template`.
3//!
4//! The built-in template lists every non-legacy control at its registry
5//! default, so the output does not depend on a default a retail build may
6//! change. Its wavetables are one sawtooth cycle and its sample is seeded
7//! white noise, both generated here. It is not the app's own Init patch.
8
9use std::path::PathBuf;
10
11use base64::Engine as _;
12use base64::engine::general_purpose::STANDARD;
13use serde_json::{Map, Value, json};
14
15use crate::cast::{f64_to_f32, pcm16, unit_interval, usize_to_f64};
16use crate::preset::format::PresetModel;
17use crate::preset::registry::ParamRegistry;
18use crate::preset::template::{Severity, TemplateCheck, TemplateHelp, TemplateSource};
19use crate::preset::vital::codec::VitalModel;
20use crate::preset::vital::writers::FRAME_SAMPLES;
21use crate::preset::vital::{DEFAULT_STAMP, RETAIL_BUILD, RETAIL_ONLY_KEYS, VitalVersion, registry};
22
23/// Samples in the default sample: one second at 44.1 kHz.
24const NOISE_SAMPLES: usize = 44_100;
25
26const NOISE_SEED: u64 = 0x005E_ED0F_0000_5A7C;
27
28/// The template rules of Vital.
29#[derive(Clone, Copy, Debug, Default)]
30pub struct VitalTemplate;
31
32impl TemplateSource for VitalTemplate {
33    fn help(&self) -> TemplateHelp {
34        TemplateHelp(
35            "Save a preset from Vital and pass it with --template, or leave --template out to \
36             use the built-in default. Vital keeps user presets in ~/Music/Vital/User/Presets \
37             on macOS"
38                .to_owned(),
39        )
40    }
41
42    fn candidates(&self) -> Vec<PathBuf> {
43        Vec::new()
44    }
45
46    fn builtin(&self) -> Option<Vec<u8>> {
47        serde_json::to_vec(&builtin_root()).ok()
48    }
49
50    fn check(&self, model: &dyn PresetModel) -> Vec<TemplateCheck> {
51        let Some(model) = model.as_any().downcast_ref::<VitalModel>() else {
52            return vec![TemplateCheck {
53                severity: Severity::Error,
54                text: "the template is not a Vital preset".to_owned(),
55                fix: "Pass a .vital file".to_owned(),
56            }];
57        };
58        check_root(model.root())
59    }
60}
61
62fn error(text: impl Into<String>, fix: impl Into<String>) -> TemplateCheck {
63    TemplateCheck {
64        severity: Severity::Error,
65        text: text.into(),
66        fix: fix.into(),
67    }
68}
69
70fn warning(text: impl Into<String>, fix: impl Into<String>) -> TemplateCheck {
71    TemplateCheck {
72        severity: Severity::Warning,
73        text: text.into(),
74        fix: fix.into(),
75    }
76}
77
78fn check_root(root: &Map<String, Value>) -> Vec<TemplateCheck> {
79    let mut checks = Vec::new();
80    let settings = root.get("settings").and_then(Value::as_object);
81    if settings
82        .and_then(|settings| settings.get("sample"))
83        .is_none()
84    {
85        checks.push(error(
86            "the template has no `settings.sample`",
87            "Vital throws on load without it; save the template again from Vital",
88        ));
89    }
90    let stamp = root.get("synth_version").and_then(Value::as_str);
91    match stamp.map(str::parse::<VitalVersion>) {
92        // The load gate compares only the major and minor (`load_save.cpp`
93        // of 1.0.6), so 1.0.8 loads in 1.0.7 and gets no warning.
94        Some(Ok(version))
95            if RETAIL_BUILD
96                .parse::<VitalVersion>()
97                .is_ok_and(|retail| !version.accepted_by(&retail)) =>
98        {
99            checks.push(warning(
100                format!("the template is stamped {version}, which the stable build {RETAIL_BUILD} refuses"),
101                format!(
102                    "The output keeps the stamp, and Vital {RETAIL_BUILD} refuses a newer major or minor; pass a template saved by the build that loads the file"
103                ),
104            ));
105        }
106        Some(Ok(version)) if version.major >= 1 => {}
107        Some(Ok(version)) => checks.push(error(
108            format!("the template was saved by Vital {version}, older than 1.0.0"),
109            "Open it in a current Vital and save it again; an old stamp makes Vital rewrite values on load",
110        )),
111        Some(Err(cause)) => checks.push(error(
112            format!("the template stamp {cause}"),
113            "Save the template again from Vital",
114        )),
115        None => checks.push(error(
116            "the template has no `synth_version` text",
117            "Save the template again from Vital",
118        )),
119    }
120    let Some(settings) = settings else {
121        return checks;
122    };
123    if settings.contains_key("sub_octave") {
124        checks.push(error(
125            "the template holds `sub_octave`, a key of builds before 0.5.0",
126            "Open it in a current Vital and save it again",
127        ));
128    }
129    if let Ok(registry) = registry() {
130        for (key, value) in settings {
131            let Some(spec) = registry.get(key) else {
132                continue;
133            };
134            match value.as_f64() {
135                None => checks.push(error(
136                    format!("control `{key}` holds {value}, not a number"),
137                    "This writer keeps only numbers in a control; save the template again from Vital",
138                )),
139                // Vital loads such a value as it is, and factory presets
140                // hold some; the edits leave it unless one names the key.
141                Some(number) if !spec.accepts_saved(number) => checks.push(warning(
142                    format!(
143                        "control `{key}` holds {number}, outside {}",
144                        spec.range_text()
145                    ),
146                    "Vital applies it unclamped; edit the control to bring it into the range",
147                )),
148                Some(_) => {}
149            }
150        }
151    }
152    let length = |key: &str| settings.get(key).and_then(Value::as_array).map(Vec::len);
153    if length("wavetables").is_none_or(|n| n < 3) {
154        checks.push(warning(
155            "the template holds fewer than 3 wavetables",
156            "Vital keeps the tables of the patch it had loaded; save a template with all three oscillators",
157        ));
158    }
159    if length("lfos").is_none_or(|n| n < 8) {
160        checks.push(warning(
161            "the template holds fewer than 8 LFO shapes",
162            "Vital keeps the shapes of the patch it had loaded; save the template again from Vital",
163        ));
164    }
165    if !settings.contains_key("modulations") {
166        checks.push(warning(
167            "the template has no `modulations` list",
168            "Vital clears every route on load; save the template again from Vital",
169        ));
170    }
171    let retail: Vec<&str> = RETAIL_ONLY_KEYS
172        .into_iter()
173        .filter(|key| settings.contains_key(*key))
174        .collect();
175    if !retail.is_empty() {
176        checks.push(warning(
177            format!("the template holds retail-only keys: {}", retail.join(", ")),
178            "Keep the stamp it came with; the output copies it, and the open engine cannot read these keys",
179        ));
180    }
181    checks
182}
183
184/// The built-in item of a structured list, used to fill a short list.
185pub(crate) fn default_item(list: &str) -> Value {
186    match list {
187        "wavetables" => saw_wavetable(),
188        "lfos" => triangle_lfo(),
189        _ => json!({"source": "", "destination": ""}),
190    }
191}
192
193/// The built-in template as a JSON tree.
194fn builtin_root() -> Value {
195    let mut settings = Map::new();
196    if let Ok(registry) = registry() {
197        for spec in registry.iter().filter(|spec| !spec.legacy) {
198            settings.insert(spec.key.clone(), json!(spec.default));
199        }
200    }
201    settings.insert("sample".to_owned(), noise_sample());
202    let empty_slot = json!({"source": "", "destination": ""});
203    settings.insert("modulations".to_owned(), Value::Array(vec![empty_slot; 64]));
204    settings.insert(
205        "wavetables".to_owned(),
206        Value::Array(vec![saw_wavetable(); 3]),
207    );
208    settings.insert("lfos".to_owned(), Value::Array(vec![triangle_lfo(); 8]));
209    json!({
210        "synth_version": DEFAULT_STAMP,
211        "preset_name": "Init",
212        "author": "sound-to-patch",
213        "comments": "",
214        "preset_style": "",
215        "macro1": "MACRO 1",
216        "macro2": "MACRO 2",
217        "macro3": "MACRO 3",
218        "macro4": "MACRO 4",
219        "settings": settings,
220    })
221}
222
223/// One rising sawtooth cycle from -1, as base64 of little-endian 32-bit
224/// floats.
225pub(crate) fn saw_frame() -> String {
226    let bytes: Vec<u8> = (0..FRAME_SAMPLES)
227        .flat_map(|i| {
228            let x = 2.0 * usize_to_f64(i) / usize_to_f64(FRAME_SAMPLES) - 1.0;
229            f64_to_f32(x).to_le_bytes()
230        })
231        .collect();
232    STANDARD.encode(bytes)
233}
234
235fn saw_wavetable() -> Value {
236    json!({
237        "groups": [{
238            "components": [{
239                "keyframes": [{"position": 0, "wave_data": saw_frame()}],
240                "type": "Wave Source",
241                // Linear keyframe interpolation and frequency-domain
242                // interpolation, the defaults of a new wave source.
243                "interpolation_style": 1,
244                "interpolation": 1,
245            }],
246        }],
247        "name": "Saw",
248        "author": "sound-to-patch",
249        "version": DEFAULT_STAMP,
250        "remove_all_dc": false,
251        "full_normalize": false,
252    })
253}
254
255/// A triangle that starts at the bottom, peaks halfway and returns. The app
256/// draws `1 - y`, so `y = 1` is the bottom.
257fn triangle_lfo() -> Value {
258    json!({
259        "num_points": 3,
260        "points": [0.0, 1.0, 0.5, 0.0, 1.0, 1.0],
261        "powers": [0.0, 0.0, 0.0],
262        "name": "Triangle",
263        "smooth": false,
264    })
265}
266
267/// White noise in ±0.9 from a fixed seed, as base64 of mono 16-bit PCM.
268pub(crate) fn noise_samples() -> String {
269    let mut state = NOISE_SEED;
270    let bytes: Vec<u8> = (0..NOISE_SAMPLES)
271        .flat_map(|_| {
272            // splitmix64
273            state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
274            let mut z = state;
275            z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
276            z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
277            z ^= z >> 31;
278            pcm16(0.9 * (2.0 * unit_interval(z) - 1.0)).to_le_bytes()
279        })
280        .collect();
281    STANDARD.encode(bytes)
282}
283
284fn noise_sample() -> Value {
285    json!({
286        "name": "White Noise",
287        "length": NOISE_SAMPLES,
288        "sample_rate": 44_100,
289        "samples": noise_samples(),
290    })
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296    use crate::preset::format::PresetFormat;
297    use crate::preset::vital::FORMAT;
298
299    fn builtin() -> Vec<u8> {
300        VitalTemplate.builtin().unwrap()
301    }
302
303    #[test]
304    fn the_builtin_reads_and_passes_its_check() {
305        let model = FORMAT.read(&builtin()).unwrap();
306        assert_eq!(VitalTemplate.check(model.as_ref()), Vec::new());
307        assert_eq!(model.unknown_keys(), Vec::<String>::new());
308    }
309
310    #[test]
311    fn the_builtin_holds_every_non_legacy_control() {
312        let root: Value = serde_json::from_slice(&builtin()).unwrap();
313        let settings = root["settings"].as_object().unwrap();
314        let registry = registry().unwrap();
315        let non_legacy = registry.iter().filter(|spec| !spec.legacy).count();
316        assert_eq!(non_legacy, 772);
317        assert_eq!(settings.len(), non_legacy + 4);
318        for spec in registry.iter() {
319            assert_eq!(
320                settings.contains_key(&spec.key),
321                !spec.legacy,
322                "{}",
323                spec.key
324            );
325        }
326    }
327
328    #[test]
329    fn embedded_blobs_have_the_sizes_vital_reads() {
330        let root: Value = serde_json::from_slice(&builtin()).unwrap();
331        for table in root["settings"]["wavetables"].as_array().unwrap() {
332            let data = table["groups"][0]["components"][0]["keyframes"][0]["wave_data"]
333                .as_str()
334                .unwrap();
335            assert_eq!(data.len(), 10_924);
336            assert_eq!(STANDARD.decode(data).unwrap().len(), 8192);
337        }
338        let samples = root["settings"]["sample"]["samples"].as_str().unwrap();
339        assert_eq!(samples.len(), 117_600);
340        let pcm = STANDARD.decode(samples).unwrap();
341        assert_eq!(pcm.len(), 88_200);
342        let peak = pcm
343            .as_chunks::<2>()
344            .0
345            .iter()
346            .map(|pair| i16::from_le_bytes(*pair).unsigned_abs())
347            .max()
348            .unwrap();
349        assert!(peak <= 29_490 && peak > 29_000, "{peak}");
350    }
351
352    #[test]
353    fn the_saw_rises_from_minus_one() {
354        let bytes = STANDARD.decode(saw_frame()).unwrap();
355        let sample = |i: usize| f32::from_le_bytes(bytes[4 * i..4 * i + 4].try_into().unwrap());
356        assert_eq!(sample(0), -1.0);
357        assert_eq!(sample(1024), 0.0);
358        assert_eq!(sample(2047), 1.0 - 2.0 / 2048.0);
359    }
360
361    #[test]
362    fn two_runs_give_identical_bytes() {
363        assert_eq!(builtin(), builtin());
364    }
365
366    #[test]
367    fn check_reports_each_problem() {
368        let checks = |text: &str| {
369            let root: Value = serde_json::from_str(text).unwrap();
370            check_root(root.as_object().unwrap())
371        };
372        let texts = |found: Vec<TemplateCheck>| -> Vec<(Severity, String)> {
373            found.into_iter().map(|c| (c.severity, c.text)).collect()
374        };
375        let full_shapes =
376            r#""modulations":[],"wavetables":[{},{},{}],"lfos":[{},{},{},{},{},{},{},{}]"#;
377        let no_sample = format!(r#"{{"synth_version":"1.0.6","settings":{{{full_shapes}}}}}"#);
378        assert_eq!(
379            texts(checks(&no_sample)),
380            [(
381                Severity::Error,
382                "the template has no `settings.sample`".to_owned()
383            )]
384        );
385        let old =
386            format!(r#"{{"synth_version":"0.9.9","settings":{{"sample":{{}},{full_shapes}}}}}"#);
387        assert_eq!(checks(&old)[0].severity, Severity::Error);
388        assert!(checks(&old)[0].text.contains("older than 1.0.0"));
389        let sub = format!(
390            r#"{{"synth_version":"1.0.6","settings":{{"sample":{{}},"sub_octave":1,{full_shapes}}}}}"#
391        );
392        assert!(checks(&sub)[0].text.contains("sub_octave"));
393        let bool_control = format!(
394            r#"{{"synth_version":"1.0.6","settings":{{"sample":{{}},"filter_1_on":true,{full_shapes}}}}}"#
395        );
396        assert!(checks(&bool_control)[0].text.contains("filter_1_on"));
397        let outside = format!(
398            r#"{{"synth_version":"1.0.6","settings":{{"sample":{{}},"filter_2_cutoff":140.0,{full_shapes}}}}}"#
399        );
400        assert_eq!(
401            checks(&outside)[0].text,
402            "control `filter_2_cutoff` holds 140, outside 8.0 to 136.0"
403        );
404        let short =
405            r#"{"synth_version":"1.0.6","settings":{"sample":{},"wavetables":[{}],"lfos":[]}}"#;
406        let found = texts(checks(short));
407        assert_eq!(found.len(), 3, "{found:?}");
408        assert!(
409            found
410                .iter()
411                .all(|(severity, _)| *severity == Severity::Warning)
412        );
413        let retail = format!(
414            r#"{{"synth_version":"1.5.5","settings":{{"sample":{{}},"custom_warps":[],{full_shapes}}}}}"#
415        );
416        assert_eq!(
417            texts(checks(&retail)),
418            [
419                (
420                    Severity::Warning,
421                    "the template is stamped 1.5.5, which the stable build 1.0.7 refuses"
422                        .to_owned()
423                ),
424                (
425                    Severity::Warning,
426                    "the template holds retail-only keys: custom_warps".to_owned()
427                )
428            ]
429        );
430        for stamp in ["1.0.7", "1.0.8", "1.0.99"] {
431            let patch = format!(
432                r#"{{"synth_version":"{stamp}","settings":{{"sample":{{}},{full_shapes}}}}}"#
433            );
434            assert_eq!(texts(checks(&patch)), [], "{stamp}");
435        }
436    }
437}