#![forbid(unsafe_code)]
pub mod ducking;
pub mod metadata;
pub mod mixing;
pub mod synthesis;
pub mod timing;
use crate::{AudioError, AudioResult};
pub use ducking::{AutomaticDucker, DuckingConfig, VoiceActivityDetector};
pub use metadata::{
AudioDescriptionMetadata, ContentClassification, CueMixingStrategy, DescriptionCue,
LanguageTag, SpeakerAge, SpeakerGender, SpeakerInfo, TimingMode, VoiceCharacteristics,
WcagLevel,
};
pub use mixing::{AudioMixer, MixingConfig, MixingMode};
pub use synthesis::{
EmphasisLevel, MockTtsEngine, PronunciationDictionary, ProsodyControl, SsmlProcessor,
SynthesisConfig, TtsEngine, TtsSynthesizer, VoiceInfo,
};
pub use timing::{
CueScheduler, SubtitleSync, TimeGap, Timeline, TimingConfig, TimingIssue, TimingPrecision,
TimingValidation,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum MixingStrategy {
Replace,
Mix,
#[default]
Duck,
Pause,
}
impl From<MixingStrategy> for MixingMode {
fn from(strategy: MixingStrategy) -> Self {
match strategy {
MixingStrategy::Replace => MixingMode::Replace,
MixingStrategy::Mix => MixingMode::Mix,
MixingStrategy::Duck => MixingMode::Duck,
MixingStrategy::Pause => MixingMode::Pause,
}
}
}
pub struct AudioDescriptionMixer {
metadata: AudioDescriptionMetadata,
mixer: AudioMixer,
timeline: Timeline,
tts: Option<TtsSynthesizer<MockTtsEngine>>,
sample_rate: f64,
channels: usize,
}
impl AudioDescriptionMixer {
#[must_use]
pub fn new(
strategy: MixingStrategy,
metadata: AudioDescriptionMetadata,
sample_rate: f64,
channels: usize,
) -> Self {
let mixing_config = match strategy {
MixingStrategy::Replace => MixingConfig::new(MixingMode::Replace),
MixingStrategy::Mix => MixingConfig::gentle(),
MixingStrategy::Duck => MixingConfig::broadcast(),
MixingStrategy::Pause => MixingConfig::new(MixingMode::Pause),
};
let mixer = AudioMixer::new(mixing_config, sample_rate, channels);
let timing_config = if metadata.timing_mode == TimingMode::Extended {
TimingConfig::flexible()
} else {
TimingConfig::broadcast()
};
let timeline = Timeline::new(timing_config, sample_rate);
Self {
metadata,
mixer,
timeline,
tts: None,
sample_rate,
channels,
}
}
#[must_use]
pub fn metadata(&self) -> &AudioDescriptionMetadata {
&self.metadata
}
pub fn metadata_mut(&mut self) -> &mut AudioDescriptionMetadata {
&mut self.metadata
}
#[must_use]
pub fn timeline(&self) -> &Timeline {
&self.timeline
}
pub fn timeline_mut(&mut self) -> &mut Timeline {
&mut self.timeline
}
pub fn set_mixing_config(&mut self, config: MixingConfig) {
self.mixer.set_config(config);
}
#[must_use]
pub fn mixing_config(&self) -> &MixingConfig {
self.mixer.config()
}
pub fn enable_tts(&mut self, config: SynthesisConfig) {
let engine = MockTtsEngine::new(self.sample_rate as u32);
self.tts = Some(TtsSynthesizer::new(engine, config));
}
pub fn disable_tts(&mut self) {
self.tts = None;
}
pub fn add_cue(&mut self, cue: DescriptionCue) -> AudioResult<()> {
cue.validate()?;
self.timeline.add_cue(cue)
}
pub fn remove_cue(&mut self, cue_id: &str) -> bool {
self.timeline.remove_cue(cue_id)
}
#[must_use]
pub fn get_cue_at(&self, time: f64) -> Option<&DescriptionCue> {
self.timeline.get_cue_at(time)
}
pub fn validate(&self) -> AudioResult<()> {
self.metadata.validate()?;
let timing_validation = self.timeline.validate();
if !timing_validation.is_valid {
return Err(AudioError::InvalidParameter(format!(
"Timeline validation failed: {} issues",
timing_validation.issues.len()
)));
}
Ok(())
}
pub fn process(
&mut self,
main_audio: &[f64],
ad_audio: &[f64],
output: &mut [f64],
ad_active: bool,
) -> AudioResult<()> {
let num_samples = main_audio.len() / self.channels;
self.mixer
.mix_interleaved(main_audio, ad_audio, output, num_samples, ad_active)
}
pub fn process_planar(
&mut self,
main_channels: &[Vec<f64>],
ad_channels: &[Vec<f64>],
output_channels: &mut [Vec<f64>],
ad_active: bool,
) -> AudioResult<()> {
self.mixer
.mix_planar(main_channels, ad_channels, output_channels, ad_active)
}
pub fn synthesize_cue(&mut self, cue_id: &str) -> AudioResult<Vec<f64>> {
let tts = self
.tts
.as_mut()
.ok_or_else(|| AudioError::InvalidParameter("TTS not enabled".to_string()))?;
let cues = self.timeline.get_all_cues();
let cue = cues
.iter()
.find(|c| c.id == cue_id)
.ok_or_else(|| AudioError::InvalidParameter(format!("Cue not found: {}", cue_id)))?;
let text = cue
.text
.as_deref()
.ok_or_else(|| AudioError::InvalidParameter("Cue has no text".to_string()))?;
tts.synthesize(text)
}
#[must_use]
pub fn peak_level(&self) -> f64 {
self.mixer.peak_level()
}
pub fn reset(&mut self) {
self.mixer.reset();
if let Some(ref mut tts) = self.tts {
tts.clear_cache();
}
}
#[must_use]
pub fn sample_rate(&self) -> f64 {
self.sample_rate
}
#[must_use]
pub fn channels(&self) -> usize {
self.channels
}
}
pub struct AudioDescriptionMixerBuilder {
strategy: MixingStrategy,
metadata: AudioDescriptionMetadata,
sample_rate: f64,
channels: usize,
mixing_config: Option<MixingConfig>,
timing_config: Option<TimingConfig>,
tts_config: Option<SynthesisConfig>,
}
impl AudioDescriptionMixerBuilder {
#[must_use]
pub fn new(metadata: AudioDescriptionMetadata, sample_rate: f64, channels: usize) -> Self {
Self {
strategy: MixingStrategy::Duck,
metadata,
sample_rate,
channels,
mixing_config: None,
timing_config: None,
tts_config: None,
}
}
#[must_use]
pub fn with_strategy(mut self, strategy: MixingStrategy) -> Self {
self.strategy = strategy;
self
}
#[must_use]
pub fn with_mixing_config(mut self, config: MixingConfig) -> Self {
self.mixing_config = Some(config);
self
}
#[must_use]
pub fn with_timing_config(mut self, config: TimingConfig) -> Self {
self.timing_config = Some(config);
self
}
#[must_use]
pub fn with_tts(mut self, config: SynthesisConfig) -> Self {
self.tts_config = Some(config);
self
}
#[must_use]
pub fn build(self) -> AudioDescriptionMixer {
let mut mixer = AudioDescriptionMixer::new(
self.strategy,
self.metadata,
self.sample_rate,
self.channels,
);
if let Some(config) = self.mixing_config {
mixer.set_mixing_config(config);
}
if let Some(config) = self.timing_config {
mixer.timeline = Timeline::new(config, self.sample_rate);
}
if let Some(config) = self.tts_config {
mixer.enable_tts(config);
}
mixer
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_audio_description_mixer_creation() {
let metadata = AudioDescriptionMetadata::new("track1", LanguageTag::new("en"));
let mixer = AudioDescriptionMixer::new(MixingStrategy::Duck, metadata, 48000.0, 2);
assert_eq!(mixer.sample_rate(), 48000.0);
assert_eq!(mixer.channels(), 2);
}
#[test]
fn test_mixer_add_cue() {
let metadata = AudioDescriptionMetadata::new("track1", LanguageTag::new("en"));
let mut mixer = AudioDescriptionMixer::new(MixingStrategy::Duck, metadata, 48000.0, 2);
let cue = DescriptionCue::new("cue1", 1.0, 2.0).with_text("Test description");
mixer.add_cue(cue).unwrap();
assert_eq!(mixer.timeline().cue_count(), 1);
}
#[test]
fn test_mixer_process() {
let metadata = AudioDescriptionMetadata::new("track1", LanguageTag::new("en"));
let mut mixer = AudioDescriptionMixer::new(MixingStrategy::Mix, metadata, 48000.0, 2);
let main_audio = vec![0.5; 200];
let ad_audio = vec![0.3; 200];
let mut output = vec![0.0; 200];
mixer
.process(&main_audio, &ad_audio, &mut output, false)
.unwrap();
assert!(output.iter().any(|&s| s > 0.0));
}
#[test]
fn test_mixer_builder() {
let metadata = AudioDescriptionMetadata::new("track1", LanguageTag::new("en"));
let mixer = AudioDescriptionMixerBuilder::new(metadata, 48000.0, 2)
.with_strategy(MixingStrategy::Duck)
.build();
assert_eq!(mixer.sample_rate(), 48000.0);
}
#[test]
fn test_mixer_validation() {
let mut metadata = AudioDescriptionMetadata::new("track1", LanguageTag::new("en"));
metadata.track_id = String::new();
let mixer = AudioDescriptionMixer::new(MixingStrategy::Duck, metadata, 48000.0, 2);
assert!(mixer.validate().is_err());
}
#[test]
fn test_mixer_with_tts() {
let metadata = AudioDescriptionMetadata::new("track1", LanguageTag::new("en"));
let mut mixer = AudioDescriptionMixer::new(MixingStrategy::Duck, metadata, 48000.0, 2);
let tts_config = SynthesisConfig::default();
mixer.enable_tts(tts_config);
let cue = DescriptionCue::new("cue1", 1.0, 2.0).with_text("Test");
mixer.add_cue(cue).unwrap();
let samples = mixer.synthesize_cue("cue1").unwrap();
assert!(!samples.is_empty());
}
}