// SPDX-License-Identifier: AGPL-3.0-or-later #![allow(dead_code)] use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; use std::thread::{self, JoinHandle}; use fluxer_audio_apm::{ APM_MAX_FRAME_SAMPLES, ApmConfig, ApmError, AudioProcessor, StubAudioProcessor, expected_frame_samples, }; use fluxer_audio_mix::{ AUDIO_OUTPUT_FRAMES, AudioMixError, AudioMixSession, MAX_MIX_SOURCES, MixTickResult, SourceRing, SourceRingConsumer, SourceRingProducer, }; use fluxer_audio_timing::{AudioTimingSmoother, SourceGainRamp, StaleSourceTracker}; use fluxer_rt_thread::{ MonotonicClock, PriorityProfile, RealtimePriorityGuard, RtError, RtOutcome, SystemMonotonicClock, TickDriver, }; use crate::ignore_audio_runtime::{ IgnoreAudioPolicy, IgnoreAudioSourceResetEvent, IgnoreAudioSourceState, IgnoreAudioTick, compute_source_age_ns, }; pub const MIX_TICK_PERIOD_NS: u64 = 21_333_333; pub const SOURCE_RING_CAP_FRAMES: usize = 8_192; pub const MIX_SAMPLE_RATE_HZ: u32 = 48_000; pub const MIX_CHANNELS: u16 = 1; pub const OUTPUT_BUFFER_POOL_DEPTH: usize = 4; const NEVER_PUSHED_SENTINEL: u64 = u64::MAX; pub struct DynMonotonicClock { inner: Arc, } impl DynMonotonicClock { pub fn new(inner: Arc) -> Self { Self { inner } } } impl MonotonicClock for DynMonotonicClock { fn now_ns(&self) -> u64 { self.inner.now_ns() } } #[derive(Debug)] pub enum MixRuntimeError { NoSources, TooManySources { requested: usize, limit: usize }, Mix(AudioMixError), Apm(ApmError), Rt(RtError), AlreadyRunning, ThreadSpawn, } impl core::fmt::Display for MixRuntimeError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { MixRuntimeError::NoSources => write!(f, "mix runtime requires at least one source"), MixRuntimeError::TooManySources { requested, limit } => { write!(f, "source count {requested} exceeds limit {limit}") } MixRuntimeError::Mix(e) => write!(f, "{e}"), MixRuntimeError::Apm(e) => write!(f, "{e}"), MixRuntimeError::Rt(e) => write!(f, "{e}"), MixRuntimeError::AlreadyRunning => write!(f, "mix runtime already running"), MixRuntimeError::ThreadSpawn => write!(f, "failed to spawn mix thread"), } } } impl std::error::Error for MixRuntimeError {} impl From for MixRuntimeError { fn from(value: AudioMixError) -> Self { MixRuntimeError::Mix(value) } } impl From for MixRuntimeError { fn from(value: ApmError) -> Self { MixRuntimeError::Apm(value) } } #[derive(Debug, Clone)] pub struct MixOutputFrame { pub samples: Arc<[i16; AUDIO_OUTPUT_FRAMES]>, pub tick_index: u64, pub scheduled_ns: u64, pub actual_ns: u64, pub total_drained: u64, pub total_silence: u64, pub saturated_samples: u32, } impl PartialEq for MixOutputFrame { fn eq(&self, other: &Self) -> bool { self.tick_index == other.tick_index && self.scheduled_ns == other.scheduled_ns && self.actual_ns == other.actual_ns && self.total_drained == other.total_drained && self.total_silence == other.total_silence && self.saturated_samples == other.saturated_samples && self.samples.as_ref() == other.samples.as_ref() } } pub trait MixOutputSink: Send { fn on_mixed_frame(&mut self, frame: &MixOutputFrame); fn on_source_reset(&mut self, event: &IgnoreAudioSourceResetEvent); } pub struct NullMixOutputSink; impl MixOutputSink for NullMixOutputSink { fn on_mixed_frame(&mut self, _frame: &MixOutputFrame) {} fn on_source_reset(&mut self, _event: &IgnoreAudioSourceResetEvent) {} } pub struct CapturedMixOutputSink { frames: Arc>>, resets: Arc>>, } impl CapturedMixOutputSink { pub fn new() -> Self { Self { frames: Arc::new(Mutex::new(Vec::new())), resets: Arc::new(Mutex::new(Vec::new())), } } pub fn frames(&self) -> Arc>> { Arc::clone(&self.frames) } pub fn resets(&self) -> Arc>> { Arc::clone(&self.resets) } } impl Default for CapturedMixOutputSink { fn default() -> Self { Self::new() } } impl MixOutputSink for CapturedMixOutputSink { fn on_mixed_frame(&mut self, frame: &MixOutputFrame) { if let Ok(mut guard) = self.frames.lock() { guard.push(frame.clone()); } } fn on_source_reset(&mut self, event: &IgnoreAudioSourceResetEvent) { if let Ok(mut guard) = self.resets.lock() { guard.push(*event); } } } pub struct SourceFreshnessHandle { last_push_ns: Arc, registered_at_ns: u64, clock: Arc, } impl SourceFreshnessHandle { pub fn mark_push_now(&self) { let now = self.clock.now_ns(); assert!(now > 0); self.last_push_ns.store(now, Ordering::Release); } pub fn mark_push_at(&self, push_ns: u64) { assert!(push_ns > 0); self.last_push_ns.store(push_ns, Ordering::Release); } pub fn last_push_ns(&self) -> u64 { self.last_push_ns.load(Ordering::Acquire) } pub fn registered_at_ns(&self) -> u64 { assert!(self.registered_at_ns != NEVER_PUSHED_SENTINEL); self.registered_at_ns } } pub struct CaptureSource { pub source_id: u64, pub producer: SourceRingProducer, pub apm: StubAudioProcessor, apm_sample_rate_hz: u32, apm_channels: u16, apm_frame_samples: usize, freshness: SourceFreshnessHandle, } impl CaptureSource { pub fn create( source_id: u64, sample_rate_hz: u32, channels: u16, ) -> Result<(Self, SourceRingConsumer), MixRuntimeError> { assert!(source_id != 0); assert!(sample_rate_hz >= 8_000); assert!(channels >= 1); let (producer, consumer) = SourceRing::create(SOURCE_RING_CAP_FRAMES, sample_rate_hz)?; let apm = StubAudioProcessor::new(ApmConfig::default(), sample_rate_hz, channels)?; let apm_frame_samples = expected_frame_samples(sample_rate_hz, channels); assert!(apm_frame_samples > 0); assert!(apm_frame_samples <= APM_MAX_FRAME_SAMPLES * (channels as usize)); let clock_for_freshness: Arc = Arc::new(SystemMonotonicClock::new()); let registered_at_ns = clock_for_freshness.now_ns().max(1); assert!(registered_at_ns != NEVER_PUSHED_SENTINEL); let freshness = SourceFreshnessHandle { last_push_ns: Arc::new(AtomicU64::new(NEVER_PUSHED_SENTINEL)), registered_at_ns, clock: clock_for_freshness, }; let me = Self { source_id, producer, apm, apm_sample_rate_hz: sample_rate_hz, apm_channels: channels, apm_frame_samples, freshness, }; Ok((me, consumer)) } pub fn ingest(&mut self, samples: &mut [i16]) -> Result { assert!(!samples.is_empty()); assert_eq!(samples.len() % self.apm_frame_samples, 0); for chunk in samples.chunks_mut(self.apm_frame_samples) { self.apm .process_capture_frame(chunk, self.apm_sample_rate_hz, self.apm_channels)?; } let pushed = self.producer.try_push_slice(samples); self.freshness.mark_push_now(); Ok(pushed) } pub fn ingest_skip_apm(&mut self, samples: &[i16]) -> usize { assert!(!samples.is_empty()); let pushed = self.producer.try_push_slice(samples); self.freshness.mark_push_now(); pushed } pub fn source_id(&self) -> u64 { assert!(self.source_id != 0); self.source_id } pub fn buffered_frames(&self) -> u64 { self.producer .capacity_frames() .saturating_sub(self.producer.slots()) as u64 } pub fn freshness_handle(&self) -> &SourceFreshnessHandle { &self.freshness } pub fn last_push_ns_arc(&self) -> Arc { Arc::clone(&self.freshness.last_push_ns) } pub fn apm_frames_processed(&self) -> u64 { self.apm.capture_frames_processed() } } struct BuilderSourceEntry { source_id: u64, consumer: SourceRingConsumer, last_push_ns: Arc, registered_at_ns: u64, } pub struct AudioMixRuntimeBuilder { consumers: Vec, sample_rate_hz: u32, period_ns: u64, clock: Arc, } impl AudioMixRuntimeBuilder { pub fn new() -> Self { Self { consumers: Vec::new(), sample_rate_hz: MIX_SAMPLE_RATE_HZ, period_ns: MIX_TICK_PERIOD_NS, clock: Arc::new(SystemMonotonicClock::new()), } } pub fn with_period_ns(mut self, period_ns: u64) -> Self { assert!(period_ns > 0); self.period_ns = period_ns; self } pub fn with_sample_rate_hz(mut self, sample_rate_hz: u32) -> Self { assert!(sample_rate_hz >= 8_000); self.sample_rate_hz = sample_rate_hz; self } pub fn with_clock(mut self, clock: Arc) -> Self { self.clock = clock; self } pub fn add_source(mut self, source_id: u64, consumer: SourceRingConsumer) -> Self { assert!(self.consumers.len() < MAX_MIX_SOURCES); assert!(source_id != 0); let placeholder = Arc::new(AtomicU64::new(NEVER_PUSHED_SENTINEL)); let registered_at_ns = self.clock.now_ns().max(1); assert!(registered_at_ns != NEVER_PUSHED_SENTINEL); self.consumers.push(BuilderSourceEntry { source_id, consumer, last_push_ns: placeholder, registered_at_ns, }); self } pub fn add_source_with_freshness( mut self, source_id: u64, consumer: SourceRingConsumer, last_push_ns: Arc, ) -> Self { assert!(self.consumers.len() < MAX_MIX_SOURCES); assert!(source_id != 0); let registered_at_ns = self.clock.now_ns().max(1); assert!(registered_at_ns != NEVER_PUSHED_SENTINEL); self.consumers.push(BuilderSourceEntry { source_id, consumer, last_push_ns, registered_at_ns, }); self } pub fn build( self, sink: S, ) -> Result { if self.consumers.is_empty() { return Err(MixRuntimeError::NoSources); } if self.consumers.len() > MAX_MIX_SOURCES { return Err(MixRuntimeError::TooManySources { requested: self.consumers.len(), limit: MAX_MIX_SOURCES, }); } let consumer_count = self.consumers.len(); let mut source_ids: Vec = Vec::with_capacity(consumer_count); let mut consumers: Vec = Vec::with_capacity(consumer_count); let mut last_push_ns: Vec> = Vec::with_capacity(consumer_count); let mut registered_at_ns: Vec = Vec::with_capacity(consumer_count); let mut smoothers: Vec = Vec::with_capacity(consumer_count); let mut ramps: Vec = Vec::with_capacity(consumer_count); let mut stale_tracker = StaleSourceTracker::new(consumer_count) .map_err(|_| MixRuntimeError::Mix(AudioMixError::ZeroSources))?; for entry in self.consumers { source_ids.push(entry.source_id); consumers.push(entry.consumer); last_push_ns.push(entry.last_push_ns); registered_at_ns.push(entry.registered_at_ns); let smoother = AudioTimingSmoother::new(entry.source_id, self.sample_rate_hz) .map_err(|_| MixRuntimeError::Mix(AudioMixError::ZeroSources))?; smoothers.push(smoother); ramps.push(SourceGainRamp::new()); stale_tracker .register_source(entry.source_id, entry.registered_at_ns) .map_err(|_| MixRuntimeError::Mix(AudioMixError::ZeroSources))?; } let session = AudioMixSession::new(consumers, AUDIO_OUTPUT_FRAMES)?; let policy = IgnoreAudioPolicy::new(self.sample_rate_hz) .map_err(|_| MixRuntimeError::Mix(AudioMixError::ZeroSources))?; let pool = build_output_pool(); let last_marked_push_ns = vec![NEVER_PUSHED_SENTINEL; consumer_count]; Ok(AudioMixRuntime { session: Some(session), policy: Some(policy), source_ids, last_push_ns, last_marked_push_ns, registered_at_ns, smoothers, ramps, stale_tracker, mark_pushed_total: Arc::new(AtomicU64::new(0)), clock: self.clock, sample_rate_hz: self.sample_rate_hz, period_ns: self.period_ns, sink: Some(Box::new(sink)), running: Arc::new(AtomicBool::new(false)), thread: None, rt_outcome: Arc::new(Mutex::new(None)), manual_tick_index: 0, pre_stats: [0u64; MAX_MIX_SOURCES], output_pool: pool, output_pool_cursor: 0, }) } } impl Default for AudioMixRuntimeBuilder { fn default() -> Self { Self::new() } } fn build_output_pool() -> [Arc<[i16; AUDIO_OUTPUT_FRAMES]>; OUTPUT_BUFFER_POOL_DEPTH] { [ Arc::new([0i16; AUDIO_OUTPUT_FRAMES]), Arc::new([0i16; AUDIO_OUTPUT_FRAMES]), Arc::new([0i16; AUDIO_OUTPUT_FRAMES]), Arc::new([0i16; AUDIO_OUTPUT_FRAMES]), ] } pub struct AudioMixRuntime { session: Option, policy: Option, source_ids: Vec, last_push_ns: Vec>, last_marked_push_ns: Vec, registered_at_ns: Vec, smoothers: Vec, ramps: Vec, stale_tracker: StaleSourceTracker, mark_pushed_total: Arc, clock: Arc, sample_rate_hz: u32, period_ns: u64, sink: Option>, running: Arc, thread: Option>, rt_outcome: Arc>>, manual_tick_index: u64, pre_stats: [u64; MAX_MIX_SOURCES], output_pool: [Arc<[i16; AUDIO_OUTPUT_FRAMES]>; OUTPUT_BUFFER_POOL_DEPTH], output_pool_cursor: usize, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MixRtOutcome { Acquired, PartialFallback, Denied, } impl AudioMixRuntime { pub fn source_count(&self) -> usize { assert!(!self.source_ids.is_empty()); assert!(self.source_ids.len() <= MAX_MIX_SOURCES); self.source_ids.len() } pub fn sample_rate_hz(&self) -> u32 { assert!(self.sample_rate_hz >= 8_000); self.sample_rate_hz } pub fn period_ns(&self) -> u64 { assert!(self.period_ns > 0); self.period_ns } pub fn run_one_tick_blocking( &mut self, tick_at_ns: u64, ) -> Result { let tick_index = self.manual_tick_index; self.manual_tick_index = self.manual_tick_index.saturating_add(1); let tick_info = fluxer_rt_thread::TickInfo { tick_index, scheduled_ns: tick_at_ns, actual_ns: tick_at_ns, lag_ns: 0, }; self.run_tick_internal(tick_info) } pub fn observe_source_pushes_without_mix( &mut self, tick_at_ns: u64, ) -> Result { assert!(tick_at_ns > 0); let before = self.mark_pushed_total.load(Ordering::Acquire); let session = self .session .as_mut() .ok_or(MixRuntimeError::AlreadyRunning)?; let policy = self .policy .as_mut() .ok_or(MixRuntimeError::AlreadyRunning)?; let sink = self.sink.as_mut().ok_or(MixRuntimeError::AlreadyRunning)?; let now_ns = self.clock.now_ns(); snapshot_pre_stats(session, &mut self.pre_stats); drain_pushes_into_tracker( &mut self.stale_tracker, &self.source_ids, &self.last_push_ns, &mut self.last_marked_push_ns, &self.mark_pushed_total, ); evaluate_policy_for_all_sources( policy, &self.source_ids, &self.last_push_ns, &self.registered_at_ns, &self.pre_stats, now_ns, tick_at_ns, self.period_ns, sink.as_mut(), ); advance_smoothers_and_ramps( &mut self.smoothers, &mut self.ramps, &self.last_push_ns, tick_at_ns, self.period_ns, self.sample_rate_hz, ); let after = self.mark_pushed_total.load(Ordering::Acquire); Ok(after.saturating_sub(before)) } fn run_tick_internal( &mut self, tick_info: fluxer_rt_thread::TickInfo, ) -> Result { let period_ns = self.period_ns; let session = self .session .as_mut() .ok_or(MixRuntimeError::AlreadyRunning)?; let policy = self .policy .as_mut() .ok_or(MixRuntimeError::AlreadyRunning)?; let sink = self.sink.as_mut().ok_or(MixRuntimeError::AlreadyRunning)?; let now_ns = self.clock.now_ns(); snapshot_pre_stats(session, &mut self.pre_stats); drain_pushes_into_tracker( &mut self.stale_tracker, &self.source_ids, &self.last_push_ns, &mut self.last_marked_push_ns, &self.mark_pushed_total, ); evaluate_policy_for_all_sources( policy, &self.source_ids, &self.last_push_ns, &self.registered_at_ns, &self.pre_stats, now_ns, tick_info.actual_ns, period_ns, sink.as_mut(), ); advance_smoothers_and_ramps( &mut self.smoothers, &mut self.ramps, &self.last_push_ns, tick_info.actual_ns, period_ns, self.sample_rate_hz, ); let result: MixTickResult = session.tick(tick_info); let frame = build_output_frame_pooled( session, &result, &mut self.output_pool, &mut self.output_pool_cursor, ); sink.on_mixed_frame(&frame); Ok(frame) } pub fn mark_pushed_total(&self) -> u64 { assert!(!self.source_ids.is_empty()); let value = self.mark_pushed_total.load(Ordering::Acquire); assert!(self.source_ids.len() <= MAX_MIX_SOURCES); value } pub fn mark_pushed_total_arc(&self) -> Arc { assert!(!self.source_ids.is_empty()); assert!(self.source_ids.len() <= MAX_MIX_SOURCES); Arc::clone(&self.mark_pushed_total) } pub fn current_ramp_gain(&self, index: usize) -> f32 { assert!(index < self.ramps.len()); self.ramps[index].current_gain() } pub fn smoother_initialised(&self, index: usize) -> bool { assert!(index < self.smoothers.len()); self.smoothers[index].initialised() } pub fn is_source_stale(&self, index: usize, now_ns: u64, stale_threshold_ns: u64) -> bool { assert!(index < self.source_ids.len()); assert!(self.source_ids.len() <= MAX_MIX_SOURCES); let id = self.source_ids[index]; self.stale_tracker .is_stale_at(index, id, now_ns, stale_threshold_ns) .unwrap_or(false) } pub fn start(&mut self) -> Result<(), MixRuntimeError> { if self.running.swap(true, Ordering::AcqRel) { return Err(MixRuntimeError::AlreadyRunning); } let session = self.session.take().ok_or(MixRuntimeError::AlreadyRunning)?; let policy = self.policy.take().ok_or(MixRuntimeError::AlreadyRunning)?; let sink = self.sink.take().ok_or(MixRuntimeError::AlreadyRunning)?; let source_ids = self.source_ids.clone(); let last_push_ns = self.last_push_ns.clone(); let registered_at_ns = self.registered_at_ns.clone(); let period_ns = self.period_ns; let sample_rate_hz = self.sample_rate_hz; let running = Arc::clone(&self.running); let rt_outcome = Arc::clone(&self.rt_outcome); let clock = Arc::clone(&self.clock); let pool = self.output_pool.clone(); let smoothers = std::mem::take(&mut self.smoothers); let ramps = std::mem::take(&mut self.ramps); let stale_tracker = std::mem::replace( &mut self.stale_tracker, StaleSourceTracker::new(self.source_ids.len()) .map_err(|_| MixRuntimeError::Mix(AudioMixError::ZeroSources))?, ); let last_marked = std::mem::take(&mut self.last_marked_push_ns); let mark_pushed_total = Arc::clone(&self.mark_pushed_total); let handle = thread::Builder::new() .name("fluxer-audio-mix".into()) .spawn(move || { spawn_mix_thread(MixThreadArgs { session, policy, source_ids, last_push_ns, registered_at_ns, smoothers, ramps, period_ns, sample_rate_hz, sink, running, rt_outcome, clock, pool, stale_tracker, last_marked, mark_pushed_total, }); }) .map_err(|_| MixRuntimeError::ThreadSpawn)?; self.thread = Some(handle); Ok(()) } pub fn stop(&mut self) { self.running.store(false, Ordering::Release); if let Some(handle) = self.thread.take() { let _ = handle.join(); } } pub fn rt_outcome(&self) -> Option { match self.rt_outcome.lock() { Ok(guard) => *guard, Err(_) => None, } } pub fn last_push_ns_for(&self, index: usize) -> u64 { assert!(index < self.last_push_ns.len()); self.last_push_ns[index].load(Ordering::Acquire) } pub fn output_pool_depth(&self) -> usize { assert_eq!(self.output_pool.len(), OUTPUT_BUFFER_POOL_DEPTH); OUTPUT_BUFFER_POOL_DEPTH } } impl Drop for AudioMixRuntime { fn drop(&mut self) { self.stop(); } } struct MixThreadArgs { session: AudioMixSession, policy: IgnoreAudioPolicy, source_ids: Vec, last_push_ns: Vec>, registered_at_ns: Vec, smoothers: Vec, ramps: Vec, period_ns: u64, sample_rate_hz: u32, sink: Box, running: Arc, rt_outcome: Arc>>, clock: Arc, pool: [Arc<[i16; AUDIO_OUTPUT_FRAMES]>; OUTPUT_BUFFER_POOL_DEPTH], stale_tracker: StaleSourceTracker, last_marked: Vec, mark_pushed_total: Arc, } fn spawn_mix_thread(args: MixThreadArgs) { let MixThreadArgs { mut session, mut policy, source_ids, last_push_ns, registered_at_ns, mut smoothers, mut ramps, period_ns, sample_rate_hz, mut sink, running, rt_outcome, clock, pool, mut stale_tracker, mut last_marked, mark_pushed_total, } = args; assert!(!source_ids.is_empty()); assert_eq!(source_ids.len(), last_push_ns.len()); assert_eq!(source_ids.len(), registered_at_ns.len()); assert_eq!(source_ids.len(), last_marked.len()); let guard_result = RealtimePriorityGuard::acquire(PriorityProfile::Audio); let (rt_state, _rt_guard) = match guard_result { Ok(guard) => { let state = match guard.outcome() { RtOutcome::Acquired => MixRtOutcome::Acquired, RtOutcome::PartialFallback => MixRtOutcome::PartialFallback, }; (state, Some(guard)) } Err(_) => (MixRtOutcome::Denied, None), }; if let Ok(mut guard_slot) = rt_outcome.lock() { *guard_slot = Some(rt_state); } let mut pre_stats = [0u64; MAX_MIX_SOURCES]; let mut pool = pool; let mut cursor: usize = 0; let _ = run_mix_thread_loop(MixLoopArgs { session: &mut session, policy: &mut policy, source_ids: &source_ids, last_push_ns: &last_push_ns, registered_at_ns: ®istered_at_ns, smoothers: &mut smoothers, ramps: &mut ramps, period_ns, sample_rate_hz, sink: sink.as_mut(), running: &running, clock: &clock, pre_stats: &mut pre_stats, pool: &mut pool, cursor: &mut cursor, stale_tracker: &mut stale_tracker, last_marked: &mut last_marked, mark_pushed_total: &mark_pushed_total, }); } struct MixLoopArgs<'a> { session: &'a mut AudioMixSession, policy: &'a mut IgnoreAudioPolicy, source_ids: &'a [u64], last_push_ns: &'a [Arc], registered_at_ns: &'a [u64], smoothers: &'a mut [AudioTimingSmoother], ramps: &'a mut [SourceGainRamp], period_ns: u64, sample_rate_hz: u32, sink: &'a mut dyn MixOutputSink, running: &'a AtomicBool, clock: &'a Arc, pre_stats: &'a mut [u64; MAX_MIX_SOURCES], pool: &'a mut [Arc<[i16; AUDIO_OUTPUT_FRAMES]>; OUTPUT_BUFFER_POOL_DEPTH], cursor: &'a mut usize, stale_tracker: &'a mut StaleSourceTracker, last_marked: &'a mut [u64], mark_pushed_total: &'a Arc, } fn run_mix_thread_loop(args: MixLoopArgs<'_>) -> Result<(), MixRuntimeError> { let MixLoopArgs { session, policy, source_ids, last_push_ns, registered_at_ns, smoothers, ramps, period_ns, sample_rate_hz, sink, running, clock, pre_stats, pool, cursor, stale_tracker, last_marked, mark_pushed_total, } = args; assert!(!source_ids.is_empty()); assert_eq!(source_ids.len(), last_push_ns.len()); let driver_clock = Arc::new(DynMonotonicClock::new(Arc::clone(clock))); let mut driver = TickDriver::new(driver_clock, period_ns) .map_err(|_| MixRuntimeError::Mix(AudioMixError::ZeroSources))?; while running.load(Ordering::Acquire) { let tick_info = match driver.wait_until_next_tick() { Ok(info) => info, Err(_) => break, }; if !running.load(Ordering::Acquire) { break; } let _ = run_single_tick(SingleTickArgs { session, policy, source_ids, last_push_ns, registered_at_ns, smoothers, ramps, period_ns, sample_rate_hz, tick_info, sink, clock, pre_stats, pool, cursor, stale_tracker, last_marked, mark_pushed_total, }); } Ok(()) } struct SingleTickArgs<'a> { session: &'a mut AudioMixSession, policy: &'a mut IgnoreAudioPolicy, source_ids: &'a [u64], last_push_ns: &'a [Arc], registered_at_ns: &'a [u64], smoothers: &'a mut [AudioTimingSmoother], ramps: &'a mut [SourceGainRamp], period_ns: u64, sample_rate_hz: u32, tick_info: fluxer_rt_thread::TickInfo, sink: &'a mut dyn MixOutputSink, clock: &'a Arc, pre_stats: &'a mut [u64; MAX_MIX_SOURCES], pool: &'a mut [Arc<[i16; AUDIO_OUTPUT_FRAMES]>; OUTPUT_BUFFER_POOL_DEPTH], cursor: &'a mut usize, stale_tracker: &'a mut StaleSourceTracker, last_marked: &'a mut [u64], mark_pushed_total: &'a Arc, } fn run_single_tick(args: SingleTickArgs<'_>) -> Result { let SingleTickArgs { session, policy, source_ids, last_push_ns, registered_at_ns, smoothers, ramps, period_ns, sample_rate_hz, tick_info, sink, clock, pre_stats, pool, cursor, stale_tracker, last_marked, mark_pushed_total, } = args; assert!(!source_ids.is_empty()); assert_eq!(source_ids.len(), session.source_count()); assert_eq!(source_ids.len(), last_push_ns.len()); let now_ns = clock.now_ns(); snapshot_pre_stats(session, pre_stats); drain_pushes_into_tracker( stale_tracker, source_ids, last_push_ns, last_marked, mark_pushed_total, ); evaluate_policy_for_all_sources( policy, source_ids, last_push_ns, registered_at_ns, pre_stats, now_ns, tick_info.actual_ns, period_ns, sink, ); advance_smoothers_and_ramps( smoothers, ramps, last_push_ns, tick_info.actual_ns, period_ns, sample_rate_hz, ); let result: MixTickResult = session.tick(tick_info); let frame = build_output_frame_pooled(session, &result, pool, cursor); sink.on_mixed_frame(&frame); Ok(frame) } fn drain_pushes_into_tracker( stale_tracker: &mut StaleSourceTracker, source_ids: &[u64], last_push_ns: &[Arc], last_marked: &mut [u64], mark_pushed_total: &Arc, ) { assert_eq!(source_ids.len(), last_push_ns.len()); assert_eq!(source_ids.len(), last_marked.len()); assert_eq!(source_ids.len(), stale_tracker.len()); for idx in 0..source_ids.len() { let observed = last_push_ns[idx].load(Ordering::Acquire); if observed == NEVER_PUSHED_SENTINEL { continue; } if observed == last_marked[idx] { continue; } let source_id = source_ids[idx]; if stale_tracker .mark_pushed_at(idx, source_id, observed) .is_ok() { last_marked[idx] = observed; mark_pushed_total.fetch_add(1, Ordering::AcqRel); } } } fn advance_smoothers_and_ramps( smoothers: &mut [AudioTimingSmoother], ramps: &mut [SourceGainRamp], last_push_ns: &[Arc], tick_at_ns: u64, period_ns: u64, sample_rate_hz: u32, ) { assert_eq!(smoothers.len(), ramps.len()); assert_eq!(smoothers.len(), last_push_ns.len()); let frames_per_tick = compute_frames_per_tick(period_ns, sample_rate_hz); for index in 0..smoothers.len() { let last_push = last_push_ns[index].load(Ordering::Acquire); ramps[index].advance_tick(); if last_push == NEVER_PUSHED_SENTINEL { continue; } let smoother = &mut smoothers[index]; let input = fluxer_audio_timing::SmootherInput { media_ts_ns: last_push, frames: frames_per_tick, wall_ns: tick_at_ns, }; let _ = smoother.adjust(input); } } fn compute_frames_per_tick(period_ns: u64, sample_rate_hz: u32) -> u32 { assert!(period_ns > 0); assert!(sample_rate_hz >= 8_000); let product = (period_ns as u128) * (sample_rate_hz as u128); let frames = product / 1_000_000_000u128; let frames_u32 = frames.min(u32::MAX as u128) as u32; frames_u32.max(1) } fn snapshot_pre_stats(session: &AudioMixSession, pre_stats: &mut [u64; MAX_MIX_SOURCES]) { let stats = session.per_source_stats(); assert!(stats.len() <= MAX_MIX_SOURCES); for (slot, stat) in pre_stats.iter_mut().zip(stats.iter()) { *slot = stat.drained_count as u64; } } #[allow(clippy::too_many_arguments)] fn evaluate_policy_for_all_sources( policy: &mut IgnoreAudioPolicy, source_ids: &[u64], last_push_ns: &[Arc], registered_at_ns: &[u64], pre_stats: &[u64; MAX_MIX_SOURCES], now_ns: u64, tick_at_ns: u64, period_ns: u64, sink: &mut dyn MixOutputSink, ) { assert_eq!(source_ids.len(), last_push_ns.len()); assert_eq!(source_ids.len(), registered_at_ns.len()); let tick = IgnoreAudioTick { at_ns: tick_at_ns, period_ns, }; for (idx, source_id) in source_ids.iter().enumerate() { let drained_before = pre_stats[idx]; let last_push = last_push_ns[idx].load(Ordering::Acquire); let age_ns = compute_age_ns(last_push, registered_at_ns[idx], now_ns); let state = IgnoreAudioSourceState { id: *source_id, buffered_frames: drained_before, last_frame_age_ns: age_ns, is_muted: false, }; if let Ok(eval) = policy.evaluate(&state, tick) && let Some(event) = eval.event { sink.on_source_reset(&event); } } } fn compute_age_ns(last_push_ns: u64, registered_at_ns: u64, now_ns: u64) -> u64 { compute_source_age_ns(last_push_ns, registered_at_ns, now_ns) } fn build_output_frame_pooled( session: &AudioMixSession, result: &MixTickResult, pool: &mut [Arc<[i16; AUDIO_OUTPUT_FRAMES]>; OUTPUT_BUFFER_POOL_DEPTH], cursor: &mut usize, ) -> MixOutputFrame { let slot_index = *cursor; assert!(slot_index < OUTPUT_BUFFER_POOL_DEPTH); let src = session.last_output(); let target_arc = &mut pool[slot_index]; let target = Arc::make_mut(target_arc); target.copy_from_slice(src); assert_eq!(target.len(), AUDIO_OUTPUT_FRAMES); *cursor = (slot_index + 1) % OUTPUT_BUFFER_POOL_DEPTH; MixOutputFrame { samples: Arc::clone(&pool[slot_index]), tick_index: result.tick_index, scheduled_ns: result.scheduled_ns, actual_ns: result.actual_ns, total_drained: result.total_drained, total_silence: result.total_silence, saturated_samples: result.saturated_samples, } } pub fn audio_apm_frame_samples(sample_rate_hz: u32, channels: u16) -> usize { assert!(sample_rate_hz >= 8_000); assert!(channels >= 1); expected_frame_samples(sample_rate_hz, channels) } #[cfg(test)] pub(crate) static ALLOC_PROBE: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); #[cfg(test)] thread_local! { pub(crate) static THREAD_ALLOC_TRACK: std::cell::Cell = const { std::cell::Cell::new(false) }; pub(crate) static THREAD_ALLOC_COUNT: std::cell::Cell = const { std::cell::Cell::new(0) }; } #[cfg(test)] struct CountingAllocator; #[cfg(test)] unsafe impl std::alloc::GlobalAlloc for CountingAllocator { unsafe fn alloc(&self, layout: std::alloc::Layout) -> *mut u8 { ALLOC_PROBE.fetch_add(1, Ordering::Relaxed); let _ = THREAD_ALLOC_TRACK.try_with(|tracked| { if tracked.get() { let _ = THREAD_ALLOC_COUNT.try_with(|c| c.set(c.get().saturating_add(1))); } }); unsafe { std::alloc::System.alloc(layout) } } unsafe fn dealloc(&self, ptr: *mut u8, layout: std::alloc::Layout) { unsafe { std::alloc::System.dealloc(ptr, layout) } } } #[cfg(test)] #[global_allocator] static ALLOCATOR: CountingAllocator = CountingAllocator; #[cfg(test)] pub(crate) fn begin_thread_alloc_probe() { THREAD_ALLOC_COUNT.with(|c| c.set(0)); THREAD_ALLOC_TRACK.with(|t| t.set(true)); } #[cfg(test)] pub(crate) fn end_thread_alloc_probe() -> usize { THREAD_ALLOC_TRACK.with(|t| t.set(false)); THREAD_ALLOC_COUNT.with(|c| c.get()) } #[cfg(test)] mod tests { use super::*; use fluxer_rt_thread::TickInfo; fn snapshot_allocs() -> usize { ALLOC_PROBE.load(Ordering::Relaxed) } fn begin_thread_alloc_count() { begin_thread_alloc_probe(); } fn end_thread_alloc_count() -> usize { end_thread_alloc_probe() } fn build_test_runtime( source_count: usize, ) -> (Vec, AudioMixRuntime, CapturedMixOutputSink) { assert!(source_count > 0); let mut sources: Vec = Vec::with_capacity(source_count); let mut builder = AudioMixRuntimeBuilder::new(); for n in 0..source_count { let (source, consumer) = CaptureSource::create(n as u64 + 1, MIX_SAMPLE_RATE_HZ, MIX_CHANNELS) .expect("source"); let push_ns = source.last_push_ns_arc(); builder = builder.add_source_with_freshness(n as u64 + 1, consumer, push_ns); sources.push(source); } let sink = CapturedMixOutputSink::new(); let cloned_frames = sink.frames(); let cloned_resets = sink.resets(); let runtime = builder .build(CapturedMixOutputSink { frames: cloned_frames, resets: cloned_resets, }) .expect("build"); (sources, runtime, sink) } fn make_pure_tone(amplitude: i16, len: usize) -> Vec { assert!(len > 0); (0..len).map(|n| (n as i16 % 8) * amplitude / 8).collect() } #[derive(Debug)] struct FakeClock { value_ns: AtomicU64, } impl FakeClock { fn new(initial_ns: u64) -> Self { Self { value_ns: AtomicU64::new(initial_ns), } } fn set(&self, value_ns: u64) { self.value_ns.store(value_ns, Ordering::Release); } } impl MonotonicClock for FakeClock { fn now_ns(&self) -> u64 { self.value_ns.load(Ordering::Acquire) } } fn build_runtime_with_clock( source_count: usize, clock: Arc, ) -> (Vec, AudioMixRuntime, CapturedMixOutputSink) { assert!(source_count > 0); let mut sources: Vec = Vec::with_capacity(source_count); let mut builder = AudioMixRuntimeBuilder::new().with_clock(clock); for n in 0..source_count { let (source, consumer) = CaptureSource::create(n as u64 + 1, MIX_SAMPLE_RATE_HZ, MIX_CHANNELS) .expect("source"); let push_ns = source.last_push_ns_arc(); builder = builder.add_source_with_freshness(n as u64 + 1, consumer, push_ns); sources.push(source); } let sink = CapturedMixOutputSink::new(); let cloned_frames = sink.frames(); let cloned_resets = sink.resets(); let runtime = builder .build(CapturedMixOutputSink { frames: cloned_frames, resets: cloned_resets, }) .expect("build"); (sources, runtime, sink) } #[test] fn realtime_priority_guard_acquires_and_releases() { let guard = RealtimePriorityGuard::acquire(PriorityProfile::Audio); match guard { Ok(g) => { let is_valid = matches!( g.outcome(), RtOutcome::Acquired | RtOutcome::PartialFallback ); drop(g); assert!(is_valid); } Err(RtError::PlatformDenied(_)) => {} Err(other) => panic!("unexpected guard error: {other:?}"), } } #[test] fn source_ring_produce_consume_1000_frames_deterministic() { let (producer, mut consumer) = SourceRing::create(2048, 48_000).expect("pair"); let mut producer = producer; let payload: Vec = (0..1000).map(|n| (n as i16 % 4096) - 2048).collect(); let pushed = producer.try_push_slice(&payload); assert_eq!(pushed, 1000); let mut buf = vec![0i16; 1000]; let drained = consumer.drain_into(&mut buf); assert_eq!(drained, 1000); assert_eq!(buf, payload); } #[test] fn audio_mix_session_with_eight_pure_tone_sources_sums_to_saturated() { let (mut sources, mut runtime, sink) = build_test_runtime(8); let payload = vec![i16::MAX / 4; AUDIO_OUTPUT_FRAMES]; for source in sources.iter_mut() { let pushed = source.ingest_skip_apm(&payload); assert_eq!(pushed, AUDIO_OUTPUT_FRAMES); } let frame = runtime.run_one_tick_blocking(0).expect("frame"); assert_eq!(frame.samples.len(), AUDIO_OUTPUT_FRAMES); for sample in frame.samples.iter() { assert!(*sample > 0); } let frames_arc = sink.frames(); let frames_guard = frames_arc.lock().expect("lock"); assert_eq!(frames_guard.len(), 1); } #[test] fn tick_driver_maintains_cumulative_drift_free_schedule_over_100_ticks() { let clock = Arc::new(SystemMonotonicClock::new()); let period_ns = 1_000_000; let mut driver = TickDriver::new(clock, period_ns).expect("driver"); for _ in 0..100 { let info = driver.wait_until_next_tick().expect("tick"); assert!(info.actual_ns >= info.scheduled_ns.saturating_sub(period_ns)); } assert!(driver.next_tick_index() >= 100); } #[test] fn apm_stub_is_noop_on_capture_frame_but_counts() { let mut source = CaptureSource::create(1, MIX_SAMPLE_RATE_HZ, MIX_CHANNELS) .expect("src") .0; let original = make_pure_tone(1000, source.apm_frame_samples * 2); let mut samples = original.clone(); let before = source.apm_frames_processed(); source.ingest(&mut samples).expect("ingest"); let after = source.apm_frames_processed(); assert_eq!(samples, original); assert_eq!(after - before, 2); } #[test] fn ignore_audio_policy_reset_triggered_after_threshold() { use crate::ignore_audio_runtime::{ IgnoreAudioDecision, IgnoreAudioSourceState, IgnoreAudioTick, SOURCE_RESET_AFTER_BUFFERED_TICKS, }; let mut policy = IgnoreAudioPolicy::new(48_000).expect("ok"); let state = IgnoreAudioSourceState { id: 1, buffered_frames: (SOURCE_RESET_AFTER_BUFFERED_TICKS as u64) * 1024 + 1, last_frame_age_ns: 0, is_muted: false, }; let eval = policy .evaluate( &state, IgnoreAudioTick { at_ns: 1, period_ns: 21_333_333, }, ) .expect("ok"); assert_eq!(eval.decision, IgnoreAudioDecision::ResetSource); } #[test] fn empty_source_yields_silence_frame() { let (_sources, mut runtime, _sink) = build_test_runtime(1); let frame = runtime.run_one_tick_blocking(0).expect("frame"); for sample in frame.samples.iter() { assert_eq!(*sample, 0); } assert_eq!(frame.total_drained, 0); assert_eq!(frame.total_silence, AUDIO_OUTPUT_FRAMES as u64); } #[test] fn determinism_same_input_same_output() { let payload: Vec = (0..AUDIO_OUTPUT_FRAMES) .map(|n| (n as i16 % 1000) - 500) .collect(); let mut outputs: Vec> = Vec::new(); for _ in 0..2 { let (mut sources, mut runtime, _sink) = build_test_runtime(2); for source in sources.iter_mut() { let _ = source.ingest_skip_apm(&payload); } let frame = runtime.run_one_tick_blocking(0).expect("frame"); outputs.push(frame.samples.to_vec()); } assert_eq!(outputs[0], outputs[1]); } #[test] fn skipped_tick_increments_silence_count_on_empty_ring() { let (_sources, mut runtime, _sink) = build_test_runtime(1); let frame = runtime.run_one_tick_blocking(0).expect("frame"); assert_eq!(frame.total_silence, AUDIO_OUTPUT_FRAMES as u64); } #[test] fn end_to_end_one_source_drained_to_mix_session() { let (mut sources, mut runtime, sink) = build_test_runtime(1); let payload: Vec = (0..AUDIO_OUTPUT_FRAMES) .map(|n| (n as i16 % 2000) - 1000) .collect(); let pushed = sources[0].ingest_skip_apm(&payload); assert_eq!(pushed, AUDIO_OUTPUT_FRAMES); let _ = runtime.run_one_tick_blocking(0).expect("frame"); let frames = sink.frames(); let frames_guard = frames.lock().expect("lock"); assert_eq!(frames_guard.len(), 1); assert_eq!(frames_guard[0].samples.as_ref(), &payload[..]); } #[test] fn build_rejects_zero_sources() { let sink = NullMixOutputSink; let err = AudioMixRuntimeBuilder::new().build(sink).err(); assert!(matches!(err, Some(MixRuntimeError::NoSources))); } #[test] fn capture_source_buffered_frames_grows_on_ingest() { let mut source = CaptureSource::create(1, MIX_SAMPLE_RATE_HZ, MIX_CHANNELS) .expect("src") .0; let initial = source.buffered_frames(); assert_eq!(initial, 0); let payload = vec![5i16; 256]; let pushed = source.ingest_skip_apm(&payload); assert_eq!(pushed, 256); assert!(source.buffered_frames() > 0); } #[test] fn run_single_tick_lap_through_eight_sources_executes() { let (mut sources, mut runtime, _sink) = build_test_runtime(8); for (idx, source) in sources.iter_mut().enumerate() { let payload = vec![(idx as i16) * 100; AUDIO_OUTPUT_FRAMES]; let _ = source.ingest_skip_apm(&payload); } let frame = runtime.run_one_tick_blocking(0).expect("frame"); assert!(frame.total_drained > 0); } #[allow(clippy::no_effect_underscore_binding)] #[test] fn tick_info_synthetic_can_be_consumed() { let _ti = TickInfo { tick_index: 0, scheduled_ns: 0, actual_ns: 0, lag_ns: 0, }; } #[test] fn ingest_increments_apm_frame_count_per_chunk() { let mut source = CaptureSource::create(1, MIX_SAMPLE_RATE_HZ, MIX_CHANNELS) .expect("src") .0; assert_eq!(source.apm_frames_processed(), 0); let chunks = 7; let mut samples = vec![0i16; source.apm_frame_samples * chunks]; source.ingest(&mut samples).expect("ingest"); assert_eq!(source.apm_frames_processed(), chunks as u64); } #[test] fn stale_source_triggered_via_clock_emits_reset() { let clock = Arc::new(FakeClock::new(1_000_000)); let (sources, mut runtime, sink) = build_runtime_with_clock(1, Arc::clone(&clock) as Arc); sources[0].freshness.mark_push_at(1_000_000); clock.set(1_000_000 + 10_000_000_000); let _ = runtime.run_one_tick_blocking(0).expect("frame"); let resets = sink.resets(); let guard = resets.lock().expect("lock"); assert!(!guard.is_empty()); assert!( guard .iter() .any(|e| e.reason == crate::ignore_audio_runtime::IgnoreAudioResetReason::StaleSource) ); } #[test] fn last_frame_age_monotonic_within_quiescence() { let clock = Arc::new(FakeClock::new(1_000_000)); let (sources, mut runtime, _sink) = build_runtime_with_clock(1, Arc::clone(&clock) as Arc); sources[0].freshness.mark_push_at(1_000_000); let mut prior = 0u64; for n in 0..5 { clock.set(1_000_000 + (n + 1) * 1_000_000); let _ = runtime.run_one_tick_blocking(0).expect("frame"); let push = runtime.last_push_ns_for(0); let now = clock.now_ns(); let age = compute_age_ns(push, 1_000_000, now); assert!(age >= prior); prior = age; } } #[test] fn output_pool_buffers_rotate_modulo_depth_when_consumer_releases() { let (mut sources, mut runtime) = build_null_runtime(1); let payload = vec![123i16; AUDIO_OUTPUT_FRAMES]; let _ = sources[0].ingest_skip_apm(&payload); let f0 = runtime.run_one_tick_blocking(0).expect("frame"); let p0 = Arc::as_ptr(&f0.samples); drop(f0); let _ = runtime .run_one_tick_blocking(MIX_TICK_PERIOD_NS) .expect("frame"); let _ = runtime .run_one_tick_blocking(MIX_TICK_PERIOD_NS * 2) .expect("frame"); let _ = runtime .run_one_tick_blocking(MIX_TICK_PERIOD_NS * 3) .expect("frame"); let f4 = runtime .run_one_tick_blocking(MIX_TICK_PERIOD_NS * 4) .expect("frame"); let p4 = Arc::as_ptr(&f4.samples); assert_eq!(p0, p4); } #[test] fn output_pool_returns_distinct_buffers_when_consumer_retains() { let (mut sources, mut runtime, _sink) = build_test_runtime(1); let payload = vec![7i16; AUDIO_OUTPUT_FRAMES]; let _ = sources[0].ingest_skip_apm(&payload); let f0 = runtime.run_one_tick_blocking(0).expect("frame"); let f1 = runtime .run_one_tick_blocking(MIX_TICK_PERIOD_NS) .expect("frame"); let f2 = runtime .run_one_tick_blocking(MIX_TICK_PERIOD_NS * 2) .expect("frame"); let f3 = runtime .run_one_tick_blocking(MIX_TICK_PERIOD_NS * 3) .expect("frame"); let p0 = Arc::as_ptr(&f0.samples); let p1 = Arc::as_ptr(&f1.samples); let p2 = Arc::as_ptr(&f2.samples); let p3 = Arc::as_ptr(&f3.samples); assert!(p0 != p1); assert!(p1 != p2); assert!(p2 != p3); assert_eq!(f0.samples.len(), AUDIO_OUTPUT_FRAMES); } #[test] fn output_arc_samples_length_invariant_holds() { let (mut sources, mut runtime, _sink) = build_test_runtime(1); let payload = vec![1i16; AUDIO_OUTPUT_FRAMES]; let _ = sources[0].ingest_skip_apm(&payload); let frame = runtime.run_one_tick_blocking(0).expect("frame"); assert_eq!(frame.samples.len(), AUDIO_OUTPUT_FRAMES); assert_eq!(frame.samples.as_ref().len(), 1024); } fn build_null_runtime(source_count: usize) -> (Vec, AudioMixRuntime) { assert!(source_count > 0); let mut sources: Vec = Vec::with_capacity(source_count); let mut builder = AudioMixRuntimeBuilder::new(); for n in 0..source_count { let (source, consumer) = CaptureSource::create(n as u64 + 1, MIX_SAMPLE_RATE_HZ, MIX_CHANNELS) .expect("source"); let push_ns = source.last_push_ns_arc(); builder = builder.add_source_with_freshness(n as u64 + 1, consumer, push_ns); sources.push(source); } let runtime = builder.build(NullMixOutputSink).expect("build"); (sources, runtime) } #[test] fn tick_steady_state_does_not_allocate() { let (mut sources, mut runtime) = build_null_runtime(4); let payload = vec![10i16; AUDIO_OUTPUT_FRAMES]; for _warm in 0..OUTPUT_BUFFER_POOL_DEPTH + 2 { for source in sources.iter_mut() { let _ = source.ingest_skip_apm(&payload); } let _ = runtime.run_one_tick_blocking(0).expect("warmup"); } for source in sources.iter_mut() { let _ = source.ingest_skip_apm(&payload); } begin_thread_alloc_count(); let _ = runtime .run_one_tick_blocking(MIX_TICK_PERIOD_NS * 10) .expect("frame"); let allocations = end_thread_alloc_count(); assert_eq!( allocations, 0, "steady-state tick allocated {allocations} times", ); let _ = snapshot_allocs(); } #[test] fn freshness_handle_records_push_time() { let source = CaptureSource::create(7, MIX_SAMPLE_RATE_HZ, MIX_CHANNELS) .expect("src") .0; let initial = source.freshness_handle().last_push_ns(); assert_eq!(initial, NEVER_PUSHED_SENTINEL); source.freshness_handle().mark_push_at(42); assert_eq!(source.freshness_handle().last_push_ns(), 42); } #[test] fn compute_age_ns_uses_registration_for_never_pushed_source() { let age = compute_age_ns(NEVER_PUSHED_SENTINEL, 1_000, 5_000); assert_eq!(age, 4_000); } #[test] fn compute_age_ns_returns_zero_when_now_before_push() { let age = compute_age_ns(1_000, 500, 500); assert_eq!(age, 0); } #[test] fn compute_age_ns_returns_difference_after_push() { let age = compute_age_ns(1_000, 0, 5_000); assert_eq!(age, 4_000); } #[test] fn ramp_advances_per_tick_through_runtime() { let (mut sources, mut runtime, _sink) = build_test_runtime(1); let payload = vec![100i16; AUDIO_OUTPUT_FRAMES]; let _ = sources[0].ingest_skip_apm(&payload); assert_eq!(runtime.current_ramp_gain(0), 0.0); let _ = runtime.run_one_tick_blocking(0).expect("frame"); let g1 = runtime.current_ramp_gain(0); assert!(g1 > 0.0); for n in 1..6u64 { let _ = runtime .run_one_tick_blocking(n * MIX_TICK_PERIOD_NS) .expect("frame"); } let g_after = runtime.current_ramp_gain(0); assert_eq!(g_after, 1.0); } #[test] fn smoother_initialises_after_first_push() { let (mut sources, mut runtime, _sink) = build_test_runtime(1); assert!(!runtime.smoother_initialised(0)); let payload = vec![1i16; AUDIO_OUTPUT_FRAMES]; let _ = sources[0].ingest_skip_apm(&payload); let _ = runtime.run_one_tick_blocking(0).expect("frame"); assert!(runtime.smoother_initialised(0)); } #[test] fn registered_never_pushed_source_eligible_for_stale_after_threshold() { let clock = Arc::new(FakeClock::new(1_000_000)); let (_sources, runtime, _sink) = build_runtime_with_clock(1, Arc::clone(&clock) as Arc); clock.set(1_000_000 + 6_000_000_000); let stale = runtime.is_source_stale(0, 1_000_000 + 6_000_000_000, 5_000_000_000); assert!(stale); } #[test] fn tick_marks_pushed_when_freshness_atomic_advances() { let (mut sources, mut runtime, _sink) = build_test_runtime(1); assert_eq!(runtime.mark_pushed_total(), 0); let payload = vec![100i16; AUDIO_OUTPUT_FRAMES]; let pushed = sources[0].ingest_skip_apm(&payload); assert_eq!(pushed, AUDIO_OUTPUT_FRAMES); let _ = runtime.run_one_tick_blocking(0).expect("frame"); assert!(runtime.mark_pushed_total() >= 1); let observed_push = sources[0].freshness_handle().last_push_ns(); assert_ne!(observed_push, u64::MAX); let stale = runtime.is_source_stale(0, observed_push + 1_000_000, 5_000_000_000); assert!(!stale); } #[test] fn tick_does_not_double_count_unchanged_push_atomic() { let (mut sources, mut runtime, _sink) = build_test_runtime(1); let payload = vec![50i16; AUDIO_OUTPUT_FRAMES]; let _ = sources[0].ingest_skip_apm(&payload); let _ = runtime.run_one_tick_blocking(0).expect("frame"); let first_total = runtime.mark_pushed_total(); assert!(first_total >= 1); let _ = runtime .run_one_tick_blocking(MIX_TICK_PERIOD_NS) .expect("frame"); let second_total = runtime.mark_pushed_total(); assert_eq!(first_total, second_total); } }