1#![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#[derive(Clone, Copy, Debug, PartialEq)]
27pub enum TrimMode {
28 TimeRange {
30 start: f64,
32 end: Option<f64>,
34 },
35 Duration {
37 start: f64,
39 duration: f64,
41 },
42 SampleRange {
44 start_sample: u64,
46 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#[derive(Clone, Debug)]
62pub struct TrimConfig {
63 pub mode: TrimMode,
65 pub fade_in_ms: f64,
67 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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
170struct TrimState {
172 current_sample: u64,
174 start_sample: u64,
176 end_sample: Option<u64>,
178 fade_in_samples: u64,
180 fade_out_samples: u64,
182 output_samples: u64,
184 done: bool,
186}
187
188impl TrimState {
189 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 fn before_start(&self) -> bool {
204 self.current_sample < self.start_sample
205 }
206
207 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 fn fade_gain(&self, output_position: u64) -> f64 {
218 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 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 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 if self.before_start() {
252 self.current_sample += 1;
253 continue;
254 }
255
256 if self.past_end() {
258 self.done = true;
259 break;
260 }
261
262 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 fn is_done(&self) -> bool {
280 self.done
281 }
282
283 fn reset(&mut self) {
285 self.current_sample = 0;
286 self.output_samples = 0;
287 self.done = false;
288 }
289}
290
291pub 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 #[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 #[must_use]
338 pub fn config(&self) -> &TrimConfig {
339 &self.config
340 }
341
342 pub fn set_config(&mut self, config: TrimConfig) {
344 self.config = config;
345 self.trim_state = None; }
347
348 #[must_use]
350 pub fn is_done(&self) -> bool {
351 self.trim_state.as_ref().is_some_and(TrimState::is_done)
352 }
353
354 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 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 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 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 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 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 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 if trim_state.is_done() {
590 return Ok(None);
591 }
592
593 let samples = Self::frame_to_samples(&frame);
595
596 let output_samples = trim_state.process(&samples);
598
599 if output_samples.is_empty() || output_samples[0].is_empty() {
601 return Ok(None);
602 }
603
604 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)); }
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 let fade = state.fade_gain(0);
699 assert!(fade.abs() < f64::EPSILON);
700
701 let fade = state.fade_gain(2400); assert!((fade - 0.5).abs() < 0.01);
704
705 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 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 let config = TrimConfig::time_range(1.0, None);
766 let mut filter = TrimFilter::new(NodeId(0), "test", config);
767
768 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 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 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}