Skip to main content

oximedia_graph/filters/audio/
delay.rs

1//! Audio delay filter.
2//!
3//! This module provides a delay effect with feedback, ping-pong mode,
4//! and dry/wet mix control.
5
6#![forbid(unsafe_code)]
7#![allow(clippy::cast_sign_loss)]
8#![allow(clippy::cast_possible_wrap)]
9#![allow(clippy::cast_lossless)]
10#![allow(clippy::needless_pass_by_value)]
11#![allow(clippy::needless_range_loop)]
12#![allow(clippy::get_first)]
13#![allow(clippy::doc_markdown)]
14
15use bytes::{Bytes, BytesMut};
16use std::collections::VecDeque;
17
18use crate::error::{GraphError, GraphResult};
19use crate::frame::FilterFrame;
20use crate::node::{Node, NodeId, NodeState, NodeType};
21use crate::port::{AudioPortFormat, InputPort, OutputPort, PortFormat, PortId, PortType};
22
23use oximedia_audio::{AudioBuffer, AudioFrame, ChannelLayout};
24use oximedia_core::SampleFormat;
25
26/// Delay mode.
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
28pub enum DelayMode {
29    /// Normal delay - same delay for all channels.
30    #[default]
31    Normal,
32    /// Ping-pong stereo delay - alternates between left and right.
33    PingPong,
34}
35
36/// Configuration for the delay filter.
37#[derive(Clone, Debug)]
38pub struct DelayConfig {
39    /// Delay time in milliseconds.
40    pub delay_ms: f64,
41    /// Feedback amount (0.0 to 1.0).
42    pub feedback: f64,
43    /// Dry/wet mix (0.0 = dry only, 1.0 = wet only).
44    pub mix: f64,
45    /// Delay mode.
46    pub mode: DelayMode,
47    /// High-frequency damping factor (0.0 = none, 1.0 = full).
48    pub damping: f64,
49}
50
51impl Default for DelayConfig {
52    fn default() -> Self {
53        Self {
54            delay_ms: 250.0,
55            feedback: 0.5,
56            mix: 0.5,
57            mode: DelayMode::Normal,
58            damping: 0.0,
59        }
60    }
61}
62
63impl DelayConfig {
64    /// Create a new delay configuration.
65    #[must_use]
66    pub fn new(delay_ms: f64) -> Self {
67        Self {
68            delay_ms,
69            ..Default::default()
70        }
71    }
72
73    /// Set feedback amount.
74    #[must_use]
75    pub fn with_feedback(mut self, feedback: f64) -> Self {
76        self.feedback = feedback.clamp(0.0, 0.99);
77        self
78    }
79
80    /// Set dry/wet mix.
81    #[must_use]
82    pub fn with_mix(mut self, mix: f64) -> Self {
83        self.mix = mix.clamp(0.0, 1.0);
84        self
85    }
86
87    /// Set delay mode.
88    #[must_use]
89    pub fn with_mode(mut self, mode: DelayMode) -> Self {
90        self.mode = mode;
91        self
92    }
93
94    /// Enable ping-pong mode.
95    #[must_use]
96    pub fn ping_pong(mut self) -> Self {
97        self.mode = DelayMode::PingPong;
98        self
99    }
100
101    /// Set damping factor.
102    #[must_use]
103    pub fn with_damping(mut self, damping: f64) -> Self {
104        self.damping = damping.clamp(0.0, 1.0);
105        self
106    }
107}
108
109/// Delay line for one channel.
110#[derive(Clone, Debug)]
111struct DelayLine {
112    /// Circular buffer.
113    buffer: VecDeque<f64>,
114    /// Delay in samples.
115    delay_samples: usize,
116    /// Low-pass filter state for damping.
117    lp_state: f64,
118    /// Damping coefficient.
119    damping: f64,
120}
121
122impl DelayLine {
123    /// Create a new delay line.
124    fn new(delay_ms: f64, sample_rate: f64, damping: f64) -> Self {
125        let delay_samples = ((delay_ms * 0.001 * sample_rate) as usize).max(1);
126        let mut buffer = VecDeque::with_capacity(delay_samples + 1);
127
128        // Initialize with zeros
129        for _ in 0..delay_samples {
130            buffer.push_back(0.0);
131        }
132
133        Self {
134            buffer,
135            delay_samples,
136            lp_state: 0.0,
137            damping,
138        }
139    }
140
141    /// Process one sample through the delay line.
142    fn process(&mut self, input: f64, feedback: f64) -> f64 {
143        // Get delayed output
144        let output = self.buffer.pop_front().unwrap_or(0.0);
145
146        // Apply damping (simple one-pole low-pass)
147        let damped = if self.damping > 0.0 {
148            self.lp_state = self.lp_state + self.damping * (output - self.lp_state);
149            output - self.damping * (output - self.lp_state)
150        } else {
151            output
152        };
153
154        // Feed back into delay line
155        self.buffer.push_back(input + damped * feedback);
156
157        damped
158    }
159
160    /// Reset delay line.
161    fn reset(&mut self) {
162        self.buffer.clear();
163        for _ in 0..self.delay_samples {
164            self.buffer.push_back(0.0);
165        }
166        self.lp_state = 0.0;
167    }
168
169    /// Get current delay samples.
170    #[allow(dead_code)]
171    fn delay_samples(&self) -> usize {
172        self.delay_samples
173    }
174}
175
176/// Delay internal state.
177struct DelayState {
178    /// Delay lines per channel.
179    delay_lines: Vec<DelayLine>,
180    /// Ping-pong state (which channel to feed back to).
181    ping_pong_state: bool,
182}
183
184impl DelayState {
185    /// Create new delay state.
186    fn new(config: &DelayConfig, sample_rate: f64, channels: usize) -> Self {
187        let delay_lines = (0..channels)
188            .map(|_| DelayLine::new(config.delay_ms, sample_rate, config.damping))
189            .collect();
190
191        Self {
192            delay_lines,
193            ping_pong_state: false,
194        }
195    }
196
197    /// Process samples.
198    fn process(&mut self, samples: &mut [Vec<f64>], config: &DelayConfig) {
199        let sample_count = samples.get(0).map_or(0, Vec::len);
200        let channels = samples.len();
201
202        for i in 0..sample_count {
203            match config.mode {
204                DelayMode::Normal => {
205                    // Normal delay - each channel delayed independently
206                    for ch in 0..channels {
207                        if i < samples[ch].len() && ch < self.delay_lines.len() {
208                            let dry = samples[ch][i];
209                            let wet = self.delay_lines[ch].process(dry, config.feedback);
210                            samples[ch][i] = dry * (1.0 - config.mix) + wet * config.mix;
211                        }
212                    }
213                }
214                DelayMode::PingPong => {
215                    // Ping-pong delay - alternates between channels
216                    if channels >= 2 {
217                        let (left_dry, right_dry) = if i < samples[0].len() && i < samples[1].len()
218                        {
219                            (samples[0][i], samples[1][i])
220                        } else {
221                            (0.0, 0.0)
222                        };
223
224                        // Process with cross-feedback
225                        let (left_wet, right_wet) = if self.ping_pong_state {
226                            (
227                                self.delay_lines[0].process(right_dry, config.feedback),
228                                self.delay_lines[1].process(left_dry, config.feedback),
229                            )
230                        } else {
231                            (
232                                self.delay_lines[0].process(left_dry, config.feedback),
233                                self.delay_lines[1].process(right_dry, config.feedback),
234                            )
235                        };
236
237                        if i < samples[0].len() {
238                            samples[0][i] = left_dry * (1.0 - config.mix) + left_wet * config.mix;
239                        }
240                        if i < samples[1].len() {
241                            samples[1][i] = right_dry * (1.0 - config.mix) + right_wet * config.mix;
242                        }
243
244                        // Toggle ping-pong state
245                        self.ping_pong_state = !self.ping_pong_state;
246                    } else {
247                        // Mono - fall back to normal delay
248                        for ch in 0..channels {
249                            if i < samples[ch].len() && ch < self.delay_lines.len() {
250                                let dry = samples[ch][i];
251                                let wet = self.delay_lines[ch].process(dry, config.feedback);
252                                samples[ch][i] = dry * (1.0 - config.mix) + wet * config.mix;
253                            }
254                        }
255                    }
256                }
257            }
258        }
259    }
260
261    /// Reset state.
262    fn reset(&mut self) {
263        for line in &mut self.delay_lines {
264            line.reset();
265        }
266        self.ping_pong_state = false;
267    }
268}
269
270/// Audio delay filter.
271///
272/// This filter provides a delay effect with feedback, ping-pong stereo mode,
273/// and dry/wet mix control.
274///
275/// # Example
276///
277/// ```ignore
278/// use oximedia_graph::filters::audio::delay::{DelayFilter, DelayConfig};
279///
280/// // Create a ping-pong delay
281/// let config = DelayConfig::new(250.0)
282///     .with_feedback(0.5)
283///     .with_mix(0.5)
284///     .ping_pong();
285/// let filter = DelayFilter::new(NodeId(0), "delay", config);
286/// ```
287pub struct DelayFilter {
288    id: NodeId,
289    name: String,
290    state: NodeState,
291    config: DelayConfig,
292    delay_state: Option<DelayState>,
293    inputs: Vec<InputPort>,
294    outputs: Vec<OutputPort>,
295}
296
297impl DelayFilter {
298    /// Create a new delay filter.
299    #[must_use]
300    pub fn new(id: NodeId, name: impl Into<String>, config: DelayConfig) -> Self {
301        let audio_format = PortFormat::Audio(AudioPortFormat::any());
302
303        Self {
304            id,
305            name: name.into(),
306            state: NodeState::Idle,
307            config,
308            delay_state: None,
309            inputs: vec![InputPort::new(PortId(0), "input", PortType::Audio)
310                .with_format(audio_format.clone())],
311            outputs: vec![
312                OutputPort::new(PortId(0), "output", PortType::Audio).with_format(audio_format)
313            ],
314        }
315    }
316
317    /// Get the current configuration.
318    #[must_use]
319    pub fn config(&self) -> &DelayConfig {
320        &self.config
321    }
322
323    /// Update the configuration.
324    pub fn set_config(&mut self, config: DelayConfig) {
325        self.config = config;
326        self.delay_state = None; // Reset state
327    }
328
329    /// Set delay time.
330    pub fn set_delay_time(&mut self, delay_ms: f64) {
331        self.config.delay_ms = delay_ms;
332        self.delay_state = None; // Need to rebuild delay lines
333    }
334
335    /// Set feedback amount.
336    pub fn set_feedback(&mut self, feedback: f64) {
337        self.config.feedback = feedback.clamp(0.0, 0.99);
338    }
339
340    /// Set dry/wet mix.
341    pub fn set_mix(&mut self, mix: f64) {
342        self.config.mix = mix.clamp(0.0, 1.0);
343    }
344
345    /// Convert audio frame to f64 samples per channel.
346    fn frame_to_samples(frame: &AudioFrame) -> Vec<Vec<f64>> {
347        let channels = frame.channels.count();
348        let sample_count = frame.sample_count();
349
350        if sample_count == 0 {
351            return vec![Vec::new(); channels];
352        }
353
354        let mut output = vec![Vec::with_capacity(sample_count); channels];
355
356        match &frame.samples {
357            AudioBuffer::Interleaved(data) => {
358                Self::convert_interleaved(data, frame.format, channels, &mut output);
359            }
360            AudioBuffer::Planar(planes) => {
361                Self::convert_planar(planes, frame.format, &mut output);
362            }
363        }
364
365        output
366    }
367
368    /// Convert interleaved samples.
369    fn convert_interleaved(
370        data: &Bytes,
371        format: SampleFormat,
372        channels: usize,
373        output: &mut [Vec<f64>],
374    ) {
375        let bytes_per_sample = format.bytes_per_sample();
376        if bytes_per_sample == 0 || channels == 0 {
377            return;
378        }
379
380        let sample_count = data.len() / (bytes_per_sample * channels);
381
382        for i in 0..sample_count {
383            for ch in 0..channels {
384                let offset = (i * channels + ch) * bytes_per_sample;
385                if offset + bytes_per_sample <= data.len() {
386                    let sample =
387                        Self::bytes_to_f64(&data[offset..offset + bytes_per_sample], format);
388                    output[ch].push(sample);
389                }
390            }
391        }
392    }
393
394    /// Convert planar samples.
395    fn convert_planar(planes: &[Bytes], format: SampleFormat, output: &mut [Vec<f64>]) {
396        let bytes_per_sample = format.bytes_per_sample();
397        if bytes_per_sample == 0 {
398            return;
399        }
400
401        for (ch, plane) in planes.iter().enumerate() {
402            if ch >= output.len() {
403                break;
404            }
405            let sample_count = plane.len() / bytes_per_sample;
406            for i in 0..sample_count {
407                let offset = i * bytes_per_sample;
408                if offset + bytes_per_sample <= plane.len() {
409                    let sample =
410                        Self::bytes_to_f64(&plane[offset..offset + bytes_per_sample], format);
411                    output[ch].push(sample);
412                }
413            }
414        }
415    }
416
417    /// Convert bytes to f64 sample.
418    fn bytes_to_f64(bytes: &[u8], format: SampleFormat) -> f64 {
419        match format {
420            SampleFormat::U8 => {
421                if bytes.is_empty() {
422                    return 0.0;
423                }
424                (f64::from(bytes[0]) - 128.0) / 128.0
425            }
426            SampleFormat::S16 => {
427                if bytes.len() < 2 {
428                    return 0.0;
429                }
430                let sample = i16::from_le_bytes([bytes[0], bytes[1]]);
431                f64::from(sample) / f64::from(i16::MAX)
432            }
433            SampleFormat::S32 => {
434                if bytes.len() < 4 {
435                    return 0.0;
436                }
437                let sample = i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
438                f64::from(sample) / f64::from(i32::MAX)
439            }
440            SampleFormat::F32 => {
441                if bytes.len() < 4 {
442                    return 0.0;
443                }
444                f64::from(f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
445            }
446            SampleFormat::F64 => {
447                if bytes.len() < 8 {
448                    return 0.0;
449                }
450                f64::from_le_bytes([
451                    bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
452                ])
453            }
454            _ => 0.0,
455        }
456    }
457
458    /// Convert f64 samples to audio frame.
459    fn samples_to_frame(
460        samples: Vec<Vec<f64>>,
461        format: SampleFormat,
462        sample_rate: u32,
463        channels: ChannelLayout,
464    ) -> AudioFrame {
465        let channel_count = channels.count();
466        if samples.is_empty() || samples[0].is_empty() || channel_count == 0 {
467            return AudioFrame::new(format, sample_rate, channels);
468        }
469
470        let sample_count = samples[0].len();
471        let bytes_per_sample = format.bytes_per_sample();
472        let mut buffer = BytesMut::with_capacity(sample_count * channel_count * bytes_per_sample);
473
474        for i in 0..sample_count {
475            for ch in 0..channel_count {
476                let sample = if ch < samples.len() && i < samples[ch].len() {
477                    samples[ch][i]
478                } else {
479                    0.0
480                };
481                Self::f64_to_bytes(sample, format, &mut buffer);
482            }
483        }
484
485        let mut frame = AudioFrame::new(format, sample_rate, channels);
486        frame.samples = AudioBuffer::Interleaved(buffer.freeze());
487        frame
488    }
489
490    /// Convert f64 sample to bytes.
491    fn f64_to_bytes(sample: f64, format: SampleFormat, buffer: &mut BytesMut) {
492        let clamped = sample.clamp(-1.0, 1.0);
493
494        match format {
495            SampleFormat::U8 => {
496                let value = ((clamped * 128.0) + 128.0) as u8;
497                buffer.extend_from_slice(&[value]);
498            }
499            SampleFormat::S16 => {
500                let value = (clamped * f64::from(i16::MAX)) as i16;
501                buffer.extend_from_slice(&value.to_le_bytes());
502            }
503            SampleFormat::S32 => {
504                let value = (clamped * f64::from(i32::MAX)) as i32;
505                buffer.extend_from_slice(&value.to_le_bytes());
506            }
507            SampleFormat::F32 => {
508                #[allow(clippy::cast_possible_truncation)]
509                let value = clamped as f32;
510                buffer.extend_from_slice(&value.to_le_bytes());
511            }
512            SampleFormat::F64 => {
513                buffer.extend_from_slice(&clamped.to_le_bytes());
514            }
515            _ => {}
516        }
517    }
518}
519
520impl Node for DelayFilter {
521    fn id(&self) -> NodeId {
522        self.id
523    }
524
525    fn name(&self) -> &str {
526        &self.name
527    }
528
529    fn node_type(&self) -> NodeType {
530        NodeType::Filter
531    }
532
533    fn state(&self) -> NodeState {
534        self.state
535    }
536
537    fn set_state(&mut self, state: NodeState) -> GraphResult<()> {
538        if !self.state.can_transition_to(state) {
539            return Err(GraphError::InvalidStateTransition {
540                node: self.id,
541                from: self.state.to_string(),
542                to: state.to_string(),
543            });
544        }
545        self.state = state;
546        Ok(())
547    }
548
549    fn inputs(&self) -> &[InputPort] {
550        &self.inputs
551    }
552
553    fn outputs(&self) -> &[OutputPort] {
554        &self.outputs
555    }
556
557    fn process(&mut self, input: Option<FilterFrame>) -> GraphResult<Option<FilterFrame>> {
558        let frame = match input {
559            Some(FilterFrame::Audio(frame)) => frame,
560            Some(_) => {
561                return Err(GraphError::PortTypeMismatch {
562                    expected: "Audio".to_string(),
563                    actual: "Video".to_string(),
564                });
565            }
566            None => return Ok(None),
567        };
568
569        // Initialize delay state if needed
570        if self.delay_state.is_none() {
571            let channels = frame.channels.count();
572            self.delay_state = Some(DelayState::new(
573                &self.config,
574                f64::from(frame.sample_rate),
575                channels,
576            ));
577        }
578
579        // Convert to f64 samples
580        let mut samples = Self::frame_to_samples(&frame);
581
582        // Apply delay
583        if let Some(ref mut delay_state) = self.delay_state {
584            delay_state.process(&mut samples, &self.config);
585        }
586
587        // Convert back to frame
588        let output_frame = Self::samples_to_frame(
589            samples,
590            frame.format,
591            frame.sample_rate,
592            frame.channels.clone(),
593        );
594
595        Ok(Some(FilterFrame::Audio(output_frame)))
596    }
597
598    fn reset(&mut self) -> GraphResult<()> {
599        if let Some(ref mut state) = self.delay_state {
600            state.reset();
601        }
602        self.set_state(NodeState::Idle)
603    }
604}
605
606#[cfg(test)]
607mod tests {
608    use super::*;
609
610    #[test]
611    fn test_delay_mode_default() {
612        assert_eq!(DelayMode::default(), DelayMode::Normal);
613    }
614
615    #[test]
616    fn test_delay_config() {
617        let config = DelayConfig::new(500.0)
618            .with_feedback(0.7)
619            .with_mix(0.3)
620            .with_damping(0.2);
621
622        assert!((config.delay_ms - 500.0).abs() < f64::EPSILON);
623        assert!((config.feedback - 0.7).abs() < f64::EPSILON);
624        assert!((config.mix - 0.3).abs() < f64::EPSILON);
625        assert!((config.damping - 0.2).abs() < f64::EPSILON);
626    }
627
628    #[test]
629    fn test_feedback_clamping() {
630        let config = DelayConfig::new(250.0).with_feedback(1.5);
631        assert!((config.feedback - 0.99).abs() < f64::EPSILON);
632
633        let config = DelayConfig::new(250.0).with_feedback(-0.5);
634        assert!(config.feedback.abs() < f64::EPSILON);
635    }
636
637    #[test]
638    fn test_mix_clamping() {
639        let config = DelayConfig::new(250.0).with_mix(1.5);
640        assert!((config.mix - 1.0).abs() < f64::EPSILON);
641
642        let config = DelayConfig::new(250.0).with_mix(-0.5);
643        assert!(config.mix.abs() < f64::EPSILON);
644    }
645
646    #[test]
647    fn test_ping_pong_mode() {
648        let config = DelayConfig::new(250.0).ping_pong();
649        assert_eq!(config.mode, DelayMode::PingPong);
650    }
651
652    #[test]
653    fn test_delay_line() {
654        let mut line = DelayLine::new(10.0, 48000.0, 0.0);
655
656        // First output should be from initial buffer (silence)
657        let output = line.process(1.0, 0.0);
658        assert!(output.abs() < f64::EPSILON);
659
660        // After delay time, should hear original input
661        for _ in 0..500 {
662            line.process(0.0, 0.0);
663        }
664
665        // Reset and verify
666        line.reset();
667        let output = line.process(0.5, 0.0);
668        assert!(output.abs() < f64::EPSILON);
669    }
670
671    #[test]
672    fn test_delay_line_with_damping() {
673        let mut line = DelayLine::new(10.0, 48000.0, 0.5);
674
675        // Process some samples
676        for _ in 0..100 {
677            let output = line.process(0.5, 0.5);
678            assert!(output.is_finite());
679        }
680    }
681
682    #[test]
683    fn test_delay_filter_creation() {
684        let config = DelayConfig::new(250.0);
685        let filter = DelayFilter::new(NodeId(1), "delay", config);
686
687        assert_eq!(filter.id(), NodeId(1));
688        assert_eq!(filter.name(), "delay");
689        assert_eq!(filter.node_type(), NodeType::Filter);
690    }
691
692    #[test]
693    fn test_delay_filter_ports() {
694        let config = DelayConfig::default();
695        let filter = DelayFilter::new(NodeId(0), "test", config);
696
697        assert_eq!(filter.inputs().len(), 1);
698        assert_eq!(filter.outputs().len(), 1);
699        assert_eq!(filter.inputs()[0].port_type, PortType::Audio);
700    }
701
702    #[test]
703    fn test_set_parameters() {
704        let config = DelayConfig::new(250.0);
705        let mut filter = DelayFilter::new(NodeId(0), "test", config);
706
707        filter.set_delay_time(500.0);
708        assert!((filter.config().delay_ms - 500.0).abs() < f64::EPSILON);
709
710        filter.set_feedback(0.8);
711        assert!((filter.config().feedback - 0.8).abs() < f64::EPSILON);
712
713        filter.set_mix(0.7);
714        assert!((filter.config().mix - 0.7).abs() < f64::EPSILON);
715    }
716
717    #[test]
718    fn test_process_none() {
719        let config = DelayConfig::default();
720        let mut filter = DelayFilter::new(NodeId(0), "test", config);
721
722        let result = filter.process(None).expect("process should succeed");
723        assert!(result.is_none());
724    }
725
726    #[test]
727    fn test_process_audio() {
728        let config = DelayConfig::new(10.0).with_feedback(0.5).with_mix(0.5);
729        let mut filter = DelayFilter::new(NodeId(0), "test", config);
730
731        let mut frame = AudioFrame::new(SampleFormat::F32, 48000, ChannelLayout::Stereo);
732        let mut samples = BytesMut::new();
733        for _ in 0..200 {
734            samples.extend_from_slice(&0.5f32.to_le_bytes()); // L
735            samples.extend_from_slice(&0.5f32.to_le_bytes()); // R
736        }
737        frame.samples = AudioBuffer::Interleaved(samples.freeze());
738
739        let result = filter
740            .process(Some(FilterFrame::Audio(frame)))
741            .expect("process should succeed");
742        assert!(result.is_some());
743    }
744
745    #[test]
746    fn test_state_transitions() {
747        let config = DelayConfig::default();
748        let mut filter = DelayFilter::new(NodeId(0), "test", config);
749
750        assert!(filter.set_state(NodeState::Processing).is_ok());
751        assert_eq!(filter.state(), NodeState::Processing);
752
753        assert!(filter.reset().is_ok());
754        assert_eq!(filter.state(), NodeState::Idle);
755    }
756
757    #[test]
758    fn test_delay_state_reset() {
759        let config = DelayConfig::new(250.0);
760        let mut state = DelayState::new(&config, 48000.0, 2);
761
762        // Process some samples
763        let mut samples = vec![vec![0.5; 100], vec![0.5; 100]];
764        state.process(&mut samples, &config);
765
766        // Reset
767        state.reset();
768
769        // After reset, ping_pong_state should be false
770        assert!(!state.ping_pong_state);
771    }
772}