synthahol_phase_plant/effect/
convolver.rs

1//! [Convolver](https://kilohearts.com/products/convolver) is an effect that
2//! applies an impulse response (IR) to audio.
3//!
4//! Convolver was added to Phase Plant in version 1.8.18.
5//!
6//! | Phase Plant Version | Effect Version |
7//! |---------------------|----------------|
8//! | 1.8.18              | 1007           |
9//! | 2.0.0               | 1016           |
10//! | 2.0.12              | 1017           |
11//! | 2.0.16 to 2.1.0     | 1018           |
12
13use std::any::{type_name, Any};
14use std::io;
15use std::io::{Error, ErrorKind, Read, Seek, Write};
16
17use crate::effect::EffectVersion;
18use uom::num::Zero;
19use uom::si::f32::{Ratio, Time};
20use uom::si::ratio::percent;
21
22use crate::Snapin;
23
24use super::super::io::*;
25use super::{Effect, EffectMode};
26
27#[derive(Clone, Debug, PartialEq)]
28pub struct Convolver {
29    pub ir_name: Option<String>,
30    pub ir_path: Vec<String>,
31
32    /// Percentage of the IR length
33    pub start: Ratio,
34    pub end: Ratio,
35    pub fade_in: Ratio,
36    pub fade_out: Ratio,
37    pub stretch: Ratio,
38
39    /// Pre-delay
40    pub delay: Time,
41
42    pub sync: bool,
43    pub tone: Ratio,
44    pub feedback: Ratio,
45    pub mix: Ratio,
46    pub reverse: bool,
47}
48
49impl Convolver {
50    pub fn default_version() -> EffectVersion {
51        1018
52    }
53}
54
55impl Default for Convolver {
56    fn default() -> Self {
57        Self {
58            ir_name: None,
59            ir_path: Vec::new(),
60            start: Ratio::zero(),
61            end: Ratio::new::<percent>(100.0),
62            fade_in: Ratio::zero(),
63            fade_out: Ratio::zero(),
64            stretch: Ratio::new::<percent>(100.0),
65            delay: Time::zero(),
66            sync: false,
67            tone: Ratio::zero(),
68            feedback: Ratio::zero(),
69            mix: Ratio::new::<percent>(100.0),
70            reverse: false,
71        }
72    }
73}
74
75impl dyn Effect {
76    #[must_use]
77    pub fn as_convolver(&self) -> Option<&Convolver> {
78        self.downcast_ref::<Convolver>()
79    }
80}
81
82impl Effect for Convolver {
83    fn box_eq(&self, other: &dyn Any) -> bool {
84        other
85            .downcast_ref::<Self>()
86            .map_or(false, |other| self == other)
87    }
88
89    fn mode(&self) -> EffectMode {
90        EffectMode::Convolver
91    }
92}
93
94impl EffectRead for Convolver {
95    fn read<R: Read + Seek>(
96        reader: &mut PhasePlantReader<R>,
97        effect_version: u32,
98    ) -> io::Result<EffectReadReturn> {
99        if effect_version < 1007 {
100            return Err(Error::new(
101                ErrorKind::InvalidData,
102                format!(
103                    "Version {effect_version} of {} is not supported",
104                    type_name::<Self>()
105                ),
106            ));
107        }
108
109        // FIXME
110        let group_id = None;
111
112        let mix = reader.read_ratio()?;
113        let stretch = reader.read_ratio()?;
114        let enabled = reader.read_bool32()?;
115        let minimized = reader.read_bool32()?;
116
117        reader.expect_u32(0, "convolver_unknown_5")?;
118        reader.expect_u32(0, "convolver_unknown_6")?;
119
120        let end = reader.read_ratio()?;
121        let fade_out = reader.read_ratio()?;
122        let feedback = reader.read_ratio()?;
123        let tone = reader.read_ratio()?;
124        let start = reader.read_ratio()?;
125        let fade_in = reader.read_ratio()?;
126        let delay = reader.read_seconds()?;
127
128        reader.expect_u32(0, "convolver_unknown_7")?;
129        reader.expect_u32(4, "convolver_unknown_8")?;
130
131        let sync = reader.read_bool32()?;
132        let reverse = reader.read_bool32()?;
133
134        reader.expect_u32(0, "convolver_unknown_9")?;
135
136        let ir_name = reader.read_string_and_length()?;
137        let mut ir_path = Vec::new();
138        let path_header = reader.read_block_header()?;
139        if path_header.is_used() {
140            ir_path = match reader.read_string_and_length()? {
141                None => Vec::new(),
142                Some(path) => vec![path],
143            };
144            let header_mode_id = path_header.mode_id().expect("convolver IR header mode");
145            match header_mode_id {
146                3 => reader.expect_u8(0, "convolver_block_unknown_1")?,
147                2 => (),
148                _ => {
149                    return Err(Error::new(
150                        ErrorKind::InvalidData,
151                        format!("Unsupported convolver IR block mode {header_mode_id}"),
152                    ))
153                }
154            }
155        }
156
157        let effect = Convolver {
158            ir_name,
159            ir_path,
160            start,
161            end,
162            fade_in,
163            fade_out,
164            stretch,
165            delay,
166            sync,
167            tone,
168            feedback,
169            mix,
170            reverse,
171        };
172        Ok(EffectReadReturn::new(
173            Box::new(effect),
174            enabled,
175            minimized,
176            group_id,
177        ))
178    }
179}
180
181impl EffectWrite for Convolver {
182    fn write<W: Write + Seek>(
183        &self,
184        _writer: &mut PhasePlantWriter<W>,
185        _snapin: &Snapin,
186    ) -> io::Result<()> {
187        todo!()
188    }
189}
190
191#[cfg(test)]
192mod test {
193    use approx::assert_relative_eq;
194    use uom::si::f32::{Ratio, Time};
195    use uom::si::ratio::percent;
196    use uom::si::time::millisecond;
197
198    use crate::effect::Filter;
199    use crate::test::read_effect_preset;
200
201    use super::*;
202
203    #[test]
204    fn art_museum() {
205        let preset =
206            read_effect_preset("convolver", "convolver-art_museum-2.0.12.phaseplant").unwrap();
207        let snapin = &preset.lanes[0].snapins[0];
208        assert!(snapin.enabled);
209        assert!(!snapin.minimized);
210        assert!(!snapin.preset_edited);
211        assert!(snapin.preset_name.is_empty());
212        assert!(snapin.preset_path.is_empty());
213        let effect = snapin.effect.as_convolver().unwrap();
214        assert_eq!(effect.ir_name, Some("Art Museum".to_owned()));
215        assert_eq!(
216            effect.ir_path,
217            vec!["factory/Impulse Responses/Spaces Real/Art Museum.flac"]
218        );
219        assert_eq!(effect.start, Ratio::zero());
220        assert_eq!(effect.end, Ratio::new::<percent>(100.0));
221        assert_eq!(effect.fade_in, Ratio::zero());
222        assert_eq!(effect.fade_out, Ratio::zero());
223        assert_eq!(effect.stretch, Ratio::new::<percent>(100.0));
224        assert_eq!(effect.delay, Time::zero());
225        assert!(!effect.sync);
226        assert_eq!(effect.tone.get::<percent>(), 0.0);
227        assert_eq!(effect.feedback.get::<percent>(), 0.0);
228        assert_eq!(effect.mix.get::<percent>(), 100.0);
229        assert!(!effect.reverse);
230    }
231
232    #[test]
233    fn default() {
234        let effect = Convolver::default();
235        assert_eq!(effect.start, Ratio::zero());
236        assert_eq!(effect.end, Ratio::new::<percent>(100.0));
237        assert_eq!(effect.fade_in, Ratio::zero());
238        assert_eq!(effect.fade_out, Ratio::zero());
239        assert_eq!(effect.stretch, Ratio::new::<percent>(100.0));
240        assert_eq!(effect.delay, Time::zero());
241        assert!(!effect.sync);
242        assert_eq!(effect.tone.get::<percent>(), 0.0);
243        assert_eq!(effect.feedback.get::<percent>(), 0.0);
244        assert_eq!(effect.mix.get::<percent>(), 100.0);
245        assert!(!effect.reverse);
246    }
247
248    #[test]
249    fn disabled() {
250        let preset =
251            read_effect_preset("convolver", "convolver-disabled-2.0.16.phaseplant").unwrap();
252        let snapin = &preset.lanes[0].snapins[0];
253        assert!(!snapin.enabled);
254        assert!(!snapin.minimized);
255    }
256
257    #[test]
258    fn eq() {
259        let effect = Convolver::default();
260        assert_eq!(effect, effect);
261        assert_eq!(effect, Convolver::default());
262        assert!(!effect.box_eq(&Filter::default()));
263    }
264
265    #[test]
266    fn init() {
267        for file in &[
268            "convolver-2.0.12.phaseplant",
269            "convolver-2.0.16.phaseplant",
270            "convolver-2.1.0.phaseplant",
271        ] {
272            let preset = read_effect_preset("convolver", file).unwrap();
273            let snapin = &preset.lanes[0].snapins[0];
274            assert!(snapin.enabled);
275            assert!(!snapin.minimized);
276            assert_eq!(snapin.preset_name, "".to_string());
277            assert_eq!(snapin.preset_path, Vec::<String>::new());
278            let effect = snapin.effect.as_convolver().unwrap();
279            assert_eq!(effect, &Default::default());
280        }
281    }
282
283    #[test]
284    fn minimized() {
285        let preset =
286            read_effect_preset("convolver", "convolver-minimized-2.0.16.phaseplant").unwrap();
287        let snapin = &preset.lanes[0].snapins[0];
288        assert!(snapin.enabled);
289        assert!(snapin.minimized);
290    }
291
292    #[test]
293    fn parts() {
294        let preset =
295            read_effect_preset("convolver", "convolver-delay50-tone25-2.0.12.phaseplant").unwrap();
296        let snapin = &preset.lanes[0].snapins[0];
297        assert!(snapin.enabled);
298        assert!(!snapin.minimized);
299        let effect = snapin.effect.as_convolver().unwrap();
300        assert_relative_eq!(effect.delay.get::<millisecond>(), 50.0);
301        assert_eq!(effect.tone.get::<percent>(), 25.0);
302        assert!(!effect.sync);
303
304        let preset = read_effect_preset(
305            "convolver",
306            "convolver-fade_in25-stretch45-fade_out75-2.0.12.phaseplant",
307        )
308        .unwrap();
309        let snapin = &preset.lanes[0].snapins[0];
310        let effect = snapin.effect.as_convolver().unwrap();
311        assert_relative_eq!(effect.fade_in.get::<percent>(), 25.0, epsilon = 0.01);
312        assert_relative_eq!(effect.fade_out.get::<percent>(), 75.0, epsilon = 0.01);
313        assert_eq!(effect.stretch.get::<percent>(), 45.0);
314
315        let preset =
316            read_effect_preset("convolver", "convolver-feedback25-mix50-2.0.12.phaseplant")
317                .unwrap();
318        let snapin = &preset.lanes[0].snapins[0];
319        let effect = snapin.effect.as_convolver().unwrap();
320        assert_eq!(effect.feedback.get::<percent>(), 25.0);
321        assert_eq!(effect.mix.get::<percent>(), 50.0);
322
323        let preset = read_effect_preset(
324            "convolver",
325            "convolver-feedback75-delay25-reverse-2.0.16.phaseplant",
326        )
327        .unwrap();
328        let snapin = &preset.lanes[0].snapins[0];
329        let effect = snapin.effect.as_convolver().unwrap();
330        assert!(effect.reverse);
331        assert_relative_eq!(effect.feedback.get::<percent>(), 75.4, epsilon = 0.1);
332        assert_relative_eq!(effect.delay.get::<millisecond>(), 25.0, epsilon = 0.1);
333
334        let preset =
335            read_effect_preset("convolver", "convolver-start5-end80-2.0.12.phaseplant").unwrap();
336        let snapin = &preset.lanes[0].snapins[0];
337        let effect = snapin.effect.as_convolver().unwrap();
338        assert_relative_eq!(effect.start.get::<percent>(), 5.0, epsilon = 0.001);
339        assert_relative_eq!(effect.end.get::<percent>(), 80.0, epsilon = 0.001);
340    }
341
342    #[test]
343    fn reverse_reverb() {
344        let preset =
345            read_effect_preset("convolver", "convolver-reverse_reverb-2.1.0.phaseplant").unwrap();
346        let snapin = &preset.lanes[0].snapins[0];
347        assert!(snapin.enabled);
348        assert!(!snapin.minimized);
349        assert!(!snapin.preset_edited);
350        assert_eq!(snapin.preset_name, "Reverse Reverb");
351        assert_eq!(snapin.preset_path, vec!["factory", "Reverse Reverb.ksco"]);
352
353        let effect = snapin.effect.as_convolver().unwrap();
354        assert_eq!(effect.ir_name, Some("Modern Church".to_string()));
355        assert_eq!(
356            effect.ir_path,
357            vec!["factory/Impulse Responses/Spaces Real/Modern Church.flac"]
358        );
359        assert_relative_eq!(effect.start.get::<percent>(), 50.0, epsilon = 0.0001);
360        assert_relative_eq!(effect.end.get::<percent>(), 100.0, epsilon = 0.0001);
361        assert_relative_eq!(effect.fade_in.get::<percent>(), 10.0, epsilon = 0.0001);
362        assert_relative_eq!(effect.fade_out.get::<percent>(), 10.0, epsilon = 0.0001);
363        assert_relative_eq!(effect.stretch.get::<percent>(), 100.0, epsilon = 0.0001);
364        assert_eq!(effect.delay, Time::zero());
365        assert!(!effect.sync);
366        assert_eq!(effect.tone.get::<percent>(), 0.0);
367        assert_eq!(effect.feedback.get::<percent>(), 0.0);
368        assert_eq!(effect.mix.get::<percent>(), 100.0);
369        assert!(effect.reverse);
370    }
371
372    #[test]
373    fn sync() {
374        let preset = read_effect_preset("convolver", "convolver-sync-2.0.16.phaseplant").unwrap();
375        let snapin = &preset.lanes[0].snapins[0];
376        let effect = snapin.effect.as_convolver().unwrap();
377        assert!(effect.sync);
378    }
379}