Skip to content

Latest commit

 

History

History
50 lines (39 loc) · 6 KB

audio-codec.md

File metadata and controls

50 lines (39 loc) · 6 KB

Audio codec behavior

The host uses FFmpeg 7's libopus encoder with 48 kHz stereo Float32 input, 960 samples per packet (20 ms), 128 kbps, constrained VBR, and application=lowdelay. swresample handles the Windows endpoint's mix rate and channel layout before a bounded remainder assembles the Opus frames.

FFmpeg 7 does not expose libopus's OPUS_SET_DTX as an AVOption. Passing dtx=1 to its options dictionary would not enable it. Also, Opus restricted low-delay mode uses CELT, whose packets do not carry SILK's in-band FEC. The wrapper accepts fec=1 and packet_loss=10, but those settings must not be described as demonstrated loss protection in this mode.

For digital silence (a complete stereo frame of exact zeros), the host reopens the encoder once at the transition. This clears its tonal history and produces real three-byte Opus silence packets. The first packet after the reopen carries DISCONTINUITY; the decoder resets its prediction while retaining the packet's position in the playout timeline. No synthetic Opus bytes, speech VAD, or undocumented FFmpeg internals are used. This is silence compression, rather than libopus DTX: the host still sends 50 small packets per second while the input remains silent.

A session's audio starts at its first capture read. WASAPI buffers from Start() while the session opens its encoder, and an idle engine's first read is silence reaching back to Start(). Whole 20 ms frames that ended before the first read are discarded before encoding, so they take no sequence number. Sending them would put packets past the 100 ms send deadline, and the viewer would count each one as lost.

The codec test decodes 4.2 seconds of a -12 dBFS 1 kHz signal, including two 100 ms silence gaps. It verifies packet sequence/time, exact 960-sample decodes, sub-10-byte silence packets, the tone level before and after each gap, and a peak below 0.35 (no transition burst). A separate test covers full-scale noise, alternating samples, DC, and silence against the smallest supported 576-byte datagram. Both run on macOS and Windows.

References: FFmpeg 7 libopus wrapper, Opus encoder controls, WASAPI loopback recording, WASAPI capture buffers and timestamps.

iPad decoder and playout

The app builds the Copus product from swift-opus, pinned to commit 6f3cb6bd3ffed1fe5f06d00a962d5c191a50daf8. Its libopus source is pinned to 101a71e03bbf860aaafb7090a0e440675cb27660 (upstream v1.4 plus seven commits). Xcode/Swift Package Manager compiles the C source; no downloaded codec binaries or separate build tools are required. The app bundles both license notices. Devices and simulators use this same decoder.

AudioToolbox was investigated on Xcode 16.4 / iOS 18.6 using a packet generated by the host's FFmpeg 7 libopus settings. AudioConverterNew succeeded. The optional prime-method property returned prop; without that property, the first packet produced 840 frames and a nil/zero-length missing packet produced zero frames. The implementation therefore uses libopus's explicit 960-frame decode and nil-packet PLC API. The investigation took less than the handoff's two-hour limit. Retained probe logs are listed in the handoff progress file.

A dedicated queue decodes bounded packets ahead of an AVAudioSourceNode. The stereo PCM ring starts after 60 ms, raises its target to 120 ms after two underruns within ten seconds, and returns to 60 ms after thirty clean seconds. On overflow it drops the oldest 20 ms. Sequence gaps wait 20 ms for reordering before PLC, with bounded work for large gaps. Timestamp gaps with continuous sequence numbers add silence without loss accounting; silence already rendered is credited. Epoch changes discard old audio, while a codec discontinuity in an otherwise continuous timeline preserves queued samples.

E2E_AUDIO measures RMS and the 1 kHz Goertzel amplitude from actual rendered samples, in 100 ms windows. Packet loss and current buffered time also go into the receiver report. The simulator test matches each lost packet to a host drop by sequence number. The iPad's E2E_AUDIO line reports its timeline's wire session id and epoch, and its first and next expected sequence; every number in between was decoded or counted lost. The host's Audio stream stats line lists the sequences it dropped after missing the 100 ms send deadline (late_seqs, stale or capture queue full) and those it failed to send (failed_seqs). It also gives the capture session's audio_stream number, which changes whenever numbering restarts at 0, and the wire_stream (session/epoch) its packets used. The checker uses only the host stream with the iPad's session and epoch, and fails if there is none. If more than one capture numbered under that pair (audio was toggled off and on), nothing ties the frozen timeline to one of them. The checker then attributes nothing and passes only a clean timeline: no loss, starting at sequence 0. The iPad's timeline identity and counter coverage are checked in every case. Only host drops below the iPad's frozen next sequence count. Every sequence before the iPad's first must be a host drop. An iPad loss without a matching host drop fails, as does any send failure and any deadline drop after the stream's first 250 sequences (5 s of nominal audio). Ranges that cross the u32 sequence wrap are not supported and fail. In CI runs, deadline drops happened only in the first 0.3-4.1 s of audio, while the launching simulator app starved the three-vCPU runner. More than two drops inside that window fail under the FPS gate's enforce mode and are recorded under record on CI. Evidence from builds without stream identity keeps the original limit of two lost packets.