1use 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
23const NOISE_SAMPLES: usize = 44_100;
25
26const NOISE_SEED: u64 = 0x005E_ED0F_0000_5A7C;
27
28#[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 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 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
184pub(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
193fn 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
223pub(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 "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
255fn 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
267pub(crate) fn noise_samples() -> String {
269 let mut state = NOISE_SEED;
270 let bytes: Vec<u8> = (0..NOISE_SAMPLES)
271 .flat_map(|_| {
272 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}