/src/adhd/audio_processor/src/pipeline.rs
Line | Count | Source (jump to first uncovered line) |
1 | | // Copyright 2024 The ChromiumOS Authors |
2 | | // Use of this source code is governed by a BSD-style license that can be |
3 | | // found in the LICENSE file. |
4 | | |
5 | | use std::path::Path; |
6 | | |
7 | | use anyhow::Context; |
8 | | use hound::WavSpec; |
9 | | use hound::WavWriter; |
10 | | |
11 | | use crate::processors::WavSink; |
12 | | use crate::AudioProcessor; |
13 | | use crate::Format; |
14 | | use crate::MultiSlice; |
15 | | |
16 | | pub type ProcessorVec = Vec<Box<dyn AudioProcessor<I = f32, O = f32> + Send>>; |
17 | | |
18 | | pub struct Pipeline { |
19 | | pub input_format: Format, |
20 | | pub vec: ProcessorVec, |
21 | | } |
22 | | |
23 | | impl Pipeline { |
24 | 0 | pub fn new(input_format: Format) -> Self { |
25 | 0 | Pipeline { |
26 | 0 | input_format, |
27 | 0 | vec: Vec::new(), |
28 | 0 | } |
29 | 0 | } |
30 | | |
31 | 0 | pub fn add(&mut self, processor: impl AudioProcessor<I = f32, O = f32> + Send + 'static) { |
32 | 0 | self.vec.push(Box::new(processor)); |
33 | 0 | } Unexecuted instantiation: <audio_processor::pipeline::Pipeline>::add::<audio_processor::processors::negate::InPlaceNegateAudioProcessor<f32>> Unexecuted instantiation: <audio_processor::pipeline::Pipeline>::add::<audio_processor::processors::wav::WavSink<std::io::buffered::bufwriter::BufWriter<std::fs::File>>> Unexecuted instantiation: <audio_processor::pipeline::Pipeline>::add::<audio_processor::processors::profile::Profile<audio_processor::processors::plugin::dynamic::DynamicPluginProcessor>> Unexecuted instantiation: <audio_processor::pipeline::Pipeline>::add::<audio_processor::processors::plugin::dynamic::DynamicPluginProcessor> Unexecuted instantiation: <audio_processor::pipeline::Pipeline>::add::<audio_processor::processors::chunk_wrapper::ChunkWrapper<audio_processor::pipeline::Pipeline, f32>> Unexecuted instantiation: <audio_processor::pipeline::Pipeline>::add::<audio_processor::processors::speex::SpeexResampler> Unexecuted instantiation: <audio_processor::pipeline::Pipeline>::add::<audio_processor::processors::shuffle_channels::ShuffleChannels> Unexecuted instantiation: <audio_processor::pipeline::Pipeline>::add::<audio_processor::processors::peer::managed::ManagedBlockingSeqPacketProcessor> |
34 | | |
35 | 0 | pub fn add_wav_dump(&mut self, filename: &Path) -> anyhow::Result<()> { |
36 | 0 | let format = self.get_output_format(); |
37 | 0 | { |
38 | 0 | self.add(WavSink::new( |
39 | 0 | WavWriter::create( |
40 | 0 | filename, |
41 | 0 | WavSpec { |
42 | 0 | channels: format.channels.try_into().context("channels.try_into()")?, |
43 | 0 | sample_rate: format |
44 | 0 | .frame_rate |
45 | 0 | .try_into() |
46 | 0 | .context("frame_rate.try_into()")?, |
47 | | bits_per_sample: 32, |
48 | 0 | sample_format: hound::SampleFormat::Float, |
49 | 0 | }, |
50 | 0 | ) |
51 | 0 | .context("WavWriter::create")?, |
52 | 0 | format.block_size, |
53 | 0 | )); |
54 | 0 | anyhow::Result::<()>::Ok(()) |
55 | 0 | } |
56 | 0 | .context("add_wav_dump")?; |
57 | 0 | Ok(()) |
58 | 0 | } |
59 | | } |
60 | | |
61 | | impl AudioProcessor for Pipeline { |
62 | | type I = f32; |
63 | | type O = f32; |
64 | | |
65 | 0 | fn process<'a>( |
66 | 0 | &'a mut self, |
67 | 0 | mut input: MultiSlice<'a, Self::I>, |
68 | 0 | ) -> crate::Result<MultiSlice<'a, Self::O>> { |
69 | 0 | for processor in self.vec.iter_mut() { |
70 | 0 | input = processor.process(input)?; |
71 | | } |
72 | 0 | Ok(input) |
73 | 0 | } |
74 | | |
75 | 0 | fn get_output_format(&self) -> Format { |
76 | 0 | match self.vec.last() { |
77 | 0 | Some(last) => last.get_output_format(), |
78 | 0 | None => self.input_format, |
79 | | } |
80 | 0 | } |
81 | | } |
82 | | |
83 | | #[cfg(test)] |
84 | | mod tests { |
85 | | use anyhow::Context; |
86 | | use tempfile::TempDir; |
87 | | |
88 | | use crate::processors::NegateAudioProcessor; |
89 | | use crate::util::read_wav; |
90 | | use crate::AudioProcessor; |
91 | | use crate::Format; |
92 | | use crate::MultiBuffer; |
93 | | use crate::Pipeline; |
94 | | |
95 | | #[test] |
96 | | fn test_pipeline() -> anyhow::Result<()> { |
97 | | let tmpd = TempDir::new().context("tempdir")?; |
98 | | |
99 | | let dump1 = tmpd.path().join("dump-1.wav"); |
100 | | let dump2 = tmpd.path().join("dump-2.wav"); |
101 | | |
102 | | let mut p = Pipeline::new(Format { |
103 | | channels: 1, |
104 | | block_size: 4, |
105 | | frame_rate: 48000, |
106 | | }); |
107 | | |
108 | | p.add_wav_dump(&dump1).unwrap(); |
109 | | p.add(NegateAudioProcessor::new(Format { |
110 | | channels: 1, |
111 | | block_size: 4, |
112 | | frame_rate: 48000, |
113 | | })); |
114 | | p.add_wav_dump(&dump2).unwrap(); |
115 | | |
116 | | assert_eq!( |
117 | | p.get_output_format(), |
118 | | Format { |
119 | | channels: 1, |
120 | | block_size: 4, |
121 | | frame_rate: 48000, |
122 | | } |
123 | | ); |
124 | | |
125 | | let mut buf = MultiBuffer::from(vec![vec![1f32, 2., 3., 4.]]); |
126 | | let out = p.process(buf.as_multi_slice()).unwrap(); |
127 | | |
128 | | // Check output. |
129 | | assert_eq!(out.into_raw(), [[-1., -2., -3., -4.]]); |
130 | | // Drop p to flush wav dumps. |
131 | | drop(p); |
132 | | |
133 | | let (_, wav1) = read_wav::<f32>(&dump1).unwrap(); |
134 | | let (_, wav2) = read_wav::<f32>(&dump2).unwrap(); |
135 | | assert_eq!(wav1.to_vecs(), [[1., 2., 3., 4.]]); |
136 | | assert_eq!(wav2.to_vecs(), [[-1., -2., -3., -4.]]); |
137 | | |
138 | | Ok(()) |
139 | | } |
140 | | } |