Skip to main content

sound_to_patch/preset/vital/
writers.rs

1//! Typed writers for the structured parts of a `.vital` file: modulation
2//! routes, wavetables, LFO shapes, effect switches and order, and macro
3//! labels. Each writer checks its input before it changes the model, so a
4//! failed call leaves the model as it was.
5
6use 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
17/// The 32 modulation source names.
18pub 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/// The polarity of a route whose edit names none: bipolar for LFOs, random
54/// sources, `stereo` and `pitch_wheel`, unipolar for the rest.
55#[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
68/// Samples in one wavetable frame.
69pub const FRAME_SAMPLES: usize = 2048;
70
71/// The highest keyframe position: a table has 257 frames.
72pub const MAX_POSITION: u16 = 256;
73
74/// Base64 characters of one frame: 8192 bytes with padding.
75pub const FRAME_BASE64_CHARS: usize = 10_924;
76
77/// The most points an LFO shape holds.
78pub const MAX_LFO_POINTS: usize = 100;
79
80/// One keyframe of a wavetable: a frame position and one cycle.
81#[derive(Clone, Debug, PartialEq)]
82pub struct Keyframe {
83    /// The frame index, 0 to 256.
84    pub position: u16,
85    /// One cycle of 2048 samples.
86    pub samples: Vec<f32>,
87}
88
89/// A wavetable built from one-cycle keyframes. Frames between keyframes
90/// are interpolated by the synth when it loads the table.
91#[derive(Clone, Debug, PartialEq)]
92pub struct Wavetable {
93    /// The table name.
94    pub name: String,
95    /// The table author.
96    pub author: String,
97    /// The keyframes, in any order.
98    pub keyframes: Vec<Keyframe>,
99}
100
101/// An LFO shape: points with one curve power per point.
102#[derive(Clone, Debug, PartialEq)]
103pub struct LfoShape {
104    /// The shape name.
105    pub name: String,
106    /// `(x, y)` points, x non-decreasing from 0 to 1, y the output from 0
107    /// (bottom) to 1 (top). Two points with the same x draw a vertical
108    /// step. The file stores `1 - y`: Vital 1.0.7 draws a stored 0 at the
109    /// top, and its default triangle stores its peak as 0.
110    pub points: Vec<(f64, f64)>,
111    /// One curve power per point.
112    pub powers: Vec<f64>,
113    /// Whether the synth smooths the shape.
114    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
121/// The switch control of an effect family.
122fn 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
136/// The effect list of code 0, top to bottom: the order of a new patch.
137pub 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
149/// The number of effect orders, 9!. Codes run from 0 to one less.
150pub const EFFECT_ORDERS: u32 = 362_880;
151
152/// The `effect_chain_order` code of an effect list, top to bottom.
153///
154/// Number the effects by [`EFFECT_ORDER`]. Each effect after the first
155/// adds a mixed-radix digit: the count of effects above it with a higher
156/// number, in base `position + 1`, the first digit the most significant.
157/// Measured on Vital 1.0.7: moving Reverb
158/// to the top stores 260650, Chorus to the bottom 8, and swapping the
159/// first two 181440.
160///
161/// # Errors
162///
163/// A [`FormatError`] when the list does not hold each effect once.
164pub 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/// The effect list of an `effect_chain_order` code, top to bottom, or
182/// `None` for a code of 9! or more. The inverse of [`effect_order_code`].
183#[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    // The last effect is the digit-th highest of all nine; each earlier
195    // one is the same among those left.
196    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    /// Adds a route, or updates the slot that joins the same source and
206    /// destination, so a route never fires twice. It writes the amount,
207    /// the bipolar flag and, when given, the curve power. An update keeps
208    /// the slot's power when none is given, and always keeps its stereo
209    /// and bypass flags. A new route starts them from their defaults:
210    /// power 0, stereo and bypass off.
211    ///
212    /// # Errors
213    ///
214    /// A [`FormatError`] for an unknown source (it lists the 32 names), a
215    /// destination outside the registry (it lists close names), an amount
216    /// outside -1 to 1, a power outside -10 to 10, or a full matrix (it
217    /// names the 64-slot limit).
218    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    /// Empties a slot and resets its five controls to their defaults.
281    ///
282    /// # Errors
283    ///
284    /// A [`FormatError`] for a slot outside 0 to 63.
285    pub fn clear_route(&mut self, slot: usize) -> Result<(), FormatError> {
286        self.set_route(slot, None)
287    }
288
289    /// Writes the wavetable of oscillator `index` (0 to 2), one Wave
290    /// Source component with the keyframes sorted by position. The table
291    /// asks the synth not to remove DC or normalise, so the frames stay as
292    /// written.
293    ///
294    /// # Errors
295    ///
296    /// A [`FormatError`] for an index above 2, no keyframe, a position
297    /// above 256 or used twice, a frame that is not 2048 samples, or a
298    /// sample that is not finite.
299    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    /// Reads back the Wave Source keyframes of oscillator `index`.
373    ///
374    /// # Errors
375    ///
376    /// A [`FormatError`] when the table is missing, holds another component
377    /// first, or a `wave_data` text has the wrong length or is not base64.
378    /// The length is checked before decoding.
379    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    /// Writes the shape of LFO `index` (0 to 7).
451    ///
452    /// # Errors
453    ///
454    /// A [`FormatError`] for an index above 7, fewer than 2 or more than
455    /// 100 points, a power count that differs from the point count, x that
456    /// falls or leaves 0 to 1, y outside 0 to 1, or a value
457    /// that is not finite.
458    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    /// Switches an effect on or off.
509    ///
510    /// # Errors
511    ///
512    /// A [`FormatError`] if the model has no settings.
513    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    /// Sets the order of the effect list, top to bottom, and returns its
521    /// code.
522    ///
523    /// # Errors
524    ///
525    /// A [`FormatError`] when the list does not hold each effect once.
526    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    /// Sets the label of macro `number` (1 to 4).
533    ///
534    /// # Errors
535    ///
536    /// A [`FormatError`] for a number outside 1 to 4.
537    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    /// Replaces item `index` of a structured list, first filling the list
549    /// to `len` with copies of the template's defaults if it is shorter.
550    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
576/// The effective value of a control: the file's number, or the registry
577/// default when the key is absent.
578fn 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
590/// The switch that turns off the module a control belongs to.
591fn 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
615/// Checks a model after edits and returns its warnings.
616pub(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        // An edited control must be in range. One the edits left keeps
635        // the template's value, which Vital loads as it is.
636        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        // Vital 1.0.7 draws a stored 0 at the top, so a rising ramp of
1055        // outputs is stored falling.
1056        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        // The default triangle peaks at the centre, and 1.0.7 stores it
1069        // with its peak as 0.
1070        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        // Codes Vital 1.0.7 saved for effect lists rearranged in the app.
1089        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        // An edit of a control whose module is off is not heard; the
1157        // switch itself and a module that is on give no warning.
1158        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        // An edited control out of range is an error; the same value left
1190        // from the template is a warning, since Vital loads it as it is.
1191        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        // The 32-bit form of a bound is in range.
1206        // The top of `chorus_delay_1` is -5.64386; saved as a 32-bit float
1207        // it reads -5.64385986328125, just above.
1208        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}