1use 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
29pub 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#[derive(Clone, Debug, Default, PartialEq, Eq, clap::Args)]
46pub struct SynthArg {
47 #[arg(long, value_enum, value_name = "SYNTH")]
50 pub synth: Option<Synth>,
51}
52
53impl SynthArg {
54 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 (_, 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#[derive(Clone, Debug, PartialEq)]
107#[non_exhaustive]
108pub struct SynthCapabilities {
109 pub oscillators: OscillatorLayout,
111 pub voice_filters: u8,
113 pub fx_filter: bool,
115 pub envelopes: u8,
117 pub lfos: u8,
119 pub random_lfos: u8,
121 pub macros: u8,
123 pub mod_slots: u16,
125 pub unison_voices: (u8, u8),
127 pub polyphony_max: u8,
129 pub effects: &'static [EffectFamily],
131 pub effect_order_free: bool,
133 pub effect_repeats: bool,
135 pub lfo_sync: &'static [LfoSync],
137 pub glide: GlideRule,
139 pub velocity: VelocityRule,
141 pub bend_range_default: Semitones,
143 pub middle_c: Option<OctaveLabel>,
146}
147
148impl SynthCapabilities {
149 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
191pub struct OscillatorLayout {
192 pub wavetable: u8,
194 pub sample: bool,
196 pub sub: bool,
198 pub noise: bool,
200}
201
202#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
205#[non_exhaustive]
206pub enum EffectFamily {
207 Chorus,
209 Compressor,
211 Delay,
213 Distortion,
215 Eq,
217 Filter,
219 Flanger,
221 Phaser,
223 Reverb,
225}
226
227#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
229#[non_exhaustive]
230pub enum LfoSync {
231 Free,
233 Tempo,
235 Dotted,
237 Triplets,
239 KeyTrack,
241}
242
243#[derive(Clone, Copy, Debug, PartialEq)]
245pub struct GlideRule {
246 pub min: Seconds,
248 pub max: Seconds,
250 pub scaling: GlideScaling,
252}
253
254#[derive(Clone, Copy, Debug, PartialEq, Eq)]
256pub enum GlideScaling {
257 PerNote,
259 PerOctave,
261 Unknown,
263}
264
265#[derive(Clone, Copy, Debug, PartialEq)]
267pub struct VelocityRule {
268 pub default_track: Percent<Bipolar>,
270 pub amp_path_power: u8,
273 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 assert_eq!(vital.resolve(None), Ok(Synth::Vital));
293 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 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 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 type Break = fn(&mut SynthCapabilities);
338
339 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}