synthahol_phase_plant/
lib.rs

1//! [Phase Plant](https://kilohearts.com/products/phase_plant) is a virtual
2//! synth by Kilohearts. It stores presets in a proprietary binary format.
3//!
4//! Phase Plant presets can be combined into a bank using the
5//! [`kibank`](https://crates.io/crates/kibank) application and library.
6
7use std::fmt::{Display, Formatter};
8use std::io::{Error, ErrorKind};
9
10use strum_macros::FromRepr;
11use uom::num::Zero;
12use uom::si::f32::{Frequency, Ratio, Time};
13use uom::si::ratio::percent;
14
15pub use decibels::*;
16pub use envelope::*;
17pub use io::*;
18pub use macro_control::*;
19pub use metadata::*;
20pub use point::*;
21pub use snapin::*;
22pub use unison::*;
23pub use version::*;
24
25use crate::effect::Effect;
26use crate::generator::{Generator, GeneratorId};
27use crate::modulation::Modulation;
28use crate::modulator::{Modulator, ModulatorContainer};
29
30mod decibels;
31pub mod effect;
32mod envelope;
33pub mod generator;
34mod io;
35mod macro_control;
36mod metadata;
37pub mod modulation;
38pub mod modulator;
39mod point;
40mod snapin;
41mod text;
42mod unison;
43mod version;
44
45/// Number of generators. Unused generators in the file are ignored.
46const GENERATORS_MAX: GeneratorId = 32;
47
48/// Upper limit on the size of the JSON metadata. The length is stored as a u32 so it
49/// could be use as a denial of service if there was no other limit.
50const METADATA_LENGTH_MAX: usize = 64 * 1024;
51
52/// Number of modulator blocks. Unused modulator blocks in the file are ignored.
53const MODULATORS_MAX: usize = 32;
54
55/// Each modulator is allocated 100 bytes plus a plus a header.
56const MODULATOR_BLOCK_SIZE: usize = 100;
57
58/// How many parts are allowed when specifying a path.
59const PATH_COMPONENT_COUNT_MAX: usize = 100; // TODO: Operating system limit?
60
61/// Length of a note.
62///
63/// See also: [`PatternResolution`](effect::PatternResolution)
64#[derive(Clone, Copy, Debug, Eq, FromRepr, PartialEq)]
65#[repr(u32)]
66pub enum NoteValue {
67    // The discriminants correspond to the file format.
68    Quarter,
69    QuarterTriplet,
70    Eighth,
71    EightTriplet,
72    Sixteenth,
73    SixteenthTriplet,
74    ThirtySecond,
75    ThirtySecondTriplet,
76    SixtyFourth,
77}
78
79impl NoteValue {
80    pub(crate) fn from_id(id: u32) -> Result<Self, Error> {
81        Self::from_repr(id)
82            .ok_or_else(|| Error::new(ErrorKind::InvalidData, format!("Unknown note value {id}")))
83    }
84}
85
86impl Display for NoteValue {
87    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
88        use NoteValue::*;
89        let msg = match self {
90            Quarter => "1/4",
91            QuarterTriplet => "1/4T",
92            Eighth => "1/8",
93            EightTriplet => "1/8T",
94            Sixteenth => "1/16",
95            SixteenthTriplet => "1/16T",
96            ThirtySecond => "1/32",
97            ThirtySecondTriplet => "1/32T",
98            SixtyFourth => "1/64",
99        };
100        f.write_str(msg)
101    }
102}
103
104/// A rate defines the speed of an operation. It can be determined by frequency
105/// or based on the song tempo.
106#[derive(Clone, Debug, PartialEq)]
107pub struct Rate {
108    pub frequency: Frequency,
109    pub numerator: u32,
110    pub denominator: NoteValue,
111
112    /// If not set the rate time is used otherwise the time signature is used,
113    /// made up of the rate numerator and denominator is used.
114    pub sync: bool,
115}
116
117#[derive(Copy, Clone, Debug, Default, Eq, FromRepr, PartialEq)]
118#[repr(u32)]
119pub enum LaneDestination {
120    // The discriminants correspond to the file format. They are not the same
121    // as the destination used by `OutputGenerator`.
122    Lane2 = 2,
123    Lane3 = 0,
124    #[default]
125    Master = 1,
126    Lane1 = 3,
127    // FIXME: Value is guessed, it's not in the data for the lanes, will find it in the noise generator
128    Sideband = 5,
129}
130
131impl LaneDestination {
132    pub(crate) fn from_id(id: u32) -> Result<Self, Error> {
133        Self::from_repr(id).ok_or_else(|| {
134            Error::new(
135                ErrorKind::InvalidData,
136                format!("Unknown lane destination {id}"),
137            )
138        })
139    }
140}
141
142impl Display for LaneDestination {
143    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
144        use LaneDestination::*;
145        let msg = match self {
146            Lane2 => "Lane 2",
147            Lane3 => "Lane 3",
148            Master => "Master",
149            Lane1 => "Lane 1",
150            Sideband => "Sideband",
151        };
152        f.write_str(msg)
153    }
154}
155
156pub type LaneId = u8;
157
158#[derive(Debug, PartialEq)]
159pub struct Lane {
160    pub enabled: bool,
161
162    /// There is no restriction on the number of snapins.
163    pub snapins: Vec<Snapin>,
164
165    pub destination: LaneDestination,
166
167    /// How many lanes from left to right are poly.
168    pub poly_count: u8,
169
170    pub mute: bool,
171    pub solo: bool,
172    pub gain: Decibels,
173    pub mix: Ratio,
174}
175
176impl Lane {
177    pub const COUNT: usize = 3;
178
179    /// Find the first snapin that has an effect with the given type.
180    pub fn find_effect<T: Effect>(&self) -> Option<(&Snapin, &T)> {
181        // Returns the effect so it's already the right type
182        self.snapins
183            .iter()
184            .find(|snapin| snapin.effect.downcast_ref::<T>().is_some())
185            .map(|snapin| (snapin, snapin.effect.downcast_ref::<T>().unwrap()))
186    }
187}
188
189impl Default for Lane {
190    fn default() -> Self {
191        Self {
192            enabled: true,
193            snapins: Vec::new(),
194            destination: LaneDestination::Master,
195            poly_count: 0,
196            mute: false,
197            solo: false,
198            gain: Decibels::from_linear(1.0),
199            mix: Ratio::new::<percent>(100.0),
200        }
201    }
202}
203
204#[derive(Debug, PartialEq)]
205pub struct Preset {
206    pub format_version: Version<u32>,
207    pub generators: Vec<Box<dyn Generator>>,
208
209    pub mod_wheel_value: Ratio,
210
211    #[doc(alias = "portamento")]
212    pub glide_enabled: bool,
213    pub glide_time: f32,
214
215    #[doc(alias = "glide_auto")]
216    pub glide_legato: bool,
217
218    pub lanes: Vec<Lane>,
219
220    // TODO: Switch to an array because the number of controls is known.
221    pub macro_controls: Vec<MacroControl>,
222
223    /// Linear
224    pub master_gain: f32,
225
226    pub master_pitch: f32,
227    pub metadata: Metadata,
228    pub modulations: Vec<Modulation>,
229    pub modulator_containers: Vec<ModulatorContainer>,
230    pub polyphony: u32,
231
232    /// When enabled LFO restarts for each new voice, disabled all voices share a global LFO.
233    pub retrigger_enabled: bool,
234    pub unison: Unison,
235}
236
237impl Default for Preset {
238    fn default() -> Self {
239        Self {
240            format_version: WRITE_SAME_AS.format_version(),
241            mod_wheel_value: Ratio::zero(),
242            glide_enabled: false,
243            glide_legato: false,
244            glide_time: 0.0,
245            generators: Vec::new(),
246            lanes: vec![
247                Lane {
248                    destination: LaneDestination::Lane2,
249                    ..Default::default()
250                },
251                Lane {
252                    destination: LaneDestination::Lane3,
253                    ..Default::default()
254                },
255                Lane {
256                    destination: LaneDestination::Master,
257                    ..Default::default()
258                },
259            ],
260            macro_controls: (1..=MacroControl::COUNT)
261                .map(|n| MacroControl::new(format!("Macro {}", n)))
262                .collect(),
263            master_gain: Decibels::ZERO.linear(),
264            master_pitch: 0.0,
265            metadata: Default::default(),
266            modulations: Vec::new(),
267            modulator_containers: Vec::new(),
268            polyphony: 8,
269            retrigger_enabled: true,
270            unison: Default::default(),
271        }
272    }
273}
274
275#[cfg(test)]
276pub(crate) mod test {
277    use std::fs::File;
278    use std::io;
279    use std::io::{Cursor, Read, Seek, SeekFrom, Write};
280    use std::path::Path;
281
282    use crate::tests::test_data_path;
283    use crate::*;
284
285    pub(crate) fn load_preset(components: &[&str]) -> io::Result<Preset> {
286        let mut path = test_data_path(&[]);
287        if !path.exists() {
288            panic!("Phase Plant test data path does not exist: {path:?}");
289        }
290
291        for component in components {
292            path = path.join(component);
293        }
294        Preset::read_file(&path)
295    }
296
297    /// If set a file will be created if reading the preset back fails. Useful
298    /// for examining the preset that could not be reloaded.
299    #[allow(dead_code)]
300    const RELOAD_CREATES_FILE_ON_READ_ERROR: bool = true;
301
302    /// If set a file will be created if writing a preset fails. Useful for
303    /// examining the preset that could not be reloaded.
304    #[allow(dead_code)]
305    const RELOAD_CREATES_FILE_ON_WRITE_ERROR: bool = true;
306
307    /// Return a version of the preset that has been gone through the writing
308    /// process. Some basic assertions will be made comparing the before and
309    /// after presets. Individual tests must still check for the correct
310    /// outcome.
311    #[must_use]
312    pub(crate) fn read_preset(dir_name: &str, file_name: &str) -> Preset {
313        load_preset(&[dir_name, file_name]).expect("preset")
314        // TODO: Disabled until write completed.
315        // return rewrite_preset(&preset, file_name)
316    }
317
318    pub(crate) fn read_effect_preset(effect_name: &str, file_name: &str) -> io::Result<Preset> {
319        let preset = load_preset(&["effects", effect_name, file_name])?;
320        // TODO: Disabled until write completed.
321        // return rewrite_preset(&preset, file_name)
322        Ok(preset)
323    }
324
325    pub(crate) fn read_generator_preset(
326        generator_name: &str,
327        file_name: &str,
328    ) -> io::Result<Preset> {
329        let preset = load_preset(&["generators", generator_name, file_name])?;
330        // TODO: Disabled until write completed.
331        // return rewrite_preset(&preset, file_name)
332        Ok(preset)
333    }
334
335    pub(crate) fn read_modulator_preset(
336        modulator_name: &str,
337        file_name: &str,
338    ) -> io::Result<Preset> {
339        let preset = load_preset(&["modulators", modulator_name, file_name])?;
340        // TODO: Disabled until write completed.
341        // return rewrite_preset(&preset, file_name)
342        Ok(preset)
343    }
344
345    fn _rewrite_preset(preset: &Preset, file_name: &str) -> Preset {
346        let mut write_cursor = Cursor::new(Vec::with_capacity(16 * 1024));
347        match preset.write(&mut write_cursor) {
348            Ok(_) => {
349                let name_str = Path::new(file_name)
350                    .file_stem()
351                    .map(|s| s.to_string_lossy().to_string());
352
353                // Temporarily write out the preset.
354                #[cfg(disabled)]
355                {
356                    write_cursor.seek(SeekFrom::Start(0)).unwrap();
357                    let filename = format!(
358                        "test-{}-{}.phaseplant",
359                        name_str.clone().unwrap_or_default(),
360                        uuid::Uuid::new_v4()
361                    );
362                    let path = std::env::temp_dir().join(&filename);
363                    let mut file = File::create(&path).expect("Create file");
364                    let mut out = Vec::with_capacity(write_cursor.position() as usize);
365                    write_cursor.seek(SeekFrom::Start(0)).unwrap();
366                    write_cursor.read_to_end(&mut out).unwrap();
367                    file.write_all(&out).unwrap();
368                    println!("Test preset written to {}", path.to_string_lossy());
369                }
370
371                write_cursor.seek(SeekFrom::Start(0)).unwrap();
372
373                #[cfg(disabled)]
374                {
375                    let mut file = File::create("/tmp/reload.phaseplant").expect("Create file");
376                    let mut out = Vec::with_capacity(write_cursor.position() as usize);
377                    write_cursor.seek(SeekFrom::Start(0)).unwrap();
378                    write_cursor.read_to_end(&mut out).unwrap();
379                    file.write_all(&out).unwrap();
380                    panic!("Debug file written to {file:?}");
381                }
382
383                match Preset::read(&mut write_cursor, name_str) {
384                    Ok(written) => {
385                        // The entire presets can't be compared because of floating point equality.
386
387                        // The name entire metadata cannot be compared because Phase Plant doesn't
388                        // persist the preset name, it uses the filename.
389                        assert_eq!(preset.metadata.description, written.metadata.description);
390                        assert_eq!(preset.metadata.author, written.metadata.author);
391                        assert_eq!(preset.metadata.category, written.metadata.category);
392
393                        assert_eq!(preset.macro_controls, written.macro_controls);
394
395                        assert_eq!(preset.polyphony, written.polyphony);
396                        assert_eq!(preset.unison, written.unison);
397
398                        assert_eq!(
399                            preset.generators.len(),
400                            written.generators.len(),
401                            "number of generators"
402                        );
403                        assert_eq!(preset.lanes.len(), written.lanes.len(), "number of lanes");
404                        assert_eq!(
405                            preset.macro_controls.len(),
406                            written.macro_controls.len(),
407                            "number of macro controls"
408                        );
409                        assert_eq!(
410                            preset.modulator_containers.len(),
411                            written.modulator_containers.len(),
412                            "number of modulators"
413                        );
414
415                        written
416                    }
417                    Err(error) => {
418                        if RELOAD_CREATES_FILE_ON_READ_ERROR {
419                            let filename =
420                                format!("reload-read-error-{}.phaseplant", uuid::Uuid::new_v4());
421                            let path = std::env::temp_dir().join(filename);
422                            let mut file = File::create(&path).expect("Create file");
423
424                            let mut out = Vec::with_capacity(write_cursor.position() as usize);
425                            write_cursor.seek(SeekFrom::Start(0)).unwrap();
426                            write_cursor.read_to_end(&mut out).unwrap();
427                            file.write_all(&out).unwrap();
428                            panic!(
429                                "{:?} - debug file written to {}",
430                                error,
431                                path.to_string_lossy()
432                            );
433                        }
434                        panic!("{:?}", error);
435                    }
436                }
437            }
438            Err(error) => {
439                if RELOAD_CREATES_FILE_ON_WRITE_ERROR {
440                    let filename =
441                        format!("reload-write-error-{}.phaseplant", uuid::Uuid::new_v4());
442                    let path = std::env::temp_dir().join(filename);
443                    let mut file = File::create(&path).expect("Create file");
444
445                    let mut out = Vec::with_capacity(write_cursor.position() as usize);
446                    write_cursor.seek(SeekFrom::Start(0)).unwrap();
447                    write_cursor.read_to_end(&mut out).unwrap();
448                    file.write_all(&out).unwrap();
449                    panic!(
450                        "{:?} - debug file written to {}",
451                        error,
452                        path.to_string_lossy()
453                    );
454                }
455                panic!("{:?}", error);
456            }
457        }
458    }
459
460    #[test]
461    fn default() {
462        let preset = Preset::default();
463        assert_eq!(preset.format_version.major, 6);
464
465        // FIXME: USE FOR INIT PRESET
466        let metadata = &preset.metadata;
467        assert!(metadata.author.is_none());
468        assert!(metadata.description.is_none());
469        assert!(metadata.name.is_none());
470
471        assert!(!preset.glide_enabled);
472        assert!(!preset.glide_legato);
473        assert_eq!(preset.glide_time, 0.0);
474
475        assert_eq!(preset.lanes.len(), 3);
476        preset.lanes.iter().for_each(|lane| {
477            assert!(lane.enabled);
478            assert_eq!(lane.poly_count, 0);
479            assert!(!lane.mute);
480            assert_eq!(lane.gain.linear(), 1.0);
481            assert_eq!(lane.mix.get::<percent>(), 100.0);
482        });
483        assert_eq!(preset.lanes[0].destination, LaneDestination::Lane2);
484        assert_eq!(preset.lanes[1].destination, LaneDestination::Lane3);
485        assert_eq!(preset.lanes[2].destination, LaneDestination::Master);
486
487        assert_eq!(preset.macro_controls.len(), 8);
488        assert_eq!(preset.macro_controls[0].name, "Macro 1");
489        assert_eq!(preset.macro_controls[1].name, "Macro 2");
490        assert_eq!(preset.macro_controls[2].name, "Macro 3");
491        assert_eq!(preset.macro_controls[3].name, "Macro 4");
492        assert_eq!(preset.macro_controls[4].name, "Macro 5");
493        assert_eq!(preset.macro_controls[5].name, "Macro 6");
494        assert_eq!(preset.macro_controls[6].name, "Macro 7");
495        assert_eq!(preset.macro_controls[7].name, "Macro 8");
496
497        assert_eq!(preset.master_gain, Decibels::ZERO.linear());
498        assert_eq!(preset.metadata.author, None);
499        assert_eq!(preset.metadata.category, None);
500        assert_eq!(preset.metadata.description, None);
501        assert_eq!(preset.metadata.name, None);
502        assert_eq!(preset.polyphony, 8);
503        assert!(preset.retrigger_enabled);
504
505        assert!(preset.lanes[0].snapins.is_empty());
506
507        let unison = &preset.unison;
508        assert!(!unison.enabled);
509        assert_eq!(unison.voices, 4);
510        assert_eq!(unison.mode, UnisonMode::Smooth);
511        assert_eq!(unison.detune_cents, 25.0);
512        assert_eq!(unison.spread.get::<percent>(), 0.0);
513        assert_eq!(unison.blend.get::<percent>(), 100.0);
514        assert_eq!(unison.bias.get::<percent>(), 0.0);
515    }
516}
517
518#[cfg(test)]
519mod tests {
520    use std::path::PathBuf;
521
522    pub(crate) fn test_data_path(components: &[&str]) -> PathBuf {
523        let mut parts = vec!["tests"];
524        parts.extend_from_slice(components);
525        parts.iter().collect::<PathBuf>()
526    }
527}