Skip to main content

oximedia_graph/filters/audio/
trim.rs

1//! Audio trim filter.
2//!
3//! This module provides trimming of audio streams with support for
4//! start/end time specification and fade in/out at boundaries.
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};
16
17use crate::error::{GraphError, GraphResult};
18use crate::frame::FilterFrame;
19use crate::node::{Node, NodeId, NodeState, NodeType};
20use crate::port::{AudioPortFormat, InputPort, OutputPort, PortFormat, PortId, PortType};
21
22use oximedia_audio::{AudioBuffer, AudioFrame, ChannelLayout};
23use oximedia_core::SampleFormat;
24
25/// Trim mode for specifying the trim region.
26#[derive(Clone, Copy, Debug, PartialEq)]
27pub enum TrimMode {
28    /// Trim by start and end time in seconds.
29    TimeRange {
30        /// Start time in seconds.
31        start: f64,
32        /// End time in seconds (None = end of stream).
33        end: Option<f64>,
34    },
35    /// Trim by duration starting from a given time.
36    Duration {
37        /// Start time in seconds.
38        start: f64,
39        /// Duration in seconds.
40        duration: f64,
41    },
42    /// Trim by sample count.
43    SampleRange {
44        /// Start sample.
45        start_sample: u64,
46        /// End sample (None = end of stream).
47        end_sample: Option<u64>,
48    },
49}
50
51impl Default for TrimMode {
52    fn default() -> Self {
53        Self::TimeRange {
54            start: 0.0,
55            end: None,
56        }
57    }
58}
59
60/// Configuration for the trim filter.
61#[derive(Clone, Debug)]
62pub struct TrimConfig {
63    /// Trim mode.
64    pub mode: TrimMode,
65    /// Fade in duration in milliseconds.
66    pub fade_in_ms: f64,
67    /// Fade out duration in milliseconds.
68    pub fade_out_ms: f64,
69}
70
71impl Default for TrimConfig {
72    fn default() -> Self {
73        Self {
74            mode: TrimMode::default(),
75            fade_in_ms: 0.0,
76            fade_out_ms: 0.0,
77        }
78    }
79}
80
81impl TrimConfig {
82    /// Create a new trim configuration with time range.
83    #[must_use]
84    pub fn time_range(start: f64, end: Option<f64>) -> Self {
85        Self {
86            mode: TrimMode::TimeRange { start, end },
87            ..Default::default()
88        }
89    }
90
91    /// Create a trim configuration with duration.
92    #[must_use]
93    pub fn duration(start: f64, duration: f64) -> Self {
94        Self {
95            mode: TrimMode::Duration { start, duration },
96            ..Default::default()
97        }
98    }
99
100    /// Create a trim configuration with sample range.
101    #[must_use]
102    pub fn sample_range(start_sample: u64, end_sample: Option<u64>) -> Self {
103        Self {
104            mode: TrimMode::SampleRange {
105                start_sample,
106                end_sample,
107            },
108            ..Default::default()
109        }
110    }
111
112    /// Set fade in duration.
113    #[must_use]
114    pub fn with_fade_in(mut self, fade_in_ms: f64) -> Self {
115        self.fade_in_ms = fade_in_ms.max(0.0);
116        self
117    }
118
119    /// Set fade out duration.
120    #[must_use]
121    pub fn with_fade_out(mut self, fade_out_ms: f64) -> Self {
122        self.fade_out_ms = fade_out_ms.max(0.0);
123        self
124    }
125
126    /// Set fade in and out duration.
127    #[must_use]
128    pub fn with_fades(mut self, fade_in_ms: f64, fade_out_ms: f64) -> Self {
129        self.fade_in_ms = fade_in_ms.max(0.0);
130        self.fade_out_ms = fade_out_ms.max(0.0);
131        self
132    }
133
134    /// Get the start sample for the given sample rate.
135    #[must_use]
136    pub fn start_sample(&self, sample_rate: u32) -> u64 {
137        match self.mode {
138            TrimMode::TimeRange { start, .. } | TrimMode::Duration { start, .. } => {
139                (start * f64::from(sample_rate)) as u64
140            }
141            TrimMode::SampleRange { start_sample, .. } => start_sample,
142        }
143    }
144
145    /// Get the end sample for the given sample rate (None = no limit).
146    #[must_use]
147    pub fn end_sample(&self, sample_rate: u32) -> Option<u64> {
148        match self.mode {
149            TrimMode::TimeRange { end, .. } => end.map(|e| (e * f64::from(sample_rate)) as u64),
150            TrimMode::Duration { start, duration } => {
151                Some(((start + duration) * f64::from(sample_rate)) as u64)
152            }
153            TrimMode::SampleRange { end_sample, .. } => end_sample,
154        }
155    }
156
157    /// Get fade in duration in samples.
158    #[must_use]
159    pub fn fade_in_samples(&self, sample_rate: u32) -> u64 {
160        (self.fade_in_ms * 0.001 * f64::from(sample_rate)) as u64
161    }
162
163    /// Get fade out duration in samples.
164    #[must_use]
165    pub fn fade_out_samples(&self, sample_rate: u32) -> u64 {
166        (self.fade_out_ms * 0.001 * f64::from(sample_rate)) as u64
167    }
168}
169
170/// Trim internal state.
171struct TrimState {
172    /// Current sample position (input stream).
173    current_sample: u64,
174    /// Start sample.
175    start_sample: u64,
176    /// End sample (None = no limit).
177    end_sample: Option<u64>,
178    /// Fade in duration in samples.
179    fade_in_samples: u64,
180    /// Fade out duration in samples.
181    fade_out_samples: u64,
182    /// Number of output samples produced.
183    output_samples: u64,
184    /// Whether end of trim region has been reached.
185    done: bool,
186}
187
188impl TrimState {
189    /// Create new trim state.
190    fn new(config: &TrimConfig, sample_rate: u32) -> Self {
191        Self {
192            current_sample: 0,
193            start_sample: config.start_sample(sample_rate),
194            end_sample: config.end_sample(sample_rate),
195            fade_in_samples: config.fade_in_samples(sample_rate),
196            fade_out_samples: config.fade_out_samples(sample_rate),
197            output_samples: 0,
198            done: false,
199        }
200    }
201
202    /// Check if we're before the trim region.
203    fn before_start(&self) -> bool {
204        self.current_sample < self.start_sample
205    }
206
207    /// Check if we're past the trim region.
208    fn past_end(&self) -> bool {
209        if let Some(end) = self.end_sample {
210            self.current_sample >= end
211        } else {
212            false
213        }
214    }
215
216    /// Calculate fade gain for the current output sample.
217    fn fade_gain(&self, output_position: u64) -> f64 {
218        // Fade in
219        if self.fade_in_samples > 0 && output_position < self.fade_in_samples {
220            return output_position as f64 / self.fade_in_samples as f64;
221        }
222
223        // Fade out
224        if self.fade_out_samples > 0 {
225            if let Some(end) = self.end_sample {
226                let total_output = end.saturating_sub(self.start_sample);
227                let fade_start = total_output.saturating_sub(self.fade_out_samples);
228                if output_position >= fade_start {
229                    let fade_position = output_position - fade_start;
230                    return 1.0 - (fade_position as f64 / self.fade_out_samples as f64);
231                }
232            }
233        }
234
235        1.0
236    }
237
238    /// Process samples and return trimmed output.
239    fn process(&mut self, samples: &[Vec<f64>]) -> Vec<Vec<f64>> {
240        let sample_count = samples.get(0).map_or(0, Vec::len);
241        let channels = samples.len();
242
243        if sample_count == 0 || self.done {
244            return vec![Vec::new(); channels];
245        }
246
247        let mut output = vec![Vec::new(); channels];
248
249        for i in 0..sample_count {
250            // Check if before start
251            if self.before_start() {
252                self.current_sample += 1;
253                continue;
254            }
255
256            // Check if past end
257            if self.past_end() {
258                self.done = true;
259                break;
260            }
261
262            // Apply fade and output
263            let fade = self.fade_gain(self.output_samples);
264
265            for ch in 0..channels {
266                if i < samples[ch].len() {
267                    output[ch].push(samples[ch][i] * fade);
268                }
269            }
270
271            self.current_sample += 1;
272            self.output_samples += 1;
273        }
274
275        output
276    }
277
278    /// Check if trimming is complete.
279    fn is_done(&self) -> bool {
280        self.done
281    }
282
283    /// Reset state.
284    fn reset(&mut self) {
285        self.current_sample = 0;
286        self.output_samples = 0;
287        self.done = false;
288    }
289}
290
291/// Audio trim filter.
292///
293/// This filter trims audio streams to a specified time or sample range,
294/// with optional fade in/out at the boundaries.
295///
296/// # Example
297///
298/// ```ignore
299/// use oximedia_graph::filters::audio::trim::{TrimFilter, TrimConfig};
300///
301/// // Trim to 10-30 seconds with 100ms fades
302/// let config = TrimConfig::time_range(10.0, Some(30.0))
303///     .with_fades(100.0, 100.0);
304/// let filter = TrimFilter::new(NodeId(0), "trim", config);
305/// ```
306pub struct TrimFilter {
307    id: NodeId,
308    name: String,
309    state: NodeState,
310    config: TrimConfig,
311    trim_state: Option<TrimState>,
312    inputs: Vec<InputPort>,
313    outputs: Vec<OutputPort>,
314}
315
316impl TrimFilter {
317    /// Create a new trim filter.
318    #[must_use]
319    pub fn new(id: NodeId, name: impl Into<String>, config: TrimConfig) -> Self {
320        let audio_format = PortFormat::Audio(AudioPortFormat::any());
321
322        Self {
323            id,
324            name: name.into(),
325            state: NodeState::Idle,
326            config,
327            trim_state: None,
328            inputs: vec![InputPort::new(PortId(0), "input", PortType::Audio)
329                .with_format(audio_format.clone())],
330            outputs: vec![
331                OutputPort::new(PortId(0), "output", PortType::Audio).with_format(audio_format)
332            ],
333        }
334    }
335
336    /// Get the current configuration.
337    #[must_use]
338    pub fn config(&self) -> &TrimConfig {
339        &self.config
340    }
341
342    /// Update the configuration.
343    pub fn set_config(&mut self, config: TrimConfig) {
344        self.config = config;
345        self.trim_state = None; // Reset state
346    }
347
348    /// Check if trimming is complete.
349    #[must_use]
350    pub fn is_done(&self) -> bool {
351        self.trim_state.as_ref().is_some_and(TrimState::is_done)
352    }
353
354    /// Convert audio frame to f64 samples per channel.
355    fn frame_to_samples(frame: &AudioFrame) -> Vec<Vec<f64>> {
356        let channels = frame.channels.count();
357        let sample_count = frame.sample_count();
358
359        if sample_count == 0 {
360            return vec![Vec::new(); channels];
361        }
362
363        let mut output = vec![Vec::with_capacity(sample_count); channels];
364
365        match &frame.samples {
366            AudioBuffer::Interleaved(data) => {
367                Self::convert_interleaved(data, frame.format, channels, &mut output);
368            }
369            AudioBuffer::Planar(planes) => {
370                Self::convert_planar(planes, frame.format, &mut output);
371            }
372        }
373
374        output
375    }
376
377    /// Convert interleaved samples.
378    fn convert_interleaved(
379        data: &Bytes,
380        format: SampleFormat,
381        channels: usize,
382        output: &mut [Vec<f64>],
383    ) {
384        let bytes_per_sample = format.bytes_per_sample();
385        if bytes_per_sample == 0 || channels == 0 {
386            return;
387        }
388
389        let sample_count = data.len() / (bytes_per_sample * channels);
390
391        for i in 0..sample_count {
392            for ch in 0..channels {
393                let offset = (i * channels + ch) * bytes_per_sample;
394                if offset + bytes_per_sample <= data.len() {
395                    let sample =
396                        Self::bytes_to_f64(&data[offset..offset + bytes_per_sample], format);
397                    output[ch].push(sample);
398                }
399            }
400        }
401    }
402
403    /// Convert planar samples.
404    fn convert_planar(planes: &[Bytes], format: SampleFormat, output: &mut [Vec<f64>]) {
405        let bytes_per_sample = format.bytes_per_sample();
406        if bytes_per_sample == 0 {
407            return;
408        }
409
410        for (ch, plane) in planes.iter().enumerate() {
411            if ch >= output.len() {
412                break;
413            }
414            let sample_count = plane.len() / bytes_per_sample;
415            for i in 0..sample_count {
416                let offset = i * bytes_per_sample;
417                if offset + bytes_per_sample <= plane.len() {
418                    let sample =
419                        Self::bytes_to_f64(&plane[offset..offset + bytes_per_sample], format);
420                    output[ch].push(sample);
421                }
422            }
423        }
424    }
425
426    /// Convert bytes to f64 sample.
427    fn bytes_to_f64(bytes: &[u8], format: SampleFormat) -> f64 {
428        match format {
429            SampleFormat::U8 => {
430                if bytes.is_empty() {
431                    return 0.0;
432                }
433                (f64::from(bytes[0]) - 128.0) / 128.0
434            }
435            SampleFormat::S16 => {
436                if bytes.len() < 2 {
437                    return 0.0;
438                }
439                let sample = i16::from_le_bytes([bytes[0], bytes[1]]);
440                f64::from(sample) / f64::from(i16::MAX)
441            }
442            SampleFormat::S32 => {
443                if bytes.len() < 4 {
444                    return 0.0;
445                }
446                let sample = i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
447                f64::from(sample) / f64::from(i32::MAX)
448            }
449            SampleFormat::F32 => {
450                if bytes.len() < 4 {
451                    return 0.0;
452                }
453                f64::from(f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
454            }
455            SampleFormat::F64 => {
456                if bytes.len() < 8 {
457                    return 0.0;
458                }
459                f64::from_le_bytes([
460                    bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
461                ])
462            }
463            _ => 0.0,
464        }
465    }
466
467    /// Convert f64 samples to audio frame.
468    fn samples_to_frame(
469        samples: Vec<Vec<f64>>,
470        format: SampleFormat,
471        sample_rate: u32,
472        channels: ChannelLayout,
473    ) -> AudioFrame {
474        let channel_count = channels.count();
475        if samples.is_empty() || samples[0].is_empty() || channel_count == 0 {
476            return AudioFrame::new(format, sample_rate, channels);
477        }
478
479        let sample_count = samples[0].len();
480        let bytes_per_sample = format.bytes_per_sample();
481        let mut buffer = BytesMut::with_capacity(sample_count * channel_count * bytes_per_sample);
482
483        for i in 0..sample_count {
484            for ch in 0..channel_count {
485                let sample = if ch < samples.len() && i < samples[ch].len() {
486                    samples[ch][i]
487                } else {
488                    0.0
489                };
490                Self::f64_to_bytes(sample, format, &mut buffer);
491            }
492        }
493
494        let mut frame = AudioFrame::new(format, sample_rate, channels);
495        frame.samples = AudioBuffer::Interleaved(buffer.freeze());
496        frame
497    }
498
499    /// Convert f64 sample to bytes.
500    fn f64_to_bytes(sample: f64, format: SampleFormat, buffer: &mut BytesMut) {
501        let clamped = sample.clamp(-1.0, 1.0);
502
503        match format {
504            SampleFormat::U8 => {
505                let value = ((clamped * 128.0) + 128.0) as u8;
506                buffer.extend_from_slice(&[value]);
507            }
508            SampleFormat::S16 => {
509                let value = (clamped * f64::from(i16::MAX)) as i16;
510                buffer.extend_from_slice(&value.to_le_bytes());
511            }
512            SampleFormat::S32 => {
513                let value = (clamped * f64::from(i32::MAX)) as i32;
514                buffer.extend_from_slice(&value.to_le_bytes());
515            }
516            SampleFormat::F32 => {
517                #[allow(clippy::cast_possible_truncation)]
518                let value = clamped as f32;
519                buffer.extend_from_slice(&value.to_le_bytes());
520            }
521            SampleFormat::F64 => {
522                buffer.extend_from_slice(&clamped.to_le_bytes());
523            }
524            _ => {}
525        }
526    }
527}
528
529impl Node for TrimFilter {
530    fn id(&self) -> NodeId {
531        self.id
532    }
533
534    fn name(&self) -> &str {
535        &self.name
536    }
537
538    fn node_type(&self) -> NodeType {
539        NodeType::Filter
540    }
541
542    fn state(&self) -> NodeState {
543        self.state
544    }
545
546    fn set_state(&mut self, state: NodeState) -> GraphResult<()> {
547        if !self.state.can_transition_to(state) {
548            return Err(GraphError::InvalidStateTransition {
549                node: self.id,
550                from: self.state.to_string(),
551                to: state.to_string(),
552            });
553        }
554        self.state = state;
555        Ok(())
556    }
557
558    fn inputs(&self) -> &[InputPort] {
559        &self.inputs
560    }
561
562    fn outputs(&self) -> &[OutputPort] {
563        &self.outputs
564    }
565
566    fn process(&mut self, input: Option<FilterFrame>) -> GraphResult<Option<FilterFrame>> {
567        let frame = match input {
568            Some(FilterFrame::Audio(frame)) => frame,
569            Some(_) => {
570                return Err(GraphError::PortTypeMismatch {
571                    expected: "Audio".to_string(),
572                    actual: "Video".to_string(),
573                });
574            }
575            None => return Ok(None),
576        };
577
578        // Initialize trim state if needed
579        if self.trim_state.is_none() {
580            self.trim_state = Some(TrimState::new(&self.config, frame.sample_rate));
581        }
582
583        let trim_state = match self.trim_state.as_mut() {
584            Some(s) => s,
585            None => return Ok(None),
586        };
587
588        // Check if we're done
589        if trim_state.is_done() {
590            return Ok(None);
591        }
592
593        // Convert to f64 samples
594        let samples = Self::frame_to_samples(&frame);
595
596        // Process through trim
597        let output_samples = trim_state.process(&samples);
598
599        // If no output samples, return None
600        if output_samples.is_empty() || output_samples[0].is_empty() {
601            return Ok(None);
602        }
603
604        // Convert back to frame
605        let output_frame = Self::samples_to_frame(
606            output_samples,
607            frame.format,
608            frame.sample_rate,
609            frame.channels.clone(),
610        );
611
612        Ok(Some(FilterFrame::Audio(output_frame)))
613    }
614
615    fn reset(&mut self) -> GraphResult<()> {
616        if let Some(ref mut state) = self.trim_state {
617            state.reset();
618        }
619        self.set_state(NodeState::Idle)
620    }
621}
622
623#[cfg(test)]
624mod tests {
625    use super::*;
626
627    #[test]
628    fn test_trim_mode_default() {
629        let mode = TrimMode::default();
630        if let TrimMode::TimeRange { start, end } = mode {
631            assert!(start.abs() < f64::EPSILON);
632            assert!(end.is_none());
633        } else {
634            panic!("Expected TimeRange mode");
635        }
636    }
637
638    #[test]
639    fn test_trim_config_time_range() {
640        let config = TrimConfig::time_range(10.0, Some(30.0));
641
642        assert_eq!(config.start_sample(48000), 480000);
643        assert_eq!(config.end_sample(48000), Some(1440000));
644    }
645
646    #[test]
647    fn test_trim_config_duration() {
648        let config = TrimConfig::duration(5.0, 10.0);
649
650        assert_eq!(config.start_sample(48000), 240000);
651        assert_eq!(config.end_sample(48000), Some(720000)); // 5 + 10 = 15 seconds
652    }
653
654    #[test]
655    fn test_trim_config_sample_range() {
656        let config = TrimConfig::sample_range(1000, Some(5000));
657
658        assert_eq!(config.start_sample(48000), 1000);
659        assert_eq!(config.end_sample(48000), Some(5000));
660    }
661
662    #[test]
663    fn test_fade_settings() {
664        let config = TrimConfig::time_range(0.0, Some(10.0))
665            .with_fade_in(100.0)
666            .with_fade_out(200.0);
667
668        assert!((config.fade_in_ms - 100.0).abs() < f64::EPSILON);
669        assert!((config.fade_out_ms - 200.0).abs() < f64::EPSILON);
670
671        assert_eq!(config.fade_in_samples(48000), 4800);
672        assert_eq!(config.fade_out_samples(48000), 9600);
673    }
674
675    #[test]
676    fn test_fade_settings_combined() {
677        let config = TrimConfig::time_range(0.0, Some(10.0)).with_fades(50.0, 100.0);
678
679        assert!((config.fade_in_ms - 50.0).abs() < f64::EPSILON);
680        assert!((config.fade_out_ms - 100.0).abs() < f64::EPSILON);
681    }
682
683    #[test]
684    fn test_trim_state_before_start() {
685        let config = TrimConfig::time_range(1.0, Some(2.0));
686        let state = TrimState::new(&config, 48000);
687
688        assert!(state.before_start());
689        assert!(!state.past_end());
690    }
691
692    #[test]
693    fn test_fade_gain_calculation() {
694        let config = TrimConfig::time_range(0.0, Some(1.0)).with_fade_in(100.0);
695        let state = TrimState::new(&config, 48000);
696
697        // At start, fade should be 0
698        let fade = state.fade_gain(0);
699        assert!(fade.abs() < f64::EPSILON);
700
701        // At half fade in, should be 0.5
702        let fade = state.fade_gain(2400); // Half of 4800
703        assert!((fade - 0.5).abs() < 0.01);
704
705        // After fade in, should be 1.0
706        let fade = state.fade_gain(5000);
707        assert!((fade - 1.0).abs() < f64::EPSILON);
708    }
709
710    #[test]
711    fn test_trim_filter_creation() {
712        let config = TrimConfig::time_range(0.0, Some(10.0));
713        let filter = TrimFilter::new(NodeId(1), "trim", config);
714
715        assert_eq!(filter.id(), NodeId(1));
716        assert_eq!(filter.name(), "trim");
717        assert_eq!(filter.node_type(), NodeType::Filter);
718    }
719
720    #[test]
721    fn test_trim_filter_ports() {
722        let config = TrimConfig::default();
723        let filter = TrimFilter::new(NodeId(0), "test", config);
724
725        assert_eq!(filter.inputs().len(), 1);
726        assert_eq!(filter.outputs().len(), 1);
727        assert_eq!(filter.inputs()[0].port_type, PortType::Audio);
728    }
729
730    #[test]
731    fn test_process_none() {
732        let config = TrimConfig::default();
733        let mut filter = TrimFilter::new(NodeId(0), "test", config);
734
735        let result = filter.process(None).expect("process should succeed");
736        assert!(result.is_none());
737    }
738
739    #[test]
740    fn test_process_audio() {
741        // Trim starting from 0, no end limit
742        let config = TrimConfig::time_range(0.0, None);
743        let mut filter = TrimFilter::new(NodeId(0), "test", config);
744
745        let mut frame = AudioFrame::new(SampleFormat::F32, 48000, ChannelLayout::Mono);
746        let mut samples = BytesMut::new();
747        for _ in 0..100 {
748            samples.extend_from_slice(&0.5f32.to_le_bytes());
749        }
750        frame.samples = AudioBuffer::Interleaved(samples.freeze());
751
752        let result = filter
753            .process(Some(FilterFrame::Audio(frame)))
754            .expect("process should succeed");
755        assert!(result.is_some());
756
757        if let Some(FilterFrame::Audio(output)) = result {
758            assert_eq!(output.sample_count(), 100);
759        }
760    }
761
762    #[test]
763    fn test_trim_before_start() {
764        // Start at 1 second
765        let config = TrimConfig::time_range(1.0, None);
766        let mut filter = TrimFilter::new(NodeId(0), "test", config);
767
768        // First frame is 100 samples at 48000 Hz (< 1 second)
769        let mut frame = AudioFrame::new(SampleFormat::F32, 48000, ChannelLayout::Mono);
770        let mut samples = BytesMut::new();
771        for _ in 0..100 {
772            samples.extend_from_slice(&0.5f32.to_le_bytes());
773        }
774        frame.samples = AudioBuffer::Interleaved(samples.freeze());
775
776        let result = filter
777            .process(Some(FilterFrame::Audio(frame)))
778            .expect("process should succeed");
779        // Should return None because we're before the start
780        assert!(result.is_none());
781    }
782
783    #[test]
784    fn test_is_done() {
785        let config = TrimConfig::sample_range(0, Some(50));
786        let mut filter = TrimFilter::new(NodeId(0), "test", config);
787
788        assert!(!filter.is_done());
789
790        // Process more samples than the trim region
791        let mut frame = AudioFrame::new(SampleFormat::F32, 48000, ChannelLayout::Mono);
792        let mut samples = BytesMut::new();
793        for _ in 0..100 {
794            samples.extend_from_slice(&0.5f32.to_le_bytes());
795        }
796        frame.samples = AudioBuffer::Interleaved(samples.freeze());
797
798        let _ = filter.process(Some(FilterFrame::Audio(frame)));
799
800        assert!(filter.is_done());
801    }
802
803    #[test]
804    fn test_state_transitions() {
805        let config = TrimConfig::default();
806        let mut filter = TrimFilter::new(NodeId(0), "test", config);
807
808        assert!(filter.set_state(NodeState::Processing).is_ok());
809        assert_eq!(filter.state(), NodeState::Processing);
810
811        assert!(filter.reset().is_ok());
812        assert_eq!(filter.state(), NodeState::Idle);
813    }
814
815    #[test]
816    fn test_trim_state_reset() {
817        let config = TrimConfig::time_range(0.0, Some(1.0));
818        let mut state = TrimState::new(&config, 48000);
819
820        state.current_sample = 1000;
821        state.output_samples = 500;
822        state.done = true;
823
824        state.reset();
825
826        assert_eq!(state.current_sample, 0);
827        assert_eq!(state.output_samples, 0);
828        assert!(!state.done);
829    }
830}