feat(Ogg): add Ogg Opus playback support - #139
Open
hischampion wants to merge 3 commits into
Open
hischampion wants to merge 3 commits into
hischampion wants to merge 3 commits into
Conversation
added 3 commits
August 25, 2026 14:54
AudioFileStreamProcessor routed every kAudioFileOggType stream to the libvorbisfile processor. Ogg is a container, not a codec, and audio/ogg is shared by Vorbis, Opus, Speex and FLAC-in-Ogg, so Opus reached libvorbisfile and was rejected. Navidrome, for one, serves Opus transcodes as audio/ogg. Codec selection cannot happen at openFileStream time, because both the file hint and the Content-Type say only "Ogg". It now happens on first data, by reading the first Ogg page. - OggVorbisStreamProcessor becomes OggStreamProcessor driving an injected OggAudioDecoder. The renderer loop, ring-buffer waits and packet accounting are unchanged; only decoder references were renamed. - OggCodecSniffer reads the first page to tell OpusHead from \x01vorbis, accounting for the lacing table when locating the payload. - OpusFileBridge wraps libopusfile, mirroring VorbisFileBridge. - Unsupported Ogg codecs now report kAudioFileStreamError_UnsupportedDataFormat instead of failing silently inside libvorbisfile. Two details worth flagging for review: Opus always decodes at 48 kHz. OpusHead.input_sample_rate describes the material that was encoded, not the output, so it must not be used as the output rate; a fixture that declares 44100 guards against that regression. OFOpen rewinds the ring buffer when op_open_callbacks fails. A failed open is the normal case while the header is still arriving, but it consumes bytes first, so without the rewind every retry starts mid-stream and returns OP_ENOTFORMAT forever. This is reachable with a header larger than the 16 KB open threshold - an Opus file with embedded cover art, for instance. The same flaw exists in the Vorbis path today and is left alone here. Tests cover the sniffer and the C bridge, both of which are pure computation and need no audio hardware. OggStreamProcessor itself still requires a device.
OpusFileBridge.c arrived with a near-verbatim copy of the ring buffer in VorbisFileBridge.c: the same struct, the same rb_read/rb_write, and the same create/destroy/push/available/mark-EOF wrappers under a different prefix. Two copies drift, so this moves the buffer into OggRingBuffer.c and leaves both bridges holding only their codec-specific callbacks. Behaviour is unchanged. Decoding the test fixtures through both bridges produces byte-for-byte identical PCM before and after (705600 bytes of Vorbis, 768000 of Opus), and the codec bridges keep their existing public API, so nothing on the Swift side moves. This commit is self-contained and can be dropped if you would rather not have the Vorbis bridge touched in the same PR as the Opus addition.
The existing job runs swift build and swift test, which compile the macOS slice. This PR adds a binary xcframework dependency and a second C bridge, so two gaps are worth closing: - build-ios compiles against the iOS slices of the ogg/vorbis/opus xcframeworks, which swift build never touches. - test-asan runs the suite under Address Sanitizer. The codec bridges are C operating on raw pointers, and ASan is what makes an overrun there fail loudly instead of silently corrupting audio. Both are additive; the existing job is unchanged.
hischampion
marked this pull request as ready for review
September 17, 2026 22:08
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #137.
Opus streams don't play:
openFileStreamsends everykAudioFileOggTypestream toOggVorbisStreamProcessor, andVorbisFileBridgereturnsrc=-132 (OV_ENOTVORBIS).audio/oggis a container type shared by Vorbis, Opus, Speex and FLAC, and Navidrome serves its Opus transcodes asaudio/ogg, so Cassette, a Navidrome client built on this library, gets silence. This branch picks the codec from the first Ogg page and adds a libopusfile bridge next to the Vorbis one.Up front: I don't have a Mac, so this has never run on a device, and it was authored heavily with Claude Opus. CI is green (macOS build + test, iOS build, ASan), the bridge decodes bit-identically to ffmpeg, and valgrind is clean, but
OggStreamProcessorneeds a liveAVAudioEngineand only a device can vouch for it. If that's not something you want to carry, say so and I'll leave it on the branch.What changes
openFileStreamto first data, since neither the file hint nor the Content-Type can tell Opus from Vorbis.OggCodecSnifferreads the first page forOpusHeadvs\x01vorbis.OggVorbisStreamProcessor→OggStreamProcessor, driving an injectedOggAudioDecoder. Renderer loop, ring-buffer waits and packet accounting are untouched; pushed as a rename so the diff reads as one.OpusFileBridgewraps libopusfile, mirroringVorbisFileBridge. Dependency:sbooth/opus-binary-xcframework0.3.0, same author and packaging as the ogg/vorbis xcframeworks already inPackage.resolved.kAudioFileStreamError_UnsupportedDataFormatinstead of failing silently inside libvorbisfile.OpusHead.input_sample_ratedescribes the source material, not the output; using it as the output rate pitch-shifts.OFOpenrewinds the ring buffer whenop_open_callbacksfails. A failed open is normal while the header is still arriving, but opusfile consumes bytes first, so without the rewind every retry starts mid-stream and returnsOP_ENOTFORMATforever (any header over the 16 KB open threshold, e.g. embedded cover art).Commits
feat(Ogg)— the Opus support, self-contained.refactor(AudioCodecs)— one shared ring buffer instead of the copyOpusFileBridge.carrived with; PCM out of both bridges is byte-identical before and after. Droppable.ci— iOS build and ASan run. Droppable.Tests
20 cases, none needing audio hardware:
input_sample_rate: 44100still decodes at 48 kHz; recovery from a failed open on a >16 KB header; Vorbis and junk are rejected.Not covered:
OggStreamProcessoritself, for the device reason above.