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