1use 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
31const OVERSAMPLING: &str = "oversampling";
33const MAX_STARTS: usize = 64;
35const TUNING: &str = "tuning";
37const TEMPO: &str = "beats_per_minute";
40pub const TEMPO_PATH: &str = "settings.beats_per_minute";
42const SHOWN_CHARS: usize = 40;
44
45pub 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 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
103fn 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
117fn 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#[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#[derive(Clone, Debug, PartialEq)]
201pub struct Mismatch {
202 pub path: String,
204 pub sent: Value,
207 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
226fn 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
235pub 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#[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
328fn 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 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 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 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 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 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 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}