use crate::contracts::{AudioSink, MetricsCollector, ProcessorBuildRequest, ProcessorFactory};
use crate::core::{AudioProfile, AudioResult, AudioSpec, CaptureDiagnostics, DelayEffectConfig};
use crate::factory::ProfileProcessorFactory;
use crate::metrics::LevelMetrics;
use crate::mic_sim::{MicrophoneSimConfig, MicrophoneSimulatorFactory};
use crate::pipeline::{AudioPipeline, WavSink};
use std::collections::BTreeMap;
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct PipelineBuildRequest {
pub output_path: PathBuf,
pub profile: AudioProfile,
pub sample_rate: u32,
pub device_channels: u16,
pub device_name: Option<String>,
pub gain_db: f32,
pub limiter_threshold: f32,
pub high_pass_hz: f32,
pub noise_suppression_amount: f32,
pub noise_calibration_ms: u32,
pub delay_effect: Option<DelayEffectConfig>,
pub stage_overrides: BTreeMap<String, crate::core::ProcessorOverrideMode>,
pub microphone_sim: MicrophoneSimConfig,
}
pub struct NativePipelineBuilder<F = ProfileProcessorFactory> {
processor_factory: F,
}
impl NativePipelineBuilder<ProfileProcessorFactory> {
pub fn new() -> Self {
Self {
processor_factory: ProfileProcessorFactory::new(),
}
}
}
impl Default for NativePipelineBuilder<ProfileProcessorFactory> {
fn default() -> Self {
Self::new()
}
}
impl<F> NativePipelineBuilder<F>
where
F: ProcessorFactory,
{
#[cfg(test)]
pub fn with_factory(processor_factory: F) -> Self {
Self { processor_factory }
}
pub fn build(&self, request: PipelineBuildRequest) -> AudioResult<AudioPipeline> {
let output_spec = AudioSpec {
sample_rate: request.sample_rate,
channels: 1,
};
let processors = self
.processor_factory
.build_processors(ProcessorBuildRequest {
profile: request.profile,
gain_db: request.gain_db,
limiter_threshold: request.limiter_threshold,
high_pass_hz: request.high_pass_hz,
noise_suppression_amount: request.noise_suppression_amount,
noise_calibration_ms: request.noise_calibration_ms,
delay_effect: request.delay_effect,
stage_overrides: request.stage_overrides.clone(),
})?;
let processor_names = processors
.iter()
.map(|processor| processor.name().to_string())
.collect::<Vec<_>>();
let microphone_sim_processors =
MicrophoneSimulatorFactory::new().build_processors(request.microphone_sim)?;
let microphone_sim_processor_names = microphone_sim_processors
.iter()
.map(|processor| processor.name().to_string())
.collect::<Vec<_>>();
let microphone_sim_model = request
.microphone_sim
.active_model()
.map(|model| model.as_str().to_string());
let mut notes = request
.delay_effect
.map(|effect| vec![format!("delay_effect={}", effect.preset.as_str())])
.unwrap_or_default();
if let Some(model) = µphone_sim_model {
notes.push(format!("microphone_sim={}", model));
}
notes.push("pipeline_order=microphone_sim->voice_processing->sink".to_string());
self.build_with_parts(
output_spec,
CaptureDiagnostics {
backend: "native".to_string(),
profile: request.profile.as_str().to_string(),
profile_base: Some(request.profile.as_str().to_string()),
device_name: request.device_name,
sample_rate: Some(request.sample_rate),
channels: Some(request.device_channels),
processor_names,
processor_stage_overrides: request
.stage_overrides
.iter()
.map(|(stage, mode)| format!("{}={}", stage, mode.as_str()))
.collect(),
resolved_delay_preset: request
.delay_effect
.map(|effect| effect.preset.as_str().to_string()),
microphone_sim_model,
microphone_sim_processor_names,
notes,
..CaptureDiagnostics::default()
},
microphone_sim_processors,
processors,
Box::new(WavSink::create(&request.output_path, output_spec)?),
Box::new(LevelMetrics::default()),
)
}
fn build_with_parts(
&self,
spec: AudioSpec,
diagnostics: CaptureDiagnostics,
microphone_sim_processors: Vec<Box<dyn crate::mic_sim::contracts::MicrophoneSimProcessor>>,
processors: Vec<Box<dyn crate::contracts::AudioProcessor>>,
sink: Box<dyn AudioSink>,
metrics: Box<dyn MetricsCollector>,
) -> AudioResult<AudioPipeline> {
AudioPipeline::new(
spec,
diagnostics,
microphone_sim_processors,
processors,
sink,
metrics,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::contracts::AudioProcessor;
use crate::core::{AudioError, AudioFrame};
use std::sync::{Arc, Mutex};
struct RecordingFactory {
names: Arc<Mutex<Vec<String>>>,
}
impl ProcessorFactory for RecordingFactory {
fn build_processors(
&self,
request: ProcessorBuildRequest,
) -> AudioResult<Vec<Box<dyn AudioProcessor>>> {
self.names
.lock()
.unwrap()
.push(request.profile.as_str().to_string());
Ok(vec![Box::new(NoopProcessor)])
}
}
struct NoopProcessor;
impl AudioProcessor for NoopProcessor {
fn name(&self) -> &'static str {
"noop"
}
fn prepare(&mut self, _spec: AudioSpec) -> AudioResult<()> {
Ok(())
}
fn process(&mut self, _frame: &mut AudioFrame) -> AudioResult<()> {
Ok(())
}
}
#[test]
fn builder_uses_factory_and_populates_diagnostics() {
let names = Arc::new(Mutex::new(Vec::new()));
let builder = NativePipelineBuilder::with_factory(RecordingFactory {
names: Arc::clone(&names),
});
let output_path = std::env::temp_dir().join("zk-listen-builder-test.wav");
let mut pipeline = builder
.build(PipelineBuildRequest {
output_path: output_path.clone(),
profile: AudioProfile::VoiceHvac,
sample_rate: 44_100,
device_channels: 2,
device_name: Some("Test Mic".to_string()),
gain_db: 2.0,
limiter_threshold: 0.92,
high_pass_hz: 100.0,
noise_suppression_amount: 0.5,
noise_calibration_ms: 350,
delay_effect: None,
stage_overrides: std::collections::BTreeMap::new(),
microphone_sim: MicrophoneSimConfig::default(),
})
.map_err(|err| AudioError::new(format!("builder failed: {}", err)))
.unwrap();
let diagnostics = pipeline.finalize(Some(10)).unwrap();
assert_eq!(diagnostics.profile, "voice_hvac");
assert_eq!(diagnostics.profile_base.as_deref(), Some("voice_hvac"));
assert_eq!(diagnostics.device_name.as_deref(), Some("Test Mic"));
assert_eq!(diagnostics.processor_names, vec!["noop"]);
assert!(diagnostics.processor_stage_overrides.is_empty());
assert!(diagnostics.resolved_delay_preset.is_none());
assert!(diagnostics.microphone_sim_model.is_none());
assert!(diagnostics.microphone_sim_processor_names.is_empty());
assert!(diagnostics
.notes
.iter()
.any(|note| note == "pipeline_order=microphone_sim->voice_processing->sink"));
assert_eq!(names.lock().unwrap().as_slice(), ["voice_hvac"]);
let _ = std::fs::remove_file(output_path);
}
}