proteus_lib/dsp/effects/convolution_reverb/
mod.rs1use std::path::{Path, PathBuf};
4
5use log::{info, warn};
6use serde::{Deserialize, Serialize};
7
8use super::EffectContext;
9
10pub mod convolution;
11pub mod impulse_response;
12pub mod reverb;
13mod spec;
14
15pub use spec::{parse_impulse_response_string, ImpulseResponseSpec};
16pub(crate) use spec::{parse_impulse_response_spec, parse_impulse_response_tail_db};
17
18const DEFAULT_DRY_WET: f32 = 0.000001;
19const DEFAULT_TAIL_DB: f32 = -60.0;
20const REVERB_BATCH_BLOCKS: usize = 2;
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24#[serde(default)]
25pub struct ConvolutionReverbSettings {
26 pub impulse_response: Option<String>,
27 pub impulse_response_attachment: Option<String>,
28 pub impulse_response_path: Option<String>,
29 pub impulse_response_tail_db: Option<f32>,
30 pub impulse_response_tail: Option<f32>,
31}
32
33impl Default for ConvolutionReverbSettings {
34 fn default() -> Self {
35 Self {
36 impulse_response: None,
37 impulse_response_attachment: None,
38 impulse_response_path: None,
39 impulse_response_tail_db: None,
40 impulse_response_tail: None,
41 }
42 }
43}
44
45#[derive(Clone, Serialize, Deserialize)]
47#[serde(default)]
48pub struct ConvolutionReverbEffect {
49 pub enabled: bool,
50 #[serde(alias = "wet_dry", alias = "mix")]
51 pub dry_wet: f32,
52 #[serde(flatten)]
53 pub settings: ConvolutionReverbSettings,
54 #[serde(skip)]
55 state: Option<ConvolutionReverbState>,
56 #[serde(skip)]
57 resolved_config: Option<ResolvedConfig>,
58}
59
60impl std::fmt::Debug for ConvolutionReverbEffect {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 f.debug_struct("ConvolutionReverbEffect")
63 .field("enabled", &self.enabled)
64 .field("dry_wet", &self.dry_wet)
65 .field("settings", &self.settings)
66 .finish()
67 }
68}
69
70impl Default for ConvolutionReverbEffect {
71 fn default() -> Self {
72 Self {
73 enabled: true,
74 dry_wet: DEFAULT_DRY_WET,
75 settings: ConvolutionReverbSettings::default(),
76 state: None,
77 resolved_config: None,
78 }
79 }
80}
81
82impl ConvolutionReverbEffect {
83 pub fn new(dry_wet: f32) -> Self {
85 Self {
86 dry_wet: dry_wet.clamp(0.0, 1.0),
87 ..Default::default()
88 }
89 }
90
91 pub fn settings(&self) -> &ConvolutionReverbSettings {
93 &self.settings
94 }
95
96 pub fn settings_mut(&mut self) -> &mut ConvolutionReverbSettings {
98 &mut self.settings
99 }
100
101 pub fn process(&mut self, samples: &[f32], context: &EffectContext, drain: bool) -> Vec<f32> {
111 self.ensure_state(context);
112 if !self.enabled || self.dry_wet <= 0.0 {
113 return samples.to_vec();
114 }
115
116 let Some(state) = self.state.as_mut() else {
117 return samples.to_vec();
118 };
119
120 state.reverb.set_dry_wet(self.dry_wet);
121 state.process(samples, drain)
122 }
123
124 pub fn reset_state(&mut self) {
129 if let Some(state) = self.state.as_mut() {
130 state.reset();
131 }
132 self.state = None;
133 self.resolved_config = None;
134 }
135
136 fn ensure_state(&mut self, context: &EffectContext) {
137 let config = self.resolve_config(context);
138 if self.resolved_config.as_ref() == Some(&config) && self.state.is_some() {
139 return;
140 }
141
142 let reverb = build_reverb_with_impulse_response(
143 config.channels,
144 self.dry_wet,
145 config.impulse_spec.clone(),
146 config.container_path.as_deref(),
147 config.tail_db,
148 );
149
150 self.state = reverb.map(ConvolutionReverbState::new);
151 self.resolved_config = Some(config);
152 }
153
154 fn resolve_config(&self, context: &EffectContext) -> ResolvedConfig {
155 let impulse_spec = self
156 .settings
157 .impulse_response
158 .as_deref()
159 .and_then(parse_impulse_response_string)
160 .or_else(|| {
161 self.settings
162 .impulse_response_attachment
163 .as_deref()
164 .and_then(parse_impulse_response_string)
165 })
166 .or_else(|| {
167 self.settings
168 .impulse_response_path
169 .as_deref()
170 .and_then(parse_impulse_response_string)
171 })
172 .or_else(|| context.impulse_response_spec.clone());
173
174 let tail_db = self
175 .settings
176 .impulse_response_tail_db
177 .or(self.settings.impulse_response_tail)
178 .unwrap_or(context.impulse_response_tail_db);
179
180 ResolvedConfig {
181 channels: context.channels,
182 container_path: context.container_path.clone(),
183 impulse_spec,
184 tail_db,
185 }
186 }
187}
188
189#[derive(Debug, Clone, PartialEq)]
190struct ResolvedConfig {
191 channels: usize,
192 container_path: Option<String>,
193 impulse_spec: Option<ImpulseResponseSpec>,
194 tail_db: f32,
195}
196
197#[derive(Clone)]
198struct ConvolutionReverbState {
199 reverb: reverb::Reverb,
200 input_buffer: Vec<f32>,
201 output_buffer: Vec<f32>,
202 block_out: Vec<f32>,
203 block_samples: usize,
204}
205
206impl ConvolutionReverbState {
207 fn new(mut reverb: reverb::Reverb) -> Self {
208 info!("Using Convolution Reverb!");
209 let block_samples = reverb.block_size_samples();
210 reverb.set_dry_wet(DEFAULT_DRY_WET);
211 Self {
212 reverb,
213 input_buffer: Vec::new(),
214 output_buffer: Vec::new(),
215 block_out: Vec::new(),
216 block_samples,
217 }
218 }
219
220 fn reset(&mut self) {
221 self.reverb.clear_state();
222 self.input_buffer.clear();
223 self.output_buffer.clear();
224 self.block_out.clear();
225 self.block_samples = self.reverb.block_size_samples();
226 }
227
228 fn process(&mut self, samples: &[f32], drain: bool) -> Vec<f32> {
229 if samples.is_empty() {
230 if drain && !self.output_buffer.is_empty() {
231 let out = self.output_buffer.clone();
232 self.output_buffer.clear();
233 return out;
234 }
235 return Vec::new();
236 }
237
238 if self.block_samples == 0 {
239 return self.reverb.process(samples);
240 }
241
242 self.input_buffer.extend_from_slice(samples);
243 let batch_samples = self.block_samples * REVERB_BATCH_BLOCKS;
244 let should_flush = drain && !self.input_buffer.is_empty();
245 while self.input_buffer.len() >= batch_samples || should_flush {
246 let take = if self.input_buffer.len() >= batch_samples {
247 batch_samples
248 } else {
249 self.input_buffer.len()
250 };
251 let block: Vec<f32> = self.input_buffer.drain(0..take).collect();
252 self.reverb.process_into(&block, &mut self.block_out);
253 self.output_buffer.extend_from_slice(&self.block_out);
254 if take < batch_samples {
255 break;
256 }
257 }
258
259 let chunk_len = samples.len();
260 if self.output_buffer.len() < chunk_len {
261 let mut out = self.output_buffer.clone();
262 if out.len() < chunk_len {
263 out.resize(chunk_len, 0.0);
264 }
265 self.output_buffer.clear();
266 return out;
267 }
268
269 self.output_buffer.drain(0..chunk_len).collect()
270 }
271}
272
273fn build_reverb_with_impulse_response(
274 channels: usize,
275 dry_wet: f32,
276 impulse_spec: Option<ImpulseResponseSpec>,
277 container_path: Option<&str>,
278 tail_db: f32,
279) -> Option<reverb::Reverb> {
280 let impulse_spec = impulse_spec?;
281
282 use self::impulse_response::{
283 load_impulse_response_from_file_with_tail,
284 load_impulse_response_from_prot_attachment_with_tail,
285 };
286
287 let result = match impulse_spec {
288 ImpulseResponseSpec::Attachment(name) => container_path
289 .ok_or_else(|| "missing container path for attachment".to_string())
290 .and_then(|path| {
291 load_impulse_response_from_prot_attachment_with_tail(path, &name, Some(tail_db))
292 .map_err(|err| err.to_string())
293 }),
294 ImpulseResponseSpec::FilePath(path) => {
295 let resolved_path = resolve_impulse_response_path(container_path, &path);
296 if resolved_path.exists() {
297 load_impulse_response_from_file_with_tail(&resolved_path, Some(tail_db))
298 .map_err(|err| err.to_string())
299 } else {
300 match container_path {
301 Some(container_path) => {
302 let fallback_name = Path::new(&path)
303 .file_name()
304 .and_then(|name| name.to_str())
305 .map(|name| name.to_string());
306 if let Some(fallback_name) = fallback_name {
307 load_impulse_response_from_prot_attachment_with_tail(
308 container_path,
309 &fallback_name,
310 Some(tail_db),
311 )
312 .map_err(|err| err.to_string())
313 } else {
314 Err(format!(
315 "impulse response path not found: {}",
316 resolved_path.display()
317 ))
318 }
319 }
320 None => Err(format!(
321 "impulse response path not found: {}",
322 resolved_path.display()
323 )),
324 }
325 }
326 }
327 };
328
329 match result {
330 Ok(impulse_response) => Some(reverb::Reverb::new_with_impulse_response(
331 channels,
332 dry_wet,
333 &impulse_response,
334 )),
335 Err(err) => {
336 warn!(
337 "Failed to load impulse response ({}); skipping convolution reverb.",
338 err
339 );
340 None
341 }
342 }
343}
344
345fn resolve_impulse_response_path(container_path: Option<&str>, path: &str) -> PathBuf {
346 let path = Path::new(path);
347 if path.is_absolute() {
348 return path.to_path_buf();
349 }
350
351 if let Some(container_path) = container_path {
352 if let Some(parent) = Path::new(container_path).parent() {
353 return parent.join(path);
354 }
355 }
356
357 path.to_path_buf()
358}
359
360impl ConvolutionReverbSettings {
361 pub fn tail_db_or_default(&self) -> f32 {
363 self.impulse_response_tail_db
364 .or(self.impulse_response_tail)
365 .unwrap_or(DEFAULT_TAIL_DB)
366 }
367}