Coverage Report

Created: 2026-08-08 07:00

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/adhd/cras/server/processor/src/lib.rs
Line
Count
Source
1
// Copyright 2023 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
use std::path::PathBuf;
7
use std::ptr::NonNull;
8
use std::sync::atomic::AtomicUsize;
9
use std::sync::atomic::Ordering;
10
11
use anyhow::ensure;
12
use anyhow::Context;
13
use audio_processor::cdcfg;
14
use audio_processor::cdcfg::ResolverContext;
15
use audio_processor::config::PipelineBuilder;
16
use audio_processor::config::PreloadedProcessor;
17
use audio_processor::config::Processor;
18
use audio_processor::processors::binding::plugin_processor;
19
use audio_processor::processors::export_plugin;
20
use audio_processor::processors::peer::AudioWorkerSubprocessFactory;
21
use audio_processor::processors::CheckShape;
22
use audio_processor::processors::PluginProcessor;
23
use audio_processor::processors::ThreadedProcessor;
24
use audio_processor::AudioProcessor;
25
use audio_processor::Format;
26
use audio_processor::Pipeline;
27
use cras_common::types_internal::CrasProcessorEffect;
28
use cras_dlc::get_dlc_state_cached;
29
use cras_s2::global::cras_s2_get_beamforming_config_path;
30
use cras_s2::global::cras_s2_get_cras_processor_vars;
31
32
mod processor_override;
33
mod proto;
34
35
#[repr(C)]
36
#[derive(Clone, Copy, Debug)]
37
pub enum CrasProcessorWrapMode {
38
    WrapModeNone,
39
    /// Run the processor pipeline in a separate, dedicated thread.
40
    WrapModeDedicatedThread,
41
    /// Run the processor pipeline with a ChunkWrapper with the inner block
42
    /// size set to  [`CrasProcessorConfig::block_size`].
43
    /// In this mode, the caller is allowed to run the pipeline with a block
44
    /// size that is different from  [`CrasProcessorConfig::block_size`].
45
    WrapModeChunk,
46
    /// Like `WrapModeChunk` but the pipeline is run inside a peer processor (sandbox).
47
    /// [`CrasProcessorConfig::max_block_size`] must be set in this mode.
48
    /// WAVE dump is not supported in this mode.
49
    WrapModePeerChunk,
50
}
51
52
#[repr(C)]
53
#[derive(Clone, Debug)]
54
pub struct CrasProcessorConfig {
55
    // The number of channels after apm_processor.
56
    channels: usize,
57
    block_size: usize,
58
    frame_rate: usize,
59
60
    effect: CrasProcessorEffect,
61
62
    wrap_mode: CrasProcessorWrapMode,
63
64
    // Enable processing dumps as WAVE files.
65
    wav_dump: bool,
66
67
    /// The max block size when wrap_mode is WrapModePeerChunk.
68
    /// Used to determine buffer size to allocate for peer IPC.
69
    max_block_size: usize,
70
}
71
72
impl CrasProcessorConfig {
73
0
    fn format(&self) -> Format {
74
0
        Format {
75
0
            channels: self.channels,
76
0
            block_size: self.block_size,
77
0
            frame_rate: self.frame_rate,
78
0
        }
79
0
    }
80
}
81
82
pub struct CrasProcessor {
83
    id: usize,
84
    pipeline: Pipeline,
85
    config: CrasProcessorConfig,
86
}
87
88
impl AudioProcessor for CrasProcessor {
89
    type I = f32;
90
    type O = f32;
91
92
0
    fn process<'a>(
93
0
        &'a mut self,
94
0
        input: audio_processor::MultiSlice<'a, Self::I>,
95
0
    ) -> audio_processor::Result<audio_processor::MultiSlice<'a, Self::O>> {
96
0
        self.pipeline.process(input)
97
0
    }
98
99
0
    fn get_output_format(&self) -> audio_processor::Format {
100
0
        self.pipeline.get_output_format()
101
0
    }
102
}
103
104
struct CrasProcessorResolverContext {
105
    wav_dump_root: Option<PathBuf>,
106
    cras_processor_format: Format,
107
}
108
109
impl audio_processor::cdcfg::ResolverContext for CrasProcessorResolverContext {
110
0
    fn get_wav_dump_root(&self) -> Option<&Path> {
111
0
        self.wav_dump_root.as_deref()
112
0
    }
113
114
0
    fn get_dlc_root_path(&self, dlc_id: &str) -> anyhow::Result<PathBuf> {
115
0
        let dlc_state = get_dlc_state_cached(dlc_id);
116
0
        ensure!(dlc_state.installed, "{dlc_id} not installed");
117
0
        Ok(dlc_state.root_path.into())
118
0
    }
119
120
0
    fn get_duplicate_channel_0(&self) -> Option<usize> {
121
        // Duplicate channel 0 to the cras_processor count when requested.
122
0
        Some(self.cras_processor_format.channels)
123
0
    }
124
}
125
126
static GLOBAL_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
127
128
0
fn get_noise_cancellation_pipeline_decl(
129
0
    context: &dyn ResolverContext,
130
0
) -> anyhow::Result<Processor> {
131
0
    cdcfg::parse(
132
0
        context,
133
0
        &cras_s2_get_cras_processor_vars(),
134
0
        Path::new("/etc/cras/processor/noise_cancellation.txtpb"),
135
    )
136
0
}
137
138
0
fn get_style_transfer_pipeline_decl(context: &dyn ResolverContext) -> anyhow::Result<Processor> {
139
0
    cdcfg::parse(
140
0
        context,
141
0
        &cras_s2_get_cras_processor_vars(),
142
0
        Path::new("/etc/cras/processor/style_transfer.txtpb"),
143
    )
144
0
}
145
146
0
fn get_beamforming_pipeline_decl(context: &dyn ResolverContext) -> anyhow::Result<Processor> {
147
0
    cdcfg::parse(
148
0
        context,
149
0
        &cras_s2_get_cras_processor_vars(),
150
0
        &cras_s2_get_beamforming_config_path().context("beamforming config path unknown")?,
151
    )
152
0
}
153
154
0
fn get_echo_pipeline_decl(context: &dyn ResolverContext) -> anyhow::Result<Processor> {
155
0
    cdcfg::parse(
156
0
        context,
157
0
        &cras_s2_get_cras_processor_vars(),
158
0
        Path::new("/etc/cras/processor/echo.txtpb"),
159
    )
160
0
}
161
162
0
fn get_speaker_pipeline_decl(context: &dyn ResolverContext) -> anyhow::Result<Processor> {
163
0
    cdcfg::parse(
164
0
        context,
165
0
        &cras_s2_get_cras_processor_vars(),
166
0
        Path::new("/etc/cras/processor/speaker_plugin.txtpb"),
167
    )
168
0
}
169
170
0
fn get_headphone_pipeline_decl(context: &dyn ResolverContext) -> anyhow::Result<Processor> {
171
0
    cdcfg::parse(
172
0
        context,
173
0
        &cras_s2_get_cras_processor_vars(),
174
0
        Path::new("/etc/cras/processor/headphone_plugin.txtpb"),
175
    )
176
0
}
177
178
0
fn get_krisp_pipeline_decl(context: &dyn ResolverContext) -> anyhow::Result<Processor> {
179
0
    cdcfg::parse(
180
0
        context,
181
0
        &cras_s2_get_cras_processor_vars(),
182
0
        Path::new("/etc/cras/processor/krisp_nc.txtpb"),
183
    )
184
0
}
185
186
impl CrasProcessor {
187
0
    fn new(
188
0
        mut config: CrasProcessorConfig,
189
0
        apm_processor: Option<PluginProcessor>,
190
0
    ) -> anyhow::Result<Self> {
191
0
        let override_config = processor_override::read_system_config().input;
192
0
        if override_config.enabled {
193
0
            config.effect = CrasProcessorEffect::Overridden;
194
0
        }
195
0
        let config = config;
196
197
0
        let id = GLOBAL_ID_COUNTER.fetch_add(1, Ordering::AcqRel);
198
199
0
        let dump_base = PathBuf::from(format!("/run/cras/debug/cras_processor_{id}"));
200
201
0
        let resolver_context = CrasProcessorResolverContext {
202
0
            wav_dump_root: if config.wav_dump {
203
0
                Some(dump_base.clone())
204
            } else {
205
0
                None
206
            },
207
0
            cras_processor_format: config.format(),
208
        };
209
0
        let mut decl = vec![];
210
211
0
        if config.wav_dump {
212
0
            std::fs::create_dir_all(&dump_base).context("mkdir dump_base")?;
213
0
            decl.push(Processor::WavSink {
214
0
                path: dump_base.join("input.wav"),
215
0
            });
216
0
        }
217
218
0
        if let Some(apm_processor) = apm_processor {
219
0
            decl.push(Processor::Preloaded(PreloadedProcessor {
220
0
                description: "apm",
221
0
                processor: Box::new(apm_processor),
222
0
            }));
223
0
            decl.push(Processor::Preloaded(PreloadedProcessor {
224
0
                description: "CheckShape",
225
0
                processor: Box::new(CheckShape::new(config.format())),
226
0
            }));
227
228
0
            if config.wav_dump {
229
0
                decl.push(Processor::WavSink {
230
0
                    path: dump_base.join("post_apm.wav"),
231
0
                });
232
0
            }
233
0
        }
234
235
0
        match config.effect {
236
0
            CrasProcessorEffect::NoEffects => {
237
0
                // Do nothing.
238
0
            }
239
0
            CrasProcessorEffect::Negate => {
240
0
                decl.push(Processor::Negate);
241
0
            }
242
            CrasProcessorEffect::NoiseCancellation | CrasProcessorEffect::StyleTransfer => {
243
0
                decl.push(Processor::ShuffleChannels {
244
0
                    channel_indexes: vec![0],
245
0
                });
246
0
                decl.push(
247
0
                    get_noise_cancellation_pipeline_decl(&resolver_context)
248
0
                        .context("failed get_noise_cancellation_pipeline_decl")?,
249
                );
250
0
                if let CrasProcessorEffect::StyleTransfer = config.effect {
251
0
                    decl.push(
252
0
                        get_style_transfer_pipeline_decl(&resolver_context)
253
0
                            .context("failed get_style_transfer_pipeline_decl")?,
254
                    );
255
0
                }
256
0
                decl.push(Processor::ShuffleChannels {
257
0
                    channel_indexes: vec![0; config.channels],
258
0
                });
259
            }
260
            CrasProcessorEffect::Beamforming => {
261
0
                decl.push(
262
0
                    get_beamforming_pipeline_decl(&resolver_context)
263
0
                        .context("failed when creating beamforming pipeline")?,
264
                );
265
            }
266
            CrasProcessorEffect::GenerateEcho => {
267
0
                decl.push(
268
0
                    get_echo_pipeline_decl(&resolver_context)
269
0
                        .context("failed when creating echo pipeline")?,
270
                );
271
            }
272
            CrasProcessorEffect::SpeakerPlugin => {
273
0
                decl.push(
274
0
                    get_speaker_pipeline_decl(&resolver_context)
275
0
                        .context("failed when creating speaker plugin pipeline")?,
276
                );
277
            }
278
            CrasProcessorEffect::HeadphonePlugin => {
279
0
                decl.push(
280
0
                    get_headphone_pipeline_decl(&resolver_context)
281
0
                        .context("failed when creating headphone plugin pipeline")?,
282
                );
283
            }
284
            CrasProcessorEffect::KrispNC => {
285
0
                decl.push(Processor::ShuffleChannels {
286
0
                    channel_indexes: vec![0],
287
0
                });
288
0
                decl.push(
289
0
                    get_krisp_pipeline_decl(&resolver_context)
290
0
                        .context("failed get_krisp_pipeline_decl")?,
291
                );
292
0
                decl.push(Processor::ShuffleChannels {
293
0
                    channel_indexes: vec![0; config.channels],
294
0
                });
295
            }
296
            CrasProcessorEffect::Overridden => {
297
0
                if override_config.frame_rate != 0 {
298
0
                    decl.push(Processor::Resample {
299
0
                        output_frame_rate: override_config.frame_rate as usize,
300
0
                    });
301
0
                }
302
0
                let plugin = Processor::Plugin {
303
0
                    path: override_config.plugin_path.clone().into(),
304
0
                    constructor: override_config.constructor.clone(),
305
0
                };
306
0
                decl.push(match override_config.block_size {
307
0
                    0 => plugin, // Use existing block size if 0.
308
0
                    block_size => Processor::WrapChunk {
309
0
                        inner_block_size: block_size as usize,
310
0
                        inner: Box::new(plugin),
311
0
                        disallow_hoisting: false,
312
0
                    },
313
                });
314
            }
315
        };
316
317
        // Resample to input rate.
318
0
        decl.push(Processor::Resample {
319
0
            output_frame_rate: config.frame_rate,
320
0
        });
321
322
        // Check that the input format is the same as the output format.
323
0
        decl.push(Processor::CheckFormat {
324
0
            channels: Some(config.channels),
325
0
            block_size: Some(config.block_size),
326
0
            frame_rate: Some(config.frame_rate),
327
0
        });
328
329
0
        if config.wav_dump {
330
0
            decl.push(Processor::WavSink {
331
0
                path: dump_base.join("output.wav"),
332
0
            });
333
0
        }
334
335
0
        let mut pipeline = Processor::Pipeline { processors: decl };
336
0
        if matches!(
337
0
            config.wrap_mode,
338
            CrasProcessorWrapMode::WrapModeChunk | CrasProcessorWrapMode::WrapModePeerChunk
339
        ) {
340
0
            pipeline = Processor::WrapChunk {
341
0
                inner: Box::new(pipeline),
342
0
                inner_block_size: config.block_size,
343
0
                disallow_hoisting: true,
344
0
            };
345
0
            if matches!(config.wrap_mode, CrasProcessorWrapMode::WrapModePeerChunk) {
346
0
                pipeline = Processor::Peer {
347
0
                    processor: Box::new(pipeline),
348
0
                };
349
0
            }
350
0
        }
351
352
0
        let decl_debug = format!("{pipeline:?}");
353
0
        let pipeline = PipelineBuilder::new(Format {
354
0
            block_size: if matches!(config.wrap_mode, CrasProcessorWrapMode::WrapModePeerChunk) {
355
0
                assert_ne!(config.max_block_size, 0);
356
0
                config.max_block_size // Used for allocation.
357
            } else {
358
0
                config.block_size
359
            },
360
0
            ..config.format()
361
        })
362
        // TODO(b/349784210): Use a hardened worker factory.
363
0
        .with_worker_factory(AudioWorkerSubprocessFactory::default().with_set_thread_priority())
364
0
        .build(pipeline)
365
0
        .context("failed to build pipeline")?;
366
367
0
        log::info!("CrasProcessor #{id} created with: {config:?}");
368
0
        log::info!("CrasProcessor #{id} pipeline: {decl_debug}");
369
370
0
        Ok(CrasProcessor {
371
0
            id,
372
0
            pipeline,
373
0
            config,
374
0
        })
375
0
    }
376
}
377
378
impl Drop for CrasProcessor {
379
0
    fn drop(&mut self) {
380
0
        log::info!("CrasProcessor #{} dropped", self.id);
381
0
    }
382
}
383
384
#[repr(C)]
385
pub struct CrasProcessorCreateResult {
386
    /// The created processor.
387
    pub plugin_processor: *mut plugin_processor,
388
    /// The actual effect used in the processor.
389
    /// Might be different from what was passed to cras_processor_create.
390
    pub effect: CrasProcessorEffect,
391
}
392
393
impl CrasProcessorCreateResult {
394
0
    fn none() -> Self {
395
0
        Self {
396
0
            plugin_processor: std::ptr::null_mut(),
397
0
            effect: CrasProcessorEffect::NoEffects,
398
0
        }
399
0
    }
400
}
401
402
0
unsafe fn create_apm_processor(
403
0
    config: &CrasProcessorConfig,
404
0
    apm_plugin_processor: *mut plugin_processor,
405
0
) -> anyhow::Result<Option<PluginProcessor>> {
406
0
    let apm_processor = match NonNull::new(apm_plugin_processor) {
407
0
        Some(apm_plugin_processor) => Some(
408
0
            PluginProcessor::from_handle(apm_plugin_processor.as_ptr(), config.format())
409
0
                .context("failed PluginProcessor::from_handle")?,
410
        ),
411
0
        None => None,
412
    };
413
0
    Ok(apm_processor)
414
0
}
415
416
/// Create a CRAS processor.
417
///
418
/// Returns the created processor (might be NULL), and the applied effect.
419
///
420
/// # Safety
421
///
422
/// `config` must point to a CrasProcessorConfig struct.
423
/// `apm_plugin_processor` must point to a plugin_processor or NULL.
424
#[no_mangle]
425
0
pub unsafe extern "C" fn cras_processor_create(
426
0
    config: *const CrasProcessorConfig,
427
0
    apm_plugin_processor: *mut plugin_processor,
428
0
) -> CrasProcessorCreateResult {
429
0
    let config = match config.as_ref() {
430
0
        Some(config) => config,
431
        None => {
432
0
            return CrasProcessorCreateResult::none();
433
        }
434
    };
435
436
0
    let apm_processor = match create_apm_processor(config, apm_plugin_processor) {
437
0
        Ok(apm_processor) => apm_processor,
438
0
        Err(err) => {
439
0
            log::error!("{err:#}");
440
0
            return CrasProcessorCreateResult::none();
441
        }
442
    };
443
444
0
    let processor = match CrasProcessor::new(config.clone(), apm_processor) {
445
0
        Ok(processor) => processor,
446
0
        Err(err) => {
447
0
            log::error!(
448
0
                "CrasProcessor::new failed with {:#}, creating no-op processor",
449
                err
450
            );
451
452
0
            let config = config.clone();
453
0
            CrasProcessor::new(
454
0
                CrasProcessorConfig {
455
0
                    effect: CrasProcessorEffect::NoEffects,
456
0
                    ..config
457
0
                },
458
                // apm_processor was consumed so create it again.
459
0
                create_apm_processor(&config, apm_plugin_processor).expect("create_apm_processor should not fail given that we created it successfully once"),
460
            )
461
0
            .expect("CrasProcessor::new with CrasProcessorEffect::NoEffects should never fail")
462
        }
463
    };
464
465
0
    let effect = processor.config.effect;
466
0
    let plugin_processor = if matches!(
467
0
        config.wrap_mode,
468
        CrasProcessorWrapMode::WrapModeDedicatedThread
469
    ) {
470
0
        let threaded_processor = ThreadedProcessor::new(processor, 1);
471
0
        export_plugin(threaded_processor)
472
    } else {
473
0
        export_plugin(processor)
474
    };
475
0
    CrasProcessorCreateResult {
476
0
        plugin_processor,
477
0
        effect,
478
0
    }
479
0
}
480
481
/// Returns true if override is enabled in the system config file.
482
#[no_mangle]
483
0
pub extern "C" fn cras_processor_is_override_enabled() -> bool {
484
0
    processor_override::read_system_config().input.enabled
485
0
}