From e19acba245560a4d359f8c1708d772b7395cc67b Mon Sep 17 00:00:00 2001 From: Scott <485cfbznvv@privaterelay.appleid.com> Date: Tue, 25 Aug 2026 14:54:49 -0700 Subject: [PATCH 1/3] feat(Ogg): add Ogg Opus playback support 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. --- AudioCodecs/OpusFileBridge.c | 324 ++++++++++++++++++ AudioCodecs/include/AudioCodecs.h | 1 + AudioCodecs/include/OpusFileBridge.h | 65 ++++ AudioStreaming/OggAudio/OggAudioDecoder.swift | 69 ++++ AudioStreaming/OggAudio/OpusFileDecoder.swift | 219 ++++++++++++ .../Processors/AudioFileStreamProcessor.swift | 97 ++++-- ...ocessor.swift => OggStreamProcessor.swift} | 75 ++-- .../Streaming/Helpers/AudioFileType.swift | 2 + .../Streaming/Helpers/OggCodecSniffer.swift | 81 +++++ .../Codecs/OpusFileBridgeTests.swift | 239 +++++++++++++ .../Codecs/ogg-fixtures/README.md | 43 +++ .../ogg-fixtures/opus-declares-44k-input.opus | Bin 0 -> 29644 bytes .../ogg-fixtures/opus-large-header.opus | Bin 0 -> 35250 bytes .../ogg-fixtures/opus-tone-mono-48k.opus | Bin 0 -> 30912 bytes .../ogg-fixtures/opus-tone-stereo-48k.opus | Bin 0 -> 33085 bytes .../ogg-fixtures/vorbis-tone-stereo-44k.ogg | Bin 0 -> 9365 bytes .../Helpers/OggCodecSnifferTests.swift | 113 ++++++ Package.swift | 18 +- 18 files changed, 1282 insertions(+), 64 deletions(-) create mode 100644 AudioCodecs/OpusFileBridge.c create mode 100644 AudioCodecs/include/OpusFileBridge.h create mode 100644 AudioStreaming/OggAudio/OggAudioDecoder.swift create mode 100644 AudioStreaming/OggAudio/OpusFileDecoder.swift rename AudioStreaming/Streaming/AudioPlayer/Processors/{OggVorbisStreamProcessor.swift => OggStreamProcessor.swift} (88%) create mode 100644 AudioStreaming/Streaming/Helpers/OggCodecSniffer.swift create mode 100644 AudioStreamingTests/Codecs/OpusFileBridgeTests.swift create mode 100644 AudioStreamingTests/Codecs/ogg-fixtures/README.md create mode 100644 AudioStreamingTests/Codecs/ogg-fixtures/opus-declares-44k-input.opus create mode 100644 AudioStreamingTests/Codecs/ogg-fixtures/opus-large-header.opus create mode 100644 AudioStreamingTests/Codecs/ogg-fixtures/opus-tone-mono-48k.opus create mode 100644 AudioStreamingTests/Codecs/ogg-fixtures/opus-tone-stereo-48k.opus create mode 100644 AudioStreamingTests/Codecs/ogg-fixtures/vorbis-tone-stereo-44k.ogg create mode 100644 AudioStreamingTests/Streaming/Helpers/OggCodecSnifferTests.swift diff --git a/AudioCodecs/OpusFileBridge.c b/AudioCodecs/OpusFileBridge.c new file mode 100644 index 0000000..896209c --- /dev/null +++ b/AudioCodecs/OpusFileBridge.c @@ -0,0 +1,324 @@ +#include "include/OpusFileBridge.h" + +#include +#include +#include +#include + +// Ring buffer + opusfile callback shim. +// +// Deliberately mirrors VorbisFileBridge.c so the two Ogg codecs behave +// identically from the Swift side. The only structural difference is the +// callback signatures: opusfile uses a byte-count read (op_read_func) rather +// than libvorbisfile's fread-style (size, nmemb) pair. + +struct OFRemoteStream { + uint8_t *buf; + size_t cap, head, tail, size; + int eof; + long long pos; // Current read position in the stream + long long total_pushed; // Total bytes pushed into the buffer + pthread_mutex_t m; + pthread_cond_t cv; +}; + +static size_t rb_write(struct OFRemoteStream *s, const uint8_t *src, size_t len) { + size_t written = 0; + while (written < len) { + size_t free_space = s->cap - s->size; + if (free_space == 0) break; + size_t chunk = s->cap - s->tail; + if (chunk > len - written) chunk = len - written; + if (chunk > free_space) chunk = free_space; + memcpy(s->buf + s->tail, src + written, chunk); + s->tail = (s->tail + chunk) % s->cap; + s->size += chunk; + written += chunk; + } + return written; +} + +static size_t rb_read(struct OFRemoteStream *s, uint8_t *dst, size_t len) { + size_t read = 0; + while (read < len && s->size > 0) { + size_t chunk = s->cap - s->head; + if (chunk > s->size) chunk = s->size; + if (chunk > len - read) chunk = len - read; + memcpy(dst + read, s->buf + s->head, chunk); + s->head = (s->head + chunk) % s->cap; + s->size -= chunk; + read += chunk; + } + return read; +} + +OFStreamRef OFStreamCreate(size_t capacity_bytes) { + struct OFRemoteStream *s = (struct OFRemoteStream *)calloc(1, sizeof(struct OFRemoteStream)); + if (!s) return NULL; + s->buf = (uint8_t *)malloc(capacity_bytes); + if (!s->buf) { free(s); return NULL; } + s->cap = capacity_bytes; + pthread_mutex_init(&s->m, NULL); + pthread_cond_init(&s->cv, NULL); + return s; +} + +void OFStreamDestroy(OFStreamRef sr) { + struct OFRemoteStream *s = (struct OFRemoteStream *)sr; + if (!s) return; + pthread_mutex_destroy(&s->m); + pthread_cond_destroy(&s->cv); + free(s->buf); + free(s); +} + +size_t OFStreamAvailableBytes(OFStreamRef sr) { + struct OFRemoteStream *s = (struct OFRemoteStream *)sr; + if (!s) return 0; + pthread_mutex_lock(&s->m); + size_t sz = s->size; + pthread_mutex_unlock(&s->m); + return sz; +} + +void OFStreamPush(OFStreamRef sr, const uint8_t *data, size_t len) { + struct OFRemoteStream *s = (struct OFRemoteStream *)sr; + if (!s || !data || len == 0) return; + + pthread_mutex_lock(&s->m); + size_t written_total = 0; + while (written_total < len) { + size_t w = rb_write(s, data + written_total, len - written_total); + written_total += w; + if (written_total < len) { + // Buffer full, wait for consumer to read + pthread_cond_wait(&s->cv, &s->m); + } + } + s->total_pushed += (long long)len; + pthread_cond_broadcast(&s->cv); + pthread_mutex_unlock(&s->m); +} + +void OFStreamMarkEOF(OFStreamRef sr) { + struct OFRemoteStream *s = (struct OFRemoteStream *)sr; + if (!s) return; + pthread_mutex_lock(&s->m); + s->eof = 1; + pthread_cond_broadcast(&s->cv); + pthread_mutex_unlock(&s->m); +} + +// A decoder handle: the opusfile object plus a scratch buffer. +// +// libopusfile offers only an interleaved read (there is no equivalent of +// libvorbisfile's ov_read_float), so producing deinterleaved output needs a +// staging buffer. Allocating it per read would put a malloc in the render +// path, so the handle owns it and grows it at most once per buffer size. +struct OFFile { + OggOpusFile *of; + float *scratch; + size_t scratch_floats; +}; + +// Returns a scratch buffer of at least `floats_needed` floats, or NULL on +// allocation failure. Steady state performs no allocation. +static float *of_scratch(struct OFFile *f, size_t floats_needed) { + if (f->scratch && f->scratch_floats >= floats_needed) return f->scratch; + float *grown = (float *)realloc(f->scratch, floats_needed * sizeof(float)); + if (!grown) return NULL; + f->scratch = grown; + f->scratch_floats = floats_needed; + return grown; +} + +// MARK: - opusfile callbacks + +// op_read_func: returns bytes read, 0 on EOF, <0 on error. +// Non-blocking: returns whatever is available now, exactly like the Vorbis shim. +static int read_cb(void *stream, unsigned char *ptr, int nbytes) { + struct OFRemoteStream *s = (struct OFRemoteStream *)stream; + if (!s || nbytes <= 0) return 0; + + size_t want_bytes = (size_t)nbytes; + size_t got = 0; + + pthread_mutex_lock(&s->m); + while (got < want_bytes && s->size > 0) { + size_t chunk = rb_read(s, ptr + got, want_bytes - got); + if (chunk == 0) break; + s->pos += (long long)chunk; + got += chunk; + pthread_cond_broadcast(&s->cv); + } + pthread_mutex_unlock(&s->m); + + // got == 0 with eof set signals EOF to opusfile; got == 0 without eof is a + // short read, which opusfile also treats as end-of-stream. The Swift layer + // gates calls on availableBytes() to avoid the latter. + return (int)got; +} + +static int close_cb(void *stream) { + (void)stream; + return 0; +} + +static opus_int64 tell_cb(void *stream) { + struct OFRemoteStream *s = (struct OFRemoteStream *)stream; + if (!s) return -1; + return (opus_int64)s->pos; +} + +int OFOpen(OFStreamRef sr, OFFileRef *out_of) { + struct OFRemoteStream *s = (struct OFRemoteStream *)sr; + if (!s || !out_of) return -1; + + OpusFileCallbacks cbs; + cbs.read = read_cb; + cbs.seek = NULL; // Non-seekable streaming (seeking handled at Swift level) + cbs.tell = tell_cb; + cbs.close = close_cb; + + // A failed open is the normal case while the header is still arriving, so + // it must not damage the stream. op_open_callbacks consumes bytes through + // read_cb before it discovers the header is short, which advances the ring + // buffer past data the next attempt still needs — without the rewind below + // the retry sees a mid-stream position and every subsequent attempt fails + // with OP_ENOTFORMAT, so the track never plays. + // + // read_cb is the only consumer and it advances s->pos by exactly the bytes + // it took, so the delta is the amount to give back. Callers serialise open + // against push (OpusFileDecoder holds decoderLock across both), so no + // producer can have overwritten the reclaimed region. + pthread_mutex_lock(&s->m); + long long saved_pos = s->pos; + pthread_mutex_unlock(&s->m); + + int err = 0; + OggOpusFile *of = op_open_callbacks((void *)s, &cbs, NULL, 0, &err); + if (!of) { + pthread_mutex_lock(&s->m); + long long consumed = s->pos - saved_pos; + if (consumed > 0 && (size_t)consumed <= s->cap - s->size) { + s->head = (s->head + s->cap - ((size_t)consumed % s->cap)) % s->cap; + s->size += (size_t)consumed; + s->pos = saved_pos; + } + pthread_cond_broadcast(&s->cv); + pthread_mutex_unlock(&s->m); + return err != 0 ? err : -1; + } + + struct OFFile *f = (struct OFFile *)calloc(1, sizeof(struct OFFile)); + if (!f) { + op_free(of); + return -1; + } + f->of = of; + + *out_of = (OFFileRef)f; + return 0; +} + +void OFClear(OFFileRef fr) { + struct OFFile *f = (struct OFFile *)fr; + if (!f) return; + if (f->of) op_free(f->of); + free(f->scratch); + free(f); +} + +int OFGetInfo(OFFileRef fr, OFStreamInfo *out_info) { + struct OFFile *f = (struct OFFile *)fr; + if (!f || !f->of || !out_info) return -1; + OggOpusFile *of = f->of; + + const OpusHead *head = op_head(of, -1); + if (!head) return -1; + + // opusfile always decodes to 48 kHz regardless of the original input rate. + // head->input_sample_rate is informational only and must NOT be used as the + // output rate — doing so is the classic Opus pitch-shift bug. + out_info->sample_rate = 48000; + out_info->channels = op_channel_count(of, -1); + + // op_pcm_total requires a seekable stream; HTTP sources report -1 here and + // the Swift layer falls back to a bitrate-based duration estimate. + opus_int64 total = op_pcm_total(of, -1); + if (total >= 0) { + out_info->total_pcm_samples = (long long)total; + out_info->duration_seconds = (double)total / 48000.0; + } else { + out_info->total_pcm_samples = -1; + out_info->duration_seconds = -1; + } + + // op_bitrate() also requires a seekable stream. For live/HTTP sources fall + // back to the instantaneous estimate, which is 0 until packets decode. + opus_int32 br = op_bitrate(of, -1); + if (br <= 0) br = op_bitrate_instant(of); + out_info->bitrate_nominal = br > 0 ? (long)br : 0; + + return 0; +} + +long OFReadInterleavedFloat(OFFileRef fr, float *dst, int max_frames, int channels) { + struct OFFile *f = (struct OFFile *)fr; + if (!f || !f->of || !dst || max_frames <= 0 || channels <= 0) return -1; + + // op_read_float takes the buffer size in TOTAL floats, not frames. + int li = 0; + int frames = op_read_float(f->of, dst, max_frames * channels, &li); + if (frames < 0) return (long)frames; // OP_* error code + return (long)frames; // 0 == EOF +} + +long OFReadFloatDeinterleaved(OFFileRef fr, float **dst, int max_frames, int channels) { + struct OFFile *f = (struct OFFile *)fr; + if (!f || !f->of || !dst || max_frames <= 0 || channels <= 0) return -1; + + float *scratch = of_scratch(f, (size_t)max_frames * (size_t)channels); + if (!scratch) return -1; + + int li = 0; + int frames = op_read_float(f->of, scratch, max_frames * channels, &li); + if (frames <= 0) return (long)frames; + + // op_read_float reports the channel count of the link it just decoded; a + // chained stream can change it mid-file. Deinterleave with the stride the + // data actually has, not the one the caller asked for, or the output is + // garbled rather than merely wrong-length. + int decoded_channels = op_channel_count(f->of, li); + if (decoded_channels <= 0) decoded_channels = channels; + + for (int c = 0; c < channels; ++c) { + float *out = dst[c]; + if (!out) continue; + if (c < decoded_channels) { + for (int fr_i = 0; fr_i < frames; ++fr_i) { + out[fr_i] = scratch[fr_i * decoded_channels + c]; + } + } else { + // Caller wants more channels than this link carries; silence the rest. + for (int fr_i = 0; fr_i < frames; ++fr_i) out[fr_i] = 0.0f; + } + } + + return (long)frames; +} + +int OFSeekTime(OFFileRef fr, double time_seconds) { + struct OFFile *f = (struct OFFile *)fr; + if (!f || !f->of) return -1; + if (!op_seekable(f->of)) return -1; + // opusfile seeks by sample position at the fixed 48 kHz output rate. + opus_int64 target = (opus_int64)(time_seconds * 48000.0); + return op_pcm_seek(f->of, target); +} + +int OFIsSeekable(OFFileRef fr) { + struct OFFile *f = (struct OFFile *)fr; + if (!f || !f->of) return 0; + return op_seekable(f->of); +} diff --git a/AudioCodecs/include/AudioCodecs.h b/AudioCodecs/include/AudioCodecs.h index ee189d9..b29d1f7 100644 --- a/AudioCodecs/include/AudioCodecs.h +++ b/AudioCodecs/include/AudioCodecs.h @@ -9,5 +9,6 @@ #define AudioCodecs_h #import "VorbisFileBridge.h" +#import "OpusFileBridge.h" #endif /* AudioCodecs_h */ diff --git a/AudioCodecs/include/OpusFileBridge.h b/AudioCodecs/include/OpusFileBridge.h new file mode 100644 index 0000000..25a1736 --- /dev/null +++ b/AudioCodecs/include/OpusFileBridge.h @@ -0,0 +1,65 @@ +#ifndef OPUS_FILE_BRIDGE_H +#define OPUS_FILE_BRIDGE_H + +#include +#include + +// Opaque refs for Swift-friendly API. +// Mirrors VorbisFileBridge.h so OggStreamProcessor can drive either decoder. +typedef void * OFStreamRef; +typedef void * OFFileRef; + +#ifdef __cplusplus +extern "C" { +#endif + +// Stream info structure +typedef struct { + int sample_rate; // Always 48000 for Opus (opusfile always outputs 48 kHz) + int channels; + long long total_pcm_samples; // -1 if unknown (non-seekable stream) + double duration_seconds; // < 0 if unknown + long bitrate_nominal; // instantaneous bitrate in bits/sec, or 0 if unknown +} OFStreamInfo; + +// Stream lifecycle +OFStreamRef OFStreamCreate(size_t capacity_bytes); +void OFStreamDestroy(OFStreamRef s); +size_t OFStreamAvailableBytes(OFStreamRef s); + +// Feeding data +void OFStreamPush(OFStreamRef s, const uint8_t *data, size_t len); +void OFStreamMarkEOF(OFStreamRef s); + +// Decoder lifecycle +// Returns 0 on success, negative on error (opusfile OP_* codes) +int OFOpen(OFStreamRef s, OFFileRef *out_of); +void OFClear(OFFileRef of); + +// Query info; returns 0 on success +int OFGetInfo(OFFileRef of, OFStreamInfo *out_info); + +// Read deinterleaved float32 PCM frames into caller-provided channel pointers. +// `dst` is an array of `channels` pointers, each with room for max_frames floats. +// Returns frames read per channel, 0 on EOF, <0 on error. +// +// NOTE: opusfile has no deinterleaved read (unlike ov_read_float), so this +// deinterleaves internally into the caller's buffers. Callers must not assume +// the returned data is owned by the decoder — it is written into `dst`. +long OFReadFloatDeinterleaved(OFFileRef of, float **dst, int max_frames, int channels); + +// Read interleaved float32 PCM frames into dst (room for max_frames * channels floats). +// Returns frames read per channel, 0 on EOF, <0 on error. +long OFReadInterleavedFloat(OFFileRef of, float *dst, int max_frames, int channels); + +// Seek to a specific time in seconds; returns 0 on success, <0 on error +int OFSeekTime(OFFileRef of, double time_seconds); + +// Check if the stream is seekable; returns 1 if seekable, 0 if not +int OFIsSeekable(OFFileRef of); + +#ifdef __cplusplus +} +#endif + +#endif // OPUS_FILE_BRIDGE_H diff --git a/AudioStreaming/OggAudio/OggAudioDecoder.swift b/AudioStreaming/OggAudio/OggAudioDecoder.swift new file mode 100644 index 0000000..9cac9b8 --- /dev/null +++ b/AudioStreaming/OggAudio/OggAudioDecoder.swift @@ -0,0 +1,69 @@ +// +// OggAudioDecoder.swift +// AudioStreaming +// + +import AVFoundation +import Foundation + +/// The decoder interface `OggStreamProcessor` drives. +/// +/// Ogg is a container, not a codec: a `kAudioFileOggType` stream can carry +/// Vorbis, Opus, FLAC, or Speex. The renderer plumbing is identical for all of +/// them, so it lives in `OggStreamProcessor` and the codec-specific work sits +/// behind this protocol. +/// +/// `VorbisFileDecoder` (libvorbisfile) and `OpusFileDecoder` (libopusfile) +/// both conform. +protocol OggAudioDecoder: AnyObject { + /// Human-readable codec name, used only for log messages. + var codecName: String { get } + + /// Output sample rate in Hz. Zero until `openIfNeeded()` succeeds. + var sampleRate: Int { get } + /// Output channel count. Zero until `openIfNeeded()` succeeds. + var channels: Int { get } + /// Total duration in seconds, or a negative value when unknown (streaming). + var durationSeconds: Double { get } + /// Total PCM samples per channel, or -1 when unknown (streaming). + var totalPcmSamples: Int64 { get } + /// Nominal or instantaneous bitrate in bits/sec, or 0 when unknown. + var nominalBitrate: Int { get } + /// Deinterleaved float32 format matching `sampleRate` / `channels`. + var processingFormat: AVAudioFormat? { get } + + /// Bitrate estimates used for duration calculation when the container + /// reports neither a total sample count nor a nominal bitrate. + var fallbackBitrateStereo: Double { get } + var fallbackBitrateMono: Double { get } + + /// Allocate the ring buffer. + func create(capacityBytes: Int) + /// Release the decoder and ring buffer. + func destroy() + /// Feed compressed bytes. + func push(_ data: Data) + /// Bytes currently sitting in the ring buffer. + func availableBytes() -> Int + /// Signal that no more data is coming. + func markEOF() + /// Open the decoder once enough bytes have arrived. Throws while still short. + func openIfNeeded() throws + /// Decode into `buffer`. Returns frames written; 0 or negative means no data. + func readFrames(into buffer: AVAudioPCMBuffer, frameCount: Int) -> Int + /// Tear down and return to the pre-`create` state. + func reset() +} + +extension OggAudioDecoder { + // Vorbis-era defaults; OpusFileDecoder overrides with lower values since + // Opus is typically encoded at 96-128 kbps rather than 160-192. + var fallbackBitrateStereo: Double { 160_000 } + var fallbackBitrateMono: Double { 96_000 } +} + +// `VorbisFileDecoder` already exposes every member above with matching +// signatures, so conformance is declaration-only. +extension VorbisFileDecoder: OggAudioDecoder { + var codecName: String { "Vorbis" } +} diff --git a/AudioStreaming/OggAudio/OpusFileDecoder.swift b/AudioStreaming/OggAudio/OpusFileDecoder.swift new file mode 100644 index 0000000..8fc4dcc --- /dev/null +++ b/AudioStreaming/OggAudio/OpusFileDecoder.swift @@ -0,0 +1,219 @@ +// +// OpusFileDecoder.swift +// AudioStreaming +// + +import AudioCodecs +import AVFoundation +import Foundation +import OSLog + +/// A decoder for Ogg Opus streams using libopusfile. +/// +/// Structurally a mirror of `VorbisFileDecoder`. Two things differ: +/// +/// 1. Opus always decodes to 48 kHz. `OpusHead.input_sample_rate` describes the +/// material that was *encoded*, not the output, and using it as the output +/// rate produces a pitch-shifted stream. +/// 2. libopusfile has no deinterleaved read (no equivalent of `ov_read_float`), +/// so the C bridge deinterleaves into caller-owned buffers. That means this +/// decoder writes straight into the `AVAudioPCMBuffer` channel pointers +/// rather than memcpy'ing from decoder-owned memory. +final class OpusFileDecoder { + // Core properties + private var stream: OFStreamRef? + private var of: OFFileRef? + + // Audio format properties + private(set) var sampleRate: Int = 0 + private(set) var channels: Int = 0 + private(set) var durationSeconds: Double = -1 + private(set) var totalPcmSamples: Int64 = -1 + private(set) var nominalBitrate: Int = 0 + private(set) var processingFormat: AVAudioFormat? + + // Thread safety + private let decoderLock = NSLock() + + /// Create the stream buffer with specified capacity + /// - Parameter capacityBytes: Size of the ring buffer in bytes + func create(capacityBytes: Int) { + decoderLock.lock() + defer { decoderLock.unlock() } + + stream = OFStreamCreate(capacityBytes) + } + + /// Clean up resources + func destroy() { + decoderLock.lock() + defer { decoderLock.unlock() } + + if let of = of { OFClear(of) } + if let stream = stream { OFStreamDestroy(stream) } + of = nil + stream = nil + } + + deinit { + destroy() + } + + /// Push data into the stream buffer + /// - Parameter data: The Ogg Opus data to decode + func push(_ data: Data) { + decoderLock.lock() + defer { decoderLock.unlock() } + + data.withUnsafeBytes { rawBuf in + guard let base = rawBuf.baseAddress?.assumingMemoryBound(to: UInt8.self), + rawBuf.count > 0, + let stream = stream else { return } + + OFStreamPush(stream, base, rawBuf.count) + } + } + + /// Get the number of bytes currently available in the stream buffer + func availableBytes() -> Int { + decoderLock.lock() + defer { decoderLock.unlock() } + + guard let stream = stream else { return 0 } + return Int(OFStreamAvailableBytes(stream)) + } + + /// Mark the end of the stream + func markEOF() { + decoderLock.lock() + defer { decoderLock.unlock() } + + if let stream = stream { + OFStreamMarkEOF(stream) + } + } + + /// Try to open the Opus file if enough data is available + /// - Throws: Error if opening fails + func openIfNeeded() throws { + decoderLock.lock() + defer { decoderLock.unlock() } + + guard of == nil, let stream = stream else { return } + + var outOF: OFFileRef? + let rc = OFOpen(stream, &outOF) + if rc < 0 { + // OP_ENOTFORMAT / OP_EBADHEADER on a short read is expected — the + // caller retries as more bytes arrive. + Logger.error("Failed to open Opus file (\(rc))", category: .audioRendering) + throw NSError(domain: "OpusFileDecoder", code: Int(rc), + userInfo: [NSLocalizedDescriptionKey: "Failed to open Opus file"]) + } + + of = outOF + + var info = OFStreamInfo() + if OFGetInfo(outOF, &info) == 0 { + sampleRate = Int(info.sample_rate) + channels = Int(info.channels) + totalPcmSamples = Int64(info.total_pcm_samples) + durationSeconds = info.duration_seconds + nominalBitrate = Int(info.bitrate_nominal) + + let layoutTag: AudioChannelLayoutTag + switch channels { + case 1: layoutTag = kAudioChannelLayoutTag_Mono + case 2: layoutTag = kAudioChannelLayoutTag_Stereo + default: layoutTag = kAudioChannelLayoutTag_Unknown | UInt32(channels) + } + + guard let channelLayout = AVAudioChannelLayout(layoutTag: layoutTag) else { + Logger.error("Failed to build channel layout for \(channels) channels", + category: .audioRendering) + return + } + + processingFormat = AVAudioFormat( + commonFormat: .pcmFormatFloat32, + sampleRate: Double(sampleRate), + interleaved: false, + channelLayout: channelLayout + ) + } else { + Logger.error("Failed to get Opus stream info", category: .audioRendering) + } + } + + /// Read decoded frames into an AVAudioPCMBuffer + /// - Returns: Number of frames read; a small run of silent frames when no + /// data is available, matching `VorbisFileDecoder` so the renderer never + /// sees a zero-frame read as end-of-track. + func readFrames(into buffer: AVAudioPCMBuffer, frameCount: Int) -> Int { + decoderLock.lock() + defer { decoderLock.unlock() } + + guard let of = of, + buffer.format.channelCount > 0, + let floatChannelData = buffer.floatChannelData else { + return generateSilentFrames(into: buffer, frameCount: frameCount) + } + + let maxFrames = min(frameCount, Int(buffer.frameCapacity)) + let channelCount = min(Int(buffer.format.channelCount), channels) + guard channelCount > 0, maxFrames > 0 else { + return generateSilentFrames(into: buffer, frameCount: frameCount) + } + + // Hand the bridge the buffer's own channel pointers. It deinterleaves + // directly into them, so there is no second copy. + var channelPointers = [UnsafeMutablePointer?]() + channelPointers.reserveCapacity(channelCount) + for ch in 0.. Int in + guard let base = ptr.baseAddress else { return -1 } + return Int(OFReadFloatDeinterleaved(of, base, Int32(maxFrames), Int32(channelCount))) + } + + if framesRead <= 0 { + return generateSilentFrames(into: buffer, frameCount: frameCount) + } + + return framesRead + } + + /// Generate silent frames when no real audio data is available. + /// Prevents the renderer from treating a starved buffer as EOF. + private func generateSilentFrames(into buffer: AVAudioPCMBuffer, frameCount: Int) -> Int { + guard let floatChannelData = buffer.floatChannelData, + channels > 0 else { return 1 } + + let framesToGenerate = min(128, frameCount) + + for ch in 0.. OggStreamProcessor? { + let decoder: any OggAudioDecoder + switch codec { + case .vorbis: decoder = VorbisFileDecoder() + case .opus: decoder = OpusFileDecoder() + case .unsupported: return nil + } + let processor = OggStreamProcessor( + playerContext: playerContext, + rendererContext: rendererContext, + outputAudioFormat: outputAudioFormat, + decoder: decoder + ) + processor.processorCallback = { [weak self] effect in self?.fileStreamCallback?(effect) } + return processor } /// Opens the `AudioFileStream` @@ -79,12 +93,14 @@ final class AudioFileStreamProcessor { /// - Returns: An `OSStatus` value indicating if an error occurred or not. func openFileStream(with fileHint: AudioFileTypeID) -> OSStatus { - // Check if this is an Ogg Vorbis file + // Ogg container: defer decoder selection until the first page arrives. if fileHint == kAudioFileOggType { - isProcessingOggVorbis = true + isProcessingOgg = true + oggProcessor = nil + oggHeadBuffer.removeAll(keepingCapacity: true) return noErr } else { - isProcessingOggVorbis = false + isProcessingOgg = false let data = UnsafeMutableRawPointer.from(object: self) return AudioFileStreamOpen(data, _propertyListenerProc, _propertyPacketsProc, fileHint, &audioFileStream) } @@ -92,9 +108,11 @@ final class AudioFileStreamProcessor { /// Closes the currently open `AudioFileStream` instance, if opened. func closeFileStreamIfNeeded() { - if isProcessingOggVorbis { - isProcessingOggVorbis = false - oggVorbisProcessor.cleanup() + if isProcessingOgg { + isProcessingOgg = false + oggProcessor?.cleanup() + oggProcessor = nil + oggHeadBuffer.removeAll(keepingCapacity: false) return } @@ -114,9 +132,36 @@ final class AudioFileStreamProcessor { func parseFileStreamBytes(data: Data) -> OSStatus { guard !data.isEmpty else { return 0 } - // Check if we're processing Ogg Vorbis - if isProcessingOggVorbis { - return oggVorbisProcessor.parseOggVorbisData(data: data) + // Ogg: pick the decoder from the first page, then forward everything. + if isProcessingOgg { + if let processor = oggProcessor { + return processor.parseOggData(data: data) + } + + oggHeadBuffer.append(data) + guard let codec = OggCodecSniffer.sniff(oggHeadBuffer) else { + // Not enough bytes to decide yet. Guard against a stream that + // never resolves rather than buffering without bound. + if oggHeadBuffer.count > OggCodecSniffer.maxHeaderLength { + Logger.debug("Ogg codec undetermined after \(oggHeadBuffer.count) bytes", + category: .generic) + isProcessingOgg = false + return OSStatus(kAudioFileStreamError_UnsupportedFileType) + } + return noErr + } + + guard let processor = makeOggProcessor(for: codec) else { + Logger.debug("Unsupported codec in Ogg container", category: .generic) + isProcessingOgg = false + return OSStatus(kAudioFileStreamError_UnsupportedDataFormat) + } + + oggProcessor = processor + // Replay the buffered head so the decoder sees the stream from byte 0. + let buffered = oggHeadBuffer + oggHeadBuffer.removeAll(keepingCapacity: false) + return processor.parseOggData(data: buffered) } guard let stream = audioFileStream else { return 0 } @@ -137,8 +182,8 @@ final class AudioFileStreamProcessor { /// /// - Returns: An `OSStatus` value indicating if an error occurred or not. func flushRemainingPackets() -> OSStatus { - // Ogg Vorbis doesn't need flushing (handled internally) - if isProcessingOggVorbis { + // Ogg doesn't need flushing (handled internally) + if isProcessingOgg { return noErr } @@ -159,9 +204,9 @@ final class AudioFileStreamProcessor { return } - // If processing Ogg Vorbis, use the Ogg Vorbis processor - if isProcessingOggVorbis { - oggVorbisProcessor.processSeek() + // If processing Ogg, use the Ogg processor + if isProcessingOgg { + oggProcessor?.processSeek() return } diff --git a/AudioStreaming/Streaming/AudioPlayer/Processors/OggVorbisStreamProcessor.swift b/AudioStreaming/Streaming/AudioPlayer/Processors/OggStreamProcessor.swift similarity index 88% rename from AudioStreaming/Streaming/AudioPlayer/Processors/OggVorbisStreamProcessor.swift rename to AudioStreaming/Streaming/AudioPlayer/Processors/OggStreamProcessor.swift index 884f467..9e6950f 100644 --- a/AudioStreaming/Streaming/AudioPlayer/Processors/OggVorbisStreamProcessor.swift +++ b/AudioStreaming/Streaming/AudioPlayer/Processors/OggStreamProcessor.swift @@ -1,5 +1,5 @@ // -// OggVorbisStreamProcessor.swift +// OggStreamProcessor.swift // AudioStreaming // // Created on 25/10/2025. @@ -10,8 +10,15 @@ import AVFoundation import CoreAudio import OSLog -/// A processor for Ogg Vorbis audio streams using libvorbisfile -final class OggVorbisStreamProcessor { +/// A processor for Ogg audio streams. +/// +/// Ogg is a container, so the renderer plumbing here is codec-agnostic: it +/// drives whatever `OggAudioDecoder` it was handed. `VorbisFileDecoder` wraps +/// libvorbisfile and `OpusFileDecoder` wraps libopusfile. +/// +/// Previously named `OggVorbisStreamProcessor`, when Vorbis was the only Ogg +/// codec supported. +final class OggStreamProcessor { /// The callback to notify when processing is complete or an error occurs var processorCallback: ((FileStreamProcessorEffect) -> Void)? @@ -23,9 +30,8 @@ final class OggVorbisStreamProcessor { /// By reducing the bitrate slightly, we increase the calculated duration to match reality. private let oggContainerOverheadFactor: Double = 0.96 // 4% overhead - /// Fallback bitrate estimates when nominal bitrate is unavailable - private let fallbackBitrateStereo: Double = 160_000 // 160 kbps for stereo - private let fallbackBitrateMono: Double = 96_000 // 96 kbps for mono + // Fallback bitrate estimates now come from the decoder, since sensible + // values differ per codec (Vorbis 160/96 kbps, Opus 128/64 kbps). // MARK: - Properties @@ -33,7 +39,7 @@ final class OggVorbisStreamProcessor { private let rendererContext: AudioRendererContext private let outputAudioFormat: AudioStreamBasicDescription - private let vfDecoder = VorbisFileDecoder() + private let decoder: any OggAudioDecoder private var isInitialized = false // Audio converter for format conversion @@ -52,17 +58,20 @@ final class OggVorbisStreamProcessor { // MARK: - Initialization - /// Initialize the OggVorbisStreamProcessor + /// Initialize the OggStreamProcessor /// - Parameters: /// - playerContext: The audio player context /// - rendererContext: The audio renderer context /// - outputAudioFormat: The output audio format + /// - decoder: The codec-specific decoder to drive init(playerContext: AudioPlayerContext, rendererContext: AudioRendererContext, - outputAudioFormat: AudioStreamBasicDescription) { + outputAudioFormat: AudioStreamBasicDescription, + decoder: any OggAudioDecoder) { self.playerContext = playerContext self.rendererContext = rendererContext self.outputAudioFormat = outputAudioFormat + self.decoder = decoder } deinit { @@ -76,40 +85,40 @@ final class OggVorbisStreamProcessor { audioConverter = nil // Destroy and reset the decoder - vfDecoder.destroy() + decoder.destroy() isInitialized = false totalFramesProcessed = 0 } // MARK: - Data Processing - /// Parse Ogg Vorbis data - /// - Parameter data: The Ogg Vorbis data to parse + /// Parse Ogg data + /// - Parameter data: The Ogg data to parse /// - Returns: An OSStatus indicating success or failure - func parseOggVorbisData(data: Data) -> OSStatus { + func parseOggData(data: Data) -> OSStatus { guard let entry = playerContext.audioReadingEntry else { return 0 } dataChunkCount += 1 if !isInitialized { - vfDecoder.create(capacityBytes: 2_097_152) + decoder.create(capacityBytes: 2_097_152) isInitialized = true totalFramesProcessed = 0 } - vfDecoder.push(data) + decoder.push(data) if !entry.audioStreamState.processedDataFormat { - let availableBytes = vfDecoder.availableBytes() + let availableBytes = decoder.availableBytes() if availableBytes >= 16384 { do { - try vfDecoder.openIfNeeded() + try decoder.openIfNeeded() - if vfDecoder.sampleRate > 0 && vfDecoder.channels > 0 { + if decoder.sampleRate > 0 && decoder.channels > 0 { setupAudioFormat() - if pcmBuffer == nil, let processingFormat = vfDecoder.processingFormat { + if pcmBuffer == nil, let processingFormat = decoder.processingFormat { pcmBuffer = AVAudioPCMBuffer(pcmFormat: processingFormat, frameCapacity: UInt32(frameCount)) } } @@ -199,7 +208,7 @@ final class OggVorbisStreamProcessor { } } - let availableBytes = vfDecoder.availableBytes() + let availableBytes = decoder.availableBytes() if availableBytes < 4096 { consecutiveNoFrames += 1 if consecutiveNoFrames >= 3 { @@ -234,7 +243,7 @@ final class OggVorbisStreamProcessor { return OSStatus(-1) } - let framesRead = vfDecoder.readFrames(into: pcmBuffer, frameCount: frameCount) + let framesRead = decoder.readFrames(into: pcmBuffer, frameCount: frameCount) if framesRead <= 0 { return OSStatus(-1) @@ -249,10 +258,10 @@ final class OggVorbisStreamProcessor { // MARK: - Audio Format Setup - // Setup audio format using the processingFormat from VorbisFileDecoder + // Setup audio format using the processingFormat from the decoder private func setupAudioFormat() { guard let entry = playerContext.audioReadingEntry, - let processingFormat = vfDecoder.processingFormat else { return } + let processingFormat = decoder.processingFormat else { return } entry.lock.lock() @@ -261,21 +270,21 @@ final class OggVorbisStreamProcessor { // Store the format in the entry entry.audioStreamFormat = asbd - entry.sampleRate = Float(vfDecoder.sampleRate) - entry.packetDuration = Double(1) / Double(vfDecoder.sampleRate) + entry.sampleRate = Float(decoder.sampleRate) + entry.packetDuration = Double(1) / Double(decoder.sampleRate) // For streaming Ogg files, totalPcmSamples may not be available (returns error code) // In that case, use bitrate-based duration calculation with container overhead correction - if vfDecoder.totalPcmSamples > 0 { + if decoder.totalPcmSamples > 0 { // We have total samples - use packet offset for accurate duration - entry.audioStreamState.dataPacketOffset = UInt64(vfDecoder.totalPcmSamples) + entry.audioStreamState.dataPacketOffset = UInt64(decoder.totalPcmSamples) } else { // Streaming - use bitrate for duration estimation - if vfDecoder.nominalBitrate > 0 { - entry.audioStreamState.bitRate = Double(vfDecoder.nominalBitrate) * oggContainerOverheadFactor + if decoder.nominalBitrate > 0 { + entry.audioStreamState.bitRate = Double(decoder.nominalBitrate) * oggContainerOverheadFactor } else { - // Fallback: use typical bitrates for Vorbis quality - let estimatedBitrate = vfDecoder.channels == 2 ? fallbackBitrateStereo : fallbackBitrateMono + // Fallback: use typical bitrates for this codec + let estimatedBitrate = decoder.channels == 2 ? decoder.fallbackBitrateStereo : decoder.fallbackBitrateMono entry.audioStreamState.bitRate = estimatedBitrate * oggContainerOverheadFactor } } @@ -438,10 +447,10 @@ final class OggVorbisStreamProcessor { /// Process a seek request /// - /// Seeking is not supported for Ogg Vorbis streams. + /// Seeking is not supported for Ogg streams. /// For HTTP streams, seeking is extremely difficult because: /// 1. Need to find Ogg page boundaries - /// 2. Need Vorbis headers to initialize decoder + /// 2. Need codec headers to initialize decoder /// 3. Headers are only at the beginning of the file /// /// Note: Future enhancement could support seeking in local files diff --git a/AudioStreaming/Streaming/Helpers/AudioFileType.swift b/AudioStreaming/Streaming/Helpers/AudioFileType.swift index 742bb6f..1881e23 100644 --- a/AudioStreaming/Streaming/Helpers/AudioFileType.swift +++ b/AudioStreaming/Streaming/Helpers/AudioFileType.swift @@ -38,6 +38,7 @@ let fileTypesFromMimeType: [String: AudioFileTypeID] = "video/3gp2": kAudioFile3GP2Type, "audio/flac": kAudioFileFLACType, "audio/ogg": kAudioFileOggType, + "audio/opus": kAudioFileOggType, "audio/vorbis": kAudioFileOggType, "application/ogg": kAudioFileOggType ] @@ -66,6 +67,7 @@ let fileTypesFromFileExtension: [String: AudioFileTypeID] = "flac": kAudioFileFLACType, "ogg": kAudioFileOggType, "oga": kAudioFileOggType, + "opus": kAudioFileOggType, ] func audioFileType(fileExtension: String) -> AudioFileTypeID { diff --git a/AudioStreaming/Streaming/Helpers/OggCodecSniffer.swift b/AudioStreaming/Streaming/Helpers/OggCodecSniffer.swift new file mode 100644 index 0000000..8e57bee --- /dev/null +++ b/AudioStreaming/Streaming/Helpers/OggCodecSniffer.swift @@ -0,0 +1,81 @@ +// +// OggCodecSniffer.swift +// AudioStreaming +// + +import Foundation + +/// Identifies which codec an Ogg bitstream carries. +/// +/// This exists because the transport layer cannot tell us. Ogg is a container, +/// and the registered MIME type `audio/ogg` is shared by Vorbis, Opus, Speex, +/// and FLAC-in-Ogg. Navidrome, for instance, serves an Opus transcode as +/// `audio/ogg` (`resources/mime_types.yaml`), so a Content-Type check alone +/// routes Opus into the Vorbis decoder, which rejects it. +/// +/// The only reliable discriminator is the first packet of the first page. +enum OggCodec: Equatable { + case vorbis + case opus + /// Recognised as Ogg, but the codec is not one we decode. + case unsupported +} + +enum OggCodecSniffer { + /// Bytes needed in the worst realistic case: 27-byte page header plus a + /// 255-entry segment table plus the 8-byte codec magic. + static let maxHeaderLength = 27 + 255 + 8 + + /// Bytes needed for a typical first page (single segment). + static let typicalHeaderLength = 36 + + /// Identifies the codec from the start of an Ogg bitstream. + /// + /// - Returns: the codec, or `nil` when `bytes` is not yet long enough to + /// decide. `nil` means "feed me more", `.unsupported` means "give up". + static func sniff(_ bytes: [UInt8]) -> OggCodec? { + // Ogg page header layout (RFC 3533 §6): + // 0..3 capture pattern "OggS" + // 4 stream structure version + // 5 header type flag + // 6..13 granule position + // 14..17 bitstream serial number + // 18..21 page sequence number + // 22..25 CRC checksum + // 26 number of page segments + // 27.. segment table (one byte per segment) + // then packet data + guard bytes.count >= 27 else { return nil } + + func matches(_ ascii: String, at offset: Int) -> Bool { + let pattern = Array(ascii.utf8) + guard bytes.count >= offset + pattern.count else { return false } + return Array(bytes[offset..= payloadOffset + 8 else { return nil } + + if matches("OpusHead", at: payloadOffset) { + return .opus + } + if bytes[payloadOffset] == 0x01, matches("vorbis", at: payloadOffset + 1) { + return .vorbis + } + + // Ogg FLAC ("\x7FFLAC"), Speex ("Speex "), Theora, etc. + return .unsupported + } + + /// Convenience overload for the streaming path. + static func sniff(_ data: Data) -> OggCodec? { + sniff([UInt8](data.prefix(maxHeaderLength))) + } +} diff --git a/AudioStreamingTests/Codecs/OpusFileBridgeTests.swift b/AudioStreamingTests/Codecs/OpusFileBridgeTests.swift new file mode 100644 index 0000000..54d3b23 --- /dev/null +++ b/AudioStreamingTests/Codecs/OpusFileBridgeTests.swift @@ -0,0 +1,239 @@ +// +// OpusFileBridgeTests.swift +// AudioStreamingTests +// + +import AudioCodecs +import XCTest +@testable import AudioStreaming + +/// Exercises the libopusfile C bridge directly. +/// +/// The bridge is pure computation over bytes — no audio hardware and no +/// `AVAudioEngine` — so unlike `OggStreamProcessor` it is fully testable in CI. +/// These tests cover the ring buffer, both callback signatures, the +/// deinterleave, and the 48 kHz output rule. +final class OpusFileBridgeTests: XCTestCase { + /// Matches `OggStreamProcessor`'s ring buffer size. + private let capacity = 2 * 1024 * 1024 + /// Matches the byte count `OggStreamProcessor` waits for before opening. + private let openGate = 16384 + + // MARK: - Helpers + + private func fixture(_ name: String, _ ext: String) throws -> Data { + let url = try XCTUnwrap( + Bundle.module.url(forResource: "ogg-fixtures/\(name)", withExtension: ext), + "missing fixture \(name).\(ext)" + ) + return try Data(contentsOf: url) + } + + private struct Decoded { + var info: OFStreamInfo + var frames: Int + var peak: Float + var channels: [[Float]] + var openAttempts: Int + var openedAfterBytes: Int + } + + /// Feeds `data` through the bridge the way `OpusFileDecoder` does. + /// + /// - Parameter chunkSize: when non-nil, pushes in chunks and retries the + /// open once `openGate` bytes are buffered — the streaming path. When + /// nil, pushes everything before opening — the cached-file path. + private func decode(_ data: Data, chunkSize: Int? = nil) throws -> Decoded { + let stream = try XCTUnwrap(OFStreamCreate(capacity), "OFStreamCreate returned nil") + defer { OFStreamDestroy(stream) } + + var file: OFFileRef? + var attempts = 0 + var openedAfter = 0 + + func push(_ slice: Data) { + slice.withUnsafeBytes { raw in + guard let base = raw.baseAddress?.assumingMemoryBound(to: UInt8.self) else { return } + OFStreamPush(stream, base, raw.count) + } + } + + if let chunkSize { + var offset = 0 + while offset < data.count { + let end = min(offset + chunkSize, data.count) + push(data[offset..= openGate { + attempts += 1 + if OFOpen(stream, &file) == 0 { openedAfter = offset } + } + } + OFStreamMarkEOF(stream) + } else { + push(data) + OFStreamMarkEOF(stream) + attempts = 1 + if OFOpen(stream, &file) == 0 { openedAfter = data.count } + } + + let of = try XCTUnwrap(file, "stream never opened after \(attempts) attempt(s)") + defer { OFClear(of) } + + var info = OFStreamInfo() + XCTAssertEqual(OFGetInfo(of, &info), 0, "OFGetInfo failed") + + let channelCount = Int(info.channels) + XCTAssertGreaterThan(channelCount, 0) + + let blockFrames = 4096 + var scratch: [UnsafeMutablePointer] = (0...allocate(capacity: blockFrames) + } + defer { scratch.forEach { $0.deallocate() } } + + var collected = [[Float]](repeating: [], count: channelCount) + var peak: Float = 0 + var total = 0 + + while total < 48000 * 60 { // hard stop so a bug cannot hang CI + var pointers: [UnsafeMutablePointer?] = scratch.map { $0 } + let got = pointers.withUnsafeMutableBufferPointer { buf -> Int in + guard let base = buf.baseAddress else { return -1 } + return Int(OFReadFloatDeinterleaved(of, base, Int32(blockFrames), Int32(channelCount))) + } + if got <= 0 { break } + for channel in 0.. Double { + guard samples.count > 1 else { return 0 } + var crossings = 0 + for i in 1..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 literal 0 HcmV?d00001 diff --git a/AudioStreamingTests/Codecs/ogg-fixtures/opus-large-header.opus b/AudioStreamingTests/Codecs/ogg-fixtures/opus-large-header.opus new file mode 100644 index 0000000000000000000000000000000000000000..0466ec3b7fa75a31a86b100f16ddae7359908f98 GIT binary patch literal 35250 zcmeIaWl&sQ!mf?GySux)1$TD{?h@QW@Swq6gG+FC3-0dj7Tmp0l6l7G{pOpg_f(x9 z>?*o9y%x}1)pgZf_gZ@^n47BrfdKt+ScZI~{&i}*4gv#=s$lQpBx!1B0t~_h?A!8t zio&nye>r|l2mblp6`3cj0^oo3X!-TWRSnIZ;C}voSwmMd4rT^U24*JKU+)$F=a=aR zV>=U5NB*B<#y`iHxql8a6WLfA+5P-MI(Z-Sd+a|HY62=33BzeWjvzJmXI`8i$w@qVD)4(%&l-Sf@;F==j9=( zBIsmiK*)w`)i5-6+ui3$`cZ0!Zcb#Py?vgE^(JtG@xq;sEF z-2FQ4@$>QXC9%ijJ^IJoU|gHiPk5r(DGMsANvz@p({hW-ACHK~^KQOB5|8JfkGI-H z__&~lg-PE?Cg205i&liaI;zt!eqm-!twF{15ajVExWrHPA0Ci*VnZkvI9I_mP~)Pd z;*k~GPU&hI7J|XN5aE`!SZz*t2B6DNRk++el#?jkdLNJ3ajW7RXWdiv?MRMsq(-(T z8gq2|5OXZ8O-aV8$Gy(lKGy_wDb?=>K8WKg&QZ8TGr9-WQ1pDoA;|vZZWf&2R9VgM z^Hs{n4!{NA(_lVS74XB6kK zVs&=+UG^>tgU=G~L$3po<=yDO;6RYy5ctTFXhgqjf6tj5Z$n1Vkos~s`w&KWU9)6} z!Ho;quPlUi&R%v?M}A%cd;v{Ds6(Kngq5}JWqX(S;0p$zyaiMT4*(&3CZVW}7Ab$O zWawHFTT|?~^E3yh)Nj;U@YnLcllQF)V1{|Y|bo&|-$1H27A zncZ_)vEZ&=SYwI`UlV0KeZCjLyMdiqM4%XF!;Yw~Y51svA`P+G6d)C|w^@@&Rx~)# z4vPVUl?OB3Fv3|CZfq`a|2Ky+XK@=h3hQB>P*NGP}ymX+a7NPDiN+tG_H2Xqnb7LTCp!$&V2cEskW<34CFom zZ>K8f{u!QGQ=geH$-xF~datQl-=H)189b?6JqUnJ&+1U77PAN{3l^M@re+hv3oW+Hm=d4&c2us;v1q|3z-qK z8rS(R#io!YVyI#JTf1jkr|sX8h=OWr+(#=&YkiP;4X77R*FI*UH46x22-dE40(N|T zY(jM`fV}g1fIx^54Ele5mIw!n6P1(^3~Hy>Mr>g3&p_je-g$l@j7+cgfW2_|k{K5T zfh%|3{O(???t_!*9iPi6mj<@TkrnDm@DEaXLD%mrVFK<*%lkHoK>^x#jwFQ=j%^OT zw1LQ98!KSecP>Ylu*^CG)36%YwV92#dVmUKG}JW0VkZP~-n~bKdLNLFL9&f)+l6Ew z`Rd;(Bk+Tr*G(4lc|)SUSXi(URE$Kjol5VpDARnR#27}qq6oM`k((`$|Lg2i{(ZH7 z39KRGvPtLoyfnHZR|tjEcZ1uEF(~%d0zQh`9YQ}~-932JW0CiX*JN`BYnV_=5K(<* z!&^UMgb_`WXO4h*^$BEEGYRnL-73<6dwg}C3~VjvIYsx0VL3fXb$-HO5qYLJhPn+f zTz;Bwddp#%C`myJ9 z2G%Uz(=%ep%rccl1ZlTtTVe&Y;~5bx(#REPwqlM`!1*IX%yM?<`!cJbYdpnnyB`q` zDuR6VdFKg4@cS<)#{8SzvVk%7b#Es};&oDkn#MEr z@z7K0bZ7-v)Z35lG3Kbg@G=aqFCfg|K_uDG(CC=#XH*;4E=LS0?iL5D(87=@@V3bY zDArbj@*HVuby16P=J`?C)vL~ZW5|?ZYs?72p*^i~7}gPQl?+|_5QRlCz08pe=){wR zyyQrz_rE73DZyTg&!b@exrG<Z_Ke>UTQV6ve96yZz2*OI+ni zrxLr|8mZ?el_(taXZX^;O*B$l-Kx>js1d)yKxc7%t3&^CGq<2=Yp{l>M42{=EKy)& zt>W_F4nyzUQW6oO@={tw|5+;jvIIdA&1$0xocZy65 z!9$ankdOc~P39d|`3*>CX51%FL<%nN_0eqCq~kJ;;3VHZe~+7*0vm zuI47tP6@0G0|okn4}l=9GPivIH6}m(9NS_H46o5<$Nk&S^HbCzdLTjg!Lm@huRKqLjd+U|L!uwT0Am&pGr&1N97bmb8YVFob#}XzHPd-hTgqX{d zYhiryA-57{1;T7j_&X`cpK=A3V`rhSvWWXC#;{PdYxrl!+B71AMq&k;k1XK;Io@rupA9d%UbAgS zcH2HrKPp2ACoN)ijTb1G;UitN!2I>h<{ezwx07D(#t^dp9R%Fq+L;I*zjHD3IoG5D z1P)crpe3R#JtkqGAz2sF2k#XWeY9eF@{+b-{&?I89gXzvH9b`B^Nqo*u%rp){IV}8 z%qdL^&7KBVg(M^G5C^jsNFz75n;$=<1vhQxc1GZRMP>KUnx!4!j|GFsOId%^(TbS7 z`~1m`JP{aBx(PmV=LQOh@O!Byuge0ij)VFKGs7g4#r61;mUE%@f(CF-zg*ElPZ8N> ziaE=LK7N08qG$!6y_QMbdAy`j(ZiawVc`?qL0)HTbN9L#unHJYWcuP%MO_xObQHgb zX8as#wET*{ndK-^`YbvbyyV`yCq{3YOez$hj~a8Y6=nMR;sU)n3W*IcMflOF{XV&} zU|bvqPaY%|H8TKBWz_&BQm22kVkYXV$0-6k_p9rO zlW)}qkmW}6K(9OKBlRvF_sF8Ls$QAs`RthRGkcaoQHYZt$FBLnsd1F!S8vc%zO#VOVs9(lWZu72~Qxg1!XAYa$YfzwM9!#}%;M#$B;Nh;sclL# zp3M<6!!=8p@<8U4W7zH8SqbSHP8HW#!yX(|>C*)M?3w6vF^Rv3__XU>81kT$=DH*h zpPyjx>oN!A6XX-nzZNwe`o!oTqUL-QnA2*as*he-L$hTL(LihS5b3E9qBn8ZgksOT zpof)G1;wEUVX+#9P?83je^K-Ladx8*tL8O- zYhF9Gi3>Yg8#p(?vS?GTH^!0huCs$Nyy0QA`y8!CrPhdUYCOD2-#hMIL4c=+z82mk zZC$u)q@E-V;#Td2I5^1n>6Cr_Czk4r9B9eFYW2wvR9Q72>PnsAu z;~ZBscpEnx`r6i*oKyq;a5Pqz%AXy5kb1gbDmr9!0dT1?c5bPg;(xWsEV&v1Ewcdc zr)1`h9^45_EVlOq1Y(|2yezBePqvP+M~8e}_T0^oo7vZ*k^1H%GIwOYDo9+wgi8a( z^wCIgfde_1`?Yt1wGXP8fH1KQny&Zw?*8UWKy7&B<8p5A#Moo1Q($VgiT)@)^L%pr z6{8pqf_I7dDRd?uuex72mMf5!#!_?)HvX4BnB%niga=gjdHMy4gG$2`KPSCp;}y*LMb~Zgb_0R9ClTXZioc+AlE|=F^{m#o2h1c z;!p~_u-oV}xJ4k9?l!+LE6Ej$Ie0Sj0!Zlh)5xz8Omq8vY5XmE#{C6C>@4Y!O~B#M zXSMl zm+M4hs-H)^6_(Q<7pAcclsOOGbQjufAh96U2O#E(|H;cnB(-{5Lu6Na=43^F78sVG z0QN=y#|mANMIhvMw$lGv$r zo5oYTIDmV_2U8H_?oOQ|x*$Ly(xtQ$t*=ZoM^Mx-|d(@<@>V$#AX z-};x5RnW%jTp_D(%L-`0)Jht^SCjg3^Lb$OygN!J=0}zgg|a$95@zKFjEVg`j4%B? z*iBrhBbqK~sdr5n*t?Yxg{gRrZtsiIQVwk56wU{1L?mmwQz)LIf?@8kIbGKUEs*|t zWq{BbYy-#!Isjjp^XWu?$?0^ONesLngY1zEM0q9oX)X311n^fc7t0TV{qK1hA>=E0 zYi=)P`|?yI8ClzbFfu(CzW`50RmH-MTYb@2Z;KG4eBA~&#oL8Yc{8rf3)G5p@Ea_` z+e}yNWG!Rp%@{L6GI5lW4A=JT{(vmCZMD50X(6Jyt-`1gsk<$8yHXjY!EWejIHr=c znFSWoN&2w?)Gj1DoP0_fNS~lzk(1A;%)mx-yq9@*iTn$l(h%)x%~RkJ<+AN6ud}r5 zXv5)E8`I(xnze*c6Xm?}kJ218s;yx+5F%TQ-E*G!)b;=5WdNZ5x0iAMAwUP!ImZUa z)Pxq?aqa{zThRIkE`^B=cFEpoPE;$?+gK}Egg!s7Z7MRz^^`vzVd7)-@eJPb^YSrB0!y2zYV<@yN=p_v?k6;*Vo*2* zmBk)_us-`OYAA`X71WyZ8)V-3>a`2V?|wfuF21E|C$hK2f4@VM`ou|vP0V>}DmIOu z+hCWZj zuiT0h;4)%b3m|aFR^XN{8Kb%9ONnaQT#e|xV%FOBKk9l(GDu{>GqihM8U7_^%im(Q zMFIU&c>YH*i#dBx{`iZSwe#?gt)biaS6G}?Qp1KUWo;If60QqN8%lu)Yt<^gx{-~R zA`GcU_euxzmCKKnPgg-ET?r71GA_54weOYl$wTgxf?c^UpsV9%=g*Ob!U)uVI}72+ zqe@C4wCkzFI8VQ0o8t;;VioZMdAf;cp$+x9q+!-0%$#U#1iqo{{|I$KSjAR9hV3xo zWni`~(E#eB7hGT#aGn_u|3aH@ z1Ck##;}&1Bv7f? zLR{ho$P)Je%sk){0i}ddk$<%cw1e(TL=u?G>}|X8LH;6eIJm@evW3K`Uc6Ai&J^Sw%F(&=<=lKuEPytI=04A}7cX*dm4|feG{6j!aUcs5#^BT8$=2Z!iBwp&y2Qc>1XK+N1~8)3!ze*z_7luXiQ!dp;== zpcDIFfqY0#Hd8BsfWO!GVnSnQhK#(>K6#XdO9c8&g~GD85Qvz|660^e;A$_hENP)X za$~?b^X)+}gUG=X9rb$OB4AzCTO;#G1gn~n-`g?98N7#qYS0p|`h*F7@|mM%qven! zz}-4s_~sj_yY^h&t4KVglxvFk` z&BRQOE?HnH3-@~IONsCKBQfXKWyxjzIWhmW*^u9R2R**LV^bZMK~FGo+k2T%t_kZ_ zR{XIS!i=^k&%coIU=FPEonTgFeoU{1C33eqYf$$q5#qrL0WvNDY4OGJ^!J9jsWHp7 zi!JoD*g}N)+`LZ0Bhn!5kzn4t96T=(kvK3}E3lE!Q}q7!VgF@*jF@pFB4|z^L!@^( z{u(QdoC-O*2tngMt>G?~v+SWMBvMfK?WLL(@?=C6@LykbGqSf*EG6c5viT1{6ex8i z7nb0hKkL@~z&`G0fIL68wP3oTkAw$*P^~N^P2Yr@a5p6l*HI?M`Ix2qnN%f+b+wbt z)Dio(DI^>(UsQXZ`D=T0vx8*K6c8ej!X}{Mk__Ve?Y}QJ{*$~cbT<3xCi;)O%#Uc4 z&nwb=_>|Zk-$YYZ#umy)q`Dss+$h`5tP&$J%JPRNu3g;y)YeYB_IzebtGbDwa=swI zKk^NG-tookYAED)l#=I&Te$>1SW4cFL~;Joa>z;TXMoioc^QoBZ}M_D3XuDxzLICN z#gvu#qim4vhMp?lu@2|1OcW@RS9(4$9i;01SVg{xj7<0l2L5oPwehD5cZ|LFS1|&3 z7$s(n?I5d$tdb~dMuB9N4E|SxsK(NmEWjm#Uc>GKE7YF%zvd9NsMF^wpHRqv+uFbbt)5x7Xtxu- zm!iq>fMyKG%xE?Xo)a@z1Z+JN#Z--2zOUgYtRcz+=B{U^O|NMH1tde>)+dev{QkMl z_@}sRL((sR?PEkARm-G6f*~y^g-vEp@J(4I!aIg4G*pF=Qo( z-Y9SmT~Bb{q*skXrOCMxiHbe}VOTND1|d8f%Pa34;ROSlt3430Npc&LY&aNx&-(GU zSb)Q1ZLATWP|;tS0O|c+rzY!%xbYHp9zQ->TEcwmT5fekdisEj)Q#>7NiIl5na0pnV=~h+Vo;I+Yq!Zh3^P0P@#u zmaOJTpgq&Yqz$)z&%X78f3(bh@8iF1 zndN;qttJ-IAQa{}q4wv+k%6Em`=skF`cpycJF=BQU3}qbe5GbE#zkm#2BL?e(Rs;V ztOfI0mz7gaNi04(Bx5;&HSwjzV2<^UmUQ3D0S+Cwhm<)RlGELwW@A?`B5L?4h_94x zro7%K;?HWNgo16#r;uNRE7%(lt-O8$Wu7EJFy}#Nb8R&*+ieD$ z7C04#qc_(oT9^~{vaXGrgbwXQT*6{9APVU1=^!?6molK|N!feHM?&*1ht2*tFni(? zR>b$mvyb2PLO=eHzz_qr2+7EQrFtFEt^S}5^r@$AZ%#&Z%NPLU5CX8aw6%cvOMmgd zV`Ez6E@>g;84vu}5~K+J^RxEDV*`l2Qu%`)RV}1 zGZjLSfLt*pcGjxFe9NQ@Z4%Pp+iw;O_7^LTG^mcH$W)~sDeE)dKgDK7E{=-8QFg3F zPy`0x`0gooaQMd{XjbyBiXRexyn^gHSDll;fej-~g+_aqXIZs)+k~{wEZ_hNkws(? zgH9$Y%yuCdQ=@}ah$Z{gb*JLoDxdikpz0T9Y2Fl;QiPt{@#mO45RDQ1FLW#_`ReDt z0ZB4d+&Qr)sOjfm6+>xRAwQ7i&1C8@XT;lE;(b{5sX}R$5kCQQx~!7J$T$iEQKSr$ zdD+JgNjy9eF=_KT4g@#>(XBiOcSr>9w9XqP<}i3ys3*K^QXsAJD9%JlKknLpwL7Yr z=VHH$XB@Er;!;>h{qTq{bc?^u_bfcR;fr1K3pS5O1un@#%8Kl5=e@=t#@R9uW1|mU zIYv`?6}JP+E03Lt7Bqwk4oPWqkYLmBjQ~_fTM)gTL1bhm|AP9nF zl<`0Cdw^f&a*}J1)oN7^76o_DQTO4Q?5`uW^WZHCHS8FU&RQb(W%^)y6BtT#B+Y1{ zBdmTBe@hWcfZRvM!>B|j#e9g?d^8`8nj%Fp{uOtk$(NJ~%caV){-ifXLAE7^w|oFa zR6L^jj?j#Z!klQWJLfeVw4j0p;lhJ(8G*fS(JpRgq-%^M_<{mRI*UL$(&@{V(K?Vx zh-{3NE*YLaB+^-u32Ht&adIDt>qxwcM-6SYK%SkhOk$HX&yW~No!&erD8Lu;Niyly z7xSNWtwunPKAwlQG_^rc+ixvrtlyHF6Juqwo<^7(^sH)_9m) zCxeE-jMql^=b&qqxR9=~)g9PiY=P_y%^8^-yKQ*zA!vRwzV!E%XSt!DUcpMxywLH6 zr<6oTcqM=(I8ez1C<%+zyHi3eh)lOeJ0|=kH3U!-FhWqEz!H#MhC5HuKp4^48zxW0 zB6{$cj{O{~yvc&GMeh*%Ie>oMBP&?f@@tM-QAmV5 zba9<&6P9lv3f|UoQe737q2!6h>lFdD2`LVWl{M%dl0_}e^L?b&*eY$%!U+;1ztw;I zRa``_FG-?+B{ux8+BjxWJ8Q0?8Q489ZCImUE7rYjT@E~Z)~tZZ+#?oC86gv?-u@l% z`WV(GNyM-+A{FWQDwL8;n}1L+CskD`HzMx91T*-B0Y$yx9=mM>@)^X458PJ1p8LEt zZlBOe6>w%oxX>nVeMK9FAdBX!&S#u8sojM3438{)tix|)h6!~m2VlF|4WL+Q*@^VH z{JBLOwG9!|JMX72QauRWH0j(Dfkc#{l`PZm>0l^}P5Gmjq{@kM)5_34ep|iT-KzS2 z>+#;E0Qz@0uAln3ERpap!m*qPaNaW!|8o&t z_!x?13vx_8~#qjpuTPvC{KS8cJEZxjMs$IVFQDVoia7cZ!rbvx+yI+lG>2DesP z*oV;tC;CK2>5Dgmi7A7RczciRy(=%cTIJ0Lmx)AmfMNRFA=;{ebseo?j~KX7?qNX? z0E&rKjrjwgcepte&BB?6v}q;Gsj7<2QKe|rE@3szjE0cwxzvd)Vl0efrQNEf3ZMhv zW2mpTa$p$$BOC)R$-s*-{y7}~vTADRw8OomNcPDkB!cciyDx}l}VLgv+8gY4LecIM?#|E?TtkfOa3UmLN=RD z3Ek5Zsr_5mKJJ#=mGqdN$X{6#IwGDC!A|=Vw6|^LEE{V<;w=xgc=q7PLAnA`%f`U- ziJ-|rFr?#Dh%M32EvCkZn>@)tm{=+rQIXYiAo0{!xohoD3Z%$8YjQWK#`u368fj&B z$m{Yf)&&Z-AmIl$6WFQ&nn!;w_oW)~+7b&czkAgGW4Z4yRr0@bv9m2{%D;87d*#J0 zW~#!#T(*bAmKU!)z2z-S*JB?v)|2K4MX1}gYA=kxD`kZ*Q-f>QOj~)9uq0;k6U1rr zOSVMCKDHwXp&XVDZ)BHRNv!#EUaGUnux{|e+zl*$6-3jdWMr!cg5E76hi7_oJNis*GzXh=lBR+aoiCu#qKVoqgqyv%f2AB<+u@U{6HK2~yi#sk!^*;@g3wOCZ#LcJZ%Z zJgTZt_Qwv4|L-2TYwMVx>1U7p2vz*y>YcrRj_CW>gqmDQ2mOGOuL!_&BxcIy>IJh1 zbmN8rT0!Merau-L7U*hfQW*tK<7oW15lIDHsmBR$mCCC}VZA&}=Xy++I@&>FXvTCx z1uT*bU+-9{BelNz7icM?YHpF6f=YY_ejahum97qE%`NlMu}?N=U6L+_YDKRn*A(X^ z8j35Gj)OPhV_#_uj>L%sm725aUvud6dSo2E@N?d{`I?uARe8T899eO~qevYa*$sq` zL_W7Km7d3QaKhKK>ofL;jRznv!P5Ty4vhaK8^c#Lcvc&HxU?vL+c^nzo{Wjy^^q#zT5kV= zNJ<8no6h57vEb9f?oeVk1AP04zs0|pqZNTsC|i8CN;XrVgIZ}B$- z8a))w4>pN624mK0-8(X@$H&qQS%1>;>n|PutJS^#Qpc&gXd8Z^lR;PG4Id|JCk4gg z4+W5~lae7qAms`dIy6_BB~v=56ooT@3QO}-D_!JL>nHiUIctkRi5jaQb{>(Q4QwvqD&pt=&x- z6$P)S|CkCE&N#hzefA_Aj@o1ped5JDhEqG-R_Mi5`)$xnGvehg%#^oRavW%3SKm?i z1yDZ2Vzgc?=L7+5*HnD4|OX4%f24aUMc3ywx zv3)zF63KX*$!H>7yfp$#e2weLGSaU(>qGo`??pReh2+vVvLgi_)r_!xmfU{uB;8`0 zV?Kr$hFZojLAqmrNM3MhL0gh3o4>>NiC;=M&g&DwmyUhR9Z$C&x^UsufUCa>=$^VA`*uh&Ns&peXm=aWii3PSfXw0#;Q`fdp0%vM$x{I0gWEK?;+3`tQ- zJap5ZzhY;2ur3fAYlk3wk4wli@u?NjL3-FPgFC=>z^3H`VRmzW&^{oW94WAPxe}h1 z!5RL5a4cFdRM_XkIg1Ls8;I$N>wZ#dEgf7kZA@QtwED${Qq>>Q`IVNiDEr(6rB ze}raZAQ>j=Hn57nc;il{FYTYv57Ac$@Uj9|uxAJ*Akg2%fy@m-+7adjE*^8@N*`f` z+3BTr*;b07;H{VkUT4toL^%;23>4s0zn6e*6a|%&BjO% z3E0!4U{KTR>trLEv~YXPUv~(oHM*nt=A=LhuE7-%@L0-q)ivub#u?Y2+9~yRK7S~( z2c0F6JFFO;2vuNF(<{0= zl8`6QiT}9U!ib~~OD7fRrm&>A$}#?%w0wKNaejTD#u6DtRP(}XP0hjw)G}AmI6<=>NpcS3@A>d4 zv=~au(>R@F&YsJC`1$S-ElJ)%t#VW}jG=6@7K-Sq&2^B|8Kq^um$q(vS>til1ADOb z@4=i&5YYs5V3tYyiEG}yt}6iRt=l+IVd%hOqY$ZXqxCN_E50B*QMee(Gqx{Ci?$HMjS2uw*Glcr0*(y=9xH62)+ z94b?YkC|AlOr`n8y)bE*aD(!#En8Jv$=!e*9m2XjVL4cpe_l)Ym~j$KJa3)c%(13eY;%iHq75t!l-StxA`!1s~Df zizrKgEkrN+HgL975-0Z-6xXezm-I*r9~dVY3ju8yy4RP)(I|*kBP6!PZ)F}5%|8m8=E& zIe{bO;z;4`2yDm#J9&c@xX>b&?^oiWV&*Qs$$s4FjC<`^3Fk9s)I(E&&=f$VZVvgf zsBO`P?JvFH;D|fF@tldka^m{$CN2I`N07G+ph4=4uiorC;TtuDllv?GF!EaRuB46YSnbViQ2a$)8oh&}xSt1^vCnziqi z$%{9&?j%L`jkF&k>Y{zgPui)FC)Hgb%-i;mrThA`ntK8(Z$13k;0Jzu&o}!{ADOzo z+c_xKt77V|UnBUlJMIInp8~G&{!4fKKkrcf5Av~aJk3Ll!S)nG5A3x9K9j(n(YQ{M z=rZgmf(Nh>O}h4K{P@{L$_vl}@|8%gDl)=t(a?u@$H_rE((~thqk)(wryIs=m1tnP zWLvHi!rKC%UINVoo2rjoQ^~62hSy&Vy#(BryKUN$Fd-pXn({fPy?lq->+b2P4fmoN z%oj|bfq_NI@Qp8piHmvQ7ID&?F(v15`EkHz!a(vfzvRmL4yPgJ9z2Whm*0XeJvHYp ztzP*d?3-+eQF?PlJ>BME-$+ZQ!sq-JvX4!~2@&{T%8I|+`}X;kC6RA@{SqPWU|(e$ z?bK$85pQCtzIS7a*X5}2eeuSyM|6KB1dY2r0k=)n{PD z8lIq^*p}1#EPd3~i@OFnUj{9W0~PF)^gA7I0KZP5zGOe4{Nv8I-#aWKUkZHxiyao% zOxWV!Cr}3BqmGoJ-SmiwpG)$u2(Thif=h_F8EYgI4H%Y-amU8NL7R?Gf>aBHK<#jD z*y;4N`!ne97!Vo)b=fOK2(tr7tp~B&=DgH>ie{#;3=ODQfaUq%5DG8xq&ND+`$4F()G1aE zB6z*y;O{Sw508PG&`XSrNNwSCCr%__B4opEaZp@-#K0HQn=9y2b6Vl{7a#w5hsD43 zG1bBUkEOiP&}&R`XwV@JkU39kJrc9|p%eop@w-9^76USv?+cr?HzE$~fr~_(fg$4s zu-%V^GWf}qg`jYPfzvb`NM_!LlG32WONea@^CJyUIQC7ZZYGc~Ai^!nkiN;fj;{1) zur%K_R=sxVe^>Eprm_5Rr|w@0|>^xQ{=MbNjuL zH}F>-|Lr9s|D}$ZG3);nkbmm&HDRHBG?Bd>ex8 z8Y|vjwM>0>DL+w} zBV3gML0A#1=%}q6)0kt%(%pV?Z%t@fZyfhf6-VJRF%-KY(KQyNbKG?u8kNeHB5e3| z!AR#%9ivw3Vf=L|@ApNIV2!OmuXOx*{JQ87^yf3W`0gnPAnm#K=wwCRLP@3ShYJL~ zYyxAnByB?Ak;+veYih70w?5}K8A%o4!4S^jfxz{7Ig7h}ICZN7P#-1wym)*oD|y>j$kr|Czj`yUWh1c^s0$&{IshFTO!5Og%ZphYcH1s z+?PSZ949yyF+<47U}z+LF%6gA6Xbi^5$rzuBp(9QW?i<9O(UDNFFIb>1o(SvowG-7 zmb+!OxONi*7{W?|Uy1<(oQTJsnB0#Dj%L92qCNwfwyVjI*;=T-|H=Q`58cmj=U3d9 zdO6~E(SfCIcMp>xJSxll(wZLGbE(DC0qropavmWx9!n=sVqUvea^s4VfqVRr4W9`P uy>q7v8v|BRV`?#;oxH6QW}?hvSn9hQ*N^2odbDGOP{~_RMxQh5OaBk*Q4%Ep literal 0 HcmV?d00001 diff --git a/AudioStreamingTests/Codecs/ogg-fixtures/opus-tone-mono-48k.opus b/AudioStreamingTests/Codecs/ogg-fixtures/opus-tone-mono-48k.opus new file mode 100644 index 0000000000000000000000000000000000000000..8cdf3cb09d506e0f0a8ed86aba1f081ec5d07714 GIT binary patch literal 30912 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 [UInt8] { + var bytes = Array(capture.utf8) + bytes.append(contentsOf: [UInt8](repeating: 0, count: 22)) // through byte 25 + bytes.append(UInt8(segments.count)) // byte 26 + bytes.append(contentsOf: segments) // segment table + bytes.append(contentsOf: payload) + return bytes + } + + private func fixtureBytes(_ name: String, _ ext: String) throws -> [UInt8] { + let url = try XCTUnwrap(Bundle.module.url(forResource: "ogg-fixtures/\(name)", withExtension: ext)) + return [UInt8](try Data(contentsOf: url)) + } + + // MARK: - Real files + + func testIdentifiesRealOpusFile() throws { + XCTAssertEqual(OggCodecSniffer.sniff(try fixtureBytes("opus-tone-stereo-48k", "opus")), .opus) + } + + func testIdentifiesRealVorbisFile() throws { + XCTAssertEqual(OggCodecSniffer.sniff(try fixtureBytes("vorbis-tone-stereo-44k", "ogg")), .vorbis) + } + + /// The whole point of the sniffer: these two are both served as `audio/ogg` + /// and must not be confused with one another. + func testDistinguishesOpusFromVorbis() throws { + let opus = try fixtureBytes("opus-tone-stereo-48k", "opus") + let vorbis = try fixtureBytes("vorbis-tone-stereo-44k", "ogg") + XCTAssertEqual(OggCodecSniffer.sniff(opus), .opus) + XCTAssertEqual(OggCodecSniffer.sniff(vorbis), .vorbis) + } + + // MARK: - Synthetic pages + + func testSingleSegmentPages() { + XCTAssertEqual(OggCodecSniffer.sniff(page(segments: [19], payload: opusHead)), .opus) + XCTAssertEqual(OggCodecSniffer.sniff(page(segments: [30], payload: vorbisIdentification)), .vorbis) + } + + /// The reason the payload offset is computed rather than hardcoded: every + /// lacing entry shifts the codec magic by one more byte. + func testMultiSegmentPagesShiftThePayloadOffset() { + XCTAssertEqual(OggCodecSniffer.sniff(page(segments: [255, 255, 255, 19], payload: opusHead)), .opus) + XCTAssertEqual(OggCodecSniffer.sniff(page(segments: [255, 30], payload: vorbisIdentification)), .vorbis) + XCTAssertEqual(OggCodecSniffer.sniff(page(segments: [UInt8](repeating: 1, count: 255), payload: opusHead)), .opus) + } + + func testMaxHeaderLengthCoversTheLargestPossibleFirstPage() { + // 27-byte header + 255-entry lacing table + 8-byte codec magic. + XCTAssertEqual(OggCodecSniffer.maxHeaderLength, 290) + let worstCase = page(segments: [UInt8](repeating: 1, count: 255), payload: opusHead) + XCTAssertLessThanOrEqual(OggCodecSniffer.maxHeaderLength, worstCase.count) + } + + // MARK: - Codecs we do not decode + + func testUnsupportedOggCodecsAreReportedNotGuessed() { + let flac = [UInt8(0x7F)] + Array("FLAC".utf8) + [UInt8](repeating: 0, count: 20) + let speex = Array("Speex ".utf8) + [UInt8](repeating: 0, count: 20) + XCTAssertEqual(OggCodecSniffer.sniff(page(segments: [1], payload: flac)), .unsupported) + XCTAssertEqual(OggCodecSniffer.sniff(page(segments: [1], payload: speex)), .unsupported) + } + + func testNonOggDataIsUnsupported() { + let mp3 = Array("ID3\u{4}".utf8) + [UInt8](repeating: 0, count: 40) + XCTAssertEqual(OggCodecSniffer.sniff(mp3), .unsupported) + } + + // MARK: - Partial input + + /// `nil` means "feed me more" and must never be confused with a decision; + /// answering early would route the stream to the wrong decoder. + func testTruncatedInputAsksForMoreBytes() { + XCTAssertNil(OggCodecSniffer.sniff([UInt8]())) + XCTAssertNil(OggCodecSniffer.sniff([UInt8](repeating: 0, count: 26))) + XCTAssertNil(OggCodecSniffer.sniff(page(segments: [19], payload: []))) + + let full = page(segments: [255, 255, 255, 19], payload: opusHead) + XCTAssertNil(OggCodecSniffer.sniff(Array(full.prefix(29))), "truncated inside the segment table") + XCTAssertNil(OggCodecSniffer.sniff(Array(full.prefix(27 + 4 + 7))), "one byte short of the magic") + } + + func testDataOverloadMatchesArrayOverload() { + let bytes = page(segments: [19], payload: opusHead) + XCTAssertEqual(OggCodecSniffer.sniff(Data(bytes)), OggCodecSniffer.sniff(bytes)) + XCTAssertNil(OggCodecSniffer.sniff(Data())) + } + + /// The sniffer only ever inspects the head of the stream, so handing it a + /// whole file must be no different from handing it the first page. + func testOnlyTheHeadOfTheStreamMatters() throws { + let whole = try fixtureBytes("opus-tone-stereo-48k", "opus") + let head = Array(whole.prefix(OggCodecSniffer.maxHeaderLength)) + XCTAssertEqual(OggCodecSniffer.sniff(whole), OggCodecSniffer.sniff(head)) + } +} diff --git a/Package.swift b/Package.swift index 5249157..299fe0e 100644 --- a/Package.swift +++ b/Package.swift @@ -17,7 +17,9 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/sbooth/ogg-binary-xcframework", exact: "0.1.2"), - .package(url: "https://github.com/sbooth/vorbis-binary-xcframework", exact: "0.1.2") + .package(url: "https://github.com/sbooth/vorbis-binary-xcframework", exact: "0.1.2"), + // Ships libopus AND libopusfile () + .package(url: "https://github.com/sbooth/opus-binary-xcframework", exact: "0.3.0") ], targets: [ // C target for audio codec bridges @@ -25,7 +27,8 @@ let package = Package( name: "AudioCodecs", dependencies: [ .product(name: "ogg", package: "ogg-binary-xcframework"), - .product(name: "vorbis", package: "vorbis-binary-xcframework") + .product(name: "vorbis", package: "vorbis-binary-xcframework"), + .product(name: "opus", package: "opus-binary-xcframework") ], path: "AudioCodecs", publicHeadersPath: "include", @@ -45,7 +48,8 @@ let package = Package( dependencies: [ "AudioCodecs", .product(name: "ogg", package: "ogg-binary-xcframework"), - .product(name: "vorbis", package: "vorbis-binary-xcframework") + .product(name: "vorbis", package: "vorbis-binary-xcframework"), + .product(name: "opus", package: "opus-binary-xcframework") ], path: "AudioStreaming", exclude: ["AudioStreaming.h", "Streaming/OggVorbis", "Info.plist"], @@ -54,13 +58,17 @@ let package = Package( .testTarget( name: "AudioStreamingTests", dependencies: [ - "AudioStreaming" + "AudioStreaming", + // OpusFileBridgeTests drives the C bridge directly. + "AudioCodecs" ], path: "AudioStreamingTests", exclude: ["Info.plist", "Streaming/output"], resources: [ // Test resources for metadata stream processor tests - .copy("Streaming/Metadata Stream Processor/raw-audio-streams") + .copy("Streaming/Metadata Stream Processor/raw-audio-streams"), + // Ogg Opus / Vorbis fixtures for the codec tests + .copy("Codecs/ogg-fixtures") ] ) ] From 0d8af4c6769ffc0aa3595c5de1a5da53fd381c0a Mon Sep 17 00:00:00 2001 From: Scott <485cfbznvv@privaterelay.appleid.com> Date: Tue, 25 Aug 2026 15:00:18 -0700 Subject: [PATCH 2/3] refactor(AudioCodecs): share one ring buffer between the Ogg bridges 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. --- AudioCodecs/OggRingBuffer.c | 134 +++++++++++++++++++++++++++++++++ AudioCodecs/OggRingBuffer.h | 61 +++++++++++++++ AudioCodecs/OpusFileBridge.c | 128 ++++--------------------------- AudioCodecs/VorbisFileBridge.c | 131 ++++---------------------------- 4 files changed, 226 insertions(+), 228 deletions(-) create mode 100644 AudioCodecs/OggRingBuffer.c create mode 100644 AudioCodecs/OggRingBuffer.h diff --git a/AudioCodecs/OggRingBuffer.c b/AudioCodecs/OggRingBuffer.c new file mode 100644 index 0000000..225ee2b --- /dev/null +++ b/AudioCodecs/OggRingBuffer.c @@ -0,0 +1,134 @@ +// +// OggRingBuffer.c +// AudioCodecs +// + +#include "OggRingBuffer.h" + +#include +#include + +size_t ogg_rb_write_locked(struct OggRingBuffer *s, const uint8_t *src, size_t len) { + size_t written = 0; + while (written < len) { + size_t free_space = s->cap - s->size; + if (free_space == 0) break; + size_t chunk = s->cap - s->tail; + if (chunk > len - written) chunk = len - written; + if (chunk > free_space) chunk = free_space; + memcpy(s->buf + s->tail, src + written, chunk); + s->tail = (s->tail + chunk) % s->cap; + s->size += chunk; + written += chunk; + } + return written; +} + +size_t ogg_rb_read_locked(struct OggRingBuffer *s, uint8_t *dst, size_t len) { + size_t read = 0; + while (read < len && s->size > 0) { + size_t chunk = s->cap - s->head; + if (chunk > s->size) chunk = s->size; + if (chunk > len - read) chunk = len - read; + memcpy(dst + read, s->buf + s->head, chunk); + s->head = (s->head + chunk) % s->cap; + s->size -= chunk; + read += chunk; + } + return read; +} + +struct OggRingBuffer *ogg_rb_create(size_t capacity_bytes) { + struct OggRingBuffer *s = (struct OggRingBuffer *)calloc(1, sizeof(struct OggRingBuffer)); + if (!s) return NULL; + s->buf = (uint8_t *)malloc(capacity_bytes); + if (!s->buf) { free(s); return NULL; } + s->cap = capacity_bytes; + pthread_mutex_init(&s->m, NULL); + pthread_cond_init(&s->cv, NULL); + return s; +} + +void ogg_rb_destroy(struct OggRingBuffer *s) { + if (!s) return; + pthread_mutex_destroy(&s->m); + pthread_cond_destroy(&s->cv); + free(s->buf); + free(s); +} + +size_t ogg_rb_available(struct OggRingBuffer *s) { + if (!s) return 0; + pthread_mutex_lock(&s->m); + size_t sz = s->size; + pthread_mutex_unlock(&s->m); + return sz; +} + +void ogg_rb_push(struct OggRingBuffer *s, const uint8_t *data, size_t len) { + if (!s || !data || len == 0) return; + + pthread_mutex_lock(&s->m); + size_t written_total = 0; + while (written_total < len) { + size_t w = ogg_rb_write_locked(s, data + written_total, len - written_total); + written_total += w; + if (written_total < len) { + // Buffer full, wait for consumer to read + pthread_cond_wait(&s->cv, &s->m); + } + } + s->total_pushed += (long long)len; + pthread_cond_broadcast(&s->cv); + pthread_mutex_unlock(&s->m); +} + +void ogg_rb_mark_eof(struct OggRingBuffer *s) { + if (!s) return; + pthread_mutex_lock(&s->m); + s->eof = 1; + pthread_cond_broadcast(&s->cv); + pthread_mutex_unlock(&s->m); +} + +size_t ogg_rb_take(struct OggRingBuffer *s, uint8_t *dst, size_t len) { + if (!s || !dst || len == 0) return 0; + + size_t got = 0; + pthread_mutex_lock(&s->m); + // Read what's available NOW - don't block waiting for more data. + while (got < len && s->size > 0) { + size_t chunk = ogg_rb_read_locked(s, dst + got, len - got); + if (chunk == 0) break; + s->pos += (long long)chunk; + got += chunk; + // Allow producer to push more + pthread_cond_broadcast(&s->cv); + } + pthread_mutex_unlock(&s->m); + return got; +} + +long long ogg_rb_position(struct OggRingBuffer *s) { + if (!s) return -1; + pthread_mutex_lock(&s->m); + long long p = s->pos; + pthread_mutex_unlock(&s->m); + return p; +} + +int ogg_rb_rewind_to(struct OggRingBuffer *s, long long saved_pos) { + if (!s) return 0; + int ok = 0; + pthread_mutex_lock(&s->m); + long long consumed = s->pos - saved_pos; + if (consumed > 0 && (size_t)consumed <= s->cap - s->size) { + s->head = (s->head + s->cap - ((size_t)consumed % s->cap)) % s->cap; + s->size += (size_t)consumed; + s->pos = saved_pos; + ok = 1; + } + pthread_cond_broadcast(&s->cv); + pthread_mutex_unlock(&s->m); + return ok; +} diff --git a/AudioCodecs/OggRingBuffer.h b/AudioCodecs/OggRingBuffer.h new file mode 100644 index 0000000..a8da932 --- /dev/null +++ b/AudioCodecs/OggRingBuffer.h @@ -0,0 +1,61 @@ +// +// OggRingBuffer.h +// AudioCodecs +// +// Shared blocking ring buffer for the Ogg codec bridges. +// +// Both libvorbisfile and libopusfile are pull-based: they call a read callback +// when they want bytes. The streaming layer is push-based. This buffer bridges +// the two, blocking the producer when it fills and handing the consumer +// whatever is available without blocking. +// +// Internal to the AudioCodecs target — not part of the public umbrella header. +// + +#ifndef OGG_RING_BUFFER_H +#define OGG_RING_BUFFER_H + +#include +#include +#include + +// Fields are exposed rather than opaque because the codec bridges' seek +// callbacks reposition the buffer directly. +struct OggRingBuffer { + uint8_t *buf; + size_t cap, head, tail, size; + int eof; + long long pos; // Current read position in the stream + long long total_pushed; // Total bytes pushed into the buffer + pthread_mutex_t m; + pthread_cond_t cv; +}; + +struct OggRingBuffer *ogg_rb_create(size_t capacity_bytes); +void ogg_rb_destroy(struct OggRingBuffer *s); + +// Bytes currently buffered. +size_t ogg_rb_available(struct OggRingBuffer *s); + +// Appends `len` bytes, blocking while the buffer is full. +void ogg_rb_push(struct OggRingBuffer *s, const uint8_t *data, size_t len); + +void ogg_rb_mark_eof(struct OggRingBuffer *s); + +// Consumes up to `len` bytes into `dst` and advances the stream position. +// Returns what was available now; does not wait for more. +size_t ogg_rb_take(struct OggRingBuffer *s, uint8_t *dst, size_t len); + +// Current stream position, for callers that need to rewind later. +long long ogg_rb_position(struct OggRingBuffer *s); + +// Returns the buffer to `saved_pos`, undoing consumption since that point. +// Only valid while no producer has overwritten the reclaimed region. +// Returns 1 if the rewind happened, 0 if it was not safe. +int ogg_rb_rewind_to(struct OggRingBuffer *s, long long saved_pos); + +// Unlocked primitives, for callers already holding the lock. +size_t ogg_rb_write_locked(struct OggRingBuffer *s, const uint8_t *src, size_t len); +size_t ogg_rb_read_locked(struct OggRingBuffer *s, uint8_t *dst, size_t len); + +#endif // OGG_RING_BUFFER_H diff --git a/AudioCodecs/OpusFileBridge.c b/AudioCodecs/OpusFileBridge.c index 896209c..10ca672 100644 --- a/AudioCodecs/OpusFileBridge.c +++ b/AudioCodecs/OpusFileBridge.c @@ -2,9 +2,10 @@ #include #include -#include #include +#include "OggRingBuffer.h" + // Ring buffer + opusfile callback shim. // // Deliberately mirrors VorbisFileBridge.c so the two Ogg codecs behave @@ -12,101 +13,27 @@ // callback signatures: opusfile uses a byte-count read (op_read_func) rather // than libvorbisfile's fread-style (size, nmemb) pair. -struct OFRemoteStream { - uint8_t *buf; - size_t cap, head, tail, size; - int eof; - long long pos; // Current read position in the stream - long long total_pushed; // Total bytes pushed into the buffer - pthread_mutex_t m; - pthread_cond_t cv; -}; - -static size_t rb_write(struct OFRemoteStream *s, const uint8_t *src, size_t len) { - size_t written = 0; - while (written < len) { - size_t free_space = s->cap - s->size; - if (free_space == 0) break; - size_t chunk = s->cap - s->tail; - if (chunk > len - written) chunk = len - written; - if (chunk > free_space) chunk = free_space; - memcpy(s->buf + s->tail, src + written, chunk); - s->tail = (s->tail + chunk) % s->cap; - s->size += chunk; - written += chunk; - } - return written; -} - -static size_t rb_read(struct OFRemoteStream *s, uint8_t *dst, size_t len) { - size_t read = 0; - while (read < len && s->size > 0) { - size_t chunk = s->cap - s->head; - if (chunk > s->size) chunk = s->size; - if (chunk > len - read) chunk = len - read; - memcpy(dst + read, s->buf + s->head, chunk); - s->head = (s->head + chunk) % s->cap; - s->size -= chunk; - read += chunk; - } - return read; -} +// The ring buffer lives in OggRingBuffer.c, shared with VorbisFileBridge.c. +// These wrappers keep the OF* API surface the Swift layer expects. OFStreamRef OFStreamCreate(size_t capacity_bytes) { - struct OFRemoteStream *s = (struct OFRemoteStream *)calloc(1, sizeof(struct OFRemoteStream)); - if (!s) return NULL; - s->buf = (uint8_t *)malloc(capacity_bytes); - if (!s->buf) { free(s); return NULL; } - s->cap = capacity_bytes; - pthread_mutex_init(&s->m, NULL); - pthread_cond_init(&s->cv, NULL); - return s; + return (OFStreamRef)ogg_rb_create(capacity_bytes); } void OFStreamDestroy(OFStreamRef sr) { - struct OFRemoteStream *s = (struct OFRemoteStream *)sr; - if (!s) return; - pthread_mutex_destroy(&s->m); - pthread_cond_destroy(&s->cv); - free(s->buf); - free(s); + ogg_rb_destroy((struct OggRingBuffer *)sr); } size_t OFStreamAvailableBytes(OFStreamRef sr) { - struct OFRemoteStream *s = (struct OFRemoteStream *)sr; - if (!s) return 0; - pthread_mutex_lock(&s->m); - size_t sz = s->size; - pthread_mutex_unlock(&s->m); - return sz; + return ogg_rb_available((struct OggRingBuffer *)sr); } void OFStreamPush(OFStreamRef sr, const uint8_t *data, size_t len) { - struct OFRemoteStream *s = (struct OFRemoteStream *)sr; - if (!s || !data || len == 0) return; - - pthread_mutex_lock(&s->m); - size_t written_total = 0; - while (written_total < len) { - size_t w = rb_write(s, data + written_total, len - written_total); - written_total += w; - if (written_total < len) { - // Buffer full, wait for consumer to read - pthread_cond_wait(&s->cv, &s->m); - } - } - s->total_pushed += (long long)len; - pthread_cond_broadcast(&s->cv); - pthread_mutex_unlock(&s->m); + ogg_rb_push((struct OggRingBuffer *)sr, data, len); } void OFStreamMarkEOF(OFStreamRef sr) { - struct OFRemoteStream *s = (struct OFRemoteStream *)sr; - if (!s) return; - pthread_mutex_lock(&s->m); - s->eof = 1; - pthread_cond_broadcast(&s->cv); - pthread_mutex_unlock(&s->m); + ogg_rb_mark_eof((struct OggRingBuffer *)sr); } // A decoder handle: the opusfile object plus a scratch buffer. @@ -137,26 +64,11 @@ static float *of_scratch(struct OFFile *f, size_t floats_needed) { // op_read_func: returns bytes read, 0 on EOF, <0 on error. // Non-blocking: returns whatever is available now, exactly like the Vorbis shim. static int read_cb(void *stream, unsigned char *ptr, int nbytes) { - struct OFRemoteStream *s = (struct OFRemoteStream *)stream; - if (!s || nbytes <= 0) return 0; - - size_t want_bytes = (size_t)nbytes; - size_t got = 0; - - pthread_mutex_lock(&s->m); - while (got < want_bytes && s->size > 0) { - size_t chunk = rb_read(s, ptr + got, want_bytes - got); - if (chunk == 0) break; - s->pos += (long long)chunk; - got += chunk; - pthread_cond_broadcast(&s->cv); - } - pthread_mutex_unlock(&s->m); - + if (!stream || nbytes <= 0) return 0; // got == 0 with eof set signals EOF to opusfile; got == 0 without eof is a // short read, which opusfile also treats as end-of-stream. The Swift layer // gates calls on availableBytes() to avoid the latter. - return (int)got; + return (int)ogg_rb_take((struct OggRingBuffer *)stream, ptr, (size_t)nbytes); } static int close_cb(void *stream) { @@ -165,13 +77,13 @@ static int close_cb(void *stream) { } static opus_int64 tell_cb(void *stream) { - struct OFRemoteStream *s = (struct OFRemoteStream *)stream; + struct OggRingBuffer *s = (struct OggRingBuffer *)stream; if (!s) return -1; return (opus_int64)s->pos; } int OFOpen(OFStreamRef sr, OFFileRef *out_of) { - struct OFRemoteStream *s = (struct OFRemoteStream *)sr; + struct OggRingBuffer *s = (struct OggRingBuffer *)sr; if (!s || !out_of) return -1; OpusFileCallbacks cbs; @@ -191,22 +103,12 @@ int OFOpen(OFStreamRef sr, OFFileRef *out_of) { // it took, so the delta is the amount to give back. Callers serialise open // against push (OpusFileDecoder holds decoderLock across both), so no // producer can have overwritten the reclaimed region. - pthread_mutex_lock(&s->m); - long long saved_pos = s->pos; - pthread_mutex_unlock(&s->m); + long long saved_pos = ogg_rb_position(s); int err = 0; OggOpusFile *of = op_open_callbacks((void *)s, &cbs, NULL, 0, &err); if (!of) { - pthread_mutex_lock(&s->m); - long long consumed = s->pos - saved_pos; - if (consumed > 0 && (size_t)consumed <= s->cap - s->size) { - s->head = (s->head + s->cap - ((size_t)consumed % s->cap)) % s->cap; - s->size += (size_t)consumed; - s->pos = saved_pos; - } - pthread_cond_broadcast(&s->cv); - pthread_mutex_unlock(&s->m); + ogg_rb_rewind_to(s, saved_pos); return err != 0 ? err : -1; } diff --git a/AudioCodecs/VorbisFileBridge.c b/AudioCodecs/VorbisFileBridge.c index 3645eb5..012ca7e 100644 --- a/AudioCodecs/VorbisFileBridge.c +++ b/AudioCodecs/VorbisFileBridge.c @@ -2,146 +2,47 @@ #include #include -#include #include -struct VFRemoteStream { - uint8_t *buf; - size_t cap, head, tail, size; - int eof; - long long pos; // Current read position in the stream - long long total_pushed; // Total bytes pushed into the buffer - pthread_mutex_t m; - pthread_cond_t cv; -}; +#include "OggRingBuffer.h" -// Simple ring buffer write -static size_t rb_write(struct VFRemoteStream *s, const uint8_t *src, size_t len) { - size_t written = 0; - while (written < len) { - size_t free_space = s->cap - s->size; - if (free_space == 0) break; - size_t chunk = s->cap - s->tail; - if (chunk > len - written) chunk = len - written; - if (chunk > free_space) chunk = free_space; - memcpy(s->buf + s->tail, src + written, chunk); - s->tail = (s->tail + chunk) % s->cap; - s->size += chunk; - written += chunk; - } - return written; -} - -// Simple ring buffer read -static size_t rb_read(struct VFRemoteStream *s, uint8_t *dst, size_t len) { - size_t read = 0; - while (read < len && s->size > 0) { - size_t chunk = s->cap - s->head; - if (chunk > s->size) chunk = s->size; - if (chunk > len - read) chunk = len - read; - memcpy(dst + read, s->buf + s->head, chunk); - s->head = (s->head + chunk) % s->cap; - s->size -= chunk; - read += chunk; - } - return read; -} +// The ring buffer lives in OggRingBuffer.c, shared with OpusFileBridge.c. +// These wrappers keep the VF* API surface the Swift layer expects. -// Create a stream buffer VFStreamRef VFStreamCreate(size_t capacity_bytes) { - struct VFRemoteStream *s = (struct VFRemoteStream *)calloc(1, sizeof(struct VFRemoteStream)); - if (!s) return NULL; - s->buf = (uint8_t *)malloc(capacity_bytes); - if (!s->buf) { free(s); return NULL; } - s->cap = capacity_bytes; - pthread_mutex_init(&s->m, NULL); - pthread_cond_init(&s->cv, NULL); - return s; + return (VFStreamRef)ogg_rb_create(capacity_bytes); } -// Destroy a stream buffer void VFStreamDestroy(VFStreamRef sr) { - struct VFRemoteStream *s = (struct VFRemoteStream *)sr; - if (!s) return; - pthread_mutex_destroy(&s->m); - pthread_cond_destroy(&s->cv); - free(s->buf); - free(s); + ogg_rb_destroy((struct OggRingBuffer *)sr); } -// Get available bytes in the buffer size_t VFStreamAvailableBytes(VFStreamRef sr) { - struct VFRemoteStream *s = (struct VFRemoteStream *)sr; - if (!s) return 0; - pthread_mutex_lock(&s->m); - size_t sz = s->size; - pthread_mutex_unlock(&s->m); - return sz; + return ogg_rb_available((struct OggRingBuffer *)sr); } -// Push data into the stream void VFStreamPush(VFStreamRef sr, const uint8_t *data, size_t len) { - struct VFRemoteStream *s = (struct VFRemoteStream *)sr; - if (!s || !data || len == 0) return; - - pthread_mutex_lock(&s->m); - size_t written_total = 0; - while (written_total < len) { - size_t w = rb_write(s, data + written_total, len - written_total); - written_total += w; - if (written_total < len) { - // Buffer full, wait for consumer to read - pthread_cond_wait(&s->cv, &s->m); - } - } - s->total_pushed += (long long)len; - pthread_cond_broadcast(&s->cv); - pthread_mutex_unlock(&s->m); + ogg_rb_push((struct OggRingBuffer *)sr, data, len); } -// Mark the stream as EOF void VFStreamMarkEOF(VFStreamRef sr) { - struct VFRemoteStream *s = (struct VFRemoteStream *)sr; - if (!s) return; - pthread_mutex_lock(&s->m); - s->eof = 1; - pthread_cond_broadcast(&s->cv); - pthread_mutex_unlock(&s->m); + ogg_rb_mark_eof((struct OggRingBuffer *)sr); } // libvorbisfile callbacks // Read callback for libvorbisfile static size_t read_cb(void *ptr, size_t size, size_t nmemb, void *datasrc) { - struct VFRemoteStream *s = (struct VFRemoteStream *)datasrc; - size_t want_bytes = size * nmemb; - size_t got = 0; - - pthread_mutex_lock(&s->m); - // Read what's available NOW - don't block waiting for more data - while (got < want_bytes && s->size > 0) { - size_t chunk = rb_read(s, (uint8_t *)ptr + got, want_bytes - got); - s->pos += (long long)chunk; - got += chunk; - - if (chunk == 0) break; - // Allow producer to push more - pthread_cond_broadcast(&s->cv); - } - - // If nothing available and EOF, we're done - if (got == 0 && s->eof) { - // Return 0 to signal EOF to libvorbisfile - } - - pthread_mutex_unlock(&s->m); - - return size ? (got / size) : 0; + if (!datasrc || size == 0) return 0; + // Read what's available NOW - don't block waiting for more data. Returning + // 0 signals EOF to libvorbisfile. + size_t got = ogg_rb_take((struct OggRingBuffer *)datasrc, (uint8_t *)ptr, size * nmemb); + return got / size; } // Seek callback - seek within the ring buffer static int seek_cb(void *datasrc, ogg_int64_t offset, int whence) { - struct VFRemoteStream *s = (struct VFRemoteStream *)datasrc; + struct OggRingBuffer *s = (struct OggRingBuffer *)datasrc; if (!s) return -1; pthread_mutex_lock(&s->m); @@ -215,13 +116,13 @@ static int close_cb(void *datasrc) { // Tell callback - return current position static long tell_cb(void *datasrc) { - struct VFRemoteStream *s = (struct VFRemoteStream *)datasrc; + struct OggRingBuffer *s = (struct OggRingBuffer *)datasrc; return (long)s->pos; } // Open a vorbis file using callbacks int VFOpen(VFStreamRef sr, VFFileRef *out_vf) { - struct VFRemoteStream *s = (struct VFRemoteStream *)sr; + struct OggRingBuffer *s = (struct OggRingBuffer *)sr; if (!s || !out_vf) return -1; OggVorbis_File *vf = (OggVorbis_File *)malloc(sizeof(OggVorbis_File)); From d8830d4264e8f1503e6c56b85146a3e46d28684b Mon Sep 17 00:00:00 2001 From: Scott <485cfbznvv@privaterelay.appleid.com> Date: Tue, 25 Aug 2026 15:00:45 -0700 Subject: [PATCH 3/3] ci: build for iOS and run the codec tests under Address Sanitizer 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. --- .github/workflows/swift.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 70bfbfa..fcbbace 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -21,3 +21,33 @@ jobs: run: swift build - name: Run tests run: swift test --parallel + + build-ios: + # swift build compiles the macOS slice only. The codec dependencies ship as + # xcframeworks, so linking against their iOS slices is a separate risk and + # is not covered by the job above. + name: Build for iOS + runs-on: macOS-latest + env: + DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer + steps: + - uses: actions/checkout@v2 + - name: Build for iOS device + run: | + xcodebuild build \ + -scheme AudioStreaming \ + -destination 'generic/platform=iOS' \ + -skipPackagePluginValidation + + test-asan: + # The Ogg codec bridges are C: a ring buffer, raw pointer arithmetic, and + # callbacks driven by libvorbisfile/libopusfile. Address Sanitizer is what + # turns a silent overrun there into a failing test. + name: Test under Address Sanitizer + runs-on: macOS-latest + env: + DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer + steps: + - uses: actions/checkout@v2 + - name: Run tests with ASan + run: swift test --sanitize=address