Skip to main content

media_pp/elements/source/
audio_mixer.rs

1use std::{
2    collections::{HashMap, VecDeque},
3    sync::{
4        Arc, Mutex, Weak,
5        atomic::{AtomicU64, Ordering},
6    },
7    thread,
8    time::{Duration, Instant},
9};
10
11use crate::pp_log::{PpLog, pp_error, pp_info};
12use ffmpeg_next as ffmpeg;
13use thiserror::Error as ThisError;
14
15use crate::{
16    buffer::MediaBuffer,
17    bus::{Bus, BusEvent},
18    contract::{InputContract, MediaKind, MemoryDomain, OutputContract, PortContract},
19    control::{ControlMsg, ControlReceiver, drain_control},
20    element::{Element, ElementType, Sink, Source, SourceElement, element_pp_log},
21    elements::AudioFormat,
22    elements::filter::audio_resampler::AudioFrameResampler,
23    error::Result,
24    pad::SrcPad,
25    schedule::ActiveTimeline,
26};
27
28/// How often [`AudioMixer::run`] mixes and emits a combined frame — same
29/// role as [`crate::elements::DxgiCaptureSource`]'s own `POLL_GRANULARITY`/
30/// `crate::elements::WasapiCaptureSource`'s `POLL_INTERVAL`: bounds `Stop`
31/// latency and sets the mixer's own output granularity.
32const TICK_INTERVAL: Duration = Duration::from_millis(20);
33
34/// Errors specific to `AudioMixer`. Converts into the crate-wide `Error`
35/// via `?` (see [`crate::error::Error`]).
36#[derive(Debug, ThisError)]
37pub enum AudioMixerError {
38    /// FFmpeg rejected resampler creation or audio conversion.
39    #[error("ffmpeg error: {0}")]
40    Ffmpeg(#[from] ffmpeg_next::Error),
41
42    /// Seeking was requested on a live mixer with no stored timeline.
43    #[error("AudioMixer doesn't support seeking a live mix")]
44    SeekUnsupported,
45
46    /// An input sink received a buffer other than decoded audio or end-of-stream.
47    #[error("AudioMixer inputs only accept Audio or Eos buffers, got {0}")]
48    UnsupportedBuffer(&'static str),
49}
50
51/// Construction-time options for [`AudioMixer::new`] — the mixer's fixed
52/// *output* format. Every input is resampled to match this on the way in
53/// (see `InputBuffer::push`); the mixer never adapts to whatever an
54/// input happens to produce.
55#[derive(Debug, Clone, Copy)]
56pub struct AudioMixerOptions {
57    /// Sample rate of every mixed output frame, in hertz.
58    pub sample_rate: u32,
59    /// Channel count of every mixed output frame.
60    pub channels: u16,
61}
62
63/// The format every input is resampled to and every output frame carries.
64///
65/// One value rather than three fields, packed into a word, so an input
66/// resampling on its own thread can never catch a sample rate from one
67/// setting and a channel count from another. `format` and `channel_layout`
68/// are derived from it rather than stored: this mixer works in interleaved
69/// `f32` and the layout is whatever is default for the channel count, so a
70/// stored copy of either could only ever disagree.
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub struct MixFormat {
73    pub sample_rate: u32,
74    pub channels: u16,
75}
76
77impl MixFormat {
78    /// Interleaved `f32` — see [`AudioMixer`] on why the mix is summed in
79    /// one fixed sample format rather than whatever arrived.
80    fn sample_format(self) -> ffmpeg::format::Sample {
81        ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Packed)
82    }
83
84    fn channel_layout(self) -> ffmpeg::ChannelLayout {
85        ffmpeg::ChannelLayout::default(self.channels as i32)
86    }
87
88    fn pack(self) -> u64 {
89        ((self.sample_rate as u64) << 16) | self.channels as u64
90    }
91
92    fn unpack(packed: u64) -> Self {
93        Self {
94            sample_rate: (packed >> 16) as u32,
95            channels: packed as u16,
96        }
97    }
98}
99
100/// Where the sample deficit is measured from.
101///
102/// Not `Duration::ZERO` and zero samples, because the mix format can change
103/// while this runs and `elapsed × sample_rate` only means anything against
104/// the rate that produced the count it is compared to — see
105/// [`AudioMixer::mix_tick`].
106#[derive(Debug, Clone, Copy)]
107struct MixAnchor {
108    format: MixFormat,
109    elapsed: Duration,
110    samples: i64,
111}
112
113/// The mix format, shared between the mixer's own tick and every input
114/// resampling into it from another thread.
115#[derive(Debug)]
116struct SharedMixFormat(AtomicU64);
117
118impl SharedMixFormat {
119    fn new(format: MixFormat) -> Self {
120        Self(AtomicU64::new(format.pack()))
121    }
122
123    fn get(&self) -> MixFormat {
124        MixFormat::unpack(self.0.load(Ordering::Relaxed))
125    }
126
127    /// `false` for a format nothing could be resampled to, which leaves the
128    /// running one alone rather than a mixer summing into nothing.
129    fn set(&self, format: MixFormat) -> bool {
130        if format.sample_rate == 0 || format.channels == 0 {
131            return false;
132        }
133        self.0.store(format.pack(), Ordering::Relaxed);
134        true
135    }
136}
137
138/// One input's own resampler and accumulated (already-resampled,
139/// interleaved `f32`) samples, waiting to be drained by the next
140/// [`AudioMixer::mix_tick`]. The resampler is built lazily from the first
141/// frame this input ever sees (its `format`/`channel_layout`/`rate`
142/// self-describe — no need for [`MixerHandle::add_source`] to be told
143/// this upfront).
144struct InputBuffer {
145    /// Identity of this particular registration. The name can be reused,
146    /// but an older sink must not be allowed to touch its replacement.
147    id: u64,
148    /// Built lazily from the first frame, and rebuilt when the mix format
149    /// moves under it. Kept beside it rather than walked and invalidated
150    /// from outside: an input that notices for itself needs no lock held
151    /// across somebody else's state, and an input registered after a change
152    /// is correct without being told. The *arriving* format moving is the
153    /// resampler's own business — [`AudioFrameResampler`] rebuilds for that,
154    /// draining what the old context still held first.
155    resampler: Option<(MixFormat, AudioFrameResampler)>,
156    samples: VecDeque<f32>,
157    /// Set once this input's `Eos` arrives — [`AudioMixer::mix_tick`]
158    /// drops the input entirely once it's both `eos` and fully drained,
159    /// same as a `Tee` branch dropping out once removed. Unlike a fixed
160    /// two-track muxer, `AudioMixer` has no fixed input count to wait on:
161    /// one input reaching `Eos` just means the mix continues without it.
162    eos: bool,
163}
164
165impl InputBuffer {
166    fn push(
167        &mut self,
168        frame: &ffmpeg::frame::Audio,
169        to: MixFormat,
170    ) -> std::result::Result<(), AudioMixerError> {
171        // Rebuilt when the mix format has moved. A resampler is fixed at
172        // both ends when it is created, so one built for the old mix format
173        // would keep producing it — silently, and into a mix that no longer
174        // wants it.
175        if self
176            .resampler
177            .as_ref()
178            .is_none_or(|(built, _)| *built != to)
179        {
180            self.resampler = Some((
181                to,
182                AudioFrameResampler::new(AudioFormat::new(
183                    to.sample_format(),
184                    to.sample_rate,
185                    to.channels,
186                )),
187            ));
188        }
189        // Through the shared engine rather than a `Context` of this
190        // element's own, for the sizing above all. Handed an unallocated
191        // output frame, `Context::run` gives it room for exactly as many
192        // samples as went in — which is short by the rate ratio whenever an
193        // input is *slower* than the mix, so 44.1kHz media into a 48kHz mix
194        // left 8% of every frame behind in libswresample's own delay. It is
195        // not dropped, which would merely be a click: it comes back on the
196        // next call, which is short by the same 8% again, so the input feeds
197        // the mix at 92% of real time. The mixer fills the shortfall of each
198        // tick with silence — a chop at the tick rate — and what does arrive
199        // falls further behind its own picture every second.
200        let resampled = {
201            let (_, resampler) = self
202                .resampler
203                .as_mut()
204                .expect("just built if it was missing or stale");
205            resampler.run(frame)?
206        };
207        for output in &resampled {
208            // Raw bytes, not `plane::<f32>(0)`: `ffmpeg_next`'s `plane::<T>()`
209            // always returns exactly `output.samples()` elements of type `T`,
210            // which for **packed multi-channel** data (this mixer's own fixed
211            // `Sample::F32(Packed)` target — see `AudioMixer::new`) covers only
212            // the first `samples()` of the real `samples() * channels`
213            // interleaved scalars actually in the buffer, silently dropping
214            // every channel past the first once the mix layout has more than
215            // one. Same fix, and the same root cause, as
216            // `crate::elements::SwAudioEncoder`'s own `absorb_resampled`
217            // (found while building that element — this call predates it).
218            let samples = output.samples();
219            let channels = to.channels as usize;
220            let bytes = &output.data(0)[..samples * channels * 4];
221            let interleaved =
222                // SAFETY: `bytes` is a prefix of an FFmpeg audio plane, which is aligned
223                // well past 4 and whose length here is an exact multiple of four bytes —
224                // `samples * channels * 4`.
225                unsafe { std::slice::from_raw_parts(bytes.as_ptr() as *const f32, bytes.len() / 4) };
226            self.samples.extend(interleaved.iter().copied());
227        }
228        Ok(())
229    }
230}
231
232/// Shared state between [`AudioMixer`] and every [`MixerHandle`]/
233/// [`MixerInputSink`] derived from it — just the input map, behind one
234/// lock (same granularity [`crate::elements::Tee`]'s own `TeeShared::pads`
235/// uses: one lock for the whole collection, not one per entry, since a mix
236/// tick already needs to visit every input together anyway).
237struct MixerShared {
238    inputs: Mutex<HashMap<Arc<str>, InputBuffer>>,
239    /// What every input resamples to and every output frame carries. Here
240    /// rather than on the mixer, because the inputs reading it are on other
241    /// threads and this is the only thing they share with it.
242    format: SharedMixFormat,
243    /// Issues a distinct identity for every `add_source` call, including
244    /// replacements registered under an existing name.
245    next_input_id: AtomicU64,
246}
247
248/// A cheaply-cloneable handle for adding or removing an [`AudioMixer`]'s
249/// input sources while the pipeline is running — the mirror image of
250/// [`crate::elements::TeeHandle`]: `Tee` lets you attach/detach *outputs*
251/// from another thread; this lets you attach/detach *inputs*. Keeps only a
252/// [`Weak`] reference for the same reason `TeeHandle` does: retaining a
253/// handle after the mixer's own pipeline finishes must not keep its
254/// internal state alive forever, and every operation becomes a harmless
255/// no-op once the mixer is gone.
256#[derive(Clone)]
257pub struct MixerHandle {
258    shared: Weak<MixerShared>,
259}
260
261impl MixerHandle {
262    /// Registers a new input under `name` and returns a [`Sink`] to use as
263    /// a detached branch terminal. Build and attach it inside that source's
264    /// own `Pipeline::new` wiring closure — a
265    /// *different* pipeline/thread than this mixer's own, which is exactly
266    /// the point). `None` once the mixer itself is gone. Calling this
267    /// again with a name already in use replaces that input outright
268    /// (whatever it had buffered is dropped) rather than erroring — same
269    /// "just do what was asked" spirit as `HashMap::insert`. A sink from
270    /// the previous registration then becomes inert: its data, `Eos`, and
271    /// `Stop` cannot affect the replacement sharing its name.
272    ///
273    /// The input endpoint appears in the upstream source pipeline's graph
274    /// when attached through [`crate::element::Context::attach`]. The graph
275    /// intentionally does not invent a cross-pipeline edge to the mixer.
276    pub fn add_source(&self, name: impl Into<String>) -> Option<Box<dyn Sink>> {
277        let shared = self.shared.upgrade()?;
278        let name: Arc<str> = name.into().into();
279        let id = shared.next_input_id.fetch_add(1, Ordering::Relaxed);
280        shared.inputs.lock().unwrap().insert(
281            name.clone(),
282            InputBuffer {
283                id,
284                resampler: None,
285                samples: VecDeque::new(),
286                eos: false,
287            },
288        );
289        Some(Box::new(MixerInputSink {
290            name: name.clone(),
291            id,
292            pp_log: element_pp_log(ElementType::AudioMixerInput, &name, None),
293            shared: self.shared.clone(),
294        }))
295    }
296
297    /// The format the mix is summed into and emitted at.
298    ///
299    /// `None` once the mixer is gone.
300    pub fn mix_format(&self) -> Option<MixFormat> {
301        Some(self.shared.upgrade()?.format.get())
302    }
303
304    /// Changes it, from the next tick.
305    ///
306    /// Returns `false` for a format nothing could be resampled to, and for a
307    /// mixer that has already been dropped.
308    ///
309    /// # What moves with it
310    ///
311    /// Every input rebuilds its own resampler when it next pushes — each
312    /// remembers what its own was built for, so none has to be found and
313    /// invalidated from here, and an input registered after this call is
314    /// correct without being told.
315    ///
316    /// [`AudioMixer::time_base`] is `1/sample_rate`, and the output `pts` is
317    /// a running sample count in those units. So changing the rate re-means
318    /// every timestamp after it, while the ones already downstream were
319    /// stamped under the old one. Nothing here can repair that: a muxer
320    /// holding a time base from `avformat_write_header` will not be told, and
321    /// an encoder was opened for a channel count.
322    ///
323    /// So this is safe exactly while nothing downstream is reading timestamps
324    /// — a level meter, an idle mixer — and it is the caller's to know. In
325    /// practice: change it between recordings, not during one. The mixer
326    /// itself keeps running either way, which is the point: its `pts` stays
327    /// continuous, and a rate change is not a reason to restart the one
328    /// element every audio source in the application is registered with.
329    pub fn set_mix_format(&self, format: MixFormat) -> bool {
330        self.shared
331            .upgrade()
332            .is_some_and(|shared| shared.format.set(format))
333    }
334
335    /// Drops `name`'s input immediately, discarding whatever it had
336    /// buffered — a no-op if `name` isn't currently registered, or the
337    /// mixer is gone.
338    pub fn remove_source(&self, name: &str) {
339        if let Some(shared) = self.shared.upgrade() {
340            shared.inputs.lock().unwrap().remove(name);
341        }
342    }
343
344    /// Returns the number of inputs currently registered with the live mixer.
345    ///
346    /// Returns zero after the mixer has been dropped.
347    pub fn source_count(&self) -> usize {
348        self.shared
349            .upgrade()
350            .map(|shared| shared.inputs.lock().unwrap().len())
351            .unwrap_or(0)
352    }
353}
354
355/// One [`AudioMixer`] input, returned by [`MixerHandle::add_source`].
356/// Resamples every incoming frame to the mixer's fixed output format and
357/// appends it to this input's own buffer — the actual summing happens
358/// later, on [`AudioMixer::run`]'s own thread, not here. `consume` runs on
359/// whatever thread is driving the *upstream* source this got linked to
360/// (a different pipeline's own thread, in the normal case), so every
361/// access to the shared input map goes through `MixerShared`'s lock.
362pub struct MixerInputSink {
363    pp_log: PpLog,
364    name: Arc<str>,
365    /// Identity returned by the corresponding `add_source` call. Compared
366    /// with the map entry before every mutation so a stale sink cannot
367    /// write to or remove a same-name replacement.
368    id: u64,
369    shared: Weak<MixerShared>,
370}
371
372// SAFETY: `ffmpeg::ChannelLayout` wraps `AVChannelLayout`, which carries a
373// non-`Send` custom-layout pointer only for `AV_CHANNEL_ORDER_CUSTOM`
374// layouts. Every `ChannelLayout` here comes from `ChannelLayout::default`
375// (see `AudioMixer::new`) — a plain native layout, that pointer always
376// null — so there's nothing thread-unsafe actually being sent.
377unsafe impl Send for MixerInputSink {}
378
379impl Element for MixerInputSink {
380    fn name(&self) -> Arc<str> {
381        self.name.clone()
382    }
383
384    fn element_type(&self) -> ElementType {
385        ElementType::AudioMixerInput
386    }
387
388    fn pp_log(&self) -> &PpLog {
389        &self.pp_log
390    }
391
392    fn pp_log_mut(&mut self) -> &mut PpLog {
393        &mut self.pp_log
394    }
395}
396
397impl Sink for MixerInputSink {
398    /// Every input is summed sample by sample, so each carries decoded
399    /// audio just as the mixed output does.
400    fn input_contract(&self) -> InputContract {
401        InputContract::Fixed(PortContract::frame(
402            MediaKind::AudioFrame,
403            MemoryDomain::System,
404        ))
405    }
406
407    fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
408        let Some(shared) = self.shared.upgrade() else {
409            return Ok(()); // mixer's own pipeline already ended — nothing to feed
410        };
411        match buf {
412            MediaBuffer::Audio(frame) => {
413                let mut inputs = shared.inputs.lock().unwrap();
414                if let Some(input) = inputs.get_mut(&self.name)
415                    && input.id == self.id
416                {
417                    // Read per buffer rather than copied in at `add_source`: this
418                    // sink lives on the input's own thread, and a mix format
419                    // changed while it is running has to reach it there.
420                    input.push(&frame, shared.format.get())?;
421                }
422                // Absent means `remove_source` raced ahead of this frame;
423                // an ID mismatch means this name was replaced. Dropping
424                // the frame is correct in both cases.
425            }
426            MediaBuffer::Eos => {
427                let mut inputs = shared.inputs.lock().unwrap();
428                if let Some(input) = inputs.get_mut(&self.name)
429                    && input.id == self.id
430                {
431                    input.eos = true;
432                }
433            }
434            other => {
435                pp_error!(self, "unsupported buffer: expected Audio or Eos");
436                return Err(AudioMixerError::UnsupportedBuffer(other.kind()).into());
437            }
438        }
439        Ok(())
440    }
441
442    /// No downstream of its own to cascade to — this is a leaf input slot,
443    /// not a passthrough. `Stop` removes this input immediately, same as
444    /// [`MixerHandle::remove_source`]: `Stop` means abandon now, not drain
445    /// to a natural `Eos` (see `ControlMsg::Stop`'s own docs), and for a
446    /// live capture source — `WasapiCaptureSource`
447    /// included — `Stop` is the *only* shutdown signal that ever arrives;
448    /// it never reaches `Eos` on its own. Relying on `Eos` alone to clean
449    /// up (as an earlier version of this did) left a stale entry in
450    /// `shared.inputs` forever whenever a caller stopped its capture
451    /// pipeline normally instead of remembering to call
452    /// `MixerHandle::remove_source` by hand. `Pause`/`Resume`/`Seek` need
453    /// no handling here — this input has no thread or queue of its own to
454    /// freeze/resume, and a live capture source doesn't seek. Removal is
455    /// conditional on the registration ID: a late `Stop` from a replaced
456    /// sink must not remove the newer input using the same name.
457    fn control(&mut self, msg: ControlMsg) -> Result<()> {
458        if msg == ControlMsg::Stop
459            && let Some(shared) = self.shared.upgrade()
460        {
461            let mut inputs = shared.inputs.lock().unwrap();
462            if inputs
463                .get(&self.name)
464                .is_some_and(|input| input.id == self.id)
465            {
466                inputs.remove(&self.name);
467            }
468        }
469        Ok(())
470    }
471}
472
473/// Sums an arbitrary, dynamically-changing number of audio sources into
474/// one output stream — the structural mirror of [`crate::elements::Tee`]:
475/// `Tee` is one input fanned out to a dynamic set of outputs behind a
476/// lock; `AudioMixer` is a dynamic set of inputs (added/removed via
477/// [`MixerHandle`], from whatever thread each one's own source pipeline
478/// runs on) summed into one output. Unlike `Tee`, which is a passive
479/// [`Sink`] driven entirely by whatever calls `consume`, `AudioMixer` has
480/// to drive itself: it's a [`SourceElement`] with its own `run` thread,
481/// ticking every `TICK_INTERVAL` to sum however many samples each
482/// currently-attached input has ready — because mixing has to keep
483/// producing *something* on a steady clock even when some (or all) inputs
484/// have gone quiet, the same reason
485/// `WasapiCaptureSource` synthesizes silence for gaps
486/// rather than just emitting nothing.
487///
488/// Every input is resampled to this mixer's own `sample_rate`/`channels`,
489/// which [`MixerHandle::set_mix_format`] can change while it runs — each
490/// input notices for itself and rebuilds its own resampler. The sample
491/// format is fixed at `Sample::F32(Packed)`: float headroom during
492/// summation, the same reason real mixing consoles work in float even when
493/// everything else is integer PCM. An input
494/// short on samples for a given tick contributes silence for the
495/// shortfall rather than blocking the whole mix. Samples are summed and
496/// **hard-clipped** to `[-1.0, 1.0]`, not averaged: two or three sources
497/// is the expected case, where clipping is rare, and averaging would
498/// quietly lower the whole mix's volume every time a source count
499/// changes — a caller who wants headroom can lower an individual input's
500/// gain before it ever reaches the mixer (not implemented — nothing needs
501/// it yet).
502///
503/// `pts` is a plain, always-continuous sample count (see
504/// [`AudioMixer::time_base`]), advancing in lockstep with wall-clock time
505/// regardless of which/how many inputs are actually contributing at any
506/// moment.
507///
508/// Runs until `Stop` — never reaches `Eos` on its own, same as every
509/// other live source in this crate; an individual input reaching `Eos` or
510/// being removed just drops out of future ticks, it doesn't end the mix.
511pub struct AudioMixer {
512    pp_log: PpLog,
513    name: Arc<str>,
514    shared: Arc<MixerShared>,
515    pad: SrcPad,
516    /// Cumulative sample count across every emitted frame — see
517    /// [`AudioMixer::time_base`].
518    samples_emitted: i64,
519    /// What the deficit is measured from — see [`MixAnchor`].
520    anchor: MixAnchor,
521}
522
523// SAFETY: see `MixerInputSink`'s own `unsafe impl Send` docs — same
524// reasoning, `channel_layout` here is always `ChannelLayout::default`'s
525// plain native layout.
526unsafe impl Send for AudioMixer {}
527
528impl AudioMixer {
529    /// Starts with no inputs — add some via the returned [`MixerHandle`]
530    /// before (or any time after) wiring `AudioMixer` into a
531    /// [`crate::pipeline::Pipeline`] (`Pipeline::new` registers it as that
532    /// pipeline's own source automatically, same as any other
533    /// [`SourceElement`] — no [`crate::element::Context`] needed here,
534    /// unlike [`crate::elements::TeeBuilder::new`], since `AudioMixer` has no
535    /// chains of its own for a handle to build).
536    pub fn new(name: impl Into<String>, options: AudioMixerOptions) -> (Self, MixerHandle) {
537        let name: Arc<str> = name.into().into();
538        let pp_log = element_pp_log(ElementType::AudioMixer, &name, None);
539        pp_info!(
540            pp_log: &pp_log,
541            "created: {}Hz, {} channel(s)",
542            options.sample_rate,
543            options.channels
544        );
545
546        let format = MixFormat {
547            sample_rate: options.sample_rate,
548            channels: options.channels,
549        };
550        let shared = Arc::new(MixerShared {
551            inputs: Mutex::new(HashMap::new()),
552            format: SharedMixFormat::new(format),
553            next_input_id: AtomicU64::new(0),
554        });
555        let pad = SrcPad::with_contract(
556            format!("{name}_src"),
557            OutputContract::Fixed(PortContract::frame(
558                MediaKind::AudioFrame,
559                MemoryDomain::System,
560            )),
561        );
562        (
563            Self {
564                name: name.clone(),
565                pp_log,
566                shared: shared.clone(),
567                pad,
568                samples_emitted: 0,
569                anchor: MixAnchor {
570                    format,
571                    elapsed: Duration::ZERO,
572                    samples: 0,
573                },
574            },
575            MixerHandle {
576                shared: Arc::downgrade(&shared),
577            },
578        )
579    }
580
581    /// The unit each emitted frame's `pts` is expressed in.
582    pub fn time_base(&self) -> ffmpeg::Rational {
583        ffmpeg::Rational::new(1, self.shared.format.get().sample_rate as i32)
584    }
585
586    /// Sums however many samples are needed to keep `samples_emitted` in
587    /// lockstep with `elapsed` (a no-op if nothing's owed yet — same
588    /// wall-clock-deficit shape as
589    /// [`crate::elements::WasapiCaptureSource::fill_silence_gap`], just
590    /// summing real contributions from every input instead of emitting
591    /// pure silence). `elapsed` already excludes time spent frozen inside
592    /// `Pause` (see [`crate::schedule::ActiveTimeline`]) so a `Pause`/
593    /// `Resume` pair doesn't get summed as a burst of owed samples the
594    /// moment playback resumes. Drops any input that's both `eos` and
595    /// fully drained — it contributed its last real samples on a previous
596    /// tick and has nothing left to give.
597    fn mix_tick(&mut self, elapsed: Duration, bus: &Bus) {
598        let format = self.shared.format.get();
599        // Re-anchored when the format moves. The deficit below is
600        // `elapsed × sample_rate` against a running count, and those two are
601        // only comparable while the rate that produced them is the same one:
602        // measured straight, a drop from 48 kHz to 44.1 makes `expected` fall
603        // *below* what has already been emitted, and the mixer goes silent
604        // for the minute it takes the new rate to catch up. So the count is
605        // kept — `pts` must stay continuous — and only the deficit starts
606        // again, from here.
607        if format != self.anchor.format {
608            pp_info!(
609                self,
610                "mix format is now {}Hz, {} channel(s)",
611                format.sample_rate,
612                format.channels
613            );
614            self.anchor = MixAnchor {
615                format,
616                elapsed,
617                samples: self.samples_emitted,
618            };
619        }
620        let channels = format.channels as usize;
621        let since = elapsed.saturating_sub(self.anchor.elapsed);
622        let expected =
623            self.anchor.samples + (since.as_secs_f64() * format.sample_rate as f64) as i64;
624        let needed = (expected - self.samples_emitted).max(0) as usize;
625        if needed == 0 {
626            return;
627        }
628        let mut mixed = vec![0f32; needed * channels];
629        {
630            let mut inputs = self.shared.inputs.lock().unwrap();
631            inputs.retain(|_, input| !(input.eos && input.samples.is_empty()));
632            for input in inputs.values_mut() {
633                let take = mixed.len().min(input.samples.len());
634                for (slot, sample) in mixed.iter_mut().zip(input.samples.iter()) {
635                    *slot += *sample;
636                }
637                input.samples.drain(0..take);
638            }
639        }
640        for sample in &mut mixed {
641            *sample = sample.clamp(-1.0, 1.0);
642        }
643
644        let mut frame =
645            ffmpeg::frame::Audio::new(format.sample_format(), needed, format.channel_layout());
646        frame.set_rate(format.sample_rate);
647        // SAFETY: viewing an `f32` slice as bytes, which is always aligned and
648        // exactly `size_of_val` long. The read is only as wide as `mixed` itself;
649        // what the *destination* can take is the separate bound the comment below
650        // describes.
651        let bytes = unsafe {
652            std::slice::from_raw_parts(mixed.as_ptr() as *const u8, std::mem::size_of_val(&*mixed))
653        };
654        // `frame.data_mut(0)`'s length is FFmpeg's own padded linesize,
655        // not necessarily `mixed.len() * 4` exactly — only ever write that
656        // tight amount (same bound `frame.plane::<T>()` itself reads via
657        // `samples()`), never assume the destination's full length
658        // matches `bytes` (see `WasapiCaptureSource::build_frame`'s own
659        // identical fix).
660        frame.data_mut(0)[..bytes.len()].copy_from_slice(bytes);
661        frame.set_pts(Some(self.samples_emitted));
662        self.samples_emitted += needed as i64;
663
664        if let Err(error) = self.pad.push(MediaBuffer::Audio(Arc::new(frame))) {
665            bus.post(
666                &self.pp_log,
667                BusEvent::Error {
668                    element_type: ElementType::AudioMixer,
669                    name: self.name.clone(),
670                    error,
671                },
672            );
673        }
674    }
675}
676
677impl Element for AudioMixer {
678    fn name(&self) -> Arc<str> {
679        self.name.clone()
680    }
681
682    fn element_type(&self) -> ElementType {
683        ElementType::AudioMixer
684    }
685
686    fn pp_log(&self) -> &PpLog {
687        &self.pp_log
688    }
689
690    fn pp_log_mut(&mut self) -> &mut PpLog {
691        &mut self.pp_log
692    }
693}
694
695impl Source for AudioMixer {
696    fn src_pads(&mut self) -> &mut [SrcPad] {
697        std::slice::from_mut(&mut self.pad)
698    }
699}
700
701impl SourceElement for AudioMixer {
702    fn is_live(&self) -> bool {
703        true
704    }
705
706    fn is_seekable(&self) -> bool {
707        false
708    }
709
710    fn run(&mut self, control: &ControlReceiver, bus: &Bus) -> Result<()> {
711        pp_info!(self, "started");
712        let mut timeline = ActiveTimeline::new(Instant::now());
713        loop {
714            let outcome = drain_control(control, self, bus)?;
715            if outcome.stopped {
716                pp_info!(self, "stopped");
717                return Ok(());
718            }
719            timeline.account_pause(outcome.paused_for);
720            thread::sleep(TICK_INTERVAL);
721            self.mix_tick(timeline.elapsed(Instant::now()), bus);
722        }
723    }
724
725    fn seek(&mut self, _target: Duration) -> Result<Duration> {
726        Err(AudioMixerError::SeekUnsupported.into())
727    }
728}
729
730#[cfg(test)]
731mod tests {
732    use std::sync::{
733        Mutex as StdMutex,
734        atomic::{AtomicBool, Ordering},
735    };
736
737    use crate::pp_log::PpLog;
738
739    use super::*;
740    use crate::pipeline::Pipeline;
741
742    fn constant_frame(value: f32, samples: usize, rate: u32) -> ffmpeg::frame::Audio {
743        let mut frame = ffmpeg::frame::Audio::new(
744            ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Packed),
745            samples,
746            ffmpeg::ChannelLayout::default(1),
747        );
748        frame.set_rate(rate);
749        frame.plane_mut::<f32>(0).fill(value);
750        frame
751    }
752
753    /// A file's 44.1kHz sound into a 48kHz mix — the ordinary case, since
754    /// most media is 44.1kHz and a capture device is usually 48kHz.
755    ///
756    /// What the mixer takes per tick is fixed by the wall clock, so an input
757    /// resampled *short* is not merely quieter: the shortfall is filled with
758    /// silence at the tick rate, and what does arrive falls further behind
759    /// its own picture every second. Handed an unallocated output frame,
760    /// libswresample sizes it for as many samples as went in, which is 8%
761    /// short at this ratio.
762    #[test]
763    fn a_slower_input_arrives_at_the_mix_s_own_rate() {
764        const FRAMES: usize = 43;
765        const PER_FRAME: usize = 1024;
766        let to = MixFormat {
767            sample_rate: 48_000,
768            channels: 1,
769        };
770        let mut input = InputBuffer {
771            id: 1,
772            resampler: None,
773            samples: VecDeque::new(),
774            eos: false,
775        };
776
777        for _ in 0..FRAMES {
778            input
779                .push(&constant_frame(0.5, PER_FRAME, 44_100), to)
780                .expect("push");
781        }
782
783        let arrived = input.samples.len();
784        let expected = FRAMES * PER_FRAME * 48_000 / 44_100;
785        assert!(
786            arrived * 100 >= expected * 99,
787            "a second of 44.1kHz sound is still a second of mix: \
788             {arrived} samples arrived, {expected} expected"
789        );
790    }
791
792    struct RecordingSink {
793        pp_log: PpLog,
794        seen: Arc<StdMutex<Vec<f32>>>,
795    }
796
797    impl Element for RecordingSink {
798        fn name(&self) -> Arc<str> {
799            "recorder".into()
800        }
801        fn element_type(&self) -> ElementType {
802            ElementType::Other
803        }
804        fn pp_log(&self) -> &PpLog {
805            &self.pp_log
806        }
807        fn pp_log_mut(&mut self) -> &mut PpLog {
808            &mut self.pp_log
809        }
810    }
811
812    impl Sink for RecordingSink {
813        fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
814            if let MediaBuffer::Audio(frame) = buf
815                && frame.samples() > 0
816            {
817                self.seen.lock().unwrap().push(frame.plane::<f32>(0)[0]);
818            }
819            Ok(())
820        }
821        fn control(&mut self, _msg: ControlMsg) -> Result<()> {
822            Ok(())
823        }
824    }
825
826    /// Records the *shape* of every frame rather than a sample of it — what
827    /// a format change is visible in.
828    struct ShapeSink {
829        pp_log: PpLog,
830        seen: Arc<StdMutex<Vec<(u32, u16)>>>,
831    }
832
833    impl Element for ShapeSink {
834        fn name(&self) -> Arc<str> {
835            "shapes".into()
836        }
837        fn element_type(&self) -> ElementType {
838            ElementType::Other
839        }
840        fn pp_log(&self) -> &PpLog {
841            &self.pp_log
842        }
843        fn pp_log_mut(&mut self) -> &mut PpLog {
844            &mut self.pp_log
845        }
846    }
847
848    impl Sink for ShapeSink {
849        fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
850            if let MediaBuffer::Audio(frame) = buf
851                && frame.samples() > 0
852            {
853                self.seen
854                    .lock()
855                    .unwrap()
856                    .push((frame.rate(), frame.channel_layout().channels() as u16));
857            }
858            Ok(())
859        }
860        fn control(&mut self, _msg: ControlMsg) -> Result<()> {
861            Ok(())
862        }
863    }
864
865    fn constant_stereo_frame(
866        left: f32,
867        right: f32,
868        samples: usize,
869        rate: u32,
870    ) -> ffmpeg::frame::Audio {
871        let mut frame = ffmpeg::frame::Audio::new(
872            ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Packed),
873            samples,
874            ffmpeg::ChannelLayout::default(2),
875        );
876        frame.set_rate(rate);
877        let bytes = frame.data_mut(0);
878        let floats =
879            // SAFETY: `bytes` is this frame's own plane, which FFmpeg aligns well past
880            // 4, and `samples * 2` f32s is what the frame was allocated for.
881            unsafe { std::slice::from_raw_parts_mut(bytes.as_mut_ptr() as *mut f32, samples * 2) };
882        for pair in floats.chunks_mut(2) {
883            pair[0] = left;
884            pair[1] = right;
885        }
886        frame
887    }
888
889    struct StereoRecordingSink {
890        pp_log: PpLog,
891        seen: Arc<StdMutex<Vec<(f32, f32)>>>,
892    }
893
894    impl Element for StereoRecordingSink {
895        fn name(&self) -> Arc<str> {
896            "stereo-recorder".into()
897        }
898        fn element_type(&self) -> ElementType {
899            ElementType::Other
900        }
901        fn pp_log(&self) -> &PpLog {
902            &self.pp_log
903        }
904        fn pp_log_mut(&mut self) -> &mut PpLog {
905            &mut self.pp_log
906        }
907    }
908
909    impl Sink for StereoRecordingSink {
910        fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
911            if let MediaBuffer::Audio(frame) = buf
912                && frame.samples() > 0
913            {
914                // Raw bytes, not `plane::<f32>(0)`, for the same reason
915                // `InputBuffer::push` above does: `AudioMixer`'s output is
916                // packed multi-channel, and `plane::<T>()` only ever
917                // returns `samples()` elements regardless of channel
918                // count — reading channel 1 through it would silently
919                // read the wrong offset (still inside channel 0's data),
920                // not the second channel.
921                let samples = frame.samples();
922                let bytes = &frame.data(0)[..samples * 2 * 4];
923                // SAFETY: `bytes` is a prefix of the frame's plane, aligned by FFmpeg and
924                // cut to exactly `samples * 2 * 4` bytes just above.
925                let floats = unsafe {
926                    std::slice::from_raw_parts(bytes.as_ptr() as *const f32, samples * 2)
927                };
928                self.seen.lock().unwrap().push((floats[0], floats[1]));
929            }
930            Ok(())
931        }
932        fn control(&mut self, _msg: ControlMsg) -> Result<()> {
933            Ok(())
934        }
935    }
936
937    /// Regression test for the packed-multichannel `InputBuffer::push` bug
938    /// (see the comment there): two stereo inputs, each with distinct,
939    /// asymmetric L/R values, should sum per-channel without the channels
940    /// bleeding into each other or silently dropping to zero. Before the
941    /// fix, `plane::<f32>(0)` under-read the resampled buffer (only
942    /// `samples()` interleaved scalars instead of `samples() * channels`),
943    /// which desynced every input's channel alignment.
944    #[test]
945    fn mixes_stereo_sources_without_channel_corruption() {
946        let (mixer, handle) = AudioMixer::new(
947            "mixer",
948            AudioMixerOptions {
949                sample_rate: 48000,
950                channels: 2,
951            },
952        );
953        let seen = Arc::new(StdMutex::new(Vec::new()));
954        let sink = StereoRecordingSink {
955            seen: seen.clone(),
956            pp_log: element_pp_log(ElementType::Other, "stereo-recorder", None),
957        };
958
959        let pipeline = Pipeline::new("mixer-stereo-test", mixer, |source, ctx| {
960            let branch = ctx.branch().to(Box::new(sink))?;
961            ctx.attach(source, 0, branch)?;
962            Ok(())
963        })
964        .expect("test pipeline wiring must succeed");
965        pipeline.run().unwrap();
966
967        let mut input_a = handle.add_source("a").expect("mixer still alive");
968        let mut input_b = handle.add_source("b").expect("mixer still alive");
969
970        let stop = Arc::new(AtomicBool::new(false));
971        let feeder_stop = stop.clone();
972        let feeder = std::thread::spawn(move || {
973            while !feeder_stop.load(Ordering::Relaxed) {
974                let _ = input_a.consume(MediaBuffer::Audio(Arc::new(constant_stereo_frame(
975                    0.2, -0.1, 480, 48000,
976                ))));
977                let _ = input_b.consume(MediaBuffer::Audio(Arc::new(constant_stereo_frame(
978                    0.1, -0.2, 480, 48000,
979                ))));
980                std::thread::sleep(Duration::from_millis(10));
981            }
982        });
983
984        std::thread::sleep(Duration::from_millis(300));
985        stop.store(true, Ordering::Relaxed);
986        feeder.join().unwrap();
987        pipeline.stop();
988        pipeline.bus().log_events();
989
990        let seen = seen.lock().unwrap();
991        assert!(
992            seen.len() > 5,
993            "expected several mixed frames, got {seen:?}"
994        );
995        let steady = &seen[3..seen.len() - 2];
996        for &(left, right) in steady {
997            assert!(
998                (left - 0.3).abs() < 0.01,
999                "expected left channel ~0.3, got {left} in {seen:?}"
1000            );
1001            assert!(
1002                (right - -0.3).abs() < 0.01,
1003                "expected right channel ~-0.3, got {right} in {seen:?}"
1004            );
1005        }
1006    }
1007
1008    /// Two inputs, each pushing a constant `0.6` from their own thread
1009    /// (standing in for two independent capture pipelines), should sum to
1010    /// `1.2` and get hard-clipped to `1.0` — verifies resampling-on-first-
1011    /// frame, cross-thread `consume`, summation, and clipping all work
1012    /// together, not just in isolation.
1013    #[test]
1014    fn mixes_two_sources_and_hard_clips() {
1015        let (mixer, handle) = AudioMixer::new(
1016            "mixer",
1017            AudioMixerOptions {
1018                sample_rate: 48000,
1019                channels: 1,
1020            },
1021        );
1022        let seen = Arc::new(StdMutex::new(Vec::new()));
1023        let sink = RecordingSink {
1024            seen: seen.clone(),
1025            pp_log: element_pp_log(ElementType::Other, "recorder", None),
1026        };
1027
1028        let pipeline = Pipeline::new("mixer-test", mixer, |source, ctx| {
1029            let branch = ctx.branch().to(Box::new(sink))?;
1030            ctx.attach(source, 0, branch)?;
1031            Ok(())
1032        })
1033        .expect("test pipeline wiring must succeed");
1034        pipeline.run().unwrap();
1035
1036        let mut input_a = handle.add_source("a").expect("mixer still alive");
1037        let mut input_b = handle.add_source("b").expect("mixer still alive");
1038        assert_eq!(handle.source_count(), 2);
1039
1040        let stop = Arc::new(AtomicBool::new(false));
1041        let feeder_stop = stop.clone();
1042        let feeder = std::thread::spawn(move || {
1043            while !feeder_stop.load(Ordering::Relaxed) {
1044                let _ = input_a.consume(MediaBuffer::Audio(Arc::new(constant_frame(
1045                    0.6, 480, 48000,
1046                ))));
1047                let _ = input_b.consume(MediaBuffer::Audio(Arc::new(constant_frame(
1048                    0.6, 480, 48000,
1049                ))));
1050                std::thread::sleep(Duration::from_millis(10));
1051            }
1052        });
1053
1054        std::thread::sleep(Duration::from_millis(300));
1055        stop.store(true, Ordering::Relaxed);
1056        feeder.join().unwrap();
1057        pipeline.stop();
1058        pipeline.bus().log_events();
1059
1060        let seen = seen.lock().unwrap();
1061        assert!(
1062            seen.len() > 5,
1063            "expected several mixed frames, got {seen:?}"
1064        );
1065        // Skip the first few ticks (the feeder thread may not have caught
1066        // up yet) and the last couple (ticks after the feeder stopped but
1067        // before `pipeline.stop()` landed correctly drain to silence) —
1068        // check the steady state in between is clipped to 1.0.
1069        let steady = &seen[3..seen.len() - 2];
1070        for &value in steady {
1071            assert!(
1072                (value - 1.0).abs() < 0.01,
1073                "expected hard-clipped ~1.0, got {value} in {seen:?}"
1074            );
1075        }
1076    }
1077
1078    #[test]
1079    fn removed_source_stops_contributing() {
1080        let (mixer, handle) = AudioMixer::new(
1081            "mixer",
1082            AudioMixerOptions {
1083                sample_rate: 48000,
1084                channels: 1,
1085            },
1086        );
1087        let seen = Arc::new(StdMutex::new(Vec::new()));
1088        let sink = RecordingSink {
1089            seen: seen.clone(),
1090            pp_log: element_pp_log(ElementType::Other, "recorder", None),
1091        };
1092        let pipeline = Pipeline::new("mixer-test-2", mixer, |source, ctx| {
1093            let branch = ctx.branch().to(Box::new(sink))?;
1094            ctx.attach(source, 0, branch)?;
1095            Ok(())
1096        })
1097        .expect("test pipeline wiring must succeed");
1098        pipeline.run().unwrap();
1099
1100        let mut input_a = handle.add_source("a").unwrap();
1101        input_a
1102            .consume(MediaBuffer::Audio(Arc::new(constant_frame(
1103                0.5, 480, 48000,
1104            ))))
1105            .unwrap();
1106        std::thread::sleep(Duration::from_millis(100));
1107        handle.remove_source("a");
1108        assert_eq!(handle.source_count(), 0);
1109        seen.lock().unwrap().clear();
1110
1111        std::thread::sleep(Duration::from_millis(100));
1112        pipeline.stop();
1113        pipeline.bus().log_events();
1114
1115        assert!(
1116            seen.lock().unwrap().iter().all(|&v| v == 0.0),
1117            "removed source must not keep contributing: {:?}",
1118            *seen.lock().unwrap()
1119        );
1120    }
1121
1122    /// The mixer must keep emitting with nothing feeding it at all.
1123    ///
1124    /// It is what a recording attached later is made of, and what the
1125    /// application's own mixer dock is built on: a file whose audio track
1126    /// stopped because the last source was removed would be a worse answer
1127    /// than one carrying silence. The compositor's own version of this — an
1128    /// empty Scene still composites black — had to be fixed once, so this
1129    /// says so for the mixer before anybody has to find out.
1130    ///
1131    /// `removed_source_stops_contributing` above cannot see it: it asserts
1132    /// every sample that arrives is zero, which an empty vector satisfies.
1133    #[test]
1134    fn a_mixer_with_no_sources_still_emits_silence() {
1135        let (mixer, handle) = AudioMixer::new(
1136            "mixer",
1137            AudioMixerOptions {
1138                sample_rate: 48000,
1139                channels: 1,
1140            },
1141        );
1142        let seen = Arc::new(StdMutex::new(Vec::new()));
1143        let sink = RecordingSink {
1144            seen: seen.clone(),
1145            pp_log: element_pp_log(ElementType::Other, "recorder", None),
1146        };
1147        let pipeline = Pipeline::new("mixer-test-idle", mixer, |source, ctx| {
1148            let branch = ctx.branch().to(Box::new(sink))?;
1149            ctx.attach(source, 0, branch)?;
1150            Ok(())
1151        })
1152        .expect("test pipeline wiring must succeed");
1153        pipeline.run().unwrap();
1154
1155        // Nothing is ever added: no `add_source`, no buffer, no removal.
1156        std::thread::sleep(Duration::from_millis(200));
1157        pipeline.stop();
1158        pipeline.bus().log_events();
1159
1160        let seen = seen.lock().unwrap();
1161        assert_eq!(handle.source_count(), 0, "this test adds no sources");
1162        assert!(
1163            !seen.is_empty(),
1164            "the mixer must go on emitting with nothing feeding it"
1165        );
1166        assert!(
1167            seen.iter().all(|&sample| sample == 0.0),
1168            "what it emits with no sources must be silence: {seen:?}"
1169        );
1170    }
1171
1172    /// Regression test: a capture pipeline ending via `Stop` — the only
1173    /// shutdown signal a live source like `WasapiCaptureSource` ever sends,
1174    /// since it never reaches `Eos` on its own — used to leave a stale
1175    /// entry in the mixer's input map forever, because only `Eos` cleared
1176    /// it. `Sink::control` is what a `Queue`/`Pipeline` actually calls on
1177    /// `Stop` (mirrored by hand here, since this input isn't wired into a
1178    /// real second `Pipeline` in this test), not `consume`.
1179    #[test]
1180    fn stopped_source_is_removed_without_an_explicit_remove_source_call() {
1181        let (mixer, handle) = AudioMixer::new(
1182            "mixer",
1183            AudioMixerOptions {
1184                sample_rate: 48000,
1185                channels: 1,
1186            },
1187        );
1188        let seen = Arc::new(StdMutex::new(Vec::new()));
1189        let sink = RecordingSink {
1190            seen: seen.clone(),
1191            pp_log: element_pp_log(ElementType::Other, "recorder", None),
1192        };
1193        let pipeline = Pipeline::new("mixer-test-3", mixer, |source, ctx| {
1194            let branch = ctx.branch().to(Box::new(sink))?;
1195            ctx.attach(source, 0, branch)?;
1196            Ok(())
1197        })
1198        .expect("test pipeline wiring must succeed");
1199        pipeline.run().unwrap();
1200
1201        let mut input_a = handle.add_source("a").unwrap();
1202        input_a
1203            .consume(MediaBuffer::Audio(Arc::new(constant_frame(
1204                0.5, 480, 48000,
1205            ))))
1206            .unwrap();
1207        assert_eq!(handle.source_count(), 1);
1208
1209        // What a `Queue`/`Pipeline` actually calls on this input's own
1210        // `Sink` when its upstream capture pipeline is stopped — never
1211        // `consume(Eos)`, since `WasapiCaptureSource` doesn't send one.
1212        input_a.control(ControlMsg::Stop).unwrap();
1213
1214        assert_eq!(
1215            handle.source_count(),
1216            0,
1217            "Stop should remove the input immediately, same as remove_source"
1218        );
1219
1220        pipeline.stop();
1221        pipeline.bus().log_events();
1222    }
1223
1224    /// Re-registering a name replaces its input buffer, but callers may
1225    /// still hold the sink returned for the old registration. Every late
1226    /// operation through that stale sink must be inert rather than being
1227    /// redirected to (or deleting) the replacement merely because the map
1228    /// key is the same.
1229    #[test]
1230    fn replacing_an_input_by_name_invalidates_the_stale_sink() {
1231        let (_mixer, handle) = AudioMixer::new(
1232            "mixer",
1233            AudioMixerOptions {
1234                sample_rate: 48000,
1235                channels: 1,
1236            },
1237        );
1238        let mut stale = handle.add_source("mic").expect("mixer still alive");
1239        let mut current = handle.add_source("mic").expect("mixer still alive");
1240        assert_eq!(handle.source_count(), 1);
1241
1242        stale
1243            .consume(MediaBuffer::Audio(Arc::new(constant_frame(
1244                0.75, 480, 48000,
1245            ))))
1246            .unwrap();
1247        stale.consume(MediaBuffer::Eos).unwrap();
1248        stale.control(ControlMsg::Stop).unwrap();
1249
1250        assert_eq!(
1251            handle.source_count(),
1252            1,
1253            "a stale sink's Stop must not remove its replacement"
1254        );
1255        let shared = handle.shared.upgrade().expect("mixer still alive");
1256        {
1257            let inputs = shared.inputs.lock().unwrap();
1258            let input = inputs.get("mic").expect("replacement remains registered");
1259            assert!(
1260                input.resampler.is_none() && input.samples.is_empty(),
1261                "stale audio must not enter the replacement buffer"
1262            );
1263            assert!(!input.eos, "stale Eos must not mark the replacement ended");
1264        }
1265
1266        // The current sink still owns the registration and therefore
1267        // remains fully functional.
1268        current
1269            .consume(MediaBuffer::Audio(Arc::new(constant_frame(
1270                0.25, 480, 48000,
1271            ))))
1272            .unwrap();
1273        current.consume(MediaBuffer::Eos).unwrap();
1274        {
1275            let inputs = shared.inputs.lock().unwrap();
1276            let input = inputs.get("mic").expect("replacement remains registered");
1277            assert!(input.resampler.is_some(), "current audio was not accepted");
1278            assert!(input.eos, "current Eos was not accepted");
1279        }
1280
1281        current.control(ControlMsg::Stop).unwrap();
1282        assert_eq!(handle.source_count(), 0);
1283    }
1284
1285    /// A misrouted `Packet`/`Video` buffer used to be silently logged and
1286    /// dropped — no `BusEvent::Error`, no way for a misconfigured pipeline
1287    /// to ever find out. Matches the typed-error pattern every other
1288    /// `Sink` in this codebase already uses for a wrong `MediaBuffer`
1289    /// variant (e.g. `FileMuxerStreamSink`).
1290    #[test]
1291    fn rejects_buffers_that_are_neither_audio_nor_eos() {
1292        let (mixer, handle) = AudioMixer::new(
1293            "mixer",
1294            AudioMixerOptions {
1295                sample_rate: 48000,
1296                channels: 2,
1297            },
1298        );
1299        let mut input = handle.add_source("a").expect("mixer still alive");
1300
1301        let error = input
1302            .consume(MediaBuffer::Packet(Arc::new(ffmpeg::Packet::empty())))
1303            .expect_err("a Packet buffer must be rejected, not silently dropped");
1304        assert!(
1305            matches!(
1306                error,
1307                crate::error::Error::AudioMixerError(AudioMixerError::UnsupportedBuffer("Packet"))
1308            ),
1309            "unexpected error: {error:?}"
1310        );
1311
1312        drop(mixer);
1313    }
1314
1315    /// A rate change reaches the output frames, and the mix does not stop
1316    /// while the new rate "catches up" with the samples already emitted.
1317    ///
1318    /// That stall is the whole reason `MixAnchor` exists: the deficit is
1319    /// `elapsed × sample_rate` against a running count, and measured straight
1320    /// across a change to a lower rate it goes negative for as long as it
1321    /// takes the new rate to reach the old count — a minute of silence for a
1322    /// setting somebody just applied.
1323    #[test]
1324    fn changing_the_mix_format_keeps_the_mix_going() {
1325        let (mixer, handle) = AudioMixer::new(
1326            "mixer",
1327            AudioMixerOptions {
1328                sample_rate: 48000,
1329                channels: 2,
1330            },
1331        );
1332        let seen: Arc<StdMutex<Vec<(u32, u16)>>> = Arc::new(StdMutex::new(Vec::new()));
1333        let sink = ShapeSink {
1334            seen: seen.clone(),
1335            pp_log: element_pp_log(ElementType::Other, "shapes", None),
1336        };
1337        let pipeline = Pipeline::new("mixer-test-rate", mixer, |source, ctx| {
1338            let branch = ctx.branch().to(Box::new(sink))?;
1339            ctx.attach(source, 0, branch)?;
1340            Ok(())
1341        })
1342        .expect("test pipeline wiring must succeed");
1343        pipeline.run().unwrap();
1344
1345        std::thread::sleep(Duration::from_millis(150));
1346        assert_eq!(
1347            handle.mix_format(),
1348            Some(MixFormat {
1349                sample_rate: 48000,
1350                channels: 2
1351            })
1352        );
1353        // Down, which is the direction that stalls without the anchor, and
1354        // to mono so the frame's own shape has to move as well.
1355        assert!(handle.set_mix_format(MixFormat {
1356            sample_rate: 16000,
1357            channels: 1,
1358        }));
1359        let before = seen.lock().unwrap().len();
1360        std::thread::sleep(Duration::from_millis(250));
1361        pipeline.stop();
1362        pipeline.bus().log_events();
1363
1364        let seen = seen.lock().unwrap();
1365        assert!(
1366            seen.len() > before,
1367            "the mix stopped after the format changed: {before} frames before, {} after",
1368            seen.len()
1369        );
1370        assert_eq!(
1371            seen.last().copied(),
1372            Some((16000, 1)),
1373            "the new rate and channel count have to reach the output"
1374        );
1375    }
1376
1377    /// A format nothing could be resampled to is refused, and refusing leaves
1378    /// the running one alone rather than a mixer summing into nothing.
1379    #[test]
1380    fn an_impossible_mix_format_is_refused_and_changes_nothing() {
1381        let (_mixer, handle) = AudioMixer::new(
1382            "mixer",
1383            AudioMixerOptions {
1384                sample_rate: 48000,
1385                channels: 2,
1386            },
1387        );
1388        for refused in [
1389            MixFormat {
1390                sample_rate: 0,
1391                channels: 2,
1392            },
1393            MixFormat {
1394                sample_rate: 48000,
1395                channels: 0,
1396            },
1397        ] {
1398            assert!(!handle.set_mix_format(refused), "{refused:?} was accepted");
1399            assert_eq!(
1400                handle.mix_format(),
1401                Some(MixFormat {
1402                    sample_rate: 48000,
1403                    channels: 2
1404                })
1405            );
1406        }
1407    }
1408
1409    /// And the handle answers rather than taking effect once the mixer is
1410    /// gone, like every other method on it.
1411    #[test]
1412    fn the_mix_format_setter_reports_a_mixer_that_is_gone() {
1413        let (mixer, handle) = AudioMixer::new(
1414            "mixer",
1415            AudioMixerOptions {
1416                sample_rate: 48000,
1417                channels: 2,
1418            },
1419        );
1420        drop(mixer);
1421
1422        assert!(!handle.set_mix_format(MixFormat {
1423            sample_rate: 16000,
1424            channels: 1,
1425        }));
1426        assert_eq!(handle.mix_format(), None);
1427    }
1428
1429    /// A file whose sound is not at the mix's rate, played into the mix.
1430    ///
1431    /// The ordinary case, not a corner one: most media is 44.1kHz and most
1432    /// capture devices are 48kHz, so an application playing a file alongside
1433    /// a device has a resampler in the path whether it asked for one or not.
1434    ///
1435    /// What broke there was invisible to a test of the parts. Every element
1436    /// reported success, the mix kept coming out at its own rate, and the
1437    /// resampler was quietly handing back 8% less audio than went in — which
1438    /// this mixer, whose contract is to keep producing on a wall clock, made
1439    /// up with silence. So these measure the mix itself.
1440    mod against_a_file {
1441        use std::sync::Mutex as StdMutex;
1442
1443        use super::*;
1444        use crate::{
1445            elements::{AppSink, FileDemuxer, Pacer, SwDecoder, TestAudioOptions, TestAudioSource},
1446            pipeline::Pipeline,
1447            test_support,
1448        };
1449
1450        const MIX_RATE: u32 = 48_000;
1451        const FILE_RATE: u32 = 44_100;
1452
1453        /// How long the mix is watched. Long enough for a shortfall of a few
1454        /// percent per tick to be unmistakable, short enough to stay a test.
1455        const WATCH: Duration = Duration::from_secs(3);
1456
1457        /// How much of a mix carrying a continuous tone may be silence, in
1458        /// parts per thousand.
1459        ///
1460        /// The tone never lands exactly on 0.0 for a run of samples, so what
1461        /// this counts is the mixer's own padding: what it puts in when an
1462        /// input is short for a tick. A resampler handing back less than it
1463        /// was given pads *every* tick — 80‰ at the ratio used here. What is
1464        /// left when nothing is wrong is the occasional scheduling hiccup, a
1465        /// sub-millisecond gap every few seconds, measured at well under one
1466        /// part in a thousand.
1467        const SILENT_PER_MILLE: usize = 10;
1468
1469        /// Runs shorter than this are a waveform touching zero rather than a
1470        /// gap in it.
1471        const HOLE: usize = 4;
1472
1473        #[derive(Default, Clone)]
1474        struct MixShape {
1475            samples: usize,
1476            silent: usize,
1477            holes: usize,
1478            longest_hole: usize,
1479            current_run: usize,
1480        }
1481
1482        impl MixShape {
1483            fn absorb(&mut self, frame: &ffmpeg::frame::Audio) {
1484                let bytes = frame.samples() * frame.channels() as usize * 4;
1485                let data = &frame.data(0)[..bytes.min(frame.data(0).len())];
1486                let (samples, _) = data.as_chunks::<4>();
1487                for chunk in samples {
1488                    let sample = f32::from_le_bytes(*chunk);
1489                    self.samples += 1;
1490                    if sample == 0.0 {
1491                        self.silent += 1;
1492                        self.current_run += 1;
1493                    } else {
1494                        if self.current_run >= HOLE {
1495                            self.holes += 1;
1496                            self.longest_hole = self.longest_hole.max(self.current_run);
1497                        }
1498                        self.current_run = 0;
1499                    }
1500                }
1501            }
1502
1503            fn silent_per_mille(&self) -> usize {
1504                if self.samples == 0 {
1505                    return 0;
1506                }
1507                self.silent * 1000 / self.samples
1508            }
1509
1510            fn report(&self) -> String {
1511                format!(
1512                    "{} of {} samples silent ({}per mille), in {} hole(s), longest {}",
1513                    self.silent,
1514                    self.samples,
1515                    self.silent_per_mille(),
1516                    self.holes,
1517                    self.longest_hole
1518                )
1519            }
1520        }
1521
1522        /// A mixer running on its own pipeline, with everything it emits
1523        /// measured.
1524        fn watched_mix(rate: u32) -> (Arc<Pipeline>, MixerHandle, Arc<StdMutex<MixShape>>) {
1525            let shape = Arc::new(StdMutex::new(MixShape::default()));
1526            let listener = AppSink::new("mix-listener", {
1527                let shape = Arc::clone(&shape);
1528                move |buffer| {
1529                    if let MediaBuffer::Audio(frame) = &buffer {
1530                        shape.lock().expect("mix shape poisoned").absorb(frame);
1531                    }
1532                    Ok(())
1533                }
1534            });
1535            let (mixer, handle) = AudioMixer::new(
1536                "mixer",
1537                AudioMixerOptions {
1538                    sample_rate: rate,
1539                    channels: 2,
1540                },
1541            );
1542            let pipeline = Pipeline::new("mix", mixer, move |source, context| {
1543                let branch = context.branch().to(Box::new(listener))?;
1544                context.attach(source, 0, branch)?;
1545                Ok(())
1546            })
1547            .expect("wire the mix");
1548            pipeline.run().expect("run the mix");
1549            (pipeline, handle, shape)
1550        }
1551
1552        /// The fixture's sound, decoded, paced and pushed into the mix on a
1553        /// pipeline of its own — which is how an application does it, the
1554        /// mixer on its own thread with sources coming and going around it.
1555        ///
1556        /// The `Pacer` is not decoration. Without one the demuxer reads the
1557        /// file as fast as it decodes and the mixer's input never runs dry,
1558        /// so an input handing back less audio than it was given only fills a
1559        /// queue more slowly and nothing downstream can tell. Paced, the file
1560        /// arrives at the rate it claims and a shortfall is a tick the mixer
1561        /// has to finish with silence — which is the whole of what this
1562        /// measures.
1563        fn play_into(path: &str, mixer: &MixerHandle) -> Arc<Pipeline> {
1564            let (demuxer, streams) = FileDemuxer::open("fixture", path).expect("open the fixture");
1565            let audio = streams
1566                .iter()
1567                .find(|stream| stream.kind == ffmpeg::media::Type::Audio)
1568                .expect("the fixture has sound")
1569                .index;
1570            let parameters = demuxer
1571                .stream_parameters(audio)
1572                .expect("the audio stream describes itself");
1573            let time_base = demuxer
1574                .stream_time_base(audio)
1575                .expect("the audio stream has a unit");
1576            let decoder = SwDecoder::new("fixture-decoder", parameters).expect("open the decoder");
1577            let sink = mixer
1578                .add_source("fixture".to_owned())
1579                .expect("the mixer is running");
1580
1581            let pipeline = Pipeline::new("playback", demuxer, move |source, context| {
1582                let branch = context
1583                    .branch()
1584                    .pipe(decoder)
1585                    .queue("audio", 32)
1586                    .pipe(Pacer::new("fixture-pacer", time_base)?)
1587                    .to(sink)?;
1588                context.attach(source, audio, branch)?;
1589                Ok(())
1590            })
1591            .expect("wire the playback pipeline");
1592            pipeline.run().expect("play the fixture");
1593            pipeline
1594        }
1595
1596        /// The fixture has to be what these tests assume, or what they measure
1597        /// is something else. Cheap, and it fails at the generator rather than
1598        /// three assertions later.
1599        #[test]
1600        fn the_fixture_carries_sound_at_a_rate_the_mix_does_not_run_at() {
1601            let fixture = test_support::synthesize("mixed-rate", 2.0, FILE_RATE);
1602            let input =
1603                ffmpeg::format::input(&fixture.path).expect("the fixture is a readable container");
1604            let audio = input
1605                .streams()
1606                .find(|stream| stream.parameters().medium() == ffmpeg::media::Type::Audio)
1607                .expect("the fixture has an audio stream");
1608            let decoder = ffmpeg::codec::context::Context::from_parameters(audio.parameters())
1609                .expect("the audio stream describes itself")
1610                .decoder()
1611                .audio()
1612                .expect("it is audio");
1613
1614            assert_eq!(
1615                decoder.rate(),
1616                fixture.audio_rate,
1617                "the fixture must carry the rate it was asked for"
1618            );
1619            assert_ne!(FILE_RATE, MIX_RATE, "otherwise nothing is resampled");
1620            assert_eq!(decoder.channels(), fixture.channels);
1621            assert!(
1622                input
1623                    .streams()
1624                    .any(|stream| stream.parameters().medium() == ffmpeg::media::Type::Video),
1625                "a Source that occupies a rectangle needs its picture too"
1626            );
1627        }
1628
1629        /// A 44.1kHz file into a 48kHz mix has to fill every tick of it.
1630        ///
1631        /// Not "arrive" as in the mix keeps producing — it does that with or
1632        /// without an input, filling whatever an input is short by with
1633        /// silence, which is exactly what made this so quiet. What is
1634        /// measured is the silence.
1635        #[test]
1636        fn a_file_below_the_mix_rate_fills_every_tick_of_it() {
1637            let fixture = test_support::synthesize("mixed-rate-mix", 6.0, FILE_RATE);
1638            let (mix, handle, shape) = watched_mix(MIX_RATE);
1639            let playback = play_into(&fixture.path.to_string_lossy(), &handle);
1640
1641            // What a mix emits before its input has said anything is silence,
1642            // correctly, so the measurement starts after the first samples.
1643            thread::sleep(Duration::from_millis(500));
1644            *shape.lock().expect("mix shape poisoned") = MixShape::default();
1645            thread::sleep(WATCH);
1646            let measured = shape.lock().expect("mix shape poisoned").clone();
1647            playback.stop();
1648            mix.stop();
1649
1650            assert!(
1651                measured.samples > 0,
1652                "the mix produced nothing to look at in {WATCH:?}"
1653            );
1654            assert!(
1655                measured.silent_per_mille() <= SILENT_PER_MILLE,
1656                "a {FILE_RATE}Hz file did not fill a {MIX_RATE}Hz mix: {}",
1657                measured.report()
1658            );
1659        }
1660
1661        /// The same mixer fed at its own rate, so a failure above is read as
1662        /// what it is. Nothing is resampled here, and a hole would mean
1663        /// something other than the rate conversion.
1664        #[test]
1665        fn a_source_at_the_mix_rate_fills_it_too() {
1666            let (mix, handle, shape) = watched_mix(MIX_RATE);
1667            let tone = TestAudioSource::new(
1668                "tone",
1669                TestAudioOptions {
1670                    sample_rate: MIX_RATE,
1671                    channels: 2,
1672                    frequency: 440.0,
1673                },
1674            );
1675            let sink = handle
1676                .add_source("tone".to_owned())
1677                .expect("the mixer is running");
1678            let source = Pipeline::new("tone", tone, move |source, context| {
1679                let branch = context.branch().to(sink)?;
1680                context.attach(source, 0, branch)?;
1681                Ok(())
1682            })
1683            .expect("wire the tone");
1684            source.run().expect("run the tone");
1685
1686            thread::sleep(Duration::from_millis(500));
1687            *shape.lock().expect("mix shape poisoned") = MixShape::default();
1688            thread::sleep(WATCH);
1689            let measured = shape.lock().expect("mix shape poisoned").clone();
1690            source.stop();
1691            mix.stop();
1692
1693            assert!(
1694                measured.silent_per_mille() <= SILENT_PER_MILLE,
1695                "a source at the mix's own rate did not fill it: {}",
1696                measured.report()
1697            );
1698        }
1699    }
1700}