Skip to main content

sound_to_patch/preset/
vital.rs

1//! The Vital adapter: the `.vital` JSON format, the parameter registry, the
2//! built-in template, the capabilities and the laws that span controls.
3//!
4//! Facts come from the Vital 1.0.6 open source (GPL-3.0), restated in our
5//! own words and layout; no code or comment of it is copied. The retail app
6//! is newer than that source, so a fact carries the build it was read from.
7
8use std::fmt;
9use std::str::FromStr;
10
11use crate::preset::mapper::{
12    MapContext, Mapped, MotionFxGroups, PitchEnvelopeGroups, SynthMapper, TimbreGroups,
13};
14use crate::preset::registry::{Provenance, Registry};
15use crate::synth::{
16    EffectFamily, GlideRule, GlideScaling, LfoSync, OscillatorLayout, Synth, SynthCapabilities,
17    SynthError, VelocityRule,
18};
19use crate::units::{Percent, Seconds, Semitones};
20
21mod catalogue;
22mod codec;
23pub mod laws;
24mod registry;
25mod template;
26mod writers;
27
28pub(crate) use catalogue::placement;
29pub use codec::{VitalFormat, VitalModel};
30pub use registry::{EngineScale, OPEN_ENGINE};
31pub use template::VitalTemplate;
32pub use writers::{
33    EFFECT_ORDER, EFFECT_ORDERS, FRAME_BASE64_CHARS, FRAME_SAMPLES, Keyframe, LfoShape,
34    MAX_LFO_POINTS, MAX_POSITION, ROUTE_SOURCES, Wavetable, default_polarity, effect_order,
35    effect_order_code,
36};
37
38/// The `synth_version` stamp of files built from the built-in template:
39/// the version of the open source.
40///
41/// Stable retail 1.0.7 loads it, since the gate compares only major and
42/// minor. Builds from 1.5 on are unchecked.
43pub const DEFAULT_STAMP: &str = "1.0.6";
44
45/// The stable retail build that written files are checked against: the
46/// load check of #181 ran on it. Its saved default state holds every
47/// created control at the registry default.
48pub const RETAIL_BUILD: &str = "1.0.7";
49
50/// Keys that retail builds from 1.5 on write and the open source cannot
51/// read. A rewrite keeps them.
52pub const RETAIL_ONLY_KEYS: [&str; 5] = [
53    "custom_warps",
54    "random_values",
55    "osc_1_spectral_morph_phase",
56    "osc_2_spectral_morph_phase",
57    "osc_3_spectral_morph_phase",
58];
59
60/// The largest file the reader accepts: 64 MiB.
61///
62/// Three wavetables of 257 keyframes at 10924 base64 characters each come
63/// to 8,422,404 bytes. An embedded sample adds more, with no size the
64/// source states. No saved file has been measured yet. The largest file
65/// the generated fixtures hold is `routes.vital`, 184,701 bytes. The cap
66/// leaves about seven times the wavetable maximum for a sample. A file of
67/// that size made of short numbers can take about 2.4 GB to parse; see
68/// [`VitalFormat`].
69pub const MAX_PRESET_BYTES: usize = 64 << 20;
70
71/// The Vital format with the default size cap.
72pub static FORMAT: VitalFormat = VitalFormat::new(MAX_PRESET_BYTES);
73
74/// The structure of Vital, from the 1.0.6 source. The portamento span is
75/// 2^-10 s to 2^4 s; the bend range and the velocity amount are the
76/// registry defaults of `pitch_bend_range` and `velocity_track`.
77pub static CAPABILITIES: SynthCapabilities = SynthCapabilities {
78    oscillators: OscillatorLayout {
79        wavetable: 3,
80        sample: true,
81        sub: false,
82        noise: false,
83    },
84    voice_filters: 2,
85    fx_filter: true,
86    envelopes: 6,
87    lfos: 8,
88    random_lfos: 4,
89    macros: 4,
90    mod_slots: 64,
91    unison_voices: (1, 16),
92    polyphony_max: 32,
93    effects: &[
94        EffectFamily::Chorus,
95        EffectFamily::Compressor,
96        EffectFamily::Delay,
97        EffectFamily::Distortion,
98        EffectFamily::Eq,
99        EffectFamily::Filter,
100        EffectFamily::Flanger,
101        EffectFamily::Phaser,
102        EffectFamily::Reverb,
103    ],
104    effect_order_free: true,
105    effect_repeats: false,
106    lfo_sync: &[
107        LfoSync::Free,
108        LfoSync::Tempo,
109        LfoSync::Dotted,
110        LfoSync::Triplets,
111        LfoSync::KeyTrack,
112    ],
113    glide: GlideRule {
114        min: Seconds::from_const(0.000_976_562_5),
115        max: Seconds::from_const(16.0),
116        scaling: GlideScaling::Unknown,
117    },
118    velocity: VelocityRule {
119        default_track: Percent::from_const(0.0),
120        amp_path_power: 2,
121        provenance: Provenance::Measured,
122    },
123    bend_range_default: Semitones::from_const(2.0),
124    // The GPL source draws no note names, so only a probe of the app can
125    // settle the label.
126    middle_c: None,
127};
128
129/// The Vital mapper. Each group maps to no edit until its Vital task writes
130/// the controls: pitch and envelope through
131/// [`pitch_envelope::map`](crate::preset::mapper::pitch_envelope::map).
132pub static MAPPER: VitalMapper = VitalMapper;
133
134/// Maps layer groups to Vital edits.
135#[derive(Clone, Copy, Debug, Default)]
136pub struct VitalMapper;
137
138impl SynthMapper for VitalMapper {
139    fn synth(&self) -> Synth {
140        Synth::Vital
141    }
142
143    fn pitch_envelope(&self, _input: &PitchEnvelopeGroups, _ctx: &MapContext<'_>) -> Mapped {
144        Mapped::default()
145    }
146
147    fn timbre(&self, _input: &TimbreGroups, _ctx: &MapContext<'_>) -> Mapped {
148        Mapped::default()
149    }
150
151    fn motion_fx(&self, _input: &MotionFxGroups, _ctx: &MapContext<'_>) -> Mapped {
152        Mapped::default()
153    }
154}
155
156/// The registry of Vital.
157pub(crate) fn registry() -> Result<&'static Registry, SynthError> {
158    registry::vital_registry().map(|vital| &vital.registry)
159}
160
161/// The engine scale of a Vital key, which modulation adds before.
162///
163/// # Errors
164///
165/// [`SynthError::BrokenData`] if the built-in registry does not load.
166pub fn engine_scale(key: &str) -> Result<Option<EngineScale>, SynthError> {
167    registry::vital_registry().map(|vital| vital.scales.get(key).copied())
168}
169
170/// A Vital build stamp, `major.minor.patch`.
171#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
172pub struct VitalVersion {
173    /// Major number.
174    pub major: u32,
175    /// Minor number.
176    pub minor: u32,
177    /// Patch number.
178    pub patch: u32,
179}
180
181impl VitalVersion {
182    /// Whether an app of build `app` loads a file with this stamp: the
183    /// app refuses a file whose major and minor are newer than its own,
184    /// and ignores the patch.
185    #[must_use]
186    pub fn accepted_by(&self, app: &Self) -> bool {
187        (self.major, self.minor) <= (app.major, app.minor)
188    }
189}
190
191impl FromStr for VitalVersion {
192    type Err = String;
193
194    fn from_str(text: &str) -> Result<Self, String> {
195        let parts: Vec<&str> = text.split('.').collect();
196        let number = |part: &str| {
197            if part.is_empty() || !part.bytes().all(|byte| byte.is_ascii_digit()) {
198                return None;
199            }
200            part.parse::<u32>().ok()
201        };
202        match parts.as_slice() {
203            [major, minor, patch] => match (number(major), number(minor), number(patch)) {
204                (Some(major), Some(minor), Some(patch)) => Ok(Self {
205                    major,
206                    minor,
207                    patch,
208                }),
209                _ => Err(format!("{text:?} is not three whole numbers such as 1.5.5")),
210            },
211            _ => Err(format!("{text:?} is not three whole numbers such as 1.5.5")),
212        }
213    }
214}
215
216impl fmt::Display for VitalVersion {
217    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225    use crate::preset::registry::{ParamRegistry, Physical};
226
227    fn version(text: &str) -> VitalVersion {
228        text.parse().unwrap()
229    }
230
231    #[test]
232    fn the_version_gate_ignores_the_patch() {
233        assert!(version("1.5.5").accepted_by(&version("1.5.1")));
234        assert!(!version("1.6.0").accepted_by(&version("1.5.5")));
235        assert!(!version("1.5.5").accepted_by(&version("1.0.6")));
236        assert!(version("1.0.9").accepted_by(&version("1.0.6")));
237        assert!(version("1.0.6").accepted_by(&version("1.5.5")));
238    }
239
240    #[test]
241    fn version_text_needs_three_numbers() {
242        for text in ["1.5", "a.b.c", "1.5.5.1", "", "1..5", "1.5.-1", "1.5.+1"] {
243            assert!(text.parse::<VitalVersion>().is_err(), "{text}");
244        }
245        assert_eq!(version("1.5.5").to_string(), "1.5.5");
246        assert!(version("1.10.0") > version("1.9.9"));
247    }
248
249    #[test]
250    fn capabilities_hold_and_match_the_registry() {
251        CAPABILITIES.check().unwrap();
252        let registry = registry().unwrap();
253        let default = |key: &str| {
254            registry
255                .get(key)
256                .unwrap()
257                .physical(registry.get(key).unwrap().default)
258                .unwrap()
259        };
260        assert_eq!(
261            default("pitch_bend_range"),
262            Physical::Semitones(CAPABILITIES.bend_range_default)
263        );
264        assert_eq!(
265            default("velocity_track"),
266            Physical::PercentBipolar(CAPABILITIES.velocity.default_track)
267        );
268        let glide = registry.get("portamento_time").unwrap();
269        assert_eq!(
270            glide.physical(glide.stored_min).unwrap(),
271            Physical::Seconds(CAPABILITIES.glide.min)
272        );
273        assert_eq!(
274            glide.physical(glide.stored_max).unwrap(),
275            Physical::Seconds(CAPABILITIES.glide.max)
276        );
277        assert_eq!(
278            registry.get("polyphony").unwrap().stored_max,
279            f64::from(CAPABILITIES.polyphony_max)
280        );
281        let unison = registry.get("osc_1_unison_voices").unwrap();
282        assert_eq!(
283            (unison.stored_min, unison.stored_max),
284            (
285                f64::from(CAPABILITIES.unison_voices.0),
286                f64::from(CAPABILITIES.unison_voices.1)
287            )
288        );
289    }
290
291    #[test]
292    fn const_unit_values_pass_their_checks() {
293        assert_eq!(
294            Seconds::new(0.000_976_562_5).unwrap(),
295            CAPABILITIES.glide.min
296        );
297        assert_eq!(Seconds::new(16.0).unwrap(), CAPABILITIES.glide.max);
298        assert_eq!(
299            Percent::new(0.0).unwrap(),
300            CAPABILITIES.velocity.default_track
301        );
302        assert_eq!(
303            Semitones::new(2.0).unwrap(),
304            CAPABILITIES.bend_range_default
305        );
306    }
307
308    #[test]
309    fn engine_scale_names_the_transform() {
310        assert_eq!(
311            engine_scale("env_1_attack").unwrap(),
312            Some(EngineScale::Quartic)
313        );
314        assert_eq!(engine_scale("nope").unwrap(), None);
315    }
316}