Skip to main content

proteus_lib/dsp/effects/convolution_reverb/
spec.rs

1//! Impulse response specification and play-settings parsing helpers.
2
3use crate::container::play_settings::{
4    ConvolutionReverbSettings, EffectSettings, PlaySettingsFile,
5};
6
7/// Location of an impulse response used for convolution reverb.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum ImpulseResponseSpec {
10    Attachment(String),
11    FilePath(String),
12}
13
14/// Parse a convolution reverb impulse response spec from play settings.
15pub(crate) fn parse_impulse_response_spec(
16    play_settings: &PlaySettingsFile,
17) -> Option<ImpulseResponseSpec> {
18    if let Some(settings) = parse_convolution_settings(play_settings) {
19        if let Some(spec) = parse_impulse_response_string_or_struct(&settings) {
20            return Some(spec);
21        }
22    }
23
24    None
25}
26
27/// Parse the convolution reverb tail trim (dB) from play settings.
28pub(crate) fn parse_impulse_response_tail_db(play_settings: &PlaySettingsFile) -> Option<f32> {
29    if let Some(settings) = parse_convolution_settings(play_settings) {
30        if let Some(value) = settings.impulse_response_tail_db {
31            return Some(value);
32        }
33        if let Some(value) = settings.impulse_response_tail {
34            return Some(value);
35        }
36    }
37
38    None
39}
40
41fn parse_convolution_settings(
42    play_settings: &PlaySettingsFile,
43) -> Option<ConvolutionReverbSettings> {
44    let effects = match play_settings {
45        PlaySettingsFile::V1(file) => &file.settings.inner().effects,
46        PlaySettingsFile::V2(file) => &file.settings.inner().effects,
47        _ => return None,
48    };
49
50    for effect in effects {
51        if let EffectSettings::ConvolutionReverb(effect) = effect {
52            return Some(effect.settings.clone());
53        }
54    }
55
56    None
57}
58
59pub(crate) fn parse_impulse_response_string_or_struct(
60    settings: &ConvolutionReverbSettings,
61) -> Option<ImpulseResponseSpec> {
62    if let Some(value) = settings.impulse_response.as_deref() {
63        return parse_impulse_response_string(value);
64    }
65    if let Some(value) = settings.impulse_response_attachment.as_deref() {
66        return parse_impulse_response_string(value);
67    }
68    if let Some(value) = settings.impulse_response_path.as_deref() {
69        return parse_impulse_response_string(value);
70    }
71    None
72}
73
74/// Parse an impulse response spec string into a concrete location.
75///
76/// Supported prefixes:
77/// - `attachment:` for container attachments
78/// - `file:` for explicit file paths
79pub fn parse_impulse_response_string(value: &str) -> Option<ImpulseResponseSpec> {
80    if let Some(attachment) = value.strip_prefix("attachment:") {
81        return Some(ImpulseResponseSpec::Attachment(
82            attachment.trim().to_string(),
83        ));
84    }
85
86    if let Some(path) = value.strip_prefix("file:") {
87        return Some(ImpulseResponseSpec::FilePath(path.trim().to_string()));
88    }
89
90    Some(ImpulseResponseSpec::FilePath(value.trim().to_string()))
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use crate::container::play_settings::{
97        PlaySettingsContainer, PlaySettingsFile, PlaySettingsV2, PlaySettingsV2File,
98    };
99    use crate::dsp::effects::{AudioEffect, ConvolutionReverbEffect};
100
101    #[test]
102    fn parse_impulse_response_string_variants() {
103        assert_eq!(
104            parse_impulse_response_string("attachment:foo.wav"),
105            Some(ImpulseResponseSpec::Attachment("foo.wav".to_string()))
106        );
107        assert_eq!(
108            parse_impulse_response_string("file:/tmp/bar.wav"),
109            Some(ImpulseResponseSpec::FilePath("/tmp/bar.wav".to_string()))
110        );
111        assert_eq!(
112            parse_impulse_response_string("plain.wav"),
113            Some(ImpulseResponseSpec::FilePath("plain.wav".to_string()))
114        );
115    }
116
117    #[test]
118    fn parse_impulse_response_from_play_settings() {
119        let mut effect = ConvolutionReverbEffect::default();
120        effect.settings.impulse_response = Some("attachment:ir.wav".to_string());
121        effect.settings.impulse_response_tail_db = Some(-42.0);
122
123        let settings = PlaySettingsV2 {
124            tracks: Vec::new(),
125            effects: vec![AudioEffect::ConvolutionReverb(effect)],
126        };
127        let file = PlaySettingsV2File {
128            settings: PlaySettingsContainer::Flat(settings),
129        };
130
131        let play_settings = PlaySettingsFile::V2(file);
132        let spec = parse_impulse_response_spec(&play_settings);
133        assert_eq!(
134            spec,
135            Some(ImpulseResponseSpec::Attachment("ir.wav".to_string()))
136        );
137        let tail_db = parse_impulse_response_tail_db(&play_settings);
138        assert_eq!(tail_db, Some(-42.0));
139    }
140}