/src/adhd/audio_processor/src/config.rs
Line | Count | Source |
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::fmt::Debug; |
6 | | use std::path::PathBuf; |
7 | | use std::rc::Rc; |
8 | | use std::sync::mpsc::Sender; |
9 | | |
10 | | use anyhow::bail; |
11 | | use anyhow::Context; |
12 | | use hound::WavSpec; |
13 | | use hound::WavWriter; |
14 | | use serde::Deserialize; |
15 | | use serde::Serialize; |
16 | | |
17 | | use crate::processors::peer::ManagedBlockingSeqPacketProcessor; |
18 | | use crate::processors::peer::ThreadedWorkerFactory; |
19 | | use crate::processors::peer::WorkerFactory; |
20 | | use crate::processors::profile::Profile; |
21 | | use crate::processors::profile::ProfileStats; |
22 | | use crate::processors::ChunkWrapper; |
23 | | use crate::processors::DynamicPluginProcessor; |
24 | | use crate::processors::InPlaceNegateAudioProcessor; |
25 | | use crate::processors::SpeexResampler; |
26 | | use crate::AudioProcessor; |
27 | | use crate::Format; |
28 | | use crate::Pipeline; |
29 | | |
30 | | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] |
31 | | pub enum Processor { |
32 | | Negate, |
33 | | WavSink { |
34 | | path: PathBuf, |
35 | | }, |
36 | | Plugin { |
37 | | path: PathBuf, |
38 | | constructor: String, |
39 | | }, |
40 | | WrapChunk { |
41 | | inner: Box<Processor>, |
42 | | inner_block_size: usize, |
43 | | /// Prevents merging with the outer pipeline even if they have the same block size. |
44 | | /// Used when the outer pipeline doesn't actually have a stable block size and the |
45 | | /// ChunkWrapper is used for regulating it. |
46 | | disallow_hoisting: bool, |
47 | | }, |
48 | | Resample { |
49 | | output_frame_rate: usize, |
50 | | }, |
51 | | Pipeline { |
52 | | processors: Vec<Processor>, |
53 | | }, |
54 | | Preloaded(PreloadedProcessor), |
55 | | ShuffleChannels { |
56 | | channel_indexes: Vec<usize>, |
57 | | }, |
58 | | /// Checks the current format of the pipeline. |
59 | | /// Does not actually builds a processor, |
60 | | /// so unlike `crate::processors::CheckShape`, does not perform checks |
61 | | /// when processing audio. |
62 | | CheckFormat { |
63 | | channels: Option<usize>, |
64 | | block_size: Option<usize>, |
65 | | frame_rate: Option<usize>, |
66 | | }, |
67 | | Nothing, |
68 | | Peer { |
69 | | processor: Box<Processor>, |
70 | | }, |
71 | | } |
72 | | |
73 | | /// PreloadedProcessor is a config that describes a processor that is already created |
74 | | /// out of the config system. |
75 | | pub struct PreloadedProcessor { |
76 | | pub description: &'static str, |
77 | | pub processor: Box<dyn AudioProcessor<I = f32, O = f32> + Send>, |
78 | | } |
79 | | |
80 | | impl Serialize for PreloadedProcessor { |
81 | 0 | fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error> |
82 | 0 | where |
83 | 0 | S: serde::Serializer, |
84 | | { |
85 | 0 | Err(serde::ser::Error::custom( |
86 | 0 | "PreloadedProcessor cannot be serialized", |
87 | 0 | )) |
88 | 0 | } |
89 | | } |
90 | | |
91 | | impl<'de> Deserialize<'de> for PreloadedProcessor { |
92 | 0 | fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error> |
93 | 0 | where |
94 | 0 | D: serde::Deserializer<'de>, |
95 | | { |
96 | 0 | Err(serde::de::Error::custom( |
97 | 0 | "PreloadedProcessor cannot be deserialized", |
98 | 0 | )) |
99 | 0 | } |
100 | | } |
101 | | |
102 | | impl Debug for PreloadedProcessor { |
103 | 0 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
104 | 0 | f.debug_struct("Preloaded") |
105 | 0 | .field("description", &self.description) |
106 | 0 | .finish() |
107 | 0 | } |
108 | | } |
109 | | |
110 | | impl PartialEq for PreloadedProcessor { |
111 | 0 | fn eq(&self, other: &Self) -> bool { |
112 | 0 | std::ptr::eq(self, other) |
113 | 0 | } |
114 | | } |
115 | | |
116 | | impl Eq for PreloadedProcessor {} |
117 | | |
118 | | pub struct PipelineBuilder { |
119 | | pipeline: Pipeline, |
120 | | profile_sender: Option<Sender<ProfileStats>>, |
121 | | worker_factory: Rc<dyn WorkerFactory>, |
122 | | } |
123 | | |
124 | | impl PipelineBuilder { |
125 | 0 | pub fn new(input_format: Format) -> Self { |
126 | 0 | Self { |
127 | 0 | pipeline: Pipeline::new(input_format), |
128 | 0 | profile_sender: None, |
129 | 0 | worker_factory: Rc::new(ThreadedWorkerFactory), |
130 | 0 | } |
131 | 0 | } |
132 | | |
133 | 0 | pub fn build(mut self, config: Processor) -> anyhow::Result<Pipeline> { |
134 | 0 | self.add(config)?; |
135 | 0 | Ok(self.pipeline) |
136 | 0 | } |
137 | | |
138 | | /// Enable profiling. The results are sent with sender when the pipeline |
139 | | /// is dropped. |
140 | | /// Currently only `Plugin`s are profiled. |
141 | 0 | pub fn with_profile_sender(mut self, sender: Sender<ProfileStats>) -> Self { |
142 | 0 | self.profile_sender = Some(sender); |
143 | 0 | self |
144 | 0 | } |
145 | | |
146 | | /// Use the factory to spawn peer workers. |
147 | 0 | pub fn with_worker_factory(mut self, factory: impl WorkerFactory + 'static) -> Self { |
148 | 0 | self.worker_factory = Rc::new(factory); |
149 | 0 | self |
150 | 0 | } Unexecuted instantiation: <audio_processor::config::PipelineBuilder>::with_worker_factory::<_> Unexecuted instantiation: <audio_processor::config::PipelineBuilder>::with_worker_factory::<audio_processor::processors::peer::managed::AudioWorkerSubprocessFactory> |
151 | | |
152 | 0 | fn output_format(&self) -> Format { |
153 | 0 | self.pipeline.get_output_format() |
154 | 0 | } |
155 | | |
156 | 0 | fn child_builder(&self, input_format: Format) -> Self { |
157 | 0 | Self { |
158 | 0 | pipeline: Pipeline::new(input_format), |
159 | 0 | profile_sender: self.profile_sender.clone(), |
160 | 0 | worker_factory: self.worker_factory.clone(), |
161 | 0 | } |
162 | 0 | } |
163 | | |
164 | 0 | fn add(&mut self, config: Processor) -> anyhow::Result<()> { |
165 | | use Processor::*; |
166 | 0 | match config { |
167 | 0 | Negate => { |
168 | 0 | self.pipeline |
169 | 0 | .add(InPlaceNegateAudioProcessor::new(self.output_format())); |
170 | 0 | } |
171 | 0 | WavSink { path } => { |
172 | 0 | let output_format = self.output_format(); |
173 | 0 | self.pipeline.add(crate::processors::WavSink::new( |
174 | 0 | WavWriter::create( |
175 | 0 | &path, |
176 | | WavSpec { |
177 | 0 | channels: output_format.channels.try_into().context("channels")?, |
178 | 0 | sample_format: hound::SampleFormat::Float, |
179 | 0 | sample_rate: output_format |
180 | 0 | .frame_rate |
181 | 0 | .try_into() |
182 | 0 | .context("sample_rate")?, |
183 | | bits_per_sample: 32, |
184 | | }, |
185 | | ) |
186 | 0 | .context("WavWriter::create")?, |
187 | 0 | output_format.block_size, |
188 | | )); |
189 | | } |
190 | 0 | Plugin { path, constructor } => { |
191 | 0 | let plugin = DynamicPluginProcessor::new( |
192 | 0 | path.to_str().context("path.to_str")?, |
193 | 0 | &constructor, |
194 | 0 | self.output_format(), |
195 | | ) |
196 | 0 | .context("DynamicPluginProcessor::new")?; |
197 | 0 | if let Some(sender) = &self.profile_sender { |
198 | 0 | let mut profile = Profile::new(plugin); |
199 | 0 | profile.set_key(format!( |
200 | 0 | "{}@{}", |
201 | | constructor, |
202 | 0 | path.file_name() |
203 | 0 | .context("path.file_name() failed")? |
204 | 0 | .to_string_lossy(), |
205 | | )); |
206 | 0 | profile.set_sender(sender.clone()); |
207 | 0 | self.pipeline.add(profile); |
208 | 0 | } else { |
209 | 0 | self.pipeline.add(plugin); |
210 | 0 | } |
211 | | } |
212 | | WrapChunk { |
213 | 0 | inner, |
214 | 0 | inner_block_size, |
215 | 0 | disallow_hoisting, |
216 | | } => { |
217 | 0 | if self.output_format().block_size == inner_block_size && !disallow_hoisting { |
218 | 0 | self.add(*inner).context("inner")?; |
219 | | } else { |
220 | 0 | let inner_pipeline = self |
221 | 0 | .child_builder(Format { |
222 | 0 | block_size: inner_block_size, |
223 | 0 | ..self.output_format() |
224 | 0 | }) |
225 | 0 | .build(*inner) |
226 | 0 | .context("inner")?; |
227 | | |
228 | 0 | let inner_channels = inner_pipeline.get_output_format().channels; |
229 | | // TODO: When the inner_pipeline only has a single processor, wrap just that processor. |
230 | 0 | self.pipeline.add(ChunkWrapper::new( |
231 | 0 | inner_pipeline, |
232 | 0 | self.output_format().block_size, |
233 | 0 | inner_block_size, |
234 | 0 | self.output_format().channels, |
235 | 0 | inner_channels, |
236 | | )); |
237 | | } |
238 | | } |
239 | 0 | Resample { output_frame_rate } => { |
240 | 0 | if self.output_format().frame_rate != output_frame_rate { |
241 | 0 | self.pipeline.add( |
242 | 0 | SpeexResampler::new(self.output_format(), output_frame_rate) |
243 | 0 | .context("SpeexResampler::new")?, |
244 | | ); |
245 | 0 | } |
246 | | } |
247 | 0 | Pipeline { processors } => { |
248 | 0 | for (i, config) in processors.into_iter().enumerate() { |
249 | 0 | self.add(config) |
250 | 0 | .with_context(|| format!("pipeline processor#{i}"))?; |
251 | | } |
252 | | } |
253 | 0 | Preloaded(PreloadedProcessor { processor, .. }) => { |
254 | 0 | self.pipeline.vec.push(processor); |
255 | 0 | } |
256 | 0 | ShuffleChannels { channel_indexes } => { |
257 | | // Optimization: only shuffle channels if it would change the |
258 | | // channel layout. |
259 | 0 | if !channel_indexes |
260 | 0 | .iter() |
261 | 0 | .cloned() |
262 | 0 | .eq(0..self.output_format().channels) |
263 | | { |
264 | 0 | self.pipeline.add(crate::processors::ShuffleChannels::new( |
265 | 0 | &channel_indexes, |
266 | 0 | self.output_format(), |
267 | | )) |
268 | 0 | } |
269 | | } |
270 | | CheckFormat { |
271 | 0 | channels, |
272 | 0 | block_size, |
273 | 0 | frame_rate, |
274 | | } => { |
275 | 0 | let format = self.output_format(); |
276 | 0 | if let Some(channels) = channels { |
277 | 0 | if channels != format.channels { |
278 | 0 | bail!("expected channels {channels:?} got {format:?}"); |
279 | 0 | } |
280 | 0 | } |
281 | 0 | if let Some(block_size) = block_size { |
282 | 0 | if block_size != format.block_size { |
283 | 0 | bail!("expected block_size {block_size:?} got {format:?}"); |
284 | 0 | } |
285 | 0 | } |
286 | 0 | if let Some(frame_rate) = frame_rate { |
287 | 0 | if frame_rate != format.frame_rate { |
288 | 0 | bail!("expected frame_rate {frame_rate:?} got {format:?}"); |
289 | 0 | } |
290 | 0 | } |
291 | | } |
292 | 0 | Nothing => { |
293 | 0 | // Do nothing. |
294 | 0 | } |
295 | 0 | Peer { processor } => { |
296 | 0 | self.pipeline.add( |
297 | 0 | ManagedBlockingSeqPacketProcessor::new( |
298 | 0 | self.worker_factory.as_ref(), |
299 | 0 | self.output_format(), |
300 | 0 | *processor, |
301 | | ) |
302 | 0 | .context("ManagedBlockingSeqPacketProcessor::new")?, |
303 | | ); |
304 | | } |
305 | | } |
306 | 0 | Ok(()) |
307 | 0 | } |
308 | | } |
309 | | |
310 | | #[cfg(test)] |
311 | | mod tests { |
312 | | use core::panic; |
313 | | |
314 | | use assert_matches::assert_matches; |
315 | | use hound::WavSpec; |
316 | | |
317 | | use crate::config::PipelineBuilder; |
318 | | use crate::config::PreloadedProcessor; |
319 | | use crate::config::Processor; |
320 | | use crate::processors::NegateAudioProcessor; |
321 | | use crate::util::read_wav; |
322 | | use crate::AudioProcessor; |
323 | | use crate::Format; |
324 | | use crate::MultiBuffer; |
325 | | |
326 | | #[test] |
327 | | fn simple_pipeline() { |
328 | | use Processor::*; |
329 | | |
330 | | let tempdir = tempfile::tempdir().unwrap(); |
331 | | |
332 | | let config = Pipeline { |
333 | | processors: vec![ |
334 | | WavSink { |
335 | | path: tempdir.path().join("0.wav"), |
336 | | }, |
337 | | Negate, |
338 | | WavSink { |
339 | | path: tempdir.path().join("1.wav"), |
340 | | }, |
341 | | WrapChunk { |
342 | | inner: Box::new(Negate), |
343 | | inner_block_size: 1, |
344 | | disallow_hoisting: false, |
345 | | }, |
346 | | WavSink { |
347 | | path: tempdir.path().join("2.wav"), |
348 | | }, |
349 | | WrapChunk { |
350 | | inner: Box::new(Pipeline { |
351 | | processors: vec![ |
352 | | WavSink { |
353 | | path: tempdir.path().join("3.wav"), |
354 | | }, |
355 | | Negate, |
356 | | WavSink { |
357 | | path: tempdir.path().join("4.wav"), |
358 | | }, |
359 | | ], |
360 | | }), |
361 | | inner_block_size: 2, |
362 | | disallow_hoisting: false, |
363 | | }, |
364 | | WrapChunk { |
365 | | inner_block_size: 5, // Same block size, should pass through. |
366 | | inner: Box::new(WavSink { |
367 | | path: tempdir.path().join("5.wav"), |
368 | | }), |
369 | | disallow_hoisting: false, |
370 | | }, |
371 | | Resample { |
372 | | output_frame_rate: 48000, |
373 | | }, |
374 | | WavSink { |
375 | | path: tempdir.path().join("6.wav"), |
376 | | }, |
377 | | ], |
378 | | }; |
379 | | let mut pipeline = PipelineBuilder::new(Format { |
380 | | channels: 2, |
381 | | block_size: 5, |
382 | | frame_rate: 24000, |
383 | | }) |
384 | | .build(config) |
385 | | .unwrap(); |
386 | | |
387 | | let mut input = |
388 | | MultiBuffer::from(vec![vec![1f32, 2., 3., 4., 5.], vec![6., 7., 8., 9., 10.]]); |
389 | | let output = MultiBuffer::from(pipeline.process(input.as_multi_slice()).unwrap()); |
390 | | |
391 | | // Drop pipeline to flush WavSinks. |
392 | | drop(pipeline); |
393 | | |
394 | | let mut wavs = vec![]; |
395 | | for i in 0..7 { |
396 | | let path = tempdir.path().join(format!("{i}.wav")); |
397 | | wavs.push(read_wav::<f32>(&path).unwrap()); |
398 | | } |
399 | | |
400 | | assert_matches!( |
401 | | wavs[0].0, |
402 | | WavSpec { |
403 | | channels: 2, |
404 | | sample_rate: 24000, |
405 | | .. |
406 | | } |
407 | | ); |
408 | | assert_eq!( |
409 | | wavs[0].1.to_vecs(), |
410 | | [[1.0, 2.0, 3.0, 4.0, 5.0], [6.0, 7.0, 8.0, 9.0, 10.0]] |
411 | | ); |
412 | | |
413 | | assert_matches!( |
414 | | wavs[1].0, |
415 | | WavSpec { |
416 | | channels: 2, |
417 | | sample_rate: 24000, |
418 | | .. |
419 | | } |
420 | | ); |
421 | | assert_eq!( |
422 | | wavs[1].1.to_vecs(), |
423 | | [ |
424 | | [-1.0, -2.0, -3.0, -4.0, -5.0], |
425 | | [-6.0, -7.0, -8.0, -9.0, -10.0] |
426 | | ] |
427 | | ); |
428 | | |
429 | | assert_matches!( |
430 | | wavs[2].0, |
431 | | WavSpec { |
432 | | channels: 2, |
433 | | sample_rate: 24000, |
434 | | .. |
435 | | } |
436 | | ); |
437 | | assert_eq!( |
438 | | wavs[2].1.to_vecs(), |
439 | | [[0.0, 1.0, 2.0, 3.0, 4.0], [0.0, 6.0, 7.0, 8.0, 9.0]] |
440 | | ); |
441 | | |
442 | | assert_matches!( |
443 | | wavs[3].0, |
444 | | WavSpec { |
445 | | channels: 2, |
446 | | sample_rate: 24000, |
447 | | .. |
448 | | } |
449 | | ); |
450 | | assert_eq!( |
451 | | wavs[3].1.to_vecs(), |
452 | | [[0.0, 1.0, 2.0, 3.0], [0.0, 6.0, 7.0, 8.0]] |
453 | | ); |
454 | | |
455 | | assert_matches!( |
456 | | wavs[4].0, |
457 | | WavSpec { |
458 | | channels: 2, |
459 | | sample_rate: 24000, |
460 | | .. |
461 | | } |
462 | | ); |
463 | | assert_eq!( |
464 | | wavs[4].1.to_vecs(), |
465 | | [[-0.0, -1.0, -2.0, -3.0], [-0.0, -6.0, -7.0, -8.0]] |
466 | | ); |
467 | | |
468 | | assert_matches!( |
469 | | wavs[5].0, |
470 | | WavSpec { |
471 | | channels: 2, |
472 | | sample_rate: 24000, |
473 | | .. |
474 | | } |
475 | | ); |
476 | | assert_eq!( |
477 | | wavs[5].1.to_vecs(), |
478 | | [[0.0, 0.0, -0.0, -1.0, -2.0], [0.0, 0.0, -0.0, -6.0, -7.0]] |
479 | | ); |
480 | | |
481 | | assert_matches!( |
482 | | wavs[6].0, |
483 | | WavSpec { |
484 | | channels: 2, |
485 | | sample_rate: 48000, |
486 | | .. |
487 | | } |
488 | | ); |
489 | | assert_eq!(wavs[6].1.to_vecs(), output.to_vecs()); |
490 | | } |
491 | | |
492 | | #[test] |
493 | | fn preloaded() { |
494 | | let mut input: MultiBuffer<f32> = |
495 | | MultiBuffer::from(vec![vec![1., 2., 3., 4.], vec![5., 6., 7., 8.]]); |
496 | | |
497 | | let input_format = Format { |
498 | | channels: 2, |
499 | | block_size: 4, |
500 | | frame_rate: 48000, |
501 | | }; |
502 | | |
503 | | let mut pipeline = PipelineBuilder::new(input_format) |
504 | | .build(Processor::Preloaded(PreloadedProcessor { |
505 | | description: "preloaded negate", |
506 | | processor: Box::new(NegateAudioProcessor::new(input_format)), |
507 | | })) |
508 | | .unwrap(); |
509 | | |
510 | | let output = pipeline.process(input.as_multi_slice()).unwrap(); |
511 | | |
512 | | // output = abs(input) |
513 | | assert_eq!( |
514 | | output.into_raw(), |
515 | | [[-1., -2., -3., -4.], [-5., -6., -7., -8.]] |
516 | | ); |
517 | | } |
518 | | |
519 | | #[test] |
520 | | fn shuffle_channels() { |
521 | | let mut pipeline = PipelineBuilder::new(Format { |
522 | | channels: 2, |
523 | | block_size: 2, |
524 | | frame_rate: 48000, |
525 | | }) |
526 | | .build(Processor::ShuffleChannels { |
527 | | channel_indexes: vec![1, 0, 1], |
528 | | }) |
529 | | .unwrap(); |
530 | | |
531 | | let mut input = MultiBuffer::from(vec![vec![1., 2.], vec![3., 4.]]); |
532 | | let output = pipeline.process(input.as_multi_slice()).unwrap(); |
533 | | assert_eq!(output.into_raw(), [[3., 4.], [1., 2.], [3., 4.]]); |
534 | | } |
535 | | |
536 | | #[test] |
537 | | fn shuffle_channels_opt() { |
538 | | let pipeline = PipelineBuilder::new(Format { |
539 | | channels: 2, |
540 | | block_size: 2, |
541 | | frame_rate: 48000, |
542 | | }) |
543 | | .build(Processor::ShuffleChannels { |
544 | | channel_indexes: vec![1, 0], |
545 | | }) |
546 | | .unwrap(); |
547 | | assert_eq!( |
548 | | pipeline.vec.len(), |
549 | | 1, |
550 | | "channel swap, should not be optimized away" |
551 | | ); |
552 | | |
553 | | let pipeline = PipelineBuilder::new(Format { |
554 | | channels: 2, |
555 | | block_size: 2, |
556 | | frame_rate: 48000, |
557 | | }) |
558 | | .build(Processor::ShuffleChannels { |
559 | | channel_indexes: vec![0, 1, 1], |
560 | | }) |
561 | | .unwrap(); |
562 | | assert_eq!( |
563 | | pipeline.vec.len(), |
564 | | 1, |
565 | | "different length, should not be optimized away" |
566 | | ); |
567 | | |
568 | | let pipeline = PipelineBuilder::new(Format { |
569 | | channels: 2, |
570 | | block_size: 2, |
571 | | frame_rate: 48000, |
572 | | }) |
573 | | .build(Processor::ShuffleChannels { |
574 | | channel_indexes: vec![0], |
575 | | }) |
576 | | .unwrap(); |
577 | | assert_eq!( |
578 | | pipeline.vec.len(), |
579 | | 1, |
580 | | "different length, should not be optimized away" |
581 | | ); |
582 | | |
583 | | let pipeline = PipelineBuilder::new(Format { |
584 | | channels: 2, |
585 | | block_size: 2, |
586 | | frame_rate: 48000, |
587 | | }) |
588 | | .build(Processor::ShuffleChannels { |
589 | | channel_indexes: vec![0, 1], |
590 | | }) |
591 | | .unwrap(); |
592 | | assert_eq!(pipeline.vec.len(), 0, "should optimize"); |
593 | | } |
594 | | |
595 | | #[test] |
596 | | fn check_format() { |
597 | | let Err(err) = PipelineBuilder::new(Format { |
598 | | channels: 2, |
599 | | block_size: 2, |
600 | | frame_rate: 48000, |
601 | | }) |
602 | | .build(Processor::CheckFormat { |
603 | | channels: Some(3), |
604 | | block_size: None, |
605 | | frame_rate: None, |
606 | | }) else { |
607 | | panic!("should fail"); |
608 | | }; |
609 | | assert!(err.to_string().contains("expected channels 3"), "{err}"); |
610 | | |
611 | | let Err(err) = PipelineBuilder::new(Format { |
612 | | channels: 2, |
613 | | block_size: 2, |
614 | | frame_rate: 48000, |
615 | | }) |
616 | | .build(Processor::CheckFormat { |
617 | | channels: None, |
618 | | block_size: Some(1), |
619 | | frame_rate: None, |
620 | | }) else { |
621 | | panic!("should fail"); |
622 | | }; |
623 | | assert!(err.to_string().contains("expected block_size 1"), "{err}"); |
624 | | |
625 | | let Err(err) = PipelineBuilder::new(Format { |
626 | | channels: 2, |
627 | | block_size: 2, |
628 | | frame_rate: 48000, |
629 | | }) |
630 | | .build(Processor::CheckFormat { |
631 | | channels: None, |
632 | | block_size: None, |
633 | | frame_rate: Some(99999), |
634 | | }) else { |
635 | | panic!("should fail"); |
636 | | }; |
637 | | assert!( |
638 | | err.to_string().contains("expected frame_rate 99999"), |
639 | | "{err}" |
640 | | ); |
641 | | } |
642 | | |
643 | | #[test] |
644 | | fn peer() { |
645 | | let mut input: MultiBuffer<f32> = |
646 | | MultiBuffer::from(vec![vec![1., 2., 3., 4.], vec![5., 6., 7., 8.]]); |
647 | | |
648 | | let input_format = Format { |
649 | | channels: 2, |
650 | | block_size: 4, |
651 | | frame_rate: 48000, |
652 | | }; |
653 | | |
654 | | let mut pipeline = PipelineBuilder::new(input_format) |
655 | | .build(Processor::Peer { |
656 | | processor: Box::new(Processor::Negate), |
657 | | }) |
658 | | .unwrap(); |
659 | | |
660 | | let output = pipeline.process(input.as_multi_slice()).unwrap(); |
661 | | |
662 | | // output = abs(input) |
663 | | assert_eq!( |
664 | | output.into_raw(), |
665 | | [[-1., -2., -3., -4.], [-5., -6., -7., -8.]] |
666 | | ); |
667 | | } |
668 | | } |
669 | | |
670 | | #[cfg(test)] |
671 | | #[cfg(feature = "bazel")] |
672 | | mod bazel_tests { |
673 | | use std::env; |
674 | | use std::sync::mpsc::channel; |
675 | | use std::time::Duration; |
676 | | |
677 | | use crate::config::PipelineBuilder; |
678 | | use crate::config::Processor; |
679 | | use crate::AudioProcessor; |
680 | | use crate::Format; |
681 | | use crate::MultiBuffer; |
682 | | |
683 | | #[test] |
684 | | fn abs_process() { |
685 | | let mut input: MultiBuffer<f32> = |
686 | | MultiBuffer::from(vec![vec![1., -2., 3., -4.], vec![5., -6., 7., -8.]]); |
687 | | |
688 | | let mut pipeline = PipelineBuilder::new(Format { |
689 | | channels: 2, |
690 | | block_size: 4, |
691 | | frame_rate: 48000, |
692 | | }) |
693 | | .build(Processor::Plugin { |
694 | | path: std::env::var("LIBTEST_PLUGINS_SO").unwrap().into(), |
695 | | constructor: "abs_processor_create".into(), |
696 | | }) |
697 | | .unwrap(); |
698 | | |
699 | | let output = pipeline.process(input.as_multi_slice()).unwrap(); |
700 | | |
701 | | // output = abs(input) |
702 | | assert_eq!(output.into_raw(), [[1., 2., 3., 4.], [5., 6., 7., 8.]]); |
703 | | } |
704 | | |
705 | | #[test] |
706 | | fn profile() { |
707 | | let test_plugin_path: std::path::PathBuf = env::var("LIBTEST_PLUGINS_SO").unwrap().into(); |
708 | | let (sender, receiver) = channel(); |
709 | | |
710 | | let pipeline = PipelineBuilder::new(Format { |
711 | | channels: 2, |
712 | | block_size: 2, |
713 | | frame_rate: 48000, |
714 | | }) |
715 | | .with_profile_sender(sender) |
716 | | .build(Processor::Pipeline { |
717 | | processors: vec![ |
718 | | Processor::Plugin { |
719 | | path: test_plugin_path.clone(), |
720 | | constructor: "abs_processor_create".into(), |
721 | | }, |
722 | | Processor::WrapChunk { |
723 | | inner: Box::new(Processor::Pipeline { |
724 | | processors: vec![ |
725 | | Processor::Plugin { |
726 | | path: test_plugin_path.clone(), |
727 | | constructor: "negate_processor_create".into(), |
728 | | }, |
729 | | Processor::Plugin { |
730 | | path: test_plugin_path.clone(), |
731 | | constructor: "echo_processor_create".into(), |
732 | | }, |
733 | | ], |
734 | | }), |
735 | | inner_block_size: 4096, |
736 | | disallow_hoisting: false, |
737 | | }, |
738 | | ], |
739 | | }) |
740 | | .unwrap(); |
741 | | |
742 | | assert!(receiver.recv_timeout(Duration::ZERO).is_err()); |
743 | | |
744 | | drop(pipeline); |
745 | | let mut keys = receiver |
746 | | .into_iter() |
747 | | .map(|stat| stat.key) |
748 | | .collect::<Vec<_>>(); |
749 | | keys.sort(); |
750 | | let basename = test_plugin_path.file_name().unwrap().to_str().unwrap(); |
751 | | assert_eq!( |
752 | | keys, |
753 | | [ |
754 | | format!("abs_processor_create@{basename}"), |
755 | | format!("echo_processor_create@{basename}"), |
756 | | format!("negate_processor_create@{basename}") |
757 | | ] |
758 | | ); |
759 | | } |
760 | | } |