Skip to main content

sound_to_patch/
synth.rs

1//! The target synth: the [`Synth`] enum, the shared `--synth` argument and
2//! the structure each synth offers, [`SynthCapabilities`].
3//!
4//! Two synths are equal targets, Vital and Serum 2. Only the synths in
5//! [`Synth::READY`] have an adapter. A command resolves its target with
6//! [`SynthArg::resolve`]: the flag, the extension of a preset path on the
7//! command line, or Vital when neither names one.
8//!
9//! ```
10//! use std::path::Path;
11//! use sound_to_patch::synth::{Synth, SynthArg};
12//!
13//! let arg = SynthArg::default();
14//! assert_eq!(arg.resolve(None)?, Synth::Vital);
15//! assert_eq!(arg.resolve(Some(Path::new("bass.vital")))?, Synth::Vital);
16//! assert_eq!("serum2".parse::<Synth>()?, Synth::Serum2);
17//! # Ok::<(), sound_to_patch::synth::SynthError>(())
18//! ```
19
20use std::path::Path;
21
22#[doc(inline)]
23pub use sound_to_patch_core::synth::{Synth, SynthError};
24
25use crate::music::pitch::OctaveLabel;
26use crate::preset::registry::Provenance;
27use crate::units::{Bipolar, Percent, Seconds, Semitones};
28
29/// The structure of `synth`: oscillators, filters, modulation sources,
30/// effects and voicing rules.
31///
32/// # Errors
33///
34/// [`SynthError::NotBuilt`] for a synth outside [`Synth::READY`],
35/// because its values are not settled yet.
36pub fn capabilities(synth: Synth) -> Result<&'static SynthCapabilities, SynthError> {
37    match synth.ready()? {
38        Synth::Vital => Ok(&crate::preset::vital::CAPABILITIES),
39        Synth::Serum2 => Err(SynthError::NotBuilt(synth)),
40    }
41}
42
43/// The shared `--synth` option. A command flattens it when it needs a
44/// target synth.
45#[derive(Clone, Debug, Default, PartialEq, Eq, clap::Args)]
46pub struct SynthArg {
47    /// The target synth [default: the extension of the preset file, else
48    /// vital]
49    #[arg(long, value_enum, value_name = "SYNTH")]
50    pub synth: Option<Synth>,
51}
52
53impl SynthArg {
54    /// Resolves the target synth from the flag and an optional preset path
55    /// on the command line.
56    ///
57    /// 1. The flag and no hint: the flag.
58    /// 2. The flag and a hint with a known extension: both must agree,
59    ///    unless the extension has no adapter (see [`SynthError::FileNotReady`]).
60    /// 3. A hint with a known extension and no flag: the extension.
61    /// 4. Neither: Vital.
62    ///
63    /// The result must also be in [`Synth::READY`].
64    ///
65    /// # Errors
66    ///
67    /// [`SynthError::FileNotReady`] when the extension names a synth
68    /// without an adapter, with or without the flag, [`SynthError::Mismatch`]
69    /// when the flag and an extension with an adapter disagree, and
70    /// [`SynthError::NotBuilt`] for a synth without an adapter.
71    pub fn resolve(&self, hint: Option<&Path>) -> Result<Synth, SynthError> {
72        let from_hint = hint.and_then(|path| {
73            path.extension()
74                .and_then(|ext| ext.to_str())
75                .and_then(Synth::from_extension)
76                .map(|synth| (path, synth))
77        });
78        let synth = match (self.synth, from_hint) {
79            // Suggesting another --synth would only end in a mismatch.
80            (_, Some((path, extension))) if !Synth::READY.contains(&extension) => {
81                return Err(SynthError::FileNotReady {
82                    path: path.display().to_string(),
83                    synth: extension,
84                });
85            }
86            (Some(flag), Some((path, extension))) if flag != extension => {
87                return Err(SynthError::Mismatch {
88                    flag,
89                    path: path.display().to_string(),
90                    extension,
91                });
92            }
93            (Some(flag), _) => flag,
94            (None, Some((_, extension))) => extension,
95            (None, None) => Synth::Vital,
96        };
97        synth.ready()
98    }
99}
100
101/// What a synth offers in structure: the sources, filters, modulation,
102/// effects and voicing rules that a mapper chooses from.
103///
104/// Parameter laws and ranges live in the registry, not here. No estimator
105/// reads these values; only mappers do.
106#[derive(Clone, Debug, PartialEq)]
107#[non_exhaustive]
108pub struct SynthCapabilities {
109    /// The oscillator sources.
110    pub oscillators: OscillatorLayout,
111    /// Filters in the voice path.
112    pub voice_filters: u8,
113    /// Whether a filter sits in the effect chain.
114    pub fx_filter: bool,
115    /// Envelopes.
116    pub envelopes: u8,
117    /// Shaped LFOs.
118    pub lfos: u8,
119    /// Random LFOs.
120    pub random_lfos: u8,
121    /// Macro controls.
122    pub macros: u8,
123    /// Slots of the modulation matrix.
124    pub mod_slots: u16,
125    /// The smallest and the largest unison voice count.
126    pub unison_voices: (u8, u8),
127    /// The largest polyphony.
128    pub polyphony_max: u8,
129    /// The effects, by neutral family name.
130    pub effects: &'static [EffectFamily],
131    /// Whether the order of the effects can change.
132    pub effect_order_free: bool,
133    /// Whether one effect family can appear twice.
134    pub effect_repeats: bool,
135    /// The ways an LFO rate can be set.
136    pub lfo_sync: &'static [LfoSync],
137    /// The portamento time span and how the time scales with the interval.
138    pub glide: GlideRule,
139    /// How velocity reaches the level.
140    pub velocity: VelocityRule,
141    /// The pitch-bend range of a new patch.
142    pub bend_range_default: Semitones,
143    /// The octave label the synth's own note display gives MIDI note 60;
144    /// none until its source or a probe settles it.
145    pub middle_c: Option<OctaveLabel>,
146}
147
148impl SynthCapabilities {
149    /// Checks the rules every synth must meet.
150    ///
151    /// # Errors
152    ///
153    /// Returns the first broken rule as text.
154    pub fn check(&self) -> Result<(), String> {
155        if self.voice_filters < 1 {
156            return Err("voice_filters must be at least 1".into());
157        }
158        if self.mod_slots < 1 {
159            return Err("mod_slots must be at least 1".into());
160        }
161        if self.unison_voices.0 > self.unison_voices.1 {
162            return Err(format!(
163                "unison_voices minimum {} exceeds maximum {}",
164                self.unison_voices.0, self.unison_voices.1
165            ));
166        }
167        if self.glide.min >= self.glide.max {
168            return Err(format!(
169                "glide minimum {} is not below maximum {}",
170                self.glide.min, self.glide.max
171            ));
172        }
173        if self.effects.is_empty() {
174            return Err("effects must not be empty".into());
175        }
176        if !self.effect_repeats {
177            for (index, family) in self.effects.iter().enumerate() {
178                if self.effects[..index].contains(family) {
179                    return Err(format!(
180                        "effect {family:?} appears twice, but effect_repeats is false"
181                    ));
182                }
183            }
184        }
185        Ok(())
186    }
187}
188
189/// The oscillator sources of a synth.
190#[derive(Clone, Copy, Debug, PartialEq, Eq)]
191pub struct OscillatorLayout {
192    /// Wavetable oscillators.
193    pub wavetable: u8,
194    /// Whether a sample oscillator exists.
195    pub sample: bool,
196    /// Whether a separate sub oscillator exists.
197    pub sub: bool,
198    /// Whether a separate noise oscillator exists.
199    pub noise: bool,
200}
201
202/// An effect family by its neutral name. A report never uses a synth's own
203/// menu text.
204#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
205#[non_exhaustive]
206pub enum EffectFamily {
207    /// Chorus.
208    Chorus,
209    /// Dynamic range compressor.
210    Compressor,
211    /// Delay.
212    Delay,
213    /// Distortion: clipping, folding, bit and rate reduction.
214    Distortion,
215    /// Equalizer.
216    Eq,
217    /// A filter in the effect chain.
218    Filter,
219    /// Flanger.
220    Flanger,
221    /// Phaser.
222    Phaser,
223    /// Reverb.
224    Reverb,
225}
226
227/// A way to set an LFO rate.
228#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
229#[non_exhaustive]
230pub enum LfoSync {
231    /// A free rate in hertz.
232    Free,
233    /// A note division of the tempo.
234    Tempo,
235    /// A dotted note division.
236    Dotted,
237    /// A triplet note division.
238    Triplets,
239    /// A rate that follows the played note.
240    KeyTrack,
241}
242
243/// The portamento time span.
244#[derive(Clone, Copy, Debug, PartialEq)]
245pub struct GlideRule {
246    /// The shortest glide time.
247    pub min: Seconds,
248    /// The longest glide time.
249    pub max: Seconds,
250    /// Whether the time is per note or per octave of interval.
251    pub scaling: GlideScaling,
252}
253
254/// How the glide time relates to the interval.
255#[derive(Clone, Copy, Debug, PartialEq, Eq)]
256pub enum GlideScaling {
257    /// The time is the same for any interval.
258    PerNote,
259    /// The time is per octave of interval.
260    PerOctave,
261    /// Not settled by facts or a probe.
262    Unknown,
263}
264
265/// How velocity reaches the audio level.
266#[derive(Clone, Copy, Debug, PartialEq)]
267pub struct VelocityRule {
268    /// The velocity-to-level amount of a new patch.
269    pub default_track: Percent<Bipolar>,
270    /// The power applied to envelope times velocity on the way to the
271    /// audio gain.
272    pub amp_path_power: u8,
273    /// Where the rule comes from. A reading of the source stays interim
274    /// until a render confirms it.
275    pub provenance: Provenance,
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281
282    #[test]
283    fn resolve_follows_the_four_rules() {
284        let none = SynthArg::default();
285        let vital = SynthArg {
286            synth: Some(Synth::Vital),
287        };
288        let serum = SynthArg {
289            synth: Some(Synth::Serum2),
290        };
291        // 1. Flag, no hint.
292        assert_eq!(vital.resolve(None), Ok(Synth::Vital));
293        // 2. Flag and a known extension must agree.
294        assert_eq!(vital.resolve(Some(Path::new("a.VITAL"))), Ok(Synth::Vital));
295        let mismatch = serum
296            .resolve(Some(Path::new("a.vital")))
297            .unwrap_err()
298            .to_string();
299        assert!(
300            mismatch.contains("serum2") && mismatch.contains("vital"),
301            "{mismatch}"
302        );
303        // 3. The extension picks the synth. A file of a synth without an
304        // adapter says that only Vital files work, with or without a flag.
305        for (arg, name) in [
306            (&none, "lead.SerumPreset"),
307            (&none, "lead.serumpreset"),
308            (&serum, "lead.SerumPreset"),
309            (&vital, "lead.SerumPreset"),
310        ] {
311            let error = arg.resolve(Some(Path::new(name))).unwrap_err();
312            assert_eq!(
313                error,
314                SynthError::FileNotReady {
315                    path: name.to_owned(),
316                    synth: Synth::Serum2
317                }
318            );
319            let text = error.to_string();
320            assert!(text.contains("Serum 2 files cannot be read yet"), "{text}");
321            assert!(
322                text.contains("Only Vital files (.vital) work now"),
323                "{text}"
324            );
325            assert!(!text.contains("--synth"), "{text}");
326        }
327        // 4. Neither: Vital, also with an unknown extension.
328        assert_eq!(none.resolve(None), Ok(Synth::Vital));
329        assert_eq!(none.resolve(Some(Path::new("notes.txt"))), Ok(Synth::Vital));
330        assert_eq!(
331            serum.resolve(None),
332            Err(SynthError::NotBuilt(Synth::Serum2))
333        );
334    }
335
336    /// Breaks one rule of a capabilities value.
337    type Break = fn(&mut SynthCapabilities);
338
339    /// A test-only value that meets every rule.
340    fn sample() -> SynthCapabilities {
341        SynthCapabilities {
342            oscillators: OscillatorLayout {
343                wavetable: 2,
344                sample: false,
345                sub: true,
346                noise: true,
347            },
348            voice_filters: 1,
349            fx_filter: false,
350            envelopes: 4,
351            lfos: 4,
352            random_lfos: 0,
353            macros: 8,
354            mod_slots: 16,
355            unison_voices: (1, 8),
356            polyphony_max: 16,
357            effects: &[EffectFamily::Delay, EffectFamily::Reverb],
358            effect_order_free: true,
359            effect_repeats: false,
360            lfo_sync: &[LfoSync::Free, LfoSync::Tempo],
361            glide: GlideRule {
362                min: Seconds::new(0.0).unwrap(),
363                max: Seconds::new(10.0).unwrap(),
364                scaling: GlideScaling::PerNote,
365            },
366            velocity: VelocityRule {
367                default_track: Percent::new(100.0).unwrap(),
368                amp_path_power: 1,
369                provenance: Provenance::Interim,
370            },
371            bend_range_default: Semitones::new(2.0).unwrap(),
372            middle_c: Some(OctaveLabel::C3),
373        }
374    }
375
376    #[test]
377    fn each_broken_capability_rule_is_rejected() {
378        sample().check().unwrap();
379        let broken: [(&str, Break); 6] = [
380            ("voice_filters", |c| c.voice_filters = 0),
381            ("mod_slots", |c| c.mod_slots = 0),
382            ("unison_voices", |c| c.unison_voices = (9, 8)),
383            ("glide", |c| c.glide.max = c.glide.min),
384            ("effects must not be empty", |c| c.effects = &[]),
385            ("appears twice", |c| {
386                c.effects = &[EffectFamily::Delay, EffectFamily::Delay];
387            }),
388        ];
389        for (needle, breaks) in broken {
390            let mut value = sample();
391            breaks(&mut value);
392            let message = value.check().unwrap_err();
393            assert!(message.contains(needle), "{needle}: {message}");
394        }
395        let mut repeats = sample();
396        repeats.effects = &[EffectFamily::Delay, EffectFamily::Delay];
397        repeats.effect_repeats = true;
398        repeats.check().unwrap();
399    }
400
401    #[test]
402    fn capabilities_exist_only_for_ready_synths() {
403        for synth in Synth::READY {
404            capabilities(synth).unwrap().check().unwrap();
405        }
406        assert_eq!(
407            capabilities(Synth::Serum2),
408            Err(SynthError::NotBuilt(Synth::Serum2))
409        );
410    }
411}