1use 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
29const 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
45const STRUCTURED: [&str; 4] = ["sample", "modulations", "wavetables", "lfos"];
47
48pub(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
61pub(crate) const SLOTS: usize = 64;
63
64const SLOT_CONTROLS: [&str; 5] = ["amount", "bipolar", "power", "stereo", "bypass"];
66
67#[derive(Debug)]
75pub struct VitalFormat {
76 max_bytes: usize,
77}
78
79impl VitalFormat {
80 #[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
105const MAX_DEPTH: usize = 128;
107
108fn json_error(error: &serde_json::Error) -> FormatError {
109 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
126fn 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 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 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
226fn 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 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
315fn 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#[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
339fn 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
409fn 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
429struct 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
462struct 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#[derive(Clone, Debug, PartialEq)]
502pub struct VitalModel {
503 root: Map<String, Value>,
504}
505
506impl VitalModel {
507 #[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
564fn 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
601fn 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 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 let current = self.routes().get(slot).cloned().flatten();
704 let in_place = matches!((¤t, &route), (Some(have), Some(new))
705 if have.source == new.source && have.destination == new.destination);
706 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 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
871fn 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 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
903fn 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 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 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 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 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 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}