diff --git a/audio_processor/proto/cdcfg.proto b/audio_processor/proto/cdcfg.proto index 6f7d510a9..e89fe7996 100644 --- a/audio_processor/proto/cdcfg.proto +++ b/audio_processor/proto/cdcfg.proto @@ -38,12 +38,21 @@ message Plugin { message DlcPlugin { // The ID of the DLC. // Also known as the `--id` option of `dlcservice_util`. - string dlc_id = 1; + oneof dlc_id_oneof { + string dlc_id = 1; + string dlc_id_var = 4; + } // The path within the DLC root. - string path = 2; + oneof path_oneof { + string path = 2; + string path_var = 5; + } // The name of the processor_create function. // See also plugin_processor.h. - string constructor = 3; + oneof constructor_oneof { + string constructor = 3; + string constructor_var = 6; + } } // Wrap the inner audio processor so that it always receives diff --git a/audio_processor/src/bin/offline-pipeline.rs b/audio_processor/src/bin/offline-pipeline.rs index bb79727f1..1b40d7356 100644 --- a/audio_processor/src/bin/offline-pipeline.rs +++ b/audio_processor/src/bin/offline-pipeline.rs @@ -155,7 +155,8 @@ fn run(command: Command) { constructor: command.plugin_name, }, PluginOrPipeline::Pipeline(path) => { - parse(&NaiveResolverContext::default(), &path).unwrap_or_else(|err| panic!("{err:#}")) + parse(&NaiveResolverContext::default(), &Default::default(), &path) + .unwrap_or_else(|err| panic!("{err:#}")) } }; let pipeline_decl = vec![ diff --git a/audio_processor/src/cdcfg.rs b/audio_processor/src/cdcfg.rs index 02dce863c..485a25edc 100644 --- a/audio_processor/src/cdcfg.rs +++ b/audio_processor/src/cdcfg.rs @@ -3,6 +3,7 @@ // found in the LICENSE file. use std::cell::RefCell; +use std::collections::HashMap; use std::collections::HashSet; use std::path::Path; use std::path::PathBuf; @@ -11,6 +12,9 @@ use anyhow::bail; use anyhow::Context; use crate::config::Processor; +use crate::proto::cdcfg::dlc_plugin::Constructor_oneof; +use crate::proto::cdcfg::dlc_plugin::Dlc_id_oneof; +use crate::proto::cdcfg::dlc_plugin::Path_oneof; pub trait ResolverContext { fn get_wav_dump_root(&self) -> Option<&Path>; @@ -62,8 +66,53 @@ impl ResolverContext for DlcIdCollector { } } +enum ConstOrVar<'a> { + Const(&'a str), + Var(&'a str), +} + +impl<'a> From<&'a Dlc_id_oneof> for ConstOrVar<'a> { + fn from(value: &'a Dlc_id_oneof) -> Self { + match value { + Dlc_id_oneof::DlcId(s) => ConstOrVar::Const(s), + Dlc_id_oneof::DlcIdVar(s) => ConstOrVar::Var(s), + } + } +} + +impl<'a> From<&'a Path_oneof> for ConstOrVar<'a> { + fn from(value: &'a Path_oneof) -> Self { + match value { + Path_oneof::Path(s) => ConstOrVar::Const(s), + Path_oneof::PathVar(s) => ConstOrVar::Var(s), + } + } +} + +impl<'a> From<&'a Constructor_oneof> for ConstOrVar<'a> { + fn from(value: &'a Constructor_oneof) -> Self { + match value { + Constructor_oneof::Constructor(s) => ConstOrVar::Const(s), + Constructor_oneof::ConstructorVar(s) => ConstOrVar::Var(s), + } + } +} + +fn get_const_or_var_str<'a>( + vars: &'a HashMap, + const_or_var: ConstOrVar<'a>, +) -> anyhow::Result<&'a str> { + match const_or_var { + ConstOrVar::Const(s) => Ok(s), + ConstOrVar::Var(s) => Ok(vars + .get(s) + .with_context(|| format!("var {s:?} not found"))?), + } +} + fn resolve( context: &dyn ResolverContext, + vars: &HashMap, proto: &crate::proto::cdcfg::Processor, ) -> anyhow::Result { let Some(processor) = &proto.processor_oneof else { @@ -83,13 +132,35 @@ fn resolve( }, DlcPlugin(dlc_plugin) => Processor::Plugin { path: context - .get_dlc_root_path(&dlc_plugin.dlc_id) + .get_dlc_root_path(get_const_or_var_str( + vars, + dlc_plugin + .dlc_id_oneof + .as_ref() + .context("missing dlc_id")? + .into(), + )?) .context("context.dlc_root_path")? - .join(&dlc_plugin.path), - constructor: dlc_plugin.constructor.clone(), + .join(get_const_or_var_str( + vars, + dlc_plugin + .path_oneof + .as_ref() + .context("missing path")? + .into(), + )?), + constructor: get_const_or_var_str( + vars, + dlc_plugin + .constructor_oneof + .as_ref() + .context("missing constructor")? + .into(), + )? + .to_string(), }, WrapChunk(wrap_chunk) => Processor::WrapChunk { - inner: Box::new(resolve(context, &wrap_chunk.inner).context("wrap_chunk inner")?), + inner: Box::new(resolve(context, vars, &wrap_chunk.inner).context("wrap_chunk inner")?), inner_block_size: wrap_chunk .inner_block_size .try_into() @@ -107,7 +178,8 @@ fn resolve( .iter() .enumerate() .map(|(i, processor)| -> anyhow::Result { - resolve(context, processor).with_context(|| format!("pipeline processor {i}")) + resolve(context, vars, processor) + .with_context(|| format!("pipeline processor {i}")) }) .collect::, _>>()?, }, @@ -132,7 +204,7 @@ fn resolve( frame_rate: positive_or_none(check_format.frame_rate), }, Peer(peer) => Processor::Peer { - processor: Box::new(resolve(context, &peer.processor).context("peer.processor")?), + processor: Box::new(resolve(context, vars, &peer.processor).context("peer.processor")?), }, }) } @@ -145,42 +217,70 @@ fn positive_or_none(val: i32) -> Option { } } -fn resolve_str(context: &dyn ResolverContext, s: &str) -> anyhow::Result { +fn resolve_str( + context: &dyn ResolverContext, + vars: &HashMap, + s: &str, +) -> anyhow::Result { let cfg: crate::proto::cdcfg::Processor = protobuf::text_format::parse_from_str(s).context("protobuf text_format error")?; - resolve(context, &cfg) + resolve(context, vars, &cfg) } -pub fn parse(context: &dyn ResolverContext, path: &Path) -> anyhow::Result { +pub fn parse( + context: &dyn ResolverContext, + vars: &HashMap, + path: &Path, +) -> anyhow::Result { let s = std::fs::read_to_string(path) .with_context(|| format!("read_to_string {}", path.display()))?; - resolve_str(context, &s) + resolve_str(context, vars, &s) } /// Get all DLC IDs specified in a given processing config. -pub fn get_required_dlcs(path: &Path) -> anyhow::Result> { +pub fn get_required_dlcs( + vars: &HashMap, + path: &Path, +) -> anyhow::Result> { let resolver = DlcIdCollector::default(); - parse(&resolver, path)?; + parse(&resolver, vars, path)?; Ok(resolver.dlcs.take()) } #[cfg(test)] mod tests { + use std::collections::HashMap; use std::collections::HashSet; use std::path::PathBuf; use super::resolve_str; use super::NaiveResolverContext; + use super::ResolverContext; use crate::cdcfg::DlcIdCollector; use crate::config::Processor; + fn assert_resolve_error( + context: &dyn ResolverContext, + vars: &HashMap, + s: &str, + err_substring: &str, + ) { + let err = resolve_str(context, vars, s).unwrap_err(); + assert!( + err.to_string().contains(err_substring), + "{:?} does not contain {err_substring}", + err.to_string() + ); + } + #[test] fn invalid_text_format() { let context: NaiveResolverContext = Default::default(); - let err = resolve_str(&context, "abcd").unwrap_err(); - assert!( - err.to_string().contains("protobuf text_format error"), - "{err}" + assert_resolve_error( + &context, + &HashMap::new(), + "abcd", + "protobuf text_format error", ); } @@ -189,6 +289,7 @@ mod tests { let context: NaiveResolverContext = Default::default(); let processor = resolve_str( &context, + &HashMap::new(), r#"pipeline { processors { wrap_chunk { @@ -262,6 +363,7 @@ mod tests { let context: NaiveResolverContext = Default::default(); let err = resolve_str( &context, + &HashMap::new(), r#"pipeline { processors { shuffle_channels { @@ -290,14 +392,14 @@ mod tests { let spec = r#"maybe_wav_dump { filename: "a.wav" }"#; let context: NaiveResolverContext = Default::default(); - let processor = resolve_str(&context, spec).unwrap(); + let processor = resolve_str(&context, &HashMap::new(), spec).unwrap(); assert_eq!(processor, Processor::Nothing); let context = NaiveResolverContext { wav_dump_root: Some(PathBuf::from("/tmp")), ..Default::default() }; - let processor = resolve_str(&context, spec).unwrap(); + let processor = resolve_str(&context, &HashMap::new(), spec).unwrap(); assert_eq!( processor, Processor::WavSink { @@ -318,7 +420,7 @@ pipeline { dlc_plugin { dlc_id: "second-dlc" path: "libsecond.so" constructor: "plugin_processor_create2" } } }"#; - let processor = resolve_str(&context, spec).unwrap(); + let processor = resolve_str(&context, &HashMap::new(), spec).unwrap(); assert_eq!( processor, Processor::Pipeline { @@ -340,26 +442,82 @@ pipeline { ); let context = DlcIdCollector::default(); - resolve_str(&context, spec).unwrap(); + resolve_str(&context, &HashMap::new(), spec).unwrap(); assert_eq!( context.dlcs.borrow().clone(), HashSet::from(["nc-ap-dlc".to_string(), "second-dlc".to_string()]) ); } + #[test] + fn dlc_missing_fields() { + let context: NaiveResolverContext = Default::default(); + assert_resolve_error( + &context, + &HashMap::new(), + r#"dlc_plugin { path: "libdenoiser.so" constructor: "plugin_processor_create" }"#, + "missing dlc_id", + ); + assert_resolve_error( + &context, + &HashMap::new(), + r#"dlc_plugin { dlc_id: "nc-ap-dlc" constructor: "plugin_processor_create" }"#, + "missing path", + ); + assert_resolve_error( + &context, + &HashMap::new(), + r#"dlc_plugin { dlc_id: "nc-ap-dlc" path: "libdenoiser.so" }"#, + "missing constructor", + ); + } + + #[test] + fn dlc_var() { + let context: NaiveResolverContext = Default::default(); + let vars = HashMap::from_iter( + [("a", "aaa"), ("b", "bbb"), ("c", "ccc")] + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())), + ); + let spec = r#"dlc_plugin { dlc_id_var: "a" path_var: "b" constructor_var: "c" }"#; + let processor = resolve_str(&context, &vars, spec).unwrap(); + assert_eq!( + processor, + Processor::Plugin { + path: PathBuf::from("/run/imageloader/aaa/package/root/bbb"), + constructor: "ccc".into(), + } + ); + } + + #[test] + fn dlc_var_missing() { + let context: NaiveResolverContext = Default::default(); + assert_resolve_error( + &context, + &HashMap::new(), + r#"dlc_plugin { dlc_id_var: "xyz" path: "libdenoiser.so" constructor: "plugin_processor_create" }"#, + r#"var "xyz" not found"#, + ); + } + #[test] fn duplicate_channel_0() { let spec = "maybe_duplicate_channel_0 {}"; let context: NaiveResolverContext = Default::default(); - assert_eq!(resolve_str(&context, spec).unwrap(), Processor::Nothing); + assert_eq!( + resolve_str(&context, &HashMap::new(), spec).unwrap(), + Processor::Nothing + ); let context = NaiveResolverContext { duplicate_channel_0: Some(2), ..Default::default() }; assert_eq!( - resolve_str(&context, spec).unwrap(), + resolve_str(&context, &HashMap::new(), spec).unwrap(), Processor::ShuffleChannels { channel_indexes: vec![0; 2] } diff --git a/audio_processor/src/processors/profile.rs b/audio_processor/src/processors/profile.rs index 6b34c432d..678609683 100644 --- a/audio_processor/src/processors/profile.rs +++ b/audio_processor/src/processors/profile.rs @@ -209,7 +209,7 @@ mod tests { #[test] fn measurement_display_empty() { let m = Measurement::default(); - format!("{}", m); // should not panic, e.g. division by zero + drop(format!("{}", m)); // should not panic, e.g. division by zero } #[test] diff --git a/cras-config/for_all_boards/processor/beamforming.txtpb b/cras-config/for_all_boards/processor/beamforming.txtpb index 99fca0675..f583591d2 100644 --- a/cras-config/for_all_boards/processor/beamforming.txtpb +++ b/cras-config/for_all_boards/processor/beamforming.txtpb @@ -23,8 +23,8 @@ pipeline { peer { processor { dlc_plugin { - dlc_id: "intelligo-beamforming-dlc" - path: "libigo_processor.so" + dlc_id_var: "beamforming_dlc_id" + path_var: "beamforming_dlc_path" constructor: "plugin_processor_create" } } diff --git a/cras/server/ini/src/lib.rs b/cras/server/ini/src/lib.rs index a39c7ebe4..d95993802 100644 --- a/cras/server/ini/src/lib.rs +++ b/cras/server/ini/src/lib.rs @@ -12,7 +12,7 @@ use anyhow::Context; use indexmap::IndexMap; use utf8cstring::Utf8CString; -type CrasIniMap = IndexMap>; +pub type CrasIniMap = IndexMap>; fn parse_string(s: impl Into) -> anyhow::Result { let mut dict = IndexMap::>::new(); diff --git a/cras/server/processor/src/lib.rs b/cras/server/processor/src/lib.rs index 70e282ec0..03c71d688 100644 --- a/cras/server/processor/src/lib.rs +++ b/cras/server/processor/src/lib.rs @@ -27,6 +27,7 @@ use audio_processor::ProcessorVec; use cras_common::types_internal::CrasProcessorEffect; use cras_dlc::get_dlc_state_cached; use cras_s2::global::cras_s2_get_beamforming_config_path; +use cras_s2::global::cras_s2_get_cras_processor_vars; mod processor_override; mod proto; @@ -112,6 +113,7 @@ fn get_noise_cancellation_pipeline_decl( ) -> anyhow::Result { cdcfg::parse( context, + &cras_s2_get_cras_processor_vars(), Path::new("/etc/cras/processor/noise_cancellation.txtpb"), ) } @@ -119,6 +121,7 @@ fn get_noise_cancellation_pipeline_decl( fn get_style_transfer_pipeline_decl(context: &dyn ResolverContext) -> anyhow::Result { cdcfg::parse( context, + &cras_s2_get_cras_processor_vars(), Path::new("/etc/cras/processor/style_transfer.txtpb"), ) } @@ -126,6 +129,7 @@ fn get_style_transfer_pipeline_decl(context: &dyn ResolverContext) -> anyhow::Re fn get_beamforming_pipeline_decl(context: &dyn ResolverContext) -> anyhow::Result { cdcfg::parse( context, + &cras_s2_get_cras_processor_vars(), &cras_s2_get_beamforming_config_path().context("beamforming config path unknown")?, ) } diff --git a/cras/server/s2/src/global.rs b/cras/server/s2/src/global.rs index a5536df85..0e99ee7e6 100644 --- a/cras/server/s2/src/global.rs +++ b/cras/server/s2/src/global.rs @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +use std::collections::HashMap; use std::ffi::c_char; use std::ffi::CStr; use std::ffi::CString; @@ -312,3 +313,7 @@ pub fn cras_s2_get_beamforming_config_path() -> Option { pub extern "C" fn cras_s2_get_sr_bt_supported() -> bool { state().input.feature_tier.sr_bt_supported } + +pub fn cras_s2_get_cras_processor_vars() -> HashMap { + state().input.cras_processor_vars.clone() +} diff --git a/cras/server/s2/src/lib.rs b/cras/server/s2/src/lib.rs index 8b630fb89..751fa66d0 100644 --- a/cras/server/s2/src/lib.rs +++ b/cras/server/s2/src/lib.rs @@ -4,6 +4,7 @@ use std::collections::HashMap; use std::collections::HashSet; +use std::path::Path; use cras_common::types_internal::CrasEffectUIAppearance; use cras_common::types_internal::CRAS_NC_PROVIDER; @@ -12,6 +13,7 @@ use cras_common::types_internal::EFFECT_TYPE; use cras_common::types_internal::NC_AP_DLC; use cras_common::types_internal::SR_BT_DLC; use cras_feature_tier::CrasFeatureTier; +use cras_ini::CrasIniMap; use global::ResetIodevListForVoiceIsolation; use processing::BeamformingConfig; use serde::Serialize; @@ -35,6 +37,7 @@ struct Input { style_transfer_featured_allowed: bool, // cros_config /audio/main cras-config-dir. cras_config_dir: String, + cras_processor_vars: HashMap, beamforming_config: BeamformingConfig, active_input_node_compatible_nc_providers: CRAS_NC_PROVIDER, voice_isolation_ui_enabled: bool, @@ -378,7 +381,29 @@ impl S2 { fn read_cras_config(&mut self, cras_config_dir: &str) { self.input.cras_config_dir = cras_config_dir.into(); - self.input.beamforming_config = BeamformingConfig::probe(&self.input.cras_config_dir); + let board_ini = match cras_ini::parse_file( + &Path::new("/etc/cras") + .join(cras_config_dir) + .join("board.ini"), + ) { + Ok(map) => map, + Err(err) => { + log::error!("cannot parse board.ini {err:#}"); + CrasIniMap::default() + } + }; + self.input.cras_processor_vars = match board_ini.get("cras_processor_vars") { + Some(vars) => { + let x = HashMap::from_iter( + vars.iter() + .map(|(k, v)| (k.as_str().to_string(), v.as_str().to_string())), + ); + x + } + None => HashMap::new(), + }; + self.input.beamforming_config = + BeamformingConfig::probe(&board_ini, &self.input.cras_processor_vars); self.update(); } diff --git a/cras/server/s2/src/processing.rs b/cras/server/s2/src/processing.rs index c03c9cb23..d3cd66521 100644 --- a/cras/server/s2/src/processing.rs +++ b/cras/server/s2/src/processing.rs @@ -2,12 +2,14 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +use std::collections::HashMap; use std::collections::HashSet; use std::path::Path; use std::path::PathBuf; use anyhow::Context; use audio_processor::cdcfg; +use cras_ini::CrasIniMap; use serde::Serialize; #[derive(Serialize)] @@ -25,8 +27,8 @@ impl Default for BeamformingConfig { } impl BeamformingConfig { - pub fn probe(cras_config_dir: &str) -> Self { - match BeamformingProperties::probe(cras_config_dir) { + pub fn probe(board_ini: &CrasIniMap, cras_processor_vars: &HashMap) -> Self { + match BeamformingProperties::probe(board_ini, cras_processor_vars) { Ok(properties) => Self::Supported(properties), Err(err) => Self::Unsupported { reason: format!("{err:#}"), @@ -44,13 +46,10 @@ pub struct BeamformingProperties { } impl BeamformingProperties { - fn probe(cras_config_dir: &str) -> anyhow::Result { - let board_ini = cras_ini::parse_file( - &Path::new("/etc/cras") - .join(cras_config_dir) - .join("board.ini"), - ) - .context("cannot parse board.ini")?; + fn probe( + board_ini: &CrasIniMap, + cras_processor_vars: &HashMap, + ) -> anyhow::Result { let pipeline_file = board_ini .get("processing") .context("processing section not found in board.ini")? @@ -59,7 +58,7 @@ impl BeamformingProperties { .as_str() .to_string(); let pipeline_path = Path::new("/etc/cras/processor").join(&pipeline_file); - let required_dlcs = cdcfg::get_required_dlcs(&pipeline_path) + let required_dlcs = cdcfg::get_required_dlcs(cras_processor_vars, &pipeline_path) .context("cannot get required DLCs from pipeline file")?; Ok(BeamformingProperties { pipeline_path,