1use base64::Engine as _;
7use base64::engine::general_purpose::STANDARD;
8use serde_json::{Map, Value, json};
9
10use crate::preset::apply::place_route;
11use crate::preset::format::{FormatError, PresetModel, Route, RoutePolarity, Stored};
12use crate::preset::registry::ParamRegistry;
13use crate::preset::vital::codec::VitalModel;
14use crate::preset::vital::{DEFAULT_STAMP, registry};
15use crate::synth::EffectFamily;
16
17pub const ROUTE_SOURCES: [&str; 32] = [
19 "env_1",
20 "env_2",
21 "env_3",
22 "env_4",
23 "env_5",
24 "env_6",
25 "lfo_1",
26 "lfo_2",
27 "lfo_3",
28 "lfo_4",
29 "lfo_5",
30 "lfo_6",
31 "lfo_7",
32 "lfo_8",
33 "random_1",
34 "random_2",
35 "random_3",
36 "random_4",
37 "macro_control_1",
38 "macro_control_2",
39 "macro_control_3",
40 "macro_control_4",
41 "random",
42 "stereo",
43 "mod_wheel",
44 "pitch_wheel",
45 "note",
46 "note_in_octave",
47 "velocity",
48 "aftertouch",
49 "slide",
50 "lift",
51];
52
53#[must_use]
56pub fn default_polarity(source: &str) -> RoutePolarity {
57 if source.starts_with("lfo_")
58 || source.starts_with("random")
59 || source == "stereo"
60 || source == "pitch_wheel"
61 {
62 RoutePolarity::Bipolar
63 } else {
64 RoutePolarity::Unipolar
65 }
66}
67
68pub const FRAME_SAMPLES: usize = 2048;
70
71pub const MAX_POSITION: u16 = 256;
73
74pub const FRAME_BASE64_CHARS: usize = 10_924;
76
77pub const MAX_LFO_POINTS: usize = 100;
79
80#[derive(Clone, Debug, PartialEq)]
82pub struct Keyframe {
83 pub position: u16,
85 pub samples: Vec<f32>,
87}
88
89#[derive(Clone, Debug, PartialEq)]
92pub struct Wavetable {
93 pub name: String,
95 pub author: String,
97 pub keyframes: Vec<Keyframe>,
99}
100
101#[derive(Clone, Debug, PartialEq)]
103pub struct LfoShape {
104 pub name: String,
106 pub points: Vec<(f64, f64)>,
111 pub powers: Vec<f64>,
113 pub smooth: bool,
115}
116
117fn write_error(cause: impl Into<String>, fix: impl Into<String>) -> FormatError {
118 FormatError::new("cannot write the .vital part", cause, fix)
119}
120
121fn effect_switch(effect: EffectFamily) -> &'static str {
123 match effect {
124 EffectFamily::Chorus => "chorus_on",
125 EffectFamily::Compressor => "compressor_on",
126 EffectFamily::Delay => "delay_on",
127 EffectFamily::Distortion => "distortion_on",
128 EffectFamily::Eq => "eq_on",
129 EffectFamily::Filter => "filter_fx_on",
130 EffectFamily::Flanger => "flanger_on",
131 EffectFamily::Phaser => "phaser_on",
132 EffectFamily::Reverb => "reverb_on",
133 }
134}
135
136pub const EFFECT_ORDER: [EffectFamily; 9] = [
138 EffectFamily::Chorus,
139 EffectFamily::Compressor,
140 EffectFamily::Delay,
141 EffectFamily::Distortion,
142 EffectFamily::Eq,
143 EffectFamily::Filter,
144 EffectFamily::Flanger,
145 EffectFamily::Phaser,
146 EffectFamily::Reverb,
147];
148
149pub const EFFECT_ORDERS: u32 = 362_880;
151
152pub fn effect_order_code(order: &[EffectFamily; 9]) -> Result<u32, FormatError> {
165 let ids: Vec<usize> = order
166 .iter()
167 .filter_map(|effect| EFFECT_ORDER.iter().position(|have| have == effect))
168 .collect();
169 if (0..EFFECT_ORDER.len()).any(|id| !ids.contains(&id)) {
170 return Err(write_error(
171 format!("the effect order {order:?} repeats an effect"),
172 "List each of the nine effects once",
173 ));
174 }
175 let code = (1..ids.len()).fold(0, |code, i| {
176 code * (i + 1) + ids[..i].iter().filter(|&&above| above > ids[i]).count()
177 });
178 u32::try_from(code).map_err(|error| write_error(error.to_string(), "Report this"))
179}
180
181#[must_use]
184pub fn effect_order(code: u32) -> Option<[EffectFamily; 9]> {
185 if code >= EFFECT_ORDERS {
186 return None;
187 }
188 let mut rest = usize::try_from(code).ok()?;
189 let mut digits = [0; 9];
190 for (i, digit) in digits.iter_mut().enumerate().skip(1).rev() {
191 *digit = rest % (i + 1);
192 rest /= i + 1;
193 }
194 let mut left: Vec<usize> = (0..EFFECT_ORDER.len()).collect();
197 let mut order = EFFECT_ORDER;
198 for i in (0..order.len()).rev() {
199 order[i] = EFFECT_ORDER[left.remove(i - digits[i])];
200 }
201 Some(order)
202}
203
204impl VitalModel {
205 pub fn route(
219 &mut self,
220 source: &str,
221 destination: &str,
222 amount: f64,
223 polarity: Option<RoutePolarity>,
224 power: Option<f64>,
225 ) -> Result<usize, FormatError> {
226 if !ROUTE_SOURCES.contains(&source) {
227 return Err(write_error(
228 format!("{source:?} is not a modulation source"),
229 format!("Use one of: {}", ROUTE_SOURCES.join(", ")),
230 ));
231 }
232 let registry = registry().map_err(|error| write_error(error.to_string(), "Report this"))?;
233 if registry.get(destination).is_none_or(|spec| spec.legacy) {
234 return Err(write_error(
235 format!("{destination:?} is not a control of Vital"),
236 format!(
237 "Close names: {}",
238 registry.close_names(destination, 10).join(", ")
239 ),
240 ));
241 }
242 if !(-1.0..=1.0).contains(&amount) {
243 return Err(write_error(
244 format!("route amount {amount} is outside -1 to 1"),
245 "Use a fraction of the destination's range from -1 to 1",
246 ));
247 }
248 if let Some(power) = power
249 && !(-10.0..=10.0).contains(&power)
250 {
251 return Err(write_error(
252 format!("route power {power} is outside -10 to 10"),
253 "Use a curve power from -10 to 10",
254 ));
255 }
256 let mut route = Route {
257 source: source.to_owned(),
258 destination: destination.to_owned(),
259 amount,
260 polarity: polarity.unwrap_or_else(|| default_polarity(source)),
261 extras: Vec::new(),
262 };
263 let slot = self
264 .routes()
265 .iter()
266 .position(|have| {
267 have.as_ref()
268 .is_some_and(|have| have.source == source && have.destination == destination)
269 })
270 .or_else(|| self.routes().iter().position(Option::is_none));
271 if let (Some(slot), Some(power)) = (slot, power) {
272 route.extras.push((
273 format!("modulation_{}_power", slot + 1),
274 Stored::Float(power),
275 ));
276 }
277 place_route(self, route)
278 }
279
280 pub fn clear_route(&mut self, slot: usize) -> Result<(), FormatError> {
286 self.set_route(slot, None)
287 }
288
289 pub fn set_wavetable(&mut self, index: usize, table: &Wavetable) -> Result<(), FormatError> {
300 if index > 2 {
301 return Err(write_error(
302 format!("oscillator {} does not exist", index + 1),
303 "Vital has 3 oscillators; use index 0 to 2",
304 ));
305 }
306 if table.keyframes.is_empty() {
307 return Err(write_error(
308 "the wavetable has no keyframe",
309 "Add at least one frame",
310 ));
311 }
312 let mut keyframes: Vec<&Keyframe> = table.keyframes.iter().collect();
313 keyframes.sort_by_key(|keyframe| keyframe.position);
314 for (i, keyframe) in keyframes.iter().enumerate() {
315 if keyframe.position > MAX_POSITION {
316 return Err(write_error(
317 format!(
318 "keyframe position {} is above {MAX_POSITION}",
319 keyframe.position
320 ),
321 "Use frame positions from 0 to 256",
322 ));
323 }
324 if i > 0 && keyframes[i - 1].position == keyframe.position {
325 return Err(write_error(
326 format!("keyframe position {} appears twice", keyframe.position),
327 "Give each keyframe its own position",
328 ));
329 }
330 if keyframe.samples.len() != FRAME_SAMPLES {
331 return Err(write_error(
332 format!("a frame has {} samples", keyframe.samples.len()),
333 "Write one cycle of exactly 2048 samples",
334 ));
335 }
336 if let Some(bad) = keyframe.samples.iter().find(|sample| !sample.is_finite()) {
337 return Err(write_error(
338 format!("frame {} holds the sample {bad}", keyframe.position),
339 "Write finite samples only",
340 ));
341 }
342 }
343 let frames: Vec<Value> = keyframes
344 .iter()
345 .map(|keyframe| {
346 let bytes: Vec<u8> = keyframe
347 .samples
348 .iter()
349 .flat_map(|sample| sample.to_le_bytes())
350 .collect();
351 json!({"position": keyframe.position, "wave_data": STANDARD.encode(bytes)})
352 })
353 .collect();
354 let value = json!({
355 "groups": [{
356 "components": [{
357 "keyframes": frames,
358 "type": "Wave Source",
359 "interpolation_style": 1,
360 "interpolation": 1,
361 }],
362 }],
363 "name": table.name,
364 "author": table.author,
365 "version": DEFAULT_STAMP,
366 "remove_all_dc": false,
367 "full_normalize": false,
368 });
369 self.set_list_item("wavetables", 3, index, value)
370 }
371
372 pub fn wavetable(&self, index: usize) -> Result<Wavetable, FormatError> {
380 let missing = || {
381 write_error(
382 format!("wavetable {index} is missing or not a Wave Source"),
383 "Write it with set_wavetable",
384 )
385 };
386 let table = self
387 .root()
388 .get("settings")
389 .and_then(|settings| settings.get("wavetables"))
390 .and_then(|tables| tables.get(index))
391 .ok_or_else(missing)?;
392 let component = table
393 .pointer("/groups/0/components/0")
394 .filter(|component| {
395 component.get("type").and_then(Value::as_str) == Some("Wave Source")
396 })
397 .ok_or_else(missing)?;
398 let mut keyframes = Vec::new();
399 for keyframe in component
400 .get("keyframes")
401 .and_then(Value::as_array)
402 .ok_or_else(missing)?
403 {
404 let position = keyframe
405 .get("position")
406 .and_then(Value::as_u64)
407 .and_then(|position| u16::try_from(position).ok())
408 .ok_or_else(missing)?;
409 let text = keyframe
410 .get("wave_data")
411 .and_then(Value::as_str)
412 .ok_or_else(missing)?;
413 if text.len() != FRAME_BASE64_CHARS {
414 return Err(write_error(
415 format!(
416 "wave_data at position {position} has {} characters",
417 text.len()
418 ),
419 "A frame is 10924 base64 characters",
420 ));
421 }
422 let bytes = STANDARD.decode(text).map_err(|error| {
423 write_error(
424 format!("wave_data is not base64: {error}"),
425 "Write the frame again",
426 )
427 })?;
428 let samples = bytes
429 .as_chunks::<4>()
430 .0
431 .iter()
432 .map(|chunk| f32::from_le_bytes(*chunk))
433 .collect();
434 keyframes.push(Keyframe { position, samples });
435 }
436 let text = |key: &str| {
437 table
438 .get(key)
439 .and_then(Value::as_str)
440 .unwrap_or_default()
441 .to_owned()
442 };
443 Ok(Wavetable {
444 name: text("name"),
445 author: text("author"),
446 keyframes,
447 })
448 }
449
450 pub fn set_lfo_shape(&mut self, index: usize, shape: &LfoShape) -> Result<(), FormatError> {
459 if index > 7 {
460 return Err(write_error(
461 format!("LFO {} does not exist", index + 1),
462 "Vital has 8 LFOs; use index 0 to 7",
463 ));
464 }
465 let count = shape.points.len();
466 if !(2..=MAX_LFO_POINTS).contains(&count) {
467 return Err(write_error(
468 format!("the shape has {count} points"),
469 "Use 2 to 100 points",
470 ));
471 }
472 if shape.powers.len() != count {
473 return Err(write_error(
474 format!("{} powers for {count} points", shape.powers.len()),
475 "Give one power per point",
476 ));
477 }
478 for (i, &(x, y)) in shape.points.iter().enumerate() {
479 let ordered = i == 0 || shape.points[i - 1].0 <= x;
480 if !(0.0..=1.0).contains(&x) || !(0.0..=1.0).contains(&y) || !ordered {
481 return Err(write_error(
482 format!("point {} is ({x}, {y})", i + 1),
483 "Keep x non-decreasing within 0 to 1 and y within 0 to 1",
484 ));
485 }
486 }
487 if let Some(power) = shape.powers.iter().find(|power| !power.is_finite()) {
488 return Err(write_error(
489 format!("a power is {power}"),
490 "Write finite powers",
491 ));
492 }
493 let points: Vec<f64> = shape
494 .points
495 .iter()
496 .flat_map(|&(x, y)| [x, 1.0 - y])
497 .collect();
498 let value = json!({
499 "num_points": count,
500 "points": points,
501 "powers": shape.powers,
502 "name": shape.name,
503 "smooth": shape.smooth,
504 });
505 self.set_list_item("lfos", 8, index, value)
506 }
507
508 pub fn set_effect_on(&mut self, effect: EffectFamily, on: bool) -> Result<(), FormatError> {
514 self.set(
515 effect_switch(effect),
516 Stored::Float(if on { 1.0 } else { 0.0 }),
517 )
518 }
519
520 pub fn set_effect_order(&mut self, order: &[EffectFamily; 9]) -> Result<u32, FormatError> {
527 let code = effect_order_code(order)?;
528 self.set("effect_chain_order", Stored::Float(f64::from(code)))?;
529 Ok(code)
530 }
531
532 pub fn set_macro_label(&mut self, number: usize, label: &str) -> Result<(), FormatError> {
538 if !(1..=4).contains(&number) {
539 return Err(write_error(
540 format!("macro {number} does not exist"),
541 "Vital has macros 1 to 4",
542 ));
543 }
544 self.set_top_text(&format!("macro{number}"), label);
545 Ok(())
546 }
547
548 fn set_list_item(
551 &mut self,
552 list: &str,
553 len: usize,
554 index: usize,
555 value: Value,
556 ) -> Result<(), FormatError> {
557 let settings = self.settings_map_mut()?;
558 let items = settings
559 .entry(list)
560 .or_insert_with(|| Value::Array(Vec::new()));
561 let Value::Array(items) = items else {
562 return Err(write_error(
563 format!("`settings.{list}` is not a list"),
564 "Read the preset again from a valid file",
565 ));
566 };
567 let fill = super::template::default_item(list);
568 while items.len() < len {
569 items.push(fill.clone());
570 }
571 items[index] = value;
572 Ok(())
573 }
574}
575
576fn effective(settings: &Map<String, Value>, key: &str) -> f64 {
579 settings
580 .get(key)
581 .and_then(Value::as_f64)
582 .unwrap_or_else(|| {
583 registry()
584 .ok()
585 .and_then(|registry| registry.get(key))
586 .map_or(0.0, |spec| spec.default)
587 })
588}
589
590fn module_switch(key: &str) -> Option<String> {
592 let parts: Vec<&str> = key.splitn(3, '_').collect();
593 match parts.as_slice() {
594 ["osc" | "filter", id, _] => Some(format!("{}_{id}_on", parts[0])),
595 [family, ..]
596 if [
597 "chorus",
598 "compressor",
599 "delay",
600 "distortion",
601 "eq",
602 "flanger",
603 "phaser",
604 "reverb",
605 "sample",
606 ]
607 .contains(family) =>
608 {
609 Some(format!("{family}_on"))
610 }
611 _ => None,
612 }
613}
614
615pub(crate) fn scan(model: &VitalModel, edited: &[&str]) -> Result<Vec<String>, FormatError> {
617 let registry = registry().map_err(|error| write_error(error.to_string(), "Report this"))?;
618 let settings = model
619 .root()
620 .get("settings")
621 .and_then(Value::as_object)
622 .ok_or_else(|| write_error("the model has no settings", "Read the preset again"))?;
623 let mut out_of_range = Vec::new();
624 for (key, value) in settings {
625 let Some(spec) = registry.get(key) else {
626 continue;
627 };
628 let Some(number) = value.as_f64() else {
629 return Err(write_error(
630 format!("control {key} holds {value}, not a number"),
631 "Write a number; this writer keeps only numbers in a control",
632 ));
633 };
634 if edited.contains(&key.as_str()) && !spec.accepts(number) {
637 let inside = (spec.stored_min..=spec.stored_max).contains(&number);
638 return Err(if inside {
639 write_error(
640 format!("control {key} holds {number}, not a whole number"),
641 format!("Write a whole number from {}", spec.range_text()),
642 )
643 } else {
644 write_error(
645 format!(
646 "control {key} holds {number}, outside {}",
647 spec.range_text()
648 ),
649 "Vital applies it unclamped; write a value inside the range",
650 )
651 });
652 }
653 if !spec.accepts_saved(number) {
654 out_of_range.push(format!(
655 "control {key} keeps the template's {number}, outside {}; Vital applies it as it is",
656 spec.range_text()
657 ));
658 }
659 }
660 let mut warnings = out_of_range;
661 for (slot, route) in model.routes().iter().enumerate() {
662 let Some(route) = route else { continue };
663 if let Some(switch) = module_switch(&route.destination)
664 && effective(settings, &switch) == 0.0
665 {
666 warnings.push(format!(
667 "slot {}: {} belongs to a module that is off ({switch} = 0), so the route does nothing",
668 slot + 1,
669 route.destination
670 ));
671 }
672 }
673 for key in edited {
674 if let Some(switch) = module_switch(key)
675 && switch != *key
676 && effective(settings, &switch) == 0.0
677 {
678 let warning = format!(
679 "{key} is edited, but its module is off ({switch} = 0), so the edit is not heard; set {switch} to on"
680 );
681 if !warnings.contains(&warning) {
682 warnings.push(warning);
683 }
684 }
685 }
686 for (filter, targets) in [(1, [0.0, 2.0]), (2, [1.0, 2.0])] {
687 if effective(settings, &format!("filter_{filter}_on")) == 0.0 {
688 continue;
689 }
690 let fed = (1..=3).any(|osc| {
691 effective(settings, &format!("osc_{osc}_on")) != 0.0
692 && targets.contains(&effective(settings, &format!("osc_{osc}_destination")))
693 }) || (effective(settings, "sample_on") != 0.0
694 && targets.contains(&effective(settings, "sample_destination")))
695 || (filter == 2 && effective(settings, "filter_2_filter_input") != 0.0);
696 if !fed {
697 warnings.push(format!(
698 "filter {filter} is on, but no oscillator or sample routes into it, so it does nothing"
699 ));
700 }
701 }
702 Ok(warnings)
703}
704
705#[cfg(test)]
706mod tests {
707 use super::*;
708 use crate::preset::format::PresetFormat;
709 use crate::preset::template::TemplateSource;
710 use crate::preset::vital::{FORMAT, VitalTemplate};
711
712 fn template() -> VitalModel {
713 let model = FORMAT.read(&VitalTemplate.builtin().unwrap()).unwrap();
714 model.as_any().downcast_ref::<VitalModel>().unwrap().clone()
715 }
716
717 fn empty() -> VitalModel {
718 let model = FORMAT
719 .read(br#"{"synth_version":"1.0.6","settings":{"sample":{"length":1,"sample_rate":44100,"samples":"AAA="}}}"#)
720 .unwrap();
721 model.as_any().downcast_ref::<VitalModel>().unwrap().clone()
722 }
723
724 fn saw(position: u16, offset: f32) -> Keyframe {
725 Keyframe {
726 position,
727 samples: (0..2048u16)
728 .map(|i| f32::from(i) / 1024.0 - 1.0 + offset)
729 .collect(),
730 }
731 }
732
733 #[test]
734 fn a_route_sets_the_slot_and_its_five_controls() {
735 let mut model = empty();
736 let slot = model
737 .route("lfo_1", "filter_1_cutoff", 0.25, None, None)
738 .unwrap();
739 assert_eq!(slot, 0);
740 let route = model.routes()[0].clone().unwrap();
741 assert_eq!(
742 (route.source.as_str(), route.destination.as_str()),
743 ("lfo_1", "filter_1_cutoff")
744 );
745 assert_eq!(route.polarity, RoutePolarity::Bipolar);
746 for (name, value) in [
747 ("amount", 0.25),
748 ("bipolar", 1.0),
749 ("power", 0.0),
750 ("stereo", 0.0),
751 ("bypass", 0.0),
752 ] {
753 assert_eq!(
754 model.get(&format!("modulation_1_{name}")),
755 Some(Stored::Float(value)),
756 "{name}"
757 );
758 }
759 assert_eq!(
760 model
761 .route("lfo_1", "filter_1_cutoff", -0.5, None, Some(2.0))
762 .unwrap(),
763 0
764 );
765 assert_eq!(model.get("modulation_1_amount"), Some(Stored::Float(-0.5)));
766 assert_eq!(model.get("modulation_1_power"), Some(Stored::Float(2.0)));
767 }
768
769 #[test]
770 fn an_update_in_place_keeps_power_stereo_and_bypass() {
771 let mut model = empty();
772 model
773 .route("lfo_1", "filter_1_cutoff", 0.25, None, Some(2.0))
774 .unwrap();
775 model
776 .set("modulation_1_bypass", Stored::Float(1.0))
777 .unwrap();
778 model
779 .set("modulation_1_stereo", Stored::Float(1.0))
780 .unwrap();
781 assert_eq!(
782 model
783 .route("lfo_1", "filter_1_cutoff", 0.5, None, None)
784 .unwrap(),
785 0
786 );
787 for (name, value) in [
788 ("amount", 0.5),
789 ("power", 2.0),
790 ("bypass", 1.0),
791 ("stereo", 1.0),
792 ] {
793 assert_eq!(
794 model.get(&format!("modulation_1_{name}")),
795 Some(Stored::Float(value)),
796 "{name}"
797 );
798 }
799 }
800
801 #[test]
802 fn the_65th_route_names_the_limit() {
803 let mut model = empty();
804 let registry = registry().unwrap();
805 let keys: Vec<&str> = registry
806 .iter()
807 .filter(|spec| {
808 !spec.legacy
809 && spec.kind == crate::preset::registry::ParamKind::Float
810 && !spec.key.starts_with("modulation_")
811 })
812 .map(|spec| spec.key.as_str())
813 .take(65)
814 .collect();
815 assert_eq!(keys.len(), 65);
816 for key in &keys[..64] {
817 model.route("env_2", key, 0.1, None, None).unwrap();
818 }
819 let message = model
820 .route("env_2", keys[64], 0.1, None, None)
821 .unwrap_err()
822 .to_string();
823 assert!(message.contains("all 64 modulation slots"), "{message}");
824 }
825
826 #[test]
827 fn bad_routes_list_the_fix() {
828 let mut model = empty();
829 let message = model
830 .route("lfo_9", "filter_1_cutoff", 0.1, None, None)
831 .unwrap_err()
832 .to_string();
833 assert!(
834 message.contains("env_1, env_2") && message.contains("lift"),
835 "{message}"
836 );
837 let message = model
838 .route("lfo_1", "filter_1_cuttoff", 0.1, None, None)
839 .unwrap_err()
840 .to_string();
841 assert!(
842 message.contains("Close names: filter_1_cutoff"),
843 "{message}"
844 );
845 assert!(
846 model
847 .route("lfo_1", "filter_1_cutoff", 1.5, None, None)
848 .is_err()
849 );
850 assert!(
851 model
852 .route("lfo_1", "filter_1_cutoff", 0.5, None, Some(11.0))
853 .is_err()
854 );
855 }
856
857 #[test]
858 fn the_polarity_default_follows_the_source_family() {
859 for source in ["lfo_3", "random_2", "random", "stereo", "pitch_wheel"] {
860 assert_eq!(default_polarity(source), RoutePolarity::Bipolar, "{source}");
861 }
862 for source in [
863 "env_1",
864 "macro_control_2",
865 "mod_wheel",
866 "note",
867 "velocity",
868 "aftertouch",
869 "slide",
870 "lift",
871 "note_in_octave",
872 ] {
873 assert_eq!(
874 default_polarity(source),
875 RoutePolarity::Unipolar,
876 "{source}"
877 );
878 }
879 }
880
881 #[test]
882 fn a_cleared_slot_restores_the_model() {
883 let before = template();
884 let mut model = before.clone();
885 model
886 .route("env_2", "filter_1_cutoff", 0.25, None, Some(-3.0))
887 .unwrap();
888 assert_ne!(model, before);
889 model.clear_route(0).unwrap();
890 assert_eq!(model, before);
891 }
892
893 #[test]
894 fn a_wavetable_round_trips_bit_for_bit() {
895 let mut model = template();
896 let table = Wavetable {
897 name: "Fitted".into(),
898 author: "sound-to-patch".into(),
899 keyframes: vec![saw(128, 0.0), saw(0, 0.25), saw(256, -0.125)],
900 };
901 model.set_wavetable(1, &table).unwrap();
902 let written = FORMAT.write(&model).unwrap();
903 let read = FORMAT.read(&written).unwrap();
904 let read = read.as_any().downcast_ref::<VitalModel>().unwrap();
905 let back = read.wavetable(1).unwrap();
906 let mut expected = table.keyframes.clone();
907 expected.sort_by_key(|keyframe| keyframe.position);
908 assert_eq!(back.keyframes.len(), 3);
909 for (got, want) in back.keyframes.iter().zip(&expected) {
910 assert_eq!(got.position, want.position);
911 let got: Vec<u32> = got.samples.iter().map(|s| s.to_bits()).collect();
912 let want: Vec<u32> = want.samples.iter().map(|s| s.to_bits()).collect();
913 assert_eq!(got, want);
914 }
915 let data =
916 &read.root()["settings"]["wavetables"][1]["groups"][0]["components"][0]["keyframes"];
917 for keyframe in data.as_array().unwrap() {
918 assert_eq!(
919 keyframe["wave_data"].as_str().unwrap().len(),
920 FRAME_BASE64_CHARS
921 );
922 }
923 }
924
925 #[test]
926 fn bad_wavetables_fail_and_leave_the_model() {
927 let mut model = template();
928 let before = model.clone();
929 let twice = Wavetable {
930 name: "x".into(),
931 author: "x".into(),
932 keyframes: vec![saw(3, 0.0), saw(3, 0.0)],
933 };
934 assert!(
935 model
936 .set_wavetable(0, &twice)
937 .unwrap_err()
938 .to_string()
939 .contains("appears twice")
940 );
941 let mut nan = saw(0, 0.0);
942 nan.samples[7] = f32::NAN;
943 let nan = Wavetable {
944 name: "x".into(),
945 author: "x".into(),
946 keyframes: vec![nan],
947 };
948 assert!(
949 model
950 .set_wavetable(0, &nan)
951 .unwrap_err()
952 .to_string()
953 .contains("NaN")
954 );
955 let far = Wavetable {
956 name: "x".into(),
957 author: "x".into(),
958 keyframes: vec![saw(257, 0.0)],
959 };
960 assert!(model.set_wavetable(0, &far).is_err());
961 assert!(
962 model
963 .set_wavetable(
964 3,
965 &Wavetable {
966 name: "x".into(),
967 author: "x".into(),
968 keyframes: vec![saw(0, 0.0)]
969 }
970 )
971 .is_err()
972 );
973 assert_eq!(model, before);
974 }
975
976 #[test]
977 fn a_short_wave_data_is_refused_before_decoding() {
978 let mut model = template();
979 model
980 .set_wavetable(
981 0,
982 &Wavetable {
983 name: "x".into(),
984 author: "x".into(),
985 keyframes: vec![saw(0, 0.0)],
986 },
987 )
988 .unwrap();
989 let mut root = model.root().clone();
990 root["settings"]["wavetables"][0]["groups"][0]["components"][0]["keyframes"][0]["wave_data"] =
991 json!("AAAA");
992 let text = serde_json::to_vec(&root).unwrap();
993 let read = FORMAT.read(&text).unwrap();
994 let read = read.as_any().downcast_ref::<VitalModel>().unwrap();
995 assert!(
996 read.wavetable(0)
997 .unwrap_err()
998 .to_string()
999 .contains("4 characters")
1000 );
1001 }
1002
1003 #[test]
1004 fn lfo_shapes_take_up_to_100_points() {
1005 let shape = |n: usize| LfoShape {
1006 name: "Ramp".into(),
1007 points: (0..n)
1008 .map(|i| {
1009 (
1010 crate::cast::usize_to_f64(i) / crate::cast::usize_to_f64(n - 1),
1011 0.5,
1012 )
1013 })
1014 .collect(),
1015 powers: vec![0.0; n],
1016 smooth: false,
1017 };
1018 let mut model = template();
1019 model.set_lfo_shape(7, &shape(100)).unwrap();
1020 assert_eq!(
1021 model.root()["settings"]["lfos"][7]["num_points"],
1022 json!(100)
1023 );
1024 assert!(
1025 model
1026 .set_lfo_shape(0, &shape(101))
1027 .unwrap_err()
1028 .to_string()
1029 .contains("101 points")
1030 );
1031 let mut falling = shape(3);
1032 falling.points[2].0 = 0.2;
1033 assert!(model.set_lfo_shape(0, &falling).is_err());
1034 let mut step = shape(4);
1035 step.points[2] = (step.points[1].0, 1.0);
1036 model.set_lfo_shape(0, &step).unwrap();
1037 assert_eq!(model.root()["settings"]["lfos"][0]["num_points"], json!(4));
1038 let mut uneven = shape(3);
1039 uneven.powers.pop();
1040 assert!(model.set_lfo_shape(0, &uneven).is_err());
1041 assert!(model.set_lfo_shape(8, &shape(3)).is_err());
1042 }
1043
1044 #[test]
1045 fn every_effect_order_code_round_trips() {
1046 for code in 0..EFFECT_ORDERS {
1047 let order = effect_order(code).unwrap();
1048 assert_eq!(effect_order_code(&order).unwrap(), code);
1049 }
1050 }
1051
1052 #[test]
1053 fn an_lfo_shape_stores_its_output_upside_down() {
1054 let mut model = template();
1057 let ramp = LfoShape {
1058 name: "Ramp Up".into(),
1059 points: vec![(0.0, 0.0), (1.0, 1.0)],
1060 powers: vec![0.0, 0.0],
1061 smooth: false,
1062 };
1063 model.set_lfo_shape(0, &ramp).unwrap();
1064 assert_eq!(
1065 model.root()["settings"]["lfos"][0]["points"],
1066 json!([0.0, 1.0, 1.0, 0.0])
1067 );
1068 let triangle = LfoShape {
1071 name: "Triangle".into(),
1072 points: vec![(0.0, 0.0), (0.5, 1.0), (1.0, 0.0)],
1073 powers: vec![0.0; 3],
1074 smooth: false,
1075 };
1076 model.set_lfo_shape(0, &triangle).unwrap();
1077 assert_eq!(
1078 model.root()["settings"]["lfos"][0]["points"],
1079 json!([0.0, 1.0, 0.5, 0.0, 1.0, 1.0])
1080 );
1081 }
1082
1083 #[test]
1084 fn the_effect_order_codes_match_vital() {
1085 use EffectFamily::{
1086 Chorus, Compressor, Delay, Distortion, Eq, Filter, Flanger, Phaser, Reverb,
1087 };
1088 let saved = [
1090 (EFFECT_ORDER, 0),
1091 (
1092 [
1093 Reverb, Chorus, Compressor, Delay, Distortion, Eq, Filter, Flanger, Phaser,
1094 ],
1095 260_650,
1096 ),
1097 (
1098 [
1099 Compressor, Delay, Distortion, Eq, Filter, Flanger, Phaser, Reverb, Chorus,
1100 ],
1101 8,
1102 ),
1103 (
1104 [
1105 Compressor, Chorus, Delay, Distortion, Eq, Filter, Flanger, Phaser, Reverb,
1106 ],
1107 181_440,
1108 ),
1109 ];
1110 let mut model = template();
1111 for (order, code) in saved {
1112 assert_eq!(effect_order_code(&order).unwrap(), code);
1113 assert_eq!(effect_order(code), Some(order));
1114 assert_eq!(model.set_effect_order(&order).unwrap(), code);
1115 assert_eq!(
1116 model.get("effect_chain_order"),
1117 Some(Stored::Float(f64::from(code)))
1118 );
1119 }
1120 let twice = [Chorus; 9];
1121 let message = effect_order_code(&twice).unwrap_err().to_string();
1122 assert!(
1123 message.contains("each of the nine effects once"),
1124 "{message}"
1125 );
1126 assert_eq!(effect_order(EFFECT_ORDERS), None);
1127 model.set_effect_on(EffectFamily::Reverb, true).unwrap();
1128 assert_eq!(model.get("reverb_on"), Some(Stored::Float(1.0)));
1129 model.set_effect_on(EffectFamily::Filter, true).unwrap();
1130 assert_eq!(model.get("filter_fx_on"), Some(Stored::Float(1.0)));
1131 }
1132
1133 #[test]
1134 fn names_reject_an_unknown_style_and_macros_take_labels() {
1135 let mut model = template();
1136 assert!(model.set_tags(&["Wobble".to_owned()]).is_err());
1137 model.set_macro_label(2, "Wub").unwrap();
1138 assert_eq!(model.root()["macro2"], json!("Wub"));
1139 assert!(model.set_macro_label(5, "x").is_err());
1140 }
1141
1142 #[test]
1143 fn the_scan_warns_about_dead_routes_and_unfed_filters() {
1144 let mut model = template();
1145 assert_eq!(scan(&model, &[]).unwrap(), Vec::<String>::new());
1146 model
1147 .route("lfo_1", "filter_2_cutoff", 0.25, None, None)
1148 .unwrap();
1149 model
1150 .route("lfo_1", "reverb_size", 0.25, None, None)
1151 .unwrap();
1152 let warnings = scan(&model, &[]).unwrap();
1153 assert_eq!(warnings.len(), 2, "{warnings:?}");
1154 assert!(warnings[0].contains("filter_2_on = 0"));
1155 assert!(warnings[1].contains("reverb_on = 0"));
1156 let warnings = scan(
1159 &model,
1160 &[
1161 "filter_2_cutoff",
1162 "filter_2_cutoff",
1163 "filter_2_on",
1164 "osc_1_level",
1165 ],
1166 )
1167 .unwrap();
1168 assert_eq!(
1169 warnings[2..],
1170 [
1171 "filter_2_cutoff is edited, but its module is off (filter_2_on = 0), so the edit is not heard; set filter_2_on to on"
1172 ]
1173 );
1174 model.set("filter_2_on", Stored::Float(1.0)).unwrap();
1175 let warnings = scan(&model, &[]).unwrap();
1176 assert!(
1177 warnings
1178 .iter()
1179 .any(|w| w.contains("filter 2 is on, but no oscillator")),
1180 "{warnings:?}"
1181 );
1182 model.set("filter_1_on", Stored::Float(1.0)).unwrap();
1183 assert!(
1184 !scan(&model, &[])
1185 .unwrap()
1186 .iter()
1187 .any(|w| w.contains("filter 1 is on"))
1188 );
1189 model.set("filter_1_cutoff", Stored::Float(200.0)).unwrap();
1192 assert!(
1193 scan(&model, &["filter_1_cutoff"])
1194 .unwrap_err()
1195 .to_string()
1196 .contains("outside 8.0 to 136.0")
1197 );
1198 let warnings = scan(&model, &[]).unwrap();
1199 assert!(
1200 warnings
1201 .iter()
1202 .any(|w| w.contains("filter_1_cutoff keeps the template's 200")),
1203 "{warnings:?}"
1204 );
1205 let bound = registry()
1209 .unwrap()
1210 .get("chorus_delay_1")
1211 .unwrap()
1212 .stored_max;
1213 let narrow = f64::from(crate::cast::f64_to_f32(bound));
1214 assert!(narrow > bound);
1215 model.set("filter_1_cutoff", Stored::Float(72.0)).unwrap();
1216 model.set("chorus_delay_1", Stored::Float(narrow)).unwrap();
1217 assert!(
1218 !scan(&model, &[])
1219 .unwrap()
1220 .iter()
1221 .any(|w| w.contains("chorus_delay_1"))
1222 );
1223 }
1224
1225 #[test]
1226 fn every_writer_keeps_the_model_loadable() {
1227 let mut model = empty();
1228 model
1229 .route("velocity", "osc_1_level", 0.5, None, None)
1230 .unwrap();
1231 model
1232 .set_wavetable(
1233 2,
1234 &Wavetable {
1235 name: "x".into(),
1236 author: "x".into(),
1237 keyframes: vec![saw(0, 0.0)],
1238 },
1239 )
1240 .unwrap();
1241 model
1242 .set_lfo_shape(
1243 3,
1244 &LfoShape {
1245 name: "x".into(),
1246 points: vec![(0.0, 0.0), (1.0, 1.0)],
1247 powers: vec![0.0, 0.0],
1248 smooth: true,
1249 },
1250 )
1251 .unwrap();
1252 let written = FORMAT.write(&model).unwrap();
1253 let read = FORMAT.read(&written).unwrap();
1254 let root: Value = serde_json::from_slice(&written).unwrap();
1255 assert_eq!(root["settings"]["wavetables"].as_array().unwrap().len(), 3);
1256 assert_eq!(root["settings"]["lfos"].as_array().unwrap().len(), 8);
1257 assert_eq!(
1258 root["settings"]["modulations"].as_array().unwrap().len(),
1259 64
1260 );
1261 let read = read.as_any().downcast_ref::<VitalModel>().unwrap();
1262 scan(read, &[]).unwrap();
1263 }
1264}