Skip to main content

oximedia_graph/filters/audio/
passthrough.rs

1//! Passthrough audio filter.
2//!
3//! This filter simply passes audio frames through unchanged. It's useful for
4//! testing and as a template for more complex audio filters.
5
6use crate::error::{GraphError, GraphResult};
7use crate::frame::FilterFrame;
8use crate::node::{Node, NodeId, NodeState, NodeType};
9use crate::port::{AudioPortFormat, InputPort, OutputPort, PortFormat, PortId, PortType};
10
11/// An audio filter that passes frames through unchanged.
12///
13/// This filter is useful for:
14/// - Testing graph connectivity with audio streams
15/// - Serving as a template for custom audio filters
16/// - Acting as a source node when configured as such
17pub struct AudioPassthrough {
18    id: NodeId,
19    name: String,
20    state: NodeState,
21    node_type: NodeType,
22    inputs: Vec<InputPort>,
23    outputs: Vec<OutputPort>,
24}
25
26impl AudioPassthrough {
27    /// Create a new audio passthrough filter.
28    #[must_use]
29    pub fn new(id: NodeId, name: impl Into<String>) -> Self {
30        let audio_format = PortFormat::Audio(AudioPortFormat::any());
31
32        Self {
33            id,
34            name: name.into(),
35            state: NodeState::Idle,
36            node_type: NodeType::Filter,
37            inputs: vec![InputPort::new(PortId(0), "input", PortType::Audio)
38                .with_format(audio_format.clone())],
39            outputs: vec![
40                OutputPort::new(PortId(0), "output", PortType::Audio).with_format(audio_format)
41            ],
42        }
43    }
44
45    /// Create an audio passthrough filter configured as a source node.
46    #[must_use]
47    pub fn new_source(id: NodeId, name: impl Into<String>) -> Self {
48        let audio_format = PortFormat::Audio(AudioPortFormat::any());
49
50        Self {
51            id,
52            name: name.into(),
53            state: NodeState::Idle,
54            node_type: NodeType::Source,
55            inputs: vec![InputPort::new(PortId(0), "input", PortType::Audio)
56                .with_format(audio_format.clone())
57                .optional()],
58            outputs: vec![
59                OutputPort::new(PortId(0), "output", PortType::Audio).with_format(audio_format)
60            ],
61        }
62    }
63
64    /// Create an audio passthrough filter configured as a sink node.
65    #[must_use]
66    pub fn new_sink(id: NodeId, name: impl Into<String>) -> Self {
67        let audio_format = PortFormat::Audio(AudioPortFormat::any());
68
69        Self {
70            id,
71            name: name.into(),
72            state: NodeState::Idle,
73            node_type: NodeType::Sink,
74            inputs: vec![
75                InputPort::new(PortId(0), "input", PortType::Audio).with_format(audio_format)
76            ],
77            outputs: Vec::new(),
78        }
79    }
80}
81
82impl Node for AudioPassthrough {
83    fn id(&self) -> NodeId {
84        self.id
85    }
86
87    fn name(&self) -> &str {
88        &self.name
89    }
90
91    fn node_type(&self) -> NodeType {
92        self.node_type
93    }
94
95    fn state(&self) -> NodeState {
96        self.state
97    }
98
99    fn set_state(&mut self, state: NodeState) -> GraphResult<()> {
100        if !self.state.can_transition_to(state) {
101            return Err(GraphError::InvalidStateTransition {
102                node: self.id,
103                from: self.state.to_string(),
104                to: state.to_string(),
105            });
106        }
107        self.state = state;
108        Ok(())
109    }
110
111    fn inputs(&self) -> &[InputPort] {
112        &self.inputs
113    }
114
115    fn outputs(&self) -> &[OutputPort] {
116        &self.outputs
117    }
118
119    fn process(&mut self, input: Option<FilterFrame>) -> GraphResult<Option<FilterFrame>> {
120        match input {
121            Some(frame) if frame.is_audio() => {
122                // For sink nodes, consume the frame and produce no output
123                if self.node_type == NodeType::Sink {
124                    Ok(None)
125                } else {
126                    Ok(Some(frame))
127                }
128            }
129            Some(_) => Err(GraphError::PortTypeMismatch {
130                expected: "Audio".to_string(),
131                actual: "Video".to_string(),
132            }),
133            None => Ok(None),
134        }
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use oximedia_audio::{AudioFrame, ChannelLayout};
142    use oximedia_core::SampleFormat;
143
144    #[test]
145    fn test_audio_passthrough_creation() {
146        let filter = AudioPassthrough::new(NodeId(42), "test_filter");
147
148        assert_eq!(filter.id(), NodeId(42));
149        assert_eq!(filter.name(), "test_filter");
150        assert_eq!(filter.node_type(), NodeType::Filter);
151        assert_eq!(filter.state(), NodeState::Idle);
152    }
153
154    #[test]
155    fn test_audio_passthrough_source() {
156        let filter = AudioPassthrough::new_source(NodeId(0), "source");
157
158        assert_eq!(filter.node_type(), NodeType::Source);
159        assert!(!filter.inputs()[0].required);
160    }
161
162    #[test]
163    fn test_audio_passthrough_sink() {
164        let filter = AudioPassthrough::new_sink(NodeId(0), "sink");
165
166        assert_eq!(filter.node_type(), NodeType::Sink);
167        assert!(filter.outputs().is_empty());
168    }
169
170    #[test]
171    fn test_audio_passthrough_ports() {
172        let filter = AudioPassthrough::new(NodeId(0), "test");
173
174        assert_eq!(filter.inputs().len(), 1);
175        assert_eq!(filter.outputs().len(), 1);
176
177        let input = &filter.inputs()[0];
178        assert_eq!(input.port_type, PortType::Audio);
179
180        let output = &filter.outputs()[0];
181        assert_eq!(output.port_type, PortType::Audio);
182    }
183
184    #[test]
185    fn test_audio_passthrough_process() {
186        let mut filter = AudioPassthrough::new(NodeId(0), "test");
187
188        // Create a test frame
189        let audio = AudioFrame::new(SampleFormat::F32, 48000, ChannelLayout::Stereo);
190        let frame = FilterFrame::Audio(audio);
191
192        // Process should pass through
193        let result = filter.process(Some(frame)).expect("process should succeed");
194        assert!(result.is_some());
195        assert!(result.expect("value should be valid").is_audio());
196    }
197
198    #[test]
199    fn test_audio_passthrough_sink_process() {
200        let mut filter = AudioPassthrough::new_sink(NodeId(0), "sink");
201
202        // Create a test frame
203        let audio = AudioFrame::new(SampleFormat::F32, 48000, ChannelLayout::Stereo);
204        let frame = FilterFrame::Audio(audio);
205
206        // Sink should consume frame and produce no output
207        let result = filter.process(Some(frame)).expect("process should succeed");
208        assert!(result.is_none());
209    }
210
211    #[test]
212    fn test_audio_passthrough_no_input() {
213        let mut filter = AudioPassthrough::new(NodeId(0), "test");
214
215        let result = filter.process(None).expect("process should succeed");
216        assert!(result.is_none());
217    }
218
219    #[test]
220    fn test_state_transitions() {
221        let mut filter = AudioPassthrough::new(NodeId(0), "test");
222
223        assert!(filter.set_state(NodeState::Processing).is_ok());
224        assert_eq!(filter.state(), NodeState::Processing);
225
226        assert!(filter.set_state(NodeState::Done).is_ok());
227        assert_eq!(filter.state(), NodeState::Done);
228    }
229}