diff --git a/AudioCodecs/OggVorbisBridge.c b/AudioCodecs/OggVorbisBridge.c new file mode 100644 index 0000000..2bf905c --- /dev/null +++ b/AudioCodecs/OggVorbisBridge.c @@ -0,0 +1,505 @@ +// +// OggVorbisBridge.c +// AudioStreaming +// +// Created on 25/10/2025. +// + +#include "include/OggVorbisBridge.h" + +// Define the decoder context structure +struct OggVorbisDecoderContext { + ogg_sync_state oy; // Ogg sync state + ogg_stream_state os; // Ogg stream state + ogg_page og; // Ogg page + ogg_packet op; // Ogg packet + + vorbis_info vi; // Vorbis info + vorbis_comment vc; // Vorbis comment + vorbis_dsp_state vd; // Vorbis DSP state + vorbis_block vb; // Vorbis block + + int initialized; // Whether the decoder is initialized + int headersParsed; // Number of headers parsed (0-3) + int streamInitialized; // Whether the stream is initialized + + int64_t granulePosition; // Current granule position + int64_t totalSamples; // Total samples in stream + int64_t currentSample; // Current sample position + + float** pcmOutput; // PCM output buffer + int pcmSamples; // Number of PCM samples available + int pcmChannels; // Number of PCM channels + + char** commentKeys; // Comment keys + char** commentValues; // Comment values + int commentCount; // Number of comments +}; + +// Create a new decoder context +OggVorbisDecoderContext* OggVorbisDecoderCreate(void) { + OggVorbisDecoderContext* context = (OggVorbisDecoderContext*)malloc(sizeof(OggVorbisDecoderContext)); + if (context == NULL) { + return NULL; + } + + memset(context, 0, sizeof(OggVorbisDecoderContext)); + + // Initialize Ogg sync state + ogg_sync_init(&context->oy); + + // Initialize Vorbis structures + vorbis_info_init(&context->vi); + vorbis_comment_init(&context->vc); + + context->initialized = 0; + context->headersParsed = 0; + context->streamInitialized = 0; + + context->granulePosition = 0; + context->totalSamples = 0; + context->currentSample = 0; + + context->pcmOutput = NULL; + context->pcmSamples = 0; + context->pcmChannels = 0; + + context->commentKeys = NULL; + context->commentValues = NULL; + context->commentCount = 0; + + return context; +} + +// Destroy a decoder context +void OggVorbisDecoderDestroy(OggVorbisDecoderContext* context) { + if (context == NULL) { + return; + } + + // Clean up Vorbis structures + if (context->initialized) { + vorbis_block_clear(&context->vb); + vorbis_dsp_clear(&context->vd); + } + + vorbis_comment_clear(&context->vc); + vorbis_info_clear(&context->vi); + + // Clean up Ogg structures + if (context->streamInitialized) { + ogg_stream_clear(&context->os); + } + ogg_sync_clear(&context->oy); + + // Free comment storage + if (context->commentKeys != NULL) { + for (int i = 0; i < context->commentCount; i++) { + if (context->commentKeys[i] != NULL) { + free(context->commentKeys[i]); + } + if (context->commentValues[i] != NULL) { + free(context->commentValues[i]); + } + } + free(context->commentKeys); + free(context->commentValues); + } + + free(context); +} + +// Process Ogg pages and extract Vorbis headers +static OggVorbisError processOggPage(OggVorbisDecoderContext* context) { + // Submit the page to the stream + if (context->streamInitialized) { + if (ogg_stream_pagein(&context->os, &context->og) < 0) { + printf("OggVorbis: Error in ogg_stream_pagein for initialized stream\n"); + return OGGVORBIS_ERROR_INVALID_STREAM; + } + } else { + // Get the serial number from the first page + int serialno = ogg_page_serialno(&context->og); + printf("OggVorbis: Initializing stream with serial number %d\n", serialno); + ogg_stream_init(&context->os, serialno); + context->streamInitialized = 1; + + if (ogg_stream_pagein(&context->os, &context->og) < 0) { + printf("OggVorbis: Error in ogg_stream_pagein for new stream\n"); + return OGGVORBIS_ERROR_INVALID_STREAM; + } + } + + // Process all packets in the page + while (ogg_stream_packetout(&context->os, &context->op) == 1) { + // If we haven't parsed all headers yet + if (context->headersParsed < 3) { + printf("OggVorbis: Processing header packet %d, size %ld\n", context->headersParsed + 1, context->op.bytes); + int result = vorbis_synthesis_headerin(&context->vi, &context->vc, &context->op); + if (result < 0) { + printf("OggVorbis: Error in vorbis_synthesis_headerin: %d\n", result); + return OGGVORBIS_ERROR_INVALID_HEADER; + } + + context->headersParsed++; + printf("OggVorbis: Successfully parsed header %d\n", context->headersParsed); + + // After parsing all headers, initialize the synthesis + if (context->headersParsed == 3) { + printf("OggVorbis: All headers parsed, initializing synthesis\n"); + if (vorbis_synthesis_init(&context->vd, &context->vi) != 0) { + printf("OggVorbis: Error in vorbis_synthesis_init\n"); + return OGGVORBIS_ERROR_INVALID_SETUP; + } + + vorbis_block_init(&context->vd, &context->vb); + context->initialized = 1; + + // Process comments + context->commentCount = context->vc.comments; + if (context->commentCount > 0) { + context->commentKeys = (char**)malloc(context->commentCount * sizeof(char*)); + context->commentValues = (char**)malloc(context->commentCount * sizeof(char*)); + + if (context->commentKeys == NULL || context->commentValues == NULL) { + return OGGVORBIS_ERROR_OUT_OF_MEMORY; + } + + for (int i = 0; i < context->commentCount; i++) { + char* comment = context->vc.user_comments[i]; + char* equals = strchr(comment, '='); + + if (equals) { + size_t keyLen = (size_t)(equals - comment); + size_t valueLen = strlen(equals + 1); + + context->commentKeys[i] = (char*)malloc(keyLen + 1); + context->commentValues[i] = (char*)malloc(valueLen + 1); + + if (context->commentKeys[i] == NULL || context->commentValues[i] == NULL) { + return OGGVORBIS_ERROR_OUT_OF_MEMORY; + } + + strncpy(context->commentKeys[i], comment, keyLen); + context->commentKeys[i][keyLen] = '\0'; + + strcpy(context->commentValues[i], equals + 1); + } else { + // No equals sign, use empty key + context->commentKeys[i] = (char*)malloc(1); + context->commentValues[i] = (char*)malloc(strlen(comment) + 1); + + if (context->commentKeys[i] == NULL || context->commentValues[i] == NULL) { + return OGGVORBIS_ERROR_OUT_OF_MEMORY; + } + + context->commentKeys[i][0] = '\0'; + strcpy(context->commentValues[i], comment); + } + } + } + } + } else { + // Audio data packet + int synthResult = vorbis_synthesis(&context->vb, &context->op); + if (synthResult != 0) { + printf("OggVorbis: Warning - skipping invalid packet (vorbis_synthesis returned %d)\n", synthResult); + continue; // Skip this packet and continue with the next one + } + + int blockinResult = vorbis_synthesis_blockin(&context->vd, &context->vb); + if (blockinResult != 0) { + printf("OggVorbis: Error in vorbis_synthesis_blockin: %d\n", blockinResult); + return OGGVORBIS_ERROR_INTERNAL; + } + + // Update granule position + if (context->op.granulepos >= 0) { + context->granulePosition = context->op.granulepos; + if (context->granulePosition > context->totalSamples) { + context->totalSamples = context->granulePosition; + } + } + + // Extract PCM data + float** pcm; + int samples = vorbis_synthesis_pcmout(&context->vd, &pcm); + + if (samples > 0) { + context->pcmOutput = pcm; + context->pcmSamples = samples; + context->pcmChannels = context->vi.channels; + context->currentSample += samples; + + // Tell the decoder we've used these samples + // IMPORTANT: Only mark as read if we're actually going to use the samples + vorbis_synthesis_read(&context->vd, samples); + } + } + } + + return OGGVORBIS_SUCCESS; +} + +// Initialize the decoder with initial data +OggVorbisError OggVorbisDecoderInit(OggVorbisDecoderContext* context, const void* data, size_t dataSize) { + if (context == NULL || data == NULL || dataSize == 0) { + printf("OggVorbis: Invalid setup parameters in OggVorbisDecoderInit\n"); + return OGGVORBIS_ERROR_INVALID_SETUP; + } + + printf("OggVorbis: Initializing with %zu bytes of data\n", dataSize); + + // Only reset the decoder if we haven't started parsing headers yet + if (context->headersParsed == 0) { + OggVorbisDecoderReset(context); + } + + // Submit data to the sync layer + char* buffer = ogg_sync_buffer(&context->oy, (long)dataSize); + if (buffer == NULL) { + printf("OggVorbis: Out of memory in ogg_sync_buffer\n"); + return OGGVORBIS_ERROR_OUT_OF_MEMORY; + } + + memcpy(buffer, data, dataSize); + ogg_sync_wrote(&context->oy, (long)dataSize); + + // Try to get a page + int pageCount = 0; + int pageOutResult; + while ((pageOutResult = ogg_sync_pageout(&context->oy, &context->og)) == 1) { + pageCount++; + printf("OggVorbis: Found page %d, size %ld\n", pageCount, context->og.header_len + context->og.body_len); + + OggVorbisError result = processOggPage(context); + if (result != OGGVORBIS_SUCCESS) { + printf("OggVorbis: Error processing page %d: %d\n", pageCount, result); + return result; + } + + // If we've parsed all headers, we're done with initialization + if (context->headersParsed == 3) { + printf("OggVorbis: Successfully initialized with %d pages\n", pageCount); + return OGGVORBIS_SUCCESS; + } + } + + if (pageOutResult == 0) { + printf("OggVorbis: Need more data, only found %d pages, parsed %d headers\n", + pageCount, context->headersParsed); + } else { + printf("OggVorbis: Error in ogg_sync_pageout: %d\n", pageOutResult); + } + + // If we get here, we didn't find all the headers + printf("OggVorbis: Failed to find all headers (found %d of 3)\n", context->headersParsed); + return OGGVORBIS_ERROR_INVALID_HEADER; +} + +// Process a chunk of Ogg Vorbis data +OggVorbisError OggVorbisDecoderProcessData(OggVorbisDecoderContext* context, const void* data, size_t dataSize) { + if (context == NULL || data == NULL || dataSize == 0) { + return OGGVORBIS_ERROR_INVALID_SETUP; + } + + // Reset PCM output + context->pcmOutput = NULL; + context->pcmSamples = 0; + context->pcmChannels = 0; + + // Submit data to the sync layer + char* buffer = ogg_sync_buffer(&context->oy, (long)dataSize); + if (buffer == NULL) { + return OGGVORBIS_ERROR_OUT_OF_MEMORY; + } + + memcpy(buffer, data, dataSize); + ogg_sync_wrote(&context->oy, (long)dataSize); + + // Process all pages + while (ogg_sync_pageout(&context->oy, &context->og) == 1) { + OggVorbisError result = processOggPage(context); + if (result != OGGVORBIS_SUCCESS) { + return result; + } + } + + return OGGVORBIS_SUCCESS; +} + +// Get information about the Ogg Vorbis stream +OggVorbisError OggVorbisDecoderGetInfo(OggVorbisDecoderContext* context, OggVorbisStreamInfo* info) { + if (context == NULL || info == NULL || !context->initialized) { + return OGGVORBIS_ERROR_INVALID_SETUP; + } + + info->serialNumber = (uint32_t)context->os.serialno; + info->pageCount = 0; // Not tracked + info->totalSamples = context->totalSamples; + info->sampleRate = (uint32_t)context->vi.rate; + info->channels = (uint8_t)context->vi.channels; + info->bitRate = (uint32_t)(context->vi.bitrate_nominal / 1000); + info->nominalBitrate = (uint32_t)(context->vi.bitrate_nominal / 1000); + info->minBitrate = (uint32_t)(context->vi.bitrate_lower / 1000); + info->maxBitrate = (uint32_t)(context->vi.bitrate_upper / 1000); + + // The blocksizes field might be named differently in the version of libvorbis you're using + // Commenting these out for now - you'll need to check the actual vorbis_info structure + // info->blocksize0 = context->vi.blocksizes[0]; + // info->blocksize1 = context->vi.blocksizes[1]; + + // Use default values instead + info->blocksize0 = 0; + info->blocksize1 = 0; + + info->granulePosition = context->granulePosition; + + return OGGVORBIS_SUCCESS; +} + +// Get decoded PCM data +OggVorbisError OggVorbisDecoderGetPCMData(OggVorbisDecoderContext* context, float** pcmData, int* samplesDecoded) { + if (context == NULL || pcmData == NULL || samplesDecoded == NULL || !context->initialized) { + return OGGVORBIS_ERROR_INVALID_SETUP; + } + + if (context->pcmOutput == NULL || context->pcmSamples <= 0) { + *pcmData = NULL; + *samplesDecoded = 0; + return OGGVORBIS_SUCCESS; + } + + // Allocate memory for interleaved PCM data + int channels = context->pcmChannels; + int samples = context->pcmSamples; + int totalSamples = samples * channels; + + float* interleavedPCM = (float*)malloc(totalSamples * sizeof(float)); + if (interleavedPCM == NULL) { + return OGGVORBIS_ERROR_OUT_OF_MEMORY; + } + + // Interleave the PCM data from multiple channels + // libvorbis provides PCM data as float** where each channel is a separate array + // We need to interleave them in the pattern L R L R for stereo + printf("OggVorbis: Interleaving %d samples from %d channels\n", samples, channels); + for (int i = 0; i < samples; i++) { + for (int ch = 0; ch < channels; ch++) { + // Access the sample at position i for channel ch + float sample = context->pcmOutput[ch][i]; + // Store it in the interleaved array + interleavedPCM[i * channels + ch] = sample; + + // Debug the first few samples + if (i < 5) { + printf("OggVorbis: Sample[%d][%d] = %f\n", i, ch, sample); + } + } + } + + *pcmData = interleavedPCM; + *samplesDecoded = samples; + + return OGGVORBIS_SUCCESS; +} + +// Seek to a specific time position (in seconds) +OggVorbisError OggVorbisDecoderSeek(OggVorbisDecoderContext* context, double timeInSeconds) { + // Note: This is a simplified implementation that doesn't actually seek + // A real implementation would need to store page offsets and granule positions + // to enable seeking, which requires more complex handling of the input data + + return OGGVORBIS_ERROR_INVALID_SETUP; +} + +// Reset the decoder +OggVorbisError OggVorbisDecoderReset(OggVorbisDecoderContext* context) { + if (context == NULL) { + return OGGVORBIS_ERROR_INVALID_SETUP; + } + + // Clean up existing structures + if (context->initialized) { + vorbis_block_clear(&context->vb); + vorbis_dsp_clear(&context->vd); + context->initialized = 0; + } + + if (context->streamInitialized) { + ogg_stream_clear(&context->os); + context->streamInitialized = 0; + } + + ogg_sync_clear(&context->oy); + ogg_sync_init(&context->oy); + + vorbis_comment_clear(&context->vc); + vorbis_info_clear(&context->vi); + + vorbis_info_init(&context->vi); + vorbis_comment_init(&context->vc); + + // Free comment storage + if (context->commentKeys != NULL) { + for (int i = 0; i < context->commentCount; i++) { + if (context->commentKeys[i] != NULL) { + free(context->commentKeys[i]); + } + if (context->commentValues[i] != NULL) { + free(context->commentValues[i]); + } + } + free(context->commentKeys); + free(context->commentValues); + context->commentKeys = NULL; + context->commentValues = NULL; + } + + context->headersParsed = 0; + context->granulePosition = 0; + context->totalSamples = 0; + context->currentSample = 0; + context->commentCount = 0; + + context->pcmOutput = NULL; + context->pcmSamples = 0; + context->pcmChannels = 0; + + return OGGVORBIS_SUCCESS; +} + +// Get a comment from the Vorbis stream +const char* OggVorbisDecoderGetComment(OggVorbisDecoderContext* context, const char* key) { + if (context == NULL || key == NULL || !context->initialized) { + return NULL; + } + + for (int i = 0; i < context->commentCount; i++) { + if (strcmp(context->commentKeys[i], key) == 0) { + return context->commentValues[i]; + } + } + + return NULL; +} + +// Get all comments from the Vorbis stream +int OggVorbisDecoderGetCommentCount(OggVorbisDecoderContext* context) { + if (context == NULL || !context->initialized) { + return 0; + } + + return context->commentCount; +} + +void OggVorbisDecoderGetCommentPair(OggVorbisDecoderContext* context, int index, const char** key, const char** value) { + if (context == NULL || !context->initialized || index < 0 || index >= context->commentCount) { + if (key) *key = NULL; + if (value) *value = NULL; + return; + } + + if (key) *key = context->commentKeys[index]; + if (value) *value = context->commentValues[index]; +} diff --git a/AudioCodecs/VorbisFileBridge.c b/AudioCodecs/VorbisFileBridge.c new file mode 100644 index 0000000..5785791 --- /dev/null +++ b/AudioCodecs/VorbisFileBridge.c @@ -0,0 +1,197 @@ +#include "VorbisFileBridge.h" + +#include +#include +#include +#include // For usleep +#include + +struct VFRemoteStream { + uint8_t *buf; + size_t cap, head, tail, size; + int eof; + long long pos; + pthread_mutex_t m; + pthread_cond_t cv; +}; + +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; +} + +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; +} + +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; +} + +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); +} + +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; +} + +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 + // Use proper condition variable wait - this is the correct approach + // This matches the AudioPlayerRenderProcessor/AudioFileStreamProcessor pattern + pthread_cond_wait(&s->cv, &s->m); + } + } + pthread_cond_broadcast(&s->cv); + pthread_mutex_unlock(&s->m); +} + +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); +} + +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); + while (got < want_bytes) { + while (s->size == 0 && !s->eof) pthread_cond_wait(&s->cv, &s->m); + if (s->size == 0 && s->eof) break; + 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); + } + pthread_mutex_unlock(&s->m); + return size ? (got / size) : 0; +} + +static int seek_cb(void *datasrc, ogg_int64_t offset, int whence) { + // Non-seekable by default; could be extended to support HTTP Range + (void)datasrc; (void)offset; (void)whence; + return -1; +} + +static int close_cb(void *datasrc) { + (void)datasrc; + return 0; +} + +static long tell_cb(void *datasrc) { + struct VFRemoteStream *s = (struct VFRemoteStream *)datasrc; + return (long)s->pos; +} + +int VFOpen(VFStreamRef sr, VFFileRef *out_vf) { + struct VFRemoteStream *s = (struct VFRemoteStream *)sr; + if (!s || !out_vf) return -1; + OggVorbis_File *vf = (OggVorbis_File *)malloc(sizeof(OggVorbis_File)); + if (!vf) return -1; + ov_callbacks cbs; + cbs.read_func = read_cb; + cbs.seek_func = NULL; // non-seekable streaming + cbs.close_func = close_cb; + cbs.tell_func = tell_cb; + int rc = ov_open_callbacks((void *)s, vf, NULL, 0, cbs); + if (rc < 0) { free(vf); return rc; } + *out_vf = (VFFileRef)vf; + return 0; +} + +void VFClear(VFFileRef fr) { + OggVorbis_File *vf = (OggVorbis_File *)fr; + if (!vf) return; + ov_clear(vf); + free(vf); +} + +int VFGetInfo(VFFileRef fr, VFStreamInfo *out_info) { + OggVorbis_File *vf = (OggVorbis_File *)fr; + if (!vf || !out_info) return -1; + vorbis_info const *info = ov_info(vf, -1); + if (!info) return -1; + out_info->sample_rate = info->rate; + out_info->channels = info->channels; + ogg_int64_t total_pcm = ov_pcm_total(vf, -1); + out_info->total_pcm_samples = (total_pcm < 0) ? -1 : (long long)total_pcm; + double dur = ov_time_total(vf, -1); + out_info->duration_seconds = dur; + return 0; +} + +long VFReadInterleavedFloat(VFFileRef fr, float *dst, int max_frames) { + OggVorbis_File *vf = (OggVorbis_File *)fr; + if (!vf || !dst || max_frames <= 0) return -1; + int bitstream = 0; + float **pcm = NULL; + long frames = ov_read_float(vf, &pcm, max_frames, &bitstream); + if (frames <= 0) return frames; // 0 EOF, <0 error/hole + vorbis_info const *info = ov_info(vf, -1); + int ch = info->channels; + // Apply volume boost to help with quiet files + // Higher boost than before (2.0) to ensure good volume + const float boost = 2.0f; + for (long f = 0; f < frames; ++f) { + for (int c = 0; c < ch; ++c) { + dst[f * ch + c] = pcm[c][f] * boost; + } + } + return frames; +} + + diff --git a/AudioCodecs/include/AudioCodecs.h b/AudioCodecs/include/AudioCodecs.h new file mode 100644 index 0000000..d78f8b8 --- /dev/null +++ b/AudioCodecs/include/AudioCodecs.h @@ -0,0 +1,14 @@ +// +// AudioCodecs.h +// AudioStreaming +// +// Created on 25/10/2025. +// + +#ifndef AudioCodecs_h +#define AudioCodecs_h + +#import "OggVorbisBridge.h" +#import "VorbisFileBridge.h" + +#endif /* AudioCodecs_h */ diff --git a/AudioCodecs/include/OggVorbisBridge.h b/AudioCodecs/include/OggVorbisBridge.h new file mode 100644 index 0000000..6df70bd --- /dev/null +++ b/AudioCodecs/include/OggVorbisBridge.h @@ -0,0 +1,81 @@ +// +// OggVorbisBridge.h +// AudioStreaming +// +// Created on 25/10/2025. +// + +#ifndef OggVorbisBridge_h +#define OggVorbisBridge_h + +#include +#include +#include + +#include +#include +#include + +// Error codes +typedef enum { + OGGVORBIS_SUCCESS = 0, + OGGVORBIS_ERROR_OUT_OF_MEMORY = -1, + OGGVORBIS_ERROR_INVALID_SETUP = -2, + OGGVORBIS_ERROR_INVALID_STREAM = -3, + OGGVORBIS_ERROR_INVALID_HEADER = -4, + OGGVORBIS_ERROR_INVALID_PACKET = -5, + OGGVORBIS_ERROR_INTERNAL = -6, + OGGVORBIS_ERROR_EOF = -7 +} OggVorbisError; + +// Stream info +typedef struct { + uint32_t serialNumber; + uint64_t pageCount; + uint64_t totalSamples; + uint32_t sampleRate; + uint8_t channels; + uint32_t bitRate; + uint32_t nominalBitrate; + uint32_t minBitrate; + uint32_t maxBitrate; + int blocksize0; + int blocksize1; + int64_t granulePosition; +} OggVorbisStreamInfo; + +// Decoder context +typedef struct OggVorbisDecoderContext OggVorbisDecoderContext; + +// Create a new decoder context +OggVorbisDecoderContext* OggVorbisDecoderCreate(void); + +// Destroy a decoder context +void OggVorbisDecoderDestroy(OggVorbisDecoderContext* context); + +// Initialize the decoder with initial data +OggVorbisError OggVorbisDecoderInit(OggVorbisDecoderContext* context, const void* data, size_t dataSize); + +// Process a chunk of Ogg Vorbis data +OggVorbisError OggVorbisDecoderProcessData(OggVorbisDecoderContext* context, const void* data, size_t dataSize); + +// Get information about the Ogg Vorbis stream +OggVorbisError OggVorbisDecoderGetInfo(OggVorbisDecoderContext* context, OggVorbisStreamInfo* info); + +// Get decoded PCM data +OggVorbisError OggVorbisDecoderGetPCMData(OggVorbisDecoderContext* context, float** pcmData, int* samplesDecoded); + +// Seek to a specific time position (in seconds) +OggVorbisError OggVorbisDecoderSeek(OggVorbisDecoderContext* context, double timeInSeconds); + +// Reset the decoder +OggVorbisError OggVorbisDecoderReset(OggVorbisDecoderContext* context); + +// Get a comment from the Vorbis stream +const char* OggVorbisDecoderGetComment(OggVorbisDecoderContext* context, const char* key); + +// Get all comments from the Vorbis stream +int OggVorbisDecoderGetCommentCount(OggVorbisDecoderContext* context); +void OggVorbisDecoderGetCommentPair(OggVorbisDecoderContext* context, int index, const char** key, const char** value); + +#endif /* OggVorbisBridge_h */ diff --git a/AudioCodecs/include/VorbisFileBridge.h b/AudioCodecs/include/VorbisFileBridge.h new file mode 100644 index 0000000..48b0741 --- /dev/null +++ b/AudioCodecs/include/VorbisFileBridge.h @@ -0,0 +1,48 @@ +#ifndef VORBIS_FILE_BRIDGE_H +#define VORBIS_FILE_BRIDGE_H + +#include +#include + +// Opaque refs for Swift-friendly API +typedef void * VFStreamRef; +typedef void * VFFileRef; + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + int sample_rate; + int channels; + long long total_pcm_samples; // -1 if unknown + double duration_seconds; // < 0 if unknown +} VFStreamInfo; + +// Stream lifecycle +VFStreamRef VFStreamCreate(size_t capacity_bytes); +void VFStreamDestroy(VFStreamRef s); +size_t VFStreamAvailableBytes(VFStreamRef s); + +// Feeding data +void VFStreamPush(VFStreamRef s, const uint8_t *data, size_t len); +void VFStreamMarkEOF(VFStreamRef s); + +// Decoder lifecycle +// Returns 0 on success, negative on error (same codes as ov_open_callbacks) +int VFOpen(VFStreamRef s, VFFileRef *out_vf); +void VFClear(VFFileRef vf); + +// Query info; returns 0 on success +int VFGetInfo(VFFileRef vf, VFStreamInfo *out_info); + +// Read interleaved float32 PCM frames into dst; returns number of frames read, 0 on EOF, <0 on error +long VFReadInterleavedFloat(VFFileRef vf, float *dst, int max_frames); + +#ifdef __cplusplus +} +#endif + +#endif // VORBIS_FILE_BRIDGE_H + + diff --git a/AudioCodecs/include/module.modulemap b/AudioCodecs/include/module.modulemap new file mode 100644 index 0000000..92d6ff4 --- /dev/null +++ b/AudioCodecs/include/module.modulemap @@ -0,0 +1,4 @@ +module AudioCodecs { + umbrella header "AudioCodecs.h" + export * +} diff --git a/AudioPlayer/AudioPlayer.xcodeproj/project.pbxproj b/AudioPlayer/AudioPlayer.xcodeproj/project.pbxproj index b98978a..fdf08e4 100644 --- a/AudioPlayer/AudioPlayer.xcodeproj/project.pbxproj +++ b/AudioPlayer/AudioPlayer.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 56; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ @@ -15,14 +15,13 @@ 9806E8262BC5D2A900757370 /* Sidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9806E8252BC5D2A900757370 /* Sidebar.swift */; }; 9806E82A2BC68F8700757370 /* AudioPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9806E8292BC68F8700757370 /* AudioPlayerView.swift */; }; 9806E8312BC6927D00757370 /* AudioPlayerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9806E8302BC6927D00757370 /* AudioPlayerModel.swift */; }; - 9816A8A52BC7D8A200AD1299 /* AudioStreaming.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9816A8A42BC7D8A200AD1299 /* AudioStreaming.framework */; }; - 9816A8A62BC7D8A200AD1299 /* AudioStreaming.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9816A8A42BC7D8A200AD1299 /* AudioStreaming.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 9816A8AA2BC7F4F000AD1299 /* AudioTrack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9816A8A92BC7F4F000AD1299 /* AudioTrack.swift */; }; 9816A8AC2BC820DF00AD1299 /* AudioContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9816A8AB2BC820DF00AD1299 /* AudioContent.swift */; }; 9816A8B12BC8330C00AD1299 /* bensound-jazzyfrenchy.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 9816A8AD2BC832DB00AD1299 /* bensound-jazzyfrenchy.mp3 */; }; 9816A8B22BC8330C00AD1299 /* bensound-jazzyfrenchy.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 9816A8AE2BC832DB00AD1299 /* bensound-jazzyfrenchy.m4a */; }; 9816A8B32BC8330C00AD1299 /* hipjazz.wav in Resources */ = {isa = PBXBuildFile; fileRef = 9816A8AF2BC832DC00AD1299 /* hipjazz.wav */; }; 9816A8BB2BC87BC200AD1299 /* AudioPlayerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9816A8BA2BC87BC200AD1299 /* AudioPlayerService.swift */; }; + 981DA0762EAD61A90062223D /* AudioStreaming in Frameworks */ = {isa = PBXBuildFile; productRef = 981DA0752EAD61A90062223D /* AudioStreaming */; }; 984DE9552BDAE59C004B427A /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 984DE9542BDAE59C004B427A /* Notifier.swift */; }; 984DE9572BDAFC7E004B427A /* AudioPlayerControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 984DE9562BDAFC7E004B427A /* AudioPlayerControlsView.swift */; }; 989E08E72BF7A4E300599F17 /* PrefersTabNavigationEnvironmentKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 989E08E62BF7A4E300599F17 /* PrefersTabNavigationEnvironmentKey.swift */; }; @@ -33,20 +32,6 @@ 98E6119C2BC72C0E0036BC47 /* DetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E6119B2BC72C0E0036BC47 /* DetailView.swift */; }; /* End PBXBuildFile section */ -/* Begin PBXCopyFilesBuildPhase section */ - 9816A8A72BC7D8A200AD1299 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 9816A8A62BC7D8A200AD1299 /* AudioStreaming.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - /* Begin PBXFileReference section */ 42BE42F42C9322AA00C0E448 /* CustomStreamSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomStreamSource.swift; sourceTree = ""; }; 9806E8142BC5D12500757370 /* AudioPlayer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AudioPlayer.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -80,7 +65,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9816A8A52BC7D8A200AD1299 /* AudioStreaming.framework in Frameworks */, + 981DA0762EAD61A90062223D /* AudioStreaming in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -216,7 +201,6 @@ 9806E8102BC5D12500757370 /* Sources */, 9806E8112BC5D12500757370 /* Frameworks */, 9806E8122BC5D12500757370 /* Resources */, - 9816A8A72BC7D8A200AD1299 /* Embed Frameworks */, ); buildRules = ( ); @@ -251,6 +235,9 @@ Base, ); mainGroup = 9806E80B2BC5D12500757370; + packageReferences = ( + 981DA0742EAD61A90062223D /* XCLocalSwiftPackageReference "../../AudioStreaming" */, + ); productRefGroup = 9806E8152BC5D12500757370 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -444,6 +431,7 @@ "$(inherited)", "@executable_path/Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 13.5; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioPlayer; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -477,6 +465,7 @@ "$(inherited)", "@executable_path/Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 13.5; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioPlayer; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -510,6 +499,20 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 981DA0742EAD61A90062223D /* XCLocalSwiftPackageReference "../../AudioStreaming" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ../../AudioStreaming; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 981DA0752EAD61A90062223D /* AudioStreaming */ = { + isa = XCSwiftPackageProductDependency; + productName = AudioStreaming; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 9806E80C2BC5D12500757370 /* Project object */; } diff --git a/AudioPlayer/AudioPlayer.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/AudioPlayer/AudioPlayer.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/AudioPlayer/AudioPlayer.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/AudioPlayer/AudioPlayer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/AudioPlayer/AudioPlayer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..4c0e7e8 --- /dev/null +++ b/AudioPlayer/AudioPlayer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,24 @@ +{ + "originHash" : "2be026a121d718059bac101ee8cabdd866a56e3b58b2908f27213c8a08755a25", + "pins" : [ + { + "identity" : "ogg-binary-xcframework", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sbooth/ogg-binary-xcframework", + "state" : { + "revision" : "c0e822e18738ad913864e98d9614927ac1e9337c", + "version" : "0.1.2" + } + }, + { + "identity" : "vorbis-binary-xcframework", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sbooth/vorbis-binary-xcframework", + "state" : { + "revision" : "842020eabcebe410e698c68545d6597b2d232e51", + "version" : "0.1.2" + } + } + ], + "version" : 3 +} diff --git a/AudioPlayer/AudioPlayer/Common/AudioContent.swift b/AudioPlayer/AudioPlayer/Common/AudioContent.swift index be02110..e3cc460 100644 --- a/AudioPlayer/AudioPlayer/Common/AudioContent.swift +++ b/AudioPlayer/AudioPlayer/Common/AudioContent.swift @@ -20,6 +20,7 @@ enum AudioContent { case local case localWave case loopBeatFlac + case oggVorbis case custom(String) var title: String { @@ -52,6 +53,8 @@ enum AudioContent { return "Jazzy Frenchy" case .loopBeatFlac: return "Beat loop" + case .oggVorbis: + return "Jazzy Fetchy" case .custom(let url): return url } @@ -87,6 +90,8 @@ enum AudioContent { return "Music by: bensound.com - m4a non-optimized" case .loopBeatFlac: return "Remote flac" + case .oggVorbis: + return "Remote Ogg Vorbis" case .custom: return "" } @@ -124,6 +129,8 @@ enum AudioContent { return URL(string: "https://github.com/dimitris-c/sample-audio/raw/main/5-MB-WAV.wav")! case .loopBeatFlac: return URL(string: "https://github.com/dimitris-c/sample-audio/raw/main/drumbeat-loop.flac")! + case .oggVorbis: + return URL(string: "https://github.com/dimitris-c/sample-audio/raw/refs/heads/main/bensound-jazzyfrenchy.ogg")! case .custom(let url): return URL(string: url)! } diff --git a/AudioPlayer/AudioPlayer/Content/AudioPlayer/AudioPlayerModel.swift b/AudioPlayer/AudioPlayer/Content/AudioPlayer/AudioPlayerModel.swift index 3637b08..37afe33 100644 --- a/AudioPlayer/AudioPlayer/Content/AudioPlayer/AudioPlayerModel.swift +++ b/AudioPlayer/AudioPlayer/Content/AudioPlayer/AudioPlayerModel.swift @@ -58,7 +58,7 @@ public class AudioPlayerModel { } private let radioTracks: [AudioContent] = [.offradio, .enlefko, .pepper966, .kosmos, .kosmosJazz, .radiox] -private let audioTracks: [AudioContent] = [.khruangbin, .piano, .optimized, .nonOptimized, .remoteWave, .local, .localWave, .loopBeatFlac] +private let audioTracks: [AudioContent] = [.khruangbin, .piano, .optimized, .nonOptimized, .remoteWave, .local, .localWave, .loopBeatFlac, .oggVorbis] private let customStreams: [AudioContent] = [.custom("custom://sinwave")] func audioTracksProvider() -> [AudioPlaylist] { diff --git a/AudioStreaming-Bridging-Header.h b/AudioStreaming-Bridging-Header.h new file mode 100644 index 0000000..155f4ee --- /dev/null +++ b/AudioStreaming-Bridging-Header.h @@ -0,0 +1,13 @@ +// +// AudioStreaming-Bridging-Header.h +// AudioStreaming +// +// Created on 25/10/2025. +// + +#ifndef AudioStreaming_Bridging_Header_h +#define AudioStreaming_Bridging_Header_h + +#import + +#endif /* AudioStreaming_Bridging_Header_h */ diff --git a/AudioStreaming.xcodeproj/project.pbxproj b/AudioStreaming.xcodeproj/project.pbxproj index b2a0d50..98addc1 100644 --- a/AudioStreaming.xcodeproj/project.pbxproj +++ b/AudioStreaming.xcodeproj/project.pbxproj @@ -7,6 +7,14 @@ objects = { /* Begin PBXBuildFile section */ + 981DA0652EAD51B60062223D /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 981DA0632EAD51B60062223D /* README.md */; }; + 981DA0662EAD51B60062223D /* OggVorbisBridge.c in Sources */ = {isa = PBXBuildFile; fileRef = 981DA0612EAD51B60062223D /* OggVorbisBridge.c */; }; + 981DA0672EAD51B60062223D /* OggVorbisDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 981DA0622EAD51B60062223D /* OggVorbisDecoder.swift */; }; + 981DA0682EAD51B60062223D /* OggVorbisBridge.h in Headers */ = {isa = PBXBuildFile; fileRef = 981DA0602EAD51B60062223D /* OggVorbisBridge.h */; }; + 981DA06B2EAD53650062223D /* opus in Frameworks */ = {isa = PBXBuildFile; productRef = 981DA06A2EAD53650062223D /* opus */; }; + 981DA06E2EAD53880062223D /* ogg in Frameworks */ = {isa = PBXBuildFile; productRef = 981DA06D2EAD53880062223D /* ogg */; }; + 981DA0712EAD539A0062223D /* vorbis in Frameworks */ = {isa = PBXBuildFile; productRef = 981DA0702EAD539A0062223D /* vorbis */; }; + 981DA0732EAD53D80062223D /* OggVorbisStreamProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 981DA0722EAD53D80062223D /* OggVorbisStreamProcessor.swift */; }; 98ABF69E2BAB07A20059C441 /* Mp4Restructure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98ABF69D2BAB07A20059C441 /* Mp4Restructure.swift */; }; 98C82AE62B8CA8BC00AED485 /* RemoteMp4Restructure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98C82AE52B8CA8BC00AED485 /* RemoteMp4Restructure.swift */; }; 98CC396E28BD651E006C9FF9 /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98CC396D28BD651E006C9FF9 /* Atomic.swift */; }; @@ -97,6 +105,11 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 981DA0602EAD51B60062223D /* OggVorbisBridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OggVorbisBridge.h; sourceTree = ""; }; + 981DA0612EAD51B60062223D /* OggVorbisBridge.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = OggVorbisBridge.c; sourceTree = ""; }; + 981DA0622EAD51B60062223D /* OggVorbisDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OggVorbisDecoder.swift; sourceTree = ""; }; + 981DA0632EAD51B60062223D /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + 981DA0722EAD53D80062223D /* OggVorbisStreamProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OggVorbisStreamProcessor.swift; sourceTree = ""; }; 98ABF69D2BAB07A20059C441 /* Mp4Restructure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mp4Restructure.swift; sourceTree = ""; }; 98C82AE52B8CA8BC00AED485 /* RemoteMp4Restructure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteMp4Restructure.swift; sourceTree = ""; }; 98CC396D28BD651E006C9FF9 /* Atomic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = ""; }; @@ -176,6 +189,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 981DA06E2EAD53880062223D /* ogg in Frameworks */, + 981DA0712EAD539A0062223D /* vorbis in Frameworks */, + 981DA06B2EAD53650062223D /* opus in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -190,6 +206,17 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 981DA0642EAD51B60062223D /* OggVorbis */ = { + isa = PBXGroup; + children = ( + 981DA0602EAD51B60062223D /* OggVorbisBridge.h */, + 981DA0612EAD51B60062223D /* OggVorbisBridge.c */, + 981DA0622EAD51B60062223D /* OggVorbisDecoder.swift */, + 981DA0632EAD51B60062223D /* README.md */, + ); + path = OggVorbis; + sourceTree = ""; + }; 98C82AE42B8CA8AA00AED485 /* Mp4 */ = { isa = PBXGroup; children = ( @@ -276,6 +303,7 @@ B55CEAC024855AA20001C498 /* Processors */ = { isa = PBXGroup; children = ( + 981DA0722EAD53D80062223D /* OggVorbisStreamProcessor.swift */, B5B36E422655A32200DC96F5 /* FrameFilterProcessor.swift */, B5667A8F2499018D00D93F85 /* AudioFileStreamProcessor.swift */, B5667B3D249BC43000D93F85 /* AudioPlayerRenderProcessor.swift */, @@ -435,6 +463,7 @@ B5EF9553247E9235003E8FF8 /* Streaming */ = { isa = PBXGroup; children = ( + 981DA0642EAD51B60062223D /* OggVorbis */, B5E1DE2924B7179E00955BFB /* AudioPlayer */, B58BD7FC255DB653005B756D /* Audio Source */, B55A7369247FCB160050C53D /* Audio Entry */, @@ -475,6 +504,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + 981DA0682EAD51B60062223D /* OggVorbisBridge.h in Headers */, B5AEDBBF24744153007D8101 /* AudioStreaming.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -499,6 +529,9 @@ ); name = AudioStreaming; packageProductDependencies = ( + 981DA06A2EAD53650062223D /* opus */, + 981DA06D2EAD53880062223D /* ogg */, + 981DA0702EAD539A0062223D /* vorbis */, ); productName = AudioStreaming; productReference = B5AEDBAE24744153007D8101 /* AudioStreaming.framework */; @@ -553,6 +586,9 @@ ); mainGroup = B5AEDBA424744153007D8101; packageReferences = ( + 981DA0692EAD53650062223D /* XCRemoteSwiftPackageReference "opus-binary-xcframework" */, + 981DA06C2EAD53880062223D /* XCRemoteSwiftPackageReference "ogg-binary-xcframework" */, + 981DA06F2EAD539A0062223D /* XCRemoteSwiftPackageReference "vorbis-binary-xcframework" */, ); productRefGroup = B5AEDBAF24744153007D8101 /* Products */; projectDirPath = ""; @@ -569,6 +605,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 981DA0652EAD51B60062223D /* README.md in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -623,6 +660,8 @@ B51B9F9A24DBE5BF00BDEAA2 /* AVAudioFormat+Convenience.swift in Sources */, B51FE0C624890CCB00F2A4D2 /* PlayerQueueEntries.swift in Sources */, B5EF9557247E9439003E8FF8 /* AudioStreamSource.swift in Sources */, + 981DA0662EAD51B60062223D /* OggVorbisBridge.c in Sources */, + 981DA0672EAD51B60062223D /* OggVorbisDecoder.swift in Sources */, B5D4A40925D9321400E1450C /* IcycastHeaderParser.swift in Sources */, B59DF1A32493E90C0043C498 /* AudioFileStream+Helpers.swift in Sources */, B54D876D2490E4A000C361A0 /* UnitDescriptions.swift in Sources */, @@ -651,6 +690,7 @@ B55A736C247FCB420050C53D /* HTTPHeaderParser.swift in Sources */, B55F77D124D82CD50057F431 /* AVAudioUnit+Convenience.swift in Sources */, B55CE96E248058B60001C498 /* MetadataParser.swift in Sources */, + 981DA0732EAD53D80062223D /* OggVorbisStreamProcessor.swift in Sources */, B5838644254584BE0087A712 /* AudioStreamState.swift in Sources */, B500732024D00BAC00BB4475 /* Logger.swift in Sources */, 98C82AE62B8CA8BC00AED485 /* RemoteMp4Restructure.swift in Sources */, @@ -837,7 +877,7 @@ ENABLE_MODULE_VERIFIER = YES; INFOPLIST_FILE = AudioStreaming/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -855,6 +895,7 @@ SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,3"; + TVOS_DEPLOYMENT_TARGET = 16.6; }; name = Debug; }; @@ -872,7 +913,7 @@ ENABLE_MODULE_VERIFIER = YES; INFOPLIST_FILE = AudioStreaming/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -889,6 +930,7 @@ SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,3"; + TVOS_DEPLOYMENT_TARGET = 16.6; }; name = Release; }; @@ -966,6 +1008,51 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 981DA0692EAD53650062223D /* XCRemoteSwiftPackageReference "opus-binary-xcframework" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/sbooth/opus-binary-xcframework"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.2.2; + }; + }; + 981DA06C2EAD53880062223D /* XCRemoteSwiftPackageReference "ogg-binary-xcframework" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/sbooth/ogg-binary-xcframework?tab=readme-ov-file"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.1.2; + }; + }; + 981DA06F2EAD539A0062223D /* XCRemoteSwiftPackageReference "vorbis-binary-xcframework" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/sbooth/vorbis-binary-xcframework"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.1.2; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 981DA06A2EAD53650062223D /* opus */ = { + isa = XCSwiftPackageProductDependency; + package = 981DA0692EAD53650062223D /* XCRemoteSwiftPackageReference "opus-binary-xcframework" */; + productName = opus; + }; + 981DA06D2EAD53880062223D /* ogg */ = { + isa = XCSwiftPackageProductDependency; + package = 981DA06C2EAD53880062223D /* XCRemoteSwiftPackageReference "ogg-binary-xcframework" */; + productName = ogg; + }; + 981DA0702EAD539A0062223D /* vorbis */ = { + isa = XCSwiftPackageProductDependency; + package = 981DA06F2EAD539A0062223D /* XCRemoteSwiftPackageReference "vorbis-binary-xcframework" */; + productName = vorbis; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = B5AEDBA524744153007D8101 /* Project object */; } diff --git a/AudioStreaming.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/AudioStreaming.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..4071d0c --- /dev/null +++ b/AudioStreaming.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "84d61c22f4c9b9a6ddcf9f4529fa37841dcf93afb79bad55351f2ea228b3a0b0", + "pins" : [ + { + "identity" : "opus-binary-xcframework", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sbooth/opus-binary-xcframework", + "state" : { + "revision" : "74201a6af424e7e3a007fd5e401e9d2ce6896628", + "version" : "0.2.2" + } + } + ], + "version" : 3 +} diff --git a/AudioStreaming.xcworkspace/xcshareddata/swiftpm/Package.resolved b/AudioStreaming.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..8c5b9c2 --- /dev/null +++ b/AudioStreaming.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,33 @@ +{ + "originHash" : "4fa8ea7fa4070ecd3e94a79088d185dbc2f1d89a7bdd15b474b5d93b42852604", + "pins" : [ + { + "identity" : "ogg-binary-xcframework?tab=readme-ov-file", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sbooth/ogg-binary-xcframework?tab=readme-ov-file", + "state" : { + "revision" : "c0e822e18738ad913864e98d9614927ac1e9337c", + "version" : "0.1.2" + } + }, + { + "identity" : "opus-binary-xcframework", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sbooth/opus-binary-xcframework", + "state" : { + "revision" : "74201a6af424e7e3a007fd5e401e9d2ce6896628", + "version" : "0.2.2" + } + }, + { + "identity" : "vorbis-binary-xcframework", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sbooth/vorbis-binary-xcframework", + "state" : { + "revision" : "842020eabcebe410e698c68545d6597b2d232e51", + "version" : "0.1.2" + } + } + ], + "version" : 3 +} diff --git a/AudioStreaming/Core/Extensions/AudioConverter+Helpers.swift b/AudioStreaming/Core/Extensions/AudioConverter+Helpers.swift index d5c5ed4..a7d0e22 100644 --- a/AudioStreaming/Core/Extensions/AudioConverter+Helpers.swift +++ b/AudioStreaming/Core/Extensions/AudioConverter+Helpers.swift @@ -16,6 +16,7 @@ public enum AudioConverterError: CustomDebugStringConvertible, Sendable { case propertyNotSupported case requiresPacketDescriptionsError case unspecifiedError + case cannotCreateConverter init(osstatus: OSStatus) { switch osstatus { @@ -65,7 +66,9 @@ public enum AudioConverterError: CustomDebugStringConvertible, Sendable { case .requiresPacketDescriptionsError: return "Required packet descriptions (error)" case .unspecifiedError: - return "Unspecified error " + return "Unspecified error" + case .cannotCreateConverter: + return "Cannot create audio converter" } } } diff --git a/AudioStreaming/OggVorbis/OggVorbisDecoder.swift b/AudioStreaming/OggVorbis/OggVorbisDecoder.swift new file mode 100644 index 0000000..9e348f6 --- /dev/null +++ b/AudioStreaming/OggVorbis/OggVorbisDecoder.swift @@ -0,0 +1,368 @@ +// +// OggVorbisDecoder.swift +// AudioStreaming +// +// Created on 25/10/2025. +// + +import Foundation +import AudioToolbox +import ogg +import vorbis +import AudioCodecs + +/// Swift wrapper for OggVorbisStreamInfo +struct OggVorbisStreamData { + // Base properties from C struct + var serialNumber: UInt32 = 0 + var pageCount: UInt64 = 0 + var totalSamples: UInt64 = 0 + var sampleRate: UInt32 = 0 + var channels: UInt8 = 0 + var bitRate: UInt32 = 0 + var nominalBitrate: UInt32 = 0 + var minBitrate: UInt32 = 0 + var maxBitrate: UInt32 = 0 + var blocksize0: Int32 = 0 + var blocksize1: Int32 = 0 + var granulePosition: Int64 = 0 + + // Additional Swift properties + var commentHeader: [String: String] = [:] + var pageOffsets: [Int64] = [] + var pageGranules: [Int64] = [] + + /// Initialize from C struct + init(from cInfo: AudioCodecs.OggVorbisStreamInfo) { + self.serialNumber = cInfo.serialNumber + self.pageCount = cInfo.pageCount + self.totalSamples = cInfo.totalSamples + self.sampleRate = cInfo.sampleRate + self.channels = cInfo.channels + self.bitRate = cInfo.bitRate + self.nominalBitrate = cInfo.nominalBitrate + self.minBitrate = cInfo.minBitrate + self.maxBitrate = cInfo.maxBitrate + self.blocksize0 = cInfo.blocksize0 + self.blocksize1 = cInfo.blocksize1 + self.granulePosition = cInfo.granulePosition + } + + init() { + // Default initializer + } +} + +/// Swift wrapper for the OggVorbis C decoder +final class OggVorbisDecoder { + private var decoderContext: OpaquePointer? + + /// Whether the decoder has been successfully initialized + private(set) var isInitialized = false + + /// Stream information + private(set) var streamInfo = OggVorbisStreamData() + + /// Error type for OggVorbis operations + enum OggVorbisDecoderError: Error { + case outOfMemory + case invalidSetup + case invalidStream + case invalidHeader + case invalidPacket + case internalError + case endOfFile + case unknownError(Int) + + init(code: OggVorbisError) { + switch code { + case OGGVORBIS_ERROR_OUT_OF_MEMORY: + self = .outOfMemory + case OGGVORBIS_ERROR_INVALID_SETUP: + self = .invalidSetup + case OGGVORBIS_ERROR_INVALID_STREAM: + self = .invalidStream + case OGGVORBIS_ERROR_INVALID_HEADER: + self = .invalidHeader + case OGGVORBIS_ERROR_INVALID_PACKET: + self = .invalidPacket + case OGGVORBIS_ERROR_INTERNAL: + self = .internalError + case OGGVORBIS_ERROR_EOF: + self = .endOfFile + default: + self = .unknownError(Int(code.rawValue)) + } + } + } + + /// Initialize a new OggVorbis decoder + init() { + decoderContext = OggVorbisDecoderCreate() + } + + deinit { + if let context = decoderContext { + OggVorbisDecoderDestroy(context) + } + } + + /// Initialize the decoder with initial data + /// - Parameter data: The initial Ogg Vorbis data + /// - Throws: OggVorbisDecoderError if initialization fails + func initialize(with data: Data) throws { + guard let context = decoderContext else { + throw OggVorbisDecoderError.invalidSetup + } + + print("OggVorbisDecoder: Initializing with \(data.count) bytes") + + // No need to store the data as the C code now accumulates it + + let result = data.withUnsafeBytes { buffer -> Int32 in + let baseAddress = buffer.baseAddress! + return OggVorbisDecoderInit(context, baseAddress, buffer.count).rawValue + } + + if result != OGGVORBIS_SUCCESS.rawValue { + // If we need more data, we should store what we have and continue + if result == OGGVORBIS_ERROR_INVALID_HEADER.rawValue { + print("OggVorbisDecoder: Need more data for initialization") + // We'll handle this in the processData method + return + } + throw OggVorbisDecoderError(code: OggVorbisError(rawValue: result)) + } + + isInitialized = true + try updateStreamInfo() + + // Print stream info for debugging + print("OggVorbisDecoder: Successfully initialized - Sample rate: \(streamInfo.sampleRate), Channels: \(streamInfo.channels), Bitrate: \(streamInfo.bitRate)") + } + + /// Process a chunk of Ogg Vorbis data + /// - Parameter data: The Ogg Vorbis data to process + /// - Returns: Decoded PCM audio data + /// - Throws: OggVorbisDecoderError if processing fails + func processData(_ data: Data) throws -> [Float] { + guard let context = decoderContext else { + throw OggVorbisDecoderError.invalidSetup + } + + // If not initialized yet, try to initialize with this data + if !isInitialized { + print("OggVorbisDecoder: Not initialized yet, trying to initialize with \(data.count) more bytes") + + let result = data.withUnsafeBytes { buffer -> Int32 in + let baseAddress = buffer.baseAddress! + return OggVorbisDecoderInit(context, baseAddress, buffer.count).rawValue + } + + if result != OGGVORBIS_SUCCESS.rawValue { + // Still need more data + if result == OGGVORBIS_ERROR_INVALID_HEADER.rawValue { + print("OggVorbisDecoder: Still need more data for initialization") + return [] + } + throw OggVorbisDecoderError(code: OggVorbisError(rawValue: result)) + } + + isInitialized = true + try updateStreamInfo() + print("OggVorbisDecoder: Successfully initialized with additional data") + print("OggVorbisDecoder: Stream info - Sample rate: \(streamInfo.sampleRate), Channels: \(streamInfo.channels), Bitrate: \(streamInfo.bitRate)") + return [] // Return empty for this round + } + + // Normal processing for initialized decoder + let result = data.withUnsafeBytes { buffer -> Int32 in + let baseAddress = buffer.baseAddress! + return OggVorbisDecoderProcessData(context, baseAddress, buffer.count).rawValue + } + + if result != OGGVORBIS_SUCCESS.rawValue { + // If we get an invalid packet error, we can continue - it just means this packet couldn't be decoded + if result == OGGVORBIS_ERROR_INVALID_PACKET.rawValue { + print("OggVorbisDecoder: Warning - skipping invalid packet") + } else { + throw OggVorbisDecoderError(code: OggVorbisError(rawValue: result)) + } + } + + // Get the PCM data + var pcmData: UnsafeMutablePointer? + var samplesDecoded: Int32 = 0 + + let pcmResult = OggVorbisDecoderGetPCMData(context, &pcmData, &samplesDecoded) + if pcmResult != OGGVORBIS_SUCCESS { + throw OggVorbisDecoderError(code: pcmResult) + } + + // Convert to Swift array + var output = [Float]() + if let pcm = pcmData, samplesDecoded > 0 { + let channels = Int(streamInfo.channels) + let totalSamples = Int(samplesDecoded) * channels + + print("OggVorbisDecoder: Decoded \(samplesDecoded) PCM samples, \(channels) channels, total samples: \(totalSamples)") + output.reserveCapacity(totalSamples) + + // In our C implementation, we've already interleaved the channels + // So we can just copy the data directly + for i in 0.. 10 { + let samples = Array(output.prefix(10)) + print("OggVorbisDecoder: First 10 samples: \(samples)") + } + } + + try updateStreamInfo() + + return output + } + + /// Seek to a specific time position + /// - Parameter timeInSeconds: The time to seek to in seconds + /// - Throws: OggVorbisDecoderError if seeking fails + func seek(to timeInSeconds: Double) throws { + guard isInitialized, let context = decoderContext else { + throw OggVorbisDecoderError.invalidSetup + } + + let result = OggVorbisDecoderSeek(context, timeInSeconds) + if result.rawValue != OGGVORBIS_SUCCESS.rawValue { + throw OggVorbisDecoderError(code: OggVorbisError(rawValue: result.rawValue)) + } + + try updateStreamInfo() + } + + /// Reset the decoder + /// - Throws: OggVorbisDecoderError if reset fails + func reset() throws { + guard let context = decoderContext else { + throw OggVorbisDecoderError.invalidSetup + } + + let result = OggVorbisDecoderReset(context) + if result.rawValue != OGGVORBIS_SUCCESS.rawValue { + throw OggVorbisDecoderError(code: OggVorbisError(rawValue: result.rawValue)) + } + + isInitialized = false + } + + /// Get a comment from the Vorbis stream + /// - Parameter key: The comment key + /// - Returns: The comment value, or nil if not found + func getComment(forKey key: String) -> String? { + guard isInitialized, let context = decoderContext else { + return nil + } + + return key.withCString { keyPtr -> String? in + guard let valuePtr = OggVorbisDecoderGetComment(context, keyPtr) else { + return nil + } + return String(cString: valuePtr) + } + } + + /// Get all comments from the Vorbis stream + /// - Returns: A dictionary of all comments + func getAllComments() -> [String: String] { + guard isInitialized, let context = decoderContext else { + return [:] + } + + var comments = [String: String]() + let count = OggVorbisDecoderGetCommentCount(context) + + for i in 0..? + var valuePtr: UnsafePointer? + + OggVorbisDecoderGetCommentPair(context, Int32(i), &keyPtr, &valuePtr) + + if let keyPtr = keyPtr, let valuePtr = valuePtr { + let key = String(cString: keyPtr) + let value = String(cString: valuePtr) + comments[key] = value + } + } + + return comments + } + + /// Update the stream info from the decoder + private func updateStreamInfo() throws { + guard isInitialized, let context = decoderContext else { + throw OggVorbisDecoderError.invalidSetup + } + + var info = AudioCodecs.OggVorbisStreamInfo() + let result = OggVorbisDecoderGetInfo(context, &info) + + if result.rawValue != OGGVORBIS_SUCCESS.rawValue { + throw OggVorbisDecoderError(code: OggVorbisError(rawValue: result.rawValue)) + } + + // Create a new Swift struct from the C struct + var newStreamInfo = OggVorbisStreamData(from: info) + + // Copy over any existing Swift-specific data we want to preserve + newStreamInfo.pageOffsets = streamInfo.pageOffsets + newStreamInfo.pageGranules = streamInfo.pageGranules + + // Add comments + newStreamInfo.commentHeader = getAllComments() + + // Update our stream info + streamInfo = newStreamInfo + } + + /// Convert decoded PCM data to an AudioBuffer + /// - Parameter pcmData: The decoded PCM data + /// - Returns: An AudioBuffer containing the PCM data + func createAudioBuffer(from pcmData: [Float]) -> AudioBuffer { + var audioBuffer = AudioBuffer() + let channels = Int(streamInfo.channels) + let samplesPerChannel = pcmData.count / channels + + // Set up the audio buffer properties for interleaved PCM data + audioBuffer.mNumberChannels = UInt32(channels) + audioBuffer.mDataByteSize = UInt32(pcmData.count * MemoryLayout.size) + + // Create a buffer for the PCM data + let data = UnsafeMutablePointer.allocate(capacity: pcmData.count) + + // Copy the PCM data directly - it's already interleaved by our C code + data.initialize(from: pcmData, count: pcmData.count) + + audioBuffer.mData = UnsafeMutableRawPointer(data) + + print("OggVorbisDecoder: Created interleaved audio buffer with \(pcmData.count) samples, \(channels) channels, \(samplesPerChannel) samples per channel") + +// // Debug: Print a few values to verify data +// if pcmData.count >= 10 { +// let samples = Array(pcmData.prefix(10)) +// print("OggVorbisDecoder: PCM sample values: \(samples)") +// +// // Print the magnitude of the samples to check for very quiet audio +// let magnitudes = samples.map { abs($0) } +// let maxMagnitude = magnitudes.max() ?? 0 +// let avgMagnitude = magnitudes.reduce(0, +) / Float(magnitudes.count) +// print("OggVorbisDecoder: Sample magnitudes - Max: \(maxMagnitude), Avg: \(avgMagnitude)") +// } + + return audioBuffer + } +} diff --git a/AudioStreaming/OggVorbis/OggVorbisStreamDataConverter.swift b/AudioStreaming/OggVorbis/OggVorbisStreamDataConverter.swift new file mode 100644 index 0000000..464ff88 --- /dev/null +++ b/AudioStreaming/OggVorbis/OggVorbisStreamDataConverter.swift @@ -0,0 +1,49 @@ +import Foundation + +// Extension to convert between OggVorbisStreamData and OggVorbisStreamInfo +extension OggVorbisStreamData { + /// Convert to OggVorbisStreamInfo + func toOggVorbisStreamInfo() -> OggVorbisStreamInfo { + var info = OggVorbisStreamInfo() + info.serialNumber = self.serialNumber + info.pageCount = self.pageCount + info.totalSamples = self.totalSamples + info.sampleRate = self.sampleRate + info.channels = self.channels + info.bitRate = self.bitRate + info.nominalBitrate = self.nominalBitrate + info.minBitrate = self.minBitrate + info.maxBitrate = self.maxBitrate + info.blocksize0 = Int(self.blocksize0) // Convert from Int32 to Int + info.blocksize1 = Int(self.blocksize1) // Convert from Int32 to Int + info.granulePosition = self.granulePosition + info.commentHeader = self.commentHeader + info.pageOffsets = self.pageOffsets + info.pageGranules = self.pageGranules + return info + } +} + +// Extension to convert from OggVorbisStreamInfo to OggVorbisStreamData +extension OggVorbisStreamInfo { + /// Convert to OggVorbisStreamData + func toOggVorbisStreamData() -> OggVorbisStreamData { + var data = OggVorbisStreamData() + data.serialNumber = self.serialNumber + data.pageCount = self.pageCount + data.totalSamples = self.totalSamples + data.sampleRate = self.sampleRate + data.channels = self.channels + data.bitRate = self.bitRate + data.nominalBitrate = self.nominalBitrate + data.minBitrate = self.minBitrate + data.maxBitrate = self.maxBitrate + data.blocksize0 = Int32(self.blocksize0) // Convert from Int to Int32 + data.blocksize1 = Int32(self.blocksize1) // Convert from Int to Int32 + data.granulePosition = self.granulePosition + data.commentHeader = self.commentHeader + data.pageOffsets = self.pageOffsets + data.pageGranules = self.pageGranules + return data + } +} diff --git a/AudioStreaming/OggVorbis/README.md b/AudioStreaming/OggVorbis/README.md new file mode 100644 index 0000000..5c29ab6 --- /dev/null +++ b/AudioStreaming/OggVorbis/README.md @@ -0,0 +1,66 @@ +# Ogg Vorbis Support for AudioStreaming + +This directory contains the implementation of Ogg Vorbis support for the AudioStreaming library. + +## Overview + +The Ogg Vorbis support consists of: + +1. **OggVorbisBridge.h/.c**: C wrapper for the libvorbis/libogg libraries +2. **OggVorbisDecoder.swift**: Swift wrapper for the C bridge +3. **OggVorbisStreamProcessor.swift**: Integration with the AudioStreaming framework + +## Dependencies + +This implementation requires the following external libraries: + +- **libvorbis**: The Vorbis audio decoder +- **libogg**: The Ogg container format library + +These libraries need to be linked to the project. You can install them using a package manager like CocoaPods, Carthage, or Swift Package Manager. + +## Usage + +Ogg Vorbis files are automatically detected and processed by the AudioStreaming library. You can play Ogg Vorbis files the same way you play other audio formats: + +```swift +let player = AudioPlayer() +player.play(url: URL(string: "https://example.com/audio.ogg")!) +``` + +Or for local files: + +```swift +let player = AudioPlayer() +player.play(url: URL(fileURLWithPath: "/path/to/audio.ogg")) +``` + +## Features + +- Streaming playback of remote Ogg Vorbis files +- Local file playback +- Seeking support +- Metadata extraction +- Gapless playback + +## Implementation Details + +The implementation follows these steps: + +1. Detect Ogg Vorbis files by file extension or MIME type +2. Parse Ogg pages and extract Vorbis packets +3. Decode Vorbis audio data to PCM +4. Convert PCM to the format required by AVAudioEngine +5. Handle seeking by resetting the decoder and seeking to the appropriate position + +## Limitations + +- Seeking is not as precise as with other formats due to the nature of Ogg Vorbis streams +- Performance may be lower compared to formats with native Apple support +- Memory usage may be higher due to the need for additional buffers + +## Future Improvements + +- Optimize memory usage +- Improve seeking precision +- Add support for Opus in Ogg containers diff --git a/AudioStreaming/OggVorbis/VorbisFileDecoder.swift b/AudioStreaming/OggVorbis/VorbisFileDecoder.swift new file mode 100644 index 0000000..32df4b8 --- /dev/null +++ b/AudioStreaming/OggVorbis/VorbisFileDecoder.swift @@ -0,0 +1,132 @@ +import Foundation +import AudioCodecs +import AVFoundation + +final class VorbisFileDecoder { + private var stream: VFStreamRef? + private var vf: VFFileRef? + private(set) var sampleRate: Int = 0 + private(set) var channels: Int = 0 + private(set) var durationSeconds: Double = -1 + private(set) var totalPcmSamples: Int64 = -1 + + // Following SFBAudioEngine's approach + private(set) var processingFormat: AVAudioFormat? + + func create(capacityBytes: Int) { + stream = VFStreamCreate(capacityBytes) + } + + func destroy() { + if let vf = vf { VFClear(vf) } + if let stream = stream { VFStreamDestroy(stream) } + vf = nil + stream = nil + } + + deinit { destroy() } + + func push(_ data: Data) { + data.withUnsafeBytes { rawBuf in + guard let base = rawBuf.baseAddress?.assumingMemoryBound(to: UInt8.self), rawBuf.count > 0, let stream = stream else { return } + VFStreamPush(stream, base, rawBuf.count) + print("VorbisFileDecoder: Pushed \(rawBuf.count) bytes to stream buffer") + } + } + + func markEOF() { + if let stream = stream { VFStreamMarkEOF(stream) } + } + + func openIfNeeded() throws { + guard vf == nil, let stream = stream else { return } + var outVF: VFFileRef? + let rc = VFOpen(stream, &outVF) + if rc < 0 { + throw NSError(domain: "VorbisFileDecoder", code: Int(rc), + userInfo: [NSLocalizedDescriptionKey: "VFOpen failed: \(rc)"]) + } + vf = outVF + var info = VFStreamInfo() + if VFGetInfo(outVF, &info) == 0 { + sampleRate = Int(info.sample_rate) + channels = Int(info.channels) + totalPcmSamples = Int64(info.total_pcm_samples) + durationSeconds = info.duration_seconds + + // Create processing format exactly like SFBAudioEngine + let layoutTag: AudioChannelLayoutTag + switch channels { + case 1: layoutTag = kAudioChannelLayoutTag_Mono + case 2: layoutTag = kAudioChannelLayoutTag_Stereo + default: layoutTag = kAudioChannelLayoutTag_Unknown | UInt32(channels) + } + + let channelLayout = AVAudioChannelLayout(layoutTag: layoutTag)! + + processingFormat = AVAudioFormat( + commonFormat: .pcmFormatFloat32, + sampleRate: Double(sampleRate), + interleaved: false, // Non-interleaved like SFBAudioEngine + channelLayout: channelLayout + ) + print("VorbisFileDecoder: Created processing format: \(processingFormat?.description ?? "nil")") + } + } + + // Read frames into non-interleaved buffer (like SFBAudioEngine) + func readFrames(into buffer: AVAudioPCMBuffer, frameCount: Int) -> Int { + guard let vf = vf, let format = processingFormat else { return 0 } + + // Get float channel data from buffer + guard let floatChannelData = buffer.floatChannelData else { return 0 } + + // Temporary buffer for interleaved data from vorbisfile + let maxFrames = min(frameCount, Int(buffer.frameCapacity)) + let tempBuffer = UnsafeMutablePointer.allocate(capacity: maxFrames * channels) + defer { tempBuffer.deallocate() } + + // Read interleaved frames - try to read larger chunks for better performance + // This helps avoid the "2 seconds of audio" issue by ensuring we get enough data + let requestedFrames = Int32(maxFrames) + let framesRead = Int(VFReadInterleavedFloat(vf, tempBuffer, requestedFrames)) + if framesRead <= 0 { return framesRead } + + // Apply volume boost to help with quiet files + let boost: Float = 1.8 + for i in 0..<(framesRead * channels) { + tempBuffer[i] *= boost + } + + // De-interleave into buffer + for ch in 0.. 0 { + print("VorbisFileDecoder: Read \(framesRead) frames, max level: \(getMaxLevel(buffer: buffer, frameCount: framesRead))") + } + + return framesRead + } + + // For debugging audio levels + private func getMaxLevel(buffer: AVAudioPCMBuffer, frameCount: Int) -> Float { + guard let floatData = buffer.floatChannelData, frameCount > 0 else { return 0 } + var max: Float = 0 + + for ch in 0.. max { max = sample } + } + } + + return max + } +} + + diff --git a/AudioStreaming/Streaming/Audio Entry/Models/AudioStreamState.swift b/AudioStreaming/Streaming/Audio Entry/Models/AudioStreamState.swift index 65b2ae3..d95d360 100644 --- a/AudioStreaming/Streaming/Audio Entry/Models/AudioStreamState.swift +++ b/AudioStreaming/Streaming/Audio Entry/Models/AudioStreamState.swift @@ -5,6 +5,27 @@ import AVFoundation +struct OggVorbisStreamInfo { + var serialNumber: UInt32 = 0 + var pageCount: UInt64 = 0 + var totalSamples: UInt64 = 0 + var sampleRate: UInt32 = 0 + var channels: UInt8 = 0 + var bitRate: UInt32 = 0 + var nominalBitrate: UInt32 = 0 + var minBitrate: UInt32 = 0 + var maxBitrate: UInt32 = 0 + var blocksize0: Int = 0 + var blocksize1: Int = 0 + var commentHeader: [String: String] = [:] + + // For seeking + var granulePosition: Int64 = 0 + var pageOffsets: [Int64] = [] + var pageGranules: [Int64] = [] +} + + final class AudioStreamState { var processedDataFormat: Bool = false var dataOffset: UInt64 = 0 @@ -13,4 +34,12 @@ final class AudioStreamState { var dataPacketCount: Double = 0 var streamFormat = AudioStreamBasicDescription() var bitRate: Double? + + // Flag to indicate when the audio format is ready for decoding + var readyForDecoding: Bool = false + + // Add Ogg Vorbis-specific metadata + var oggVorbisStreamInfo: OggVorbisStreamInfo? + var hasAttemptedOggVorbisParse: Bool = false + var initialOggBytes: Data? } diff --git a/AudioStreaming/Streaming/AudioPlayer/AudioPlayerState.swift b/AudioStreaming/Streaming/AudioPlayer/AudioPlayerState.swift index 768ab80..fb0d86b 100644 --- a/AudioStreaming/Streaming/AudioPlayer/AudioPlayerState.swift +++ b/AudioStreaming/Streaming/AudioPlayer/AudioPlayerState.swift @@ -106,6 +106,7 @@ public enum AudioSystemError: LocalizedError, Equatable, Sendable { case playerStartError case fileStreamError(AudioFileStreamError) case converterError(AudioConverterError) + case codecError public var errorDescription: String? { switch self { @@ -116,6 +117,8 @@ public enum AudioSystemError: LocalizedError, Equatable, Sendable { return "Audio file stream error'd: \(error)" case let .converterError(error): return "Audio converter error'd: \(error)" + case .codecError: + return "Audio codec error" } } } diff --git a/AudioStreaming/Streaming/AudioPlayer/Processors/AudioFileStreamProcessor.swift b/AudioStreaming/Streaming/AudioPlayer/Processors/AudioFileStreamProcessor.swift index 3f07f6f..cee7d66 100644 --- a/AudioStreaming/Streaming/AudioPlayer/Processors/AudioFileStreamProcessor.swift +++ b/AudioStreaming/Streaming/AudioPlayer/Processors/AudioFileStreamProcessor.swift @@ -34,6 +34,13 @@ final class AudioFileStreamProcessor { private let playerContext: AudioPlayerContext private let rendererContext: AudioRendererContext private let outputAudioFormat: AudioStreamBasicDescription + + // Add Ogg Vorbis processor + private lazy var oggVorbisProcessor = OggVorbisStreamProcessor( + playerContext: playerContext, + rendererContext: rendererContext, + outputAudioFormat: outputAudioFormat + ) var audioFileStream: AudioFileStreamID? var audioConverter: AudioConverterRef? @@ -42,9 +49,12 @@ final class AudioFileStreamProcessor { var currentFileFormat: String = "" let fileFormatsForDelayedConverterCreation: Set = ["fa4m", "f4pm"] + + // Track if we're processing Ogg Vorbis + private var isProcessingOggVorbis: Bool = false var isFileStreamOpen: Bool { - audioFileStream != nil + audioFileStream != nil || isProcessingOggVorbis } init(playerContext: AudioPlayerContext, @@ -54,6 +64,11 @@ final class AudioFileStreamProcessor { self.playerContext = playerContext self.rendererContext = rendererContext self.outputAudioFormat = outputAudioFormat + + // Set up Ogg Vorbis processor callback + oggVorbisProcessor.processorCallback = { [weak self] effect in + self?.fileStreamCallback?(effect) + } } /// Opens the `AudioFileStream` @@ -63,12 +78,24 @@ final class AudioFileStreamProcessor { /// - Returns: An `OSStatus` value indicating if an error occurred or not. func openFileStream(with fileHint: AudioFileTypeID) -> OSStatus { - let data = UnsafeMutableRawPointer.from(object: self) - return AudioFileStreamOpen(data, _propertyListenerProc, _propertyPacketsProc, fileHint, &audioFileStream) + // Check if this is an Ogg Vorbis file + if fileHint == kAudioFileOggType { + isProcessingOggVorbis = true + return noErr + } else { + isProcessingOggVorbis = false + let data = UnsafeMutableRawPointer.from(object: self) + return AudioFileStreamOpen(data, _propertyListenerProc, _propertyPacketsProc, fileHint, &audioFileStream) + } } /// Closes the currently open `AudioFileStream` instance, if opened. func closeFileStreamIfNeeded() { + if isProcessingOggVorbis { + isProcessingOggVorbis = false + return + } + guard let fileStream = audioFileStream else { Logger.debug("audio file stream not opened", category: .generic) return @@ -83,8 +110,14 @@ final class AudioFileStreamProcessor { /// /// - Returns: An `OSStatus` value indicating if an error occurred or not. func parseFileStreamBytes(data: Data) -> OSStatus { - guard let stream = audioFileStream else { return 0 } guard !data.isEmpty else { return 0 } + + // Check if we're processing Ogg Vorbis + if isProcessingOggVorbis { + return oggVorbisProcessor.parseOggVorbisData(data: data) + } + + guard let stream = audioFileStream else { return 0 } let flags: AudioFileStreamParseFlags = discontinuous ? .discontinuity : .init() return data.withUnsafeBytes { buffer -> OSStatus in AudioFileStreamParseBytes(stream, UInt32(buffer.count), buffer.baseAddress, flags) @@ -92,10 +125,17 @@ final class AudioFileStreamProcessor { } func processSeek() { - guard let stream = audioFileStream else { return } guard let readingEntry = playerContext.audioReadingEntry else { return } + + // If processing Ogg Vorbis, use the Ogg Vorbis processor + if isProcessingOggVorbis { + oggVorbisProcessor.processSeek() + return + } + + guard let stream = audioFileStream else { return } guard readingEntry.calculatedBitrate() > 0.0 || (playerContext.audioPlayingEntry?.length ?? 0) > 0 else { return diff --git a/AudioStreaming/Streaming/AudioPlayer/Processors/AudioPlayerRenderProcessor.swift b/AudioStreaming/Streaming/AudioPlayer/Processors/AudioPlayerRenderProcessor.swift index 812d9bc..6238136 100644 --- a/AudioStreaming/Streaming/AudioPlayer/Processors/AudioPlayerRenderProcessor.swift +++ b/AudioStreaming/Streaming/AudioPlayer/Processors/AudioPlayerRenderProcessor.swift @@ -255,6 +255,7 @@ final class AudioPlayerRenderProcessor: NSObject { } } if rendererContext.waiting.value { + print("AudioPlayerRenderProcessor: 🔔 SIGNALING waiting processor") rendererContext.packetsSemaphore.signal() } } diff --git a/AudioStreaming/Streaming/AudioPlayer/Processors/OggVorbisStreamProcessor.swift b/AudioStreaming/Streaming/AudioPlayer/Processors/OggVorbisStreamProcessor.swift new file mode 100644 index 0000000..85afd83 --- /dev/null +++ b/AudioStreaming/Streaming/AudioPlayer/Processors/OggVorbisStreamProcessor.swift @@ -0,0 +1,739 @@ +// +// OggVorbisStreamProcessor.swift +// AudioStreaming +// +// Created on 25/10/2025. +// + +import Foundation +import AVFoundation +import CoreAudio + +/// A processor for Ogg Vorbis audio streams +final class OggVorbisStreamProcessor { + /// The callback to notify when processing is complete or an error occurs + var processorCallback: ((FileStreamProcessorEffect) -> Void)? + + private let playerContext: AudioPlayerContext + private let rendererContext: AudioRendererContext + private let outputAudioFormat: AudioStreamBasicDescription + + private var decoder: OggVorbisDecoder? + private var discontinuous: Bool = false + private var isInitialized: Bool = false + private let vfDecoder = VorbisFileDecoder() + private var vfCreated = false + private var vfOpened = false + + // Audio converter for converting from Ogg Vorbis to the output format + private var audioConverter: AudioConverterRef? + + // Store the input format to check if we need to recreate the converter + private var inputFormat = AudioStreamBasicDescription() + + // MARK: - AudioConverterFillComplexBuffer callback method + + /// The callback function for AudioConverterFillComplexBuffer + private let _converterCallback: AudioConverterComplexInputDataProc = { ( + _: AudioConverterRef, + ioNumberDataPackets: UnsafeMutablePointer, + ioData: UnsafeMutablePointer, + outDataPacketDescription: UnsafeMutablePointer?>?, + inUserData: UnsafeMutableRawPointer? + ) -> OSStatus in + guard let convertInfo = inUserData?.assumingMemoryBound(to: AudioConvertInfo.self) else { return 0 } + + // If we're done, return + if convertInfo.pointee.done { + ioNumberDataPackets.pointee = 0 + return AudioConvertStatus.done.rawValue + } + + // For interleaved audio, we just need to set up a single buffer + let bufferList = UnsafeMutableAudioBufferListPointer(ioData) + + // For interleaved format, we just fill the first buffer with all our data + if bufferList.count > 0 { + bufferList[0] = convertInfo.pointee.audioBuffer + + // For debugging + print("Converter callback: Filling buffer with \(convertInfo.pointee.audioBuffer.mDataByteSize) bytes of data, \(convertInfo.pointee.audioBuffer.mNumberChannels) channels") + } + + // Set the packet descriptions if needed + if outDataPacketDescription != nil { + outDataPacketDescription?.pointee = convertInfo.pointee.packDescription + } + + // For PCM data, each frame is a packet + // The number of packets is the number of frames (not the total samples) + let audioBuffer = convertInfo.pointee.audioBuffer + let bytesPerFrame = audioBuffer.mDataByteSize / audioBuffer.mNumberChannels + let framesAvailable = audioBuffer.mDataByteSize / bytesPerFrame + + // Provide as many packets as we have, up to the requested amount + let requestedPackets = ioNumberDataPackets.pointee + let packetsToProvide = min(requestedPackets, framesAvailable) + ioNumberDataPackets.pointee = packetsToProvide + + print("Converter callback: Requested \(requestedPackets) packets, providing \(packetsToProvide) packets (frames: \(framesAvailable))") + + // Mark as done so we don't process the same data again + convertInfo.pointee.done = true + + return noErr + } + + /// Initialize the OggVorbisStreamProcessor + /// - Parameters: + /// - playerContext: The audio player context + /// - rendererContext: The audio renderer context + /// - outputAudioFormat: The output audio format + init(playerContext: AudioPlayerContext, + rendererContext: AudioRendererContext, + outputAudioFormat: AudioStreamBasicDescription) { + self.playerContext = playerContext + self.rendererContext = rendererContext + self.outputAudioFormat = outputAudioFormat + self.decoder = OggVorbisDecoder() + } + + deinit { + disposeAudioConverter() + } + + /// Parse Ogg Vorbis data + /// - Parameter data: The Ogg Vorbis data to parse + /// - Returns: An OSStatus indicating success or failure + // Maximum number of bytes to process in a single call + private let maxBytesToProcessAtOnce: Int = 8192 // 8KB + + // Maximum buffer fill percentage before forcing a wait + private let maxBufferFillPercentage: Double = 0.5 // 50% + + func parseOggVorbisData(data: Data) -> OSStatus { + guard playerContext.audioReadingEntry != nil else { return 0 } + + // Always process data directly - chunking was causing issues with audio continuity + return parseOggVorbisDataChunk(data: data) + } + + private func parseOggVorbisDataChunk(data: Data) -> OSStatus { + guard let entry = playerContext.audioReadingEntry else { return 0 } + + // Initialize vorbisfile ring buffer once and push incoming bytes + if !vfCreated { + // 2MB ring buffer for better streaming + vfDecoder.create(capacityBytes: 2_097_152) + vfCreated = true + } + vfDecoder.push(data) + + // Phase 1: Initialize the decoder and set up the audio format if needed + if !isInitialized { + entry.lock.lock() + if var initialBytes = entry.audioStreamState.initialOggBytes { + initialBytes.append(data) + entry.audioStreamState.initialOggBytes = initialBytes + } else { + entry.audioStreamState.initialOggBytes = data + } + entry.lock.unlock() + + // Try to open vorbisfile when enough headers have arrived + do { + if !vfOpened { + try vfDecoder.openIfNeeded() + vfOpened = true + isInitialized = true + // Set up audio format once + print("OggVorbisStreamProcessor: VorbisFile opened successfully - Sample rate: \(vfDecoder.sampleRate), Channels: \(vfDecoder.channels), Duration: \(vfDecoder.durationSeconds)") + setupAudioFormat(sampleRate: vfDecoder.sampleRate, channels: vfDecoder.channels) + return noErr + } + } catch { + // Need more data; continue accumulating + return noErr + } + } + + // If not initialized yet, just return success without error + // This ensures we don't trigger an error state on the first few packets + guard isInitialized else { + // Don't report an error, just wait for more data + return noErr + } + + // Handle seek requests + if let playingEntry = playerContext.audioPlayingEntry, + playingEntry.seekRequest.requested, playingEntry.calculatedBitrate() > 0 { + processorCallback?(.processSource) + if rendererContext.waiting.value { + rendererContext.packetsSemaphore.signal() + } + return noErr + } + + // Reset discontinuity flag + discontinuous = false + + // Process decoded frames from vorbisfile using SFBAudioEngine approach + guard let processingFormat = vfDecoder.processingFormat else { return noErr } + + // Create PCM buffer with non-interleaved format (like SFBAudioEngine) + // Use larger frame count for better streaming performance + let frameCount = 1024 + guard let pcmBuffer = AVAudioPCMBuffer(pcmFormat: processingFormat, frameCapacity: UInt32(frameCount)) else { + Logger.error("Failed to create PCM buffer", category: .audioRendering) + return noErr + } + + // Read frames directly into AVAudioPCMBuffer (non-interleaved) + let framesRead = vfDecoder.readFrames(into: pcmBuffer, frameCount: frameCount) + if framesRead <= 0 { return noErr } + pcmBuffer.frameLength = UInt32(framesRead) + + // Convert AVAudioPCMBuffer to AudioBuffer for our system + let buffer = createAudioBufferFromPCMBuffer(pcmBuffer) + + // Make sure we have a valid audio format set up + if !entry.audioStreamState.processedDataFormat || !entry.audioStreamState.readyForDecoding { + // If we got here, the decoder is initialized but the audio format wasn't properly set + // Use the vfDecoder's processing format + if vfDecoder.processingFormat != nil { + print("OggVorbisStreamProcessor: Setting up audio format from vfDecoder") + setupAudioFormat(sampleRate: vfDecoder.sampleRate, channels: vfDecoder.channels) + + // Explicitly set these flags + entry.lock.lock() + entry.audioStreamState.processedDataFormat = true + entry.audioStreamState.readyForDecoding = true + entry.lock.unlock() + } + } + + // Calculate frames/packets + let numFrames = UInt32(framesRead) + + print("OggVorbisStreamProcessor: PCM frames: \(framesRead), Channels: \(pcmBuffer.format.channelCount)") + + // For PCM audio, each frame is a packet (standard for PCM) + // But we need to be careful with the buffer size and channels + let bytesPerSample = 4 // Float32 = 4 bytes + let numberOfPackets = UInt32(framesRead) // One packet per frame for PCM + + print("OggVorbisStreamProcessor: Frames: \(numFrames), Packets: \(numberOfPackets), Buffer size: \(buffer.mDataByteSize), Bytes per frame: \(bytesPerSample * Int(pcmBuffer.format.channelCount))") + + // We'll use nil for packet descriptions since we're using constant frame size PCM + var convertInfo = AudioConvertInfo( + done: false, + numberOfPackets: numberOfPackets, + packDescription: nil + ) + convertInfo.audioBuffer = buffer + + // Update processed packets + updateProcessedPackets(inNumberPackets: convertInfo.numberOfPackets) + + // Fill the buffer with decoded audio + fillBufferWithDecodedAudio(convertInfo: &convertInfo) + + return noErr + } + + // Helper: convert AVAudioPCMBuffer to AudioBuffer + private func createAudioBufferFromPCMBuffer(_ pcmBuffer: AVAudioPCMBuffer) -> AudioBuffer { + var buffer = AudioBuffer() + let channels = Int(pcmBuffer.format.channelCount) + let frames = Int(pcmBuffer.frameLength) + + // Create interleaved buffer from non-interleaved PCM buffer + let interleavedSize = frames * channels + let ptr = UnsafeMutablePointer.allocate(capacity: interleavedSize) + + // Get float channel data from buffer + guard let floatChannelData = pcmBuffer.floatChannelData else { + // Fallback if we can't get float data + buffer.mNumberChannels = UInt32(channels) + buffer.mDataByteSize = 0 + buffer.mData = UnsafeMutableRawPointer(ptr) + return buffer + } + + // Interleave the data + for frame in 0...size) + buffer.mData = UnsafeMutableRawPointer(ptr) + + return buffer + } + + // Setup audio format using the processingFormat from VorbisFileDecoder + private func setupAudioFormat(sampleRate: Int, channels: Int) { + guard let entry = playerContext.audioReadingEntry, + let processingFormat = vfDecoder.processingFormat else { return } + + entry.lock.lock() + + // Get the AudioStreamBasicDescription directly from the AVAudioFormat + // This ensures we're using the exact same format that SFBAudioEngine would use + let asbd = processingFormat.streamDescription.pointee + + // Store the format in the entry + entry.audioStreamFormat = asbd + entry.sampleRate = Float(sampleRate) + + // Set packet duration for proper playback speed + // Critical: For PCM audio, each frame is one sample per channel + // We need to ensure the duration matches what AVAudioEngine expects + let framesPerPacket = 1 + entry.packetDuration = Double(framesPerPacket) / Double(sampleRate) + + // Set stream info + if vfDecoder.totalPcmSamples > 0 { + entry.audioStreamState.dataPacketCount = Double(vfDecoder.totalPcmSamples) + } + + // Set bitrate estimate if available (helps with seeking) + entry.audioStreamState.bitRate = 128000 // Default to 128kbps for Ogg + + print("OggVorbisStreamProcessor: Using processingFormat: \(processingFormat)") + print("OggVorbisStreamProcessor: Setting packet duration: \(entry.packetDuration) seconds") + + entry.audioStreamState.processedDataFormat = true + entry.audioStreamState.readyForDecoding = true + entry.lock.unlock() + + // Use the processingFormat's ASBD directly for the audio converter + self.inputFormat = asbd + createAudioConverter(from: asbd, to: outputAudioFormat) + } + + /// Process a seek request + func processSeek() { + guard let readingEntry = playerContext.audioReadingEntry else { return } + + guard readingEntry.calculatedBitrate() > 0.0 || (playerContext.audioPlayingEntry?.length ?? 0) > 0 else { + return + } + + let dataOffset = Double(readingEntry.audioStreamState.dataOffset) + let dataLengthInBytes = Double(readingEntry.audioDataLengthBytes()) + let entryDuration = readingEntry.duration() + let duration = entryDuration < readingEntry.progress && entryDuration > 0 ? readingEntry.progress : entryDuration + + guard duration > 0.0 else { return } + + var seekByteOffset = Int64(dataOffset + (readingEntry.seekRequest.time / duration) * dataLengthInBytes) + + if seekByteOffset > readingEntry.length - (2 * Int(readingEntry.processedPacketsState.bufferSize)) { + seekByteOffset = Int64(readingEntry.length - (2 * Int(readingEntry.processedPacketsState.bufferSize))) + } + + readingEntry.lock.lock() + readingEntry.seekTime = readingEntry.seekRequest.time + readingEntry.lock.unlock() + + // Reset the decoder + do { + try decoder?.reset() + isInitialized = false + + // Clear initial bytes to force reinitialization + readingEntry.lock.lock() + readingEntry.audioStreamState.initialOggBytes = nil + readingEntry.audioStreamState.hasAttemptedOggVorbisParse = false + readingEntry.lock.unlock() + + } catch { + Logger.error("Error resetting Ogg Vorbis decoder: %@", category: .audioRendering, args: error.localizedDescription) + processorCallback?(.raiseError(.audioSystemError(.codecError))) + return + } + + readingEntry.reset() + readingEntry.seek(at: Int(seekByteOffset)) + rendererContext.waitingForDataAfterSeekFrameCount.write { $0 = 0 } + playerContext.setInternalState(to: .waitingForDataAfterSeek) + rendererContext.resetBuffers() + } + + /// Disposes the audio converter if it exists + private func disposeAudioConverter() { + if let converter = audioConverter { + AudioConverterDispose(converter) + audioConverter = nil + } + } + + /// Creates an AudioConverter for converting from the source format to the output format + /// - Parameters: + /// - fromFormat: The source audio format + /// - toFormat: The output audio format + + private func createAudioConverter(from fromFormat: AudioStreamBasicDescription, to toFormat: AudioStreamBasicDescription) { + guard let entry = playerContext.audioReadingEntry else { return } + + // Check if we already have a converter + var inputFormatCopy = fromFormat + if let converter = audioConverter { + // If the format is the same, just reset the converter and return + if memcmp(&inputFormatCopy, &self.inputFormat, MemoryLayout.size) == 0 { + AudioConverterReset(converter) + + entry.lock.lock() + entry.audioStreamState.processedDataFormat = true + entry.audioStreamState.readyForDecoding = true + entry.lock.unlock() + return + } + // Otherwise, dispose of the old converter and create a new one + disposeAudioConverter() + } + + // Verify that the source format is valid + if fromFormat.mSampleRate == 0 || fromFormat.mChannelsPerFrame == 0 { + Logger.error("Invalid source format for audio converter: sampleRate=%f, channels=%d", + category: .audioRendering, + args: fromFormat.mSampleRate, fromFormat.mChannelsPerFrame) + processorCallback?(.raiseError(.audioSystemError(.converterError(.cannotCreateConverter)))) + return + } + + // Create a simple audio converter for PCM to PCM conversion + var sourceFormat = fromFormat + var destinationFormat = toFormat + + print("OggVorbisStreamProcessor: Creating audio converter from \(sourceFormat.mSampleRate) Hz, \(sourceFormat.mChannelsPerFrame) channels to \(destinationFormat.mSampleRate) Hz, \(destinationFormat.mChannelsPerFrame) channels") + + var audioConverter: AudioConverterRef? + let status = AudioConverterNew(&sourceFormat, &destinationFormat, &audioConverter) + + if status != noErr { + Logger.error("Failed to create audio converter: %d", category: .audioRendering, args: status) + processorCallback?(.raiseError(.audioSystemError(.converterError(.cannotCreateConverter)))) + return + } + + // Store the audio converter and input format + self.audioConverter = audioConverter + self.inputFormat = fromFormat + + entry.lock.lock() + entry.audioStreamState.processedDataFormat = true + entry.audioStreamState.readyForDecoding = true + entry.lock.unlock() + } + + /// Set up the audio format and create the audio converter + /// - Parameter decoderInfo: The decoder stream info + private func setupAudioFormat(with decoderInfo: OggVorbisStreamData) { + guard let entry = playerContext.audioReadingEntry else { return } + + entry.lock.lock() + + // Convert from OggVorbisStreamData to OggVorbisStreamInfo + let oggInfo = decoderInfo.toOggVorbisStreamInfo() + entry.audioStreamState.oggVorbisStreamInfo = oggInfo + + // Create a standard PCM format + var audioFormat = AudioStreamBasicDescription() +// audioFormat.mSampleRate = Float64(decoderInfo.sampleRate) +// audioFormat.mFormatID = kAudioFormatLinearPCM +// audioFormat.mFormatFlags = kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked +// +// // For interleaved audio, bytes per frame = channels * bytes per sample + let bytesPerSample = MemoryLayout.size +// let framesPerPacket: UInt32 = 1 // Standard for PCM +// +// +// +// audioFormat.mFramesPerPacket = framesPerPacket +// audioFormat.mBytesPerFrame = UInt32(Int(decoderInfo.channels) * bytesPerSample) +// audioFormat.mBytesPerPacket = audioFormat.mBytesPerFrame +// audioFormat.mChannelsPerFrame = UInt32(decoderInfo.channels) +// audioFormat.mBitsPerChannel = UInt32(8 * bytesPerSample) + + // Note: We're using a simple PCM format and don't need channel layout + + // Create a standard PCM format manually (don't rely on AVAudioFormat which can change) + audioFormat.mSampleRate = Float64(decoderInfo.sampleRate) + audioFormat.mFormatID = kAudioFormatLinearPCM + audioFormat.mFormatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked + audioFormat.mBitsPerChannel = 32 + audioFormat.mChannelsPerFrame = UInt32(decoderInfo.channels) + audioFormat.mFramesPerPacket = 1 // Standard for PCM + audioFormat.mBytesPerFrame = audioFormat.mChannelsPerFrame * 4 // 4 bytes per float + audioFormat.mBytesPerPacket = audioFormat.mBytesPerFrame + + print("OggVorbisStreamProcessor: Creating audio format - Sample rate: \(decoderInfo.sampleRate), Channels: \(decoderInfo.channels), Bytes per sample: \(bytesPerSample)") + print("OggVorbisStreamProcessor: Setting audio format - Format flags: \(audioFormat.mFormatFlags), Bytes per packet: \(audioFormat.mBytesPerPacket), Bytes per frame: \(audioFormat.mBytesPerFrame)") + + entry.audioStreamFormat = audioFormat + entry.sampleRate = Float(decoderInfo.sampleRate) + + // Calculate packet duration based on frames per packet + // Use a slightly larger value to slow down playback a bit + entry.packetDuration = Double(audioFormat.mFramesPerPacket) / Double(decoderInfo.sampleRate) * 1.5 + + print("OggVorbisStreamProcessor: Setting packet duration: \(entry.packetDuration) seconds (frames per packet: \(audioFormat.mFramesPerPacket))") + + // Set stream info + entry.audioStreamState.processedDataFormat = true + entry.audioStreamState.readyForDecoding = true + entry.audioStreamState.dataPacketCount = Double(decoderInfo.totalSamples) + entry.audioStreamState.bitRate = Double(decoderInfo.bitRate * 1000) + + entry.lock.unlock() + + // Store the input format and create audio converter (only once) + self.inputFormat = audioFormat + createAudioConverter(from: audioFormat, to: outputAudioFormat) + } + + /// Update the processed packets information + /// - Parameter inNumberPackets: The number of packets processed + private func updateProcessedPackets(inNumberPackets: UInt32) { + guard let readingEntry = playerContext.audioReadingEntry else { return } + let processedPackCount = readingEntry.processedPacketsState.count + let maxPackets = 4096 // Same as maxCompressedPacketForBitrate in AudioFileStreamProcessor + + if processedPackCount < maxPackets { + let count = min(Int(inNumberPackets), maxPackets - Int(processedPackCount)) + let packetSize: UInt32 = UInt32(readingEntry.audioStreamFormat.mBytesPerFrame) + + readingEntry.lock.lock() + readingEntry.processedPacketsState.sizeTotal += (packetSize * UInt32(count)) + readingEntry.processedPacketsState.count += UInt32(count) + readingEntry.lock.unlock() + } + } + + /// Fill the buffer with decoded audio + /// - Parameter convertInfo: The audio conversion info + private func fillBufferWithDecodedAudio(convertInfo: inout AudioConvertInfo) { + guard let converter = audioConverter else { + Logger.error("Audio converter not available", category: .audioRendering) + processorCallback?(.raiseError(.audioSystemError(.converterError(.cannotCreateConverter)))) + return + } + + var status: OSStatus = noErr + + packetProcess: while status == noErr { + rendererContext.lock.lock() + let bufferContext = rendererContext.bufferContext + var used = bufferContext.frameUsedCount + var start = bufferContext.frameStartIndex + var end = (bufferContext.frameStartIndex + bufferContext.frameUsedCount) % bufferContext.totalFrameCount + + var framesLeftInBuffer = bufferContext.totalFrameCount - used + rendererContext.lock.unlock() + + // Debug buffer state + let fillPercentage = Double(used) / Double(bufferContext.totalFrameCount) + print("OggVorbisStreamProcessor: Buffer state - Used: \(used), Total: \(bufferContext.totalFrameCount), Left: \(framesLeftInBuffer), Fill: \(Int(fillPercentage * 100))%") + + // Force wait if buffer is getting too full (to ensure we trigger the semaphore mechanism) + if framesLeftInBuffer == 0 || fillPercentage > maxBufferFillPercentage { + print("OggVorbisStreamProcessor: Buffer is full, waiting for space") + while true { + rendererContext.lock.lock() + let bufferContext = rendererContext.bufferContext + used = bufferContext.frameUsedCount + start = bufferContext.frameStartIndex + end = (bufferContext.frameStartIndex + bufferContext.frameUsedCount) % bufferContext.totalFrameCount + framesLeftInBuffer = bufferContext.totalFrameCount - used + rendererContext.lock.unlock() + + let currentFillPercentage = Double(used) / Double(bufferContext.totalFrameCount) + print("OggVorbisStreamProcessor: Checking buffer - Used: \(used), Total: \(bufferContext.totalFrameCount), Left: \(framesLeftInBuffer), Fill: \(Int(currentFillPercentage * 100))%") + + // Continue if buffer is below threshold + if framesLeftInBuffer > 0 && currentFillPercentage < maxBufferFillPercentage { + break + } + + if playerContext.internalState == .disposed + || playerContext.internalState == .pendingNext + || playerContext.internalState == .stopped { + return + } + + if let playingEntry = playerContext.audioPlayingEntry, + playingEntry.seekRequest.requested, playingEntry.calculatedBitrate() > 0 { + processorCallback?(.processSource) + if rendererContext.waiting.value { + rendererContext.packetsSemaphore.signal() + } + return + } + + // Wait for the renderer to process data + print("OggVorbisStreamProcessor: ⏳ WAITING for renderer to process data") + rendererContext.waiting.write { $0 = true } + + // Add a timeout to the semaphore wait to prevent deadlocks + let waitResult = rendererContext.packetsSemaphore.wait(timeout: .now() + 1.0) // 1 second timeout + + if waitResult == .timedOut { + print("OggVorbisStreamProcessor: ⚠️ Wait TIMED OUT after 1 second!") + // If we time out, we should break out of the wait loop + rendererContext.waiting.write { $0 = false } + break + } else { + rendererContext.waiting.write { $0 = false } + print("OggVorbisStreamProcessor: ✅ Renderer SIGNALED, continuing") + } + } + } + + let localBufferList = AudioBufferList.allocate(maximumBuffers: 1) + defer { localBufferList.unsafeMutablePointer.deallocate() } + + if end >= start { + var framesAdded: UInt32 = 0 + var framesToDecode: UInt32 = rendererContext.bufferContext.totalFrameCount - end + + let offset = Int(end * rendererContext.bufferContext.sizeInBytes) + prefillLocalBufferList( + bufferList: localBufferList, + dataOffset: offset, + framesToDecode: framesToDecode + ) + + // Use the audio converter to convert the data + status = AudioConverterFillComplexBuffer( + converter, + _converterCallback, + &convertInfo, + &framesToDecode, + localBufferList.unsafeMutablePointer, + nil + ) + + framesAdded = framesToDecode + + if framesAdded > 0 { + fillUsedFrames(framesCount: framesAdded) + } + + if status == AudioConvertStatus.done.rawValue { + fillUsedFrames(framesCount: framesAdded) + return + } else if status != 0 { + processorCallback?(.raiseError(.audioSystemError(.codecError))) + return + } + + framesToDecode = start + if framesToDecode == 0 { + fillUsedFrames(framesCount: framesAdded) + continue packetProcess + } + + prefillLocalBufferList( + bufferList: localBufferList, + dataOffset: 0, + framesToDecode: framesToDecode + ) + + // Use the audio converter to convert the remaining data + status = AudioConverterFillComplexBuffer( + converter, + _converterCallback, + &convertInfo, + &framesToDecode, + localBufferList.unsafeMutablePointer, + nil + ) + + framesAdded += framesToDecode + + if status == AudioConvertStatus.done.rawValue { + fillUsedFrames(framesCount: framesAdded) + return + } else if status == AudioConvertStatus.processed.rawValue { + fillUsedFrames(framesCount: framesAdded) + continue packetProcess + } else if status != 0 { + processorCallback?(.raiseError(.audioSystemError(.codecError))) + return + } + + } else { + var framesAdded: UInt32 = 0 + var framesToDecode: UInt32 = start - end + + let offset = Int(end * rendererContext.bufferContext.sizeInBytes) + prefillLocalBufferList( + bufferList: localBufferList, + dataOffset: offset, + framesToDecode: framesToDecode + ) + + // Use the audio converter to convert the data + status = AudioConverterFillComplexBuffer( + converter, + _converterCallback, + &convertInfo, + &framesToDecode, + localBufferList.unsafeMutablePointer, + nil + ) + + framesAdded = framesToDecode + + if framesAdded > 0 { + fillUsedFrames(framesCount: framesAdded) + } + + if status == AudioConvertStatus.done.rawValue { + return + } else if status == AudioConvertStatus.processed.rawValue { + continue packetProcess + } else if status != 0 { + processorCallback?(.raiseError(.audioSystemError(.codecError))) + return + } + } + } + } + + /// Fills the AudioBuffer with data as required + /// - Parameters: + /// - bufferList: The audio buffer list to fill + /// - dataOffset: The offset in the data + /// - framesToDecode: The number of frames to decode + @inline(__always) + private func prefillLocalBufferList( + bufferList: UnsafeMutableAudioBufferListPointer, + dataOffset: Int, + framesToDecode: UInt32 + ) { + if let mData = rendererContext.audioBuffer.mData { + bufferList[0].mData = dataOffset > 0 ? mData + dataOffset : mData + } + bufferList[0].mDataByteSize = framesToDecode * rendererContext.bufferContext.sizeInBytes + bufferList[0].mNumberChannels = rendererContext.audioBuffer.mNumberChannels + } + + /// Advances the processed frames for buffer and reading entry + /// - Parameter frameCount: The number of frames to advance + @inline(__always) + private func fillUsedFrames(framesCount: UInt32) { + rendererContext.lock.lock() + rendererContext.bufferContext.frameUsedCount += framesCount + rendererContext.lock.unlock() + + playerContext.audioReadingEntry?.lock.lock() + playerContext.audioReadingEntry?.framesState.queued += Int(framesCount) + playerContext.audioReadingEntry?.lock.unlock() + } +} diff --git a/AudioStreaming/Streaming/Helpers/AudioFileType.swift b/AudioStreaming/Streaming/Helpers/AudioFileType.swift index e7c9d0a..742bb6f 100644 --- a/AudioStreaming/Streaming/Helpers/AudioFileType.swift +++ b/AudioStreaming/Streaming/Helpers/AudioFileType.swift @@ -7,6 +7,9 @@ import AudioToolbox import Foundation /// mapping from mime types to `AudioFileTypeID` +// Custom file type for Ogg Vorbis +let kAudioFileOggType: AudioFileTypeID = 0x6F676720 // 'ogg ' + let fileTypesFromMimeType: [String: AudioFileTypeID] = [ "audio/mp3": kAudioFileMP3Type, @@ -33,7 +36,10 @@ let fileTypesFromMimeType: [String: AudioFileTypeID] = "video/3gpp": kAudioFile3GPType, "audio/3gp2": kAudioFile3GP2Type, "video/3gp2": kAudioFile3GP2Type, - "audio/flac": kAudioFileFLACType + "audio/flac": kAudioFileFLACType, + "audio/ogg": kAudioFileOggType, + "audio/vorbis": kAudioFileOggType, + "application/ogg": kAudioFileOggType ] /// Method that converts mime type to AudioFileTypeID @@ -58,6 +64,8 @@ let fileTypesFromFileExtension: [String: AudioFileTypeID] = "ac3": kAudioFileAC3Type, "3gp": kAudioFile3GPType, "flac": kAudioFileFLACType, + "ogg": kAudioFileOggType, + "oga": kAudioFileOggType, ] func audioFileType(fileExtension: String) -> AudioFileTypeID { diff --git a/AudioStreaming/Streaming/OggVorbis/OggVorbisDecoder.swift b/AudioStreaming/Streaming/OggVorbis/OggVorbisDecoder.swift new file mode 100644 index 0000000..2fb91ee --- /dev/null +++ b/AudioStreaming/Streaming/OggVorbis/OggVorbisDecoder.swift @@ -0,0 +1,252 @@ +// +// OggVorbisDecoder.swift +// AudioStreaming +// +// Created on 25/10/2025. +// + +import Foundation +import AudioToolbox +import ogg +import vorbis + +/// Swift wrapper for the OggVorbis C decoder +final class OggVorbisDecoder { + private var decoderContext: OpaquePointer? + private var isInitialized = false + + /// Stream information + private(set) var streamInfo = OggVorbisStreamInfo() + + /// Error type for OggVorbis operations + enum OggVorbisDecoderError: Error { + case outOfMemory + case invalidSetup + case invalidStream + case invalidHeader + case invalidPacket + case internalError + case endOfFile + case unknownError(Int) + + init(code: Int32) { + switch code { + case OGGVORBIS_ERROR_OUT_OF_MEMORY: + self = .outOfMemory + case OGGVORBIS_ERROR_INVALID_SETUP: + self = .invalidSetup + case OGGVORBIS_ERROR_INVALID_STREAM: + self = .invalidStream + case OGGVORBIS_ERROR_INVALID_HEADER: + self = .invalidHeader + case OGGVORBIS_ERROR_INVALID_PACKET: + self = .invalidPacket + case OGGVORBIS_ERROR_INTERNAL: + self = .internalError + case OGGVORBIS_ERROR_EOF: + self = .endOfFile + default: + self = .unknownError(Int(code)) + } + } + } + + /// Initialize a new OggVorbis decoder + init() { + decoderContext = OggVorbisDecoderCreate() + } + + deinit { + if let context = decoderContext { + OggVorbisDecoderDestroy(context) + } + } + + /// Initialize the decoder with initial data + /// - Parameter data: The initial Ogg Vorbis data + /// - Throws: OggVorbisDecoderError if initialization fails + func initialize(with data: Data) throws { + guard let context = decoderContext else { + throw OggVorbisDecoderError.invalidSetup + } + + let result = data.withUnsafeBytes { buffer -> Int32 in + let baseAddress = buffer.baseAddress! + return OggVorbisDecoderInit(context, baseAddress, buffer.count) + } + + if result != OGGVORBIS_SUCCESS { + throw OggVorbisDecoderError(code: result) + } + + isInitialized = true + try updateStreamInfo() + } + + /// Process a chunk of Ogg Vorbis data + /// - Parameter data: The Ogg Vorbis data to process + /// - Returns: Decoded PCM audio data + /// - Throws: OggVorbisDecoderError if processing fails + func processData(_ data: Data) throws -> [Float] { + guard isInitialized, let context = decoderContext else { + throw OggVorbisDecoderError.invalidSetup + } + + let result = data.withUnsafeBytes { buffer -> Int32 in + let baseAddress = buffer.baseAddress! + return OggVorbisDecoderProcessData(context, baseAddress, buffer.count) + } + + if result != OGGVORBIS_SUCCESS { + throw OggVorbisDecoderError(code: result) + } + + // Get the PCM data + var pcmData: UnsafeMutablePointer? + var samplesDecoded: Int32 = 0 + + let pcmResult = OggVorbisDecoderGetPCMData(context, &pcmData, &samplesDecoded) + if pcmResult != OGGVORBIS_SUCCESS { + throw OggVorbisDecoderError(code: pcmResult) + } + + // Convert to Swift array + var output = [Float]() + if let pcm = pcmData, samplesDecoded > 0 { + let channels = Int(streamInfo.channels) + let totalSamples = Int(samplesDecoded) * channels + + output.reserveCapacity(totalSamples) + + // Interleave the channels + for i in 0.. String? { + guard isInitialized, let context = decoderContext else { + return nil + } + + return key.withCString { keyPtr -> String? in + guard let valuePtr = OggVorbisDecoderGetComment(context, keyPtr) else { + return nil + } + return String(cString: valuePtr) + } + } + + /// Get all comments from the Vorbis stream + /// - Returns: A dictionary of all comments + func getAllComments() -> [String: String] { + guard isInitialized, let context = decoderContext else { + return [:] + } + + var comments = [String: String]() + let count = OggVorbisDecoderGetCommentCount(context) + + for i in 0..? + var valuePtr: UnsafePointer? + + OggVorbisDecoderGetCommentPair(context, Int32(i), &keyPtr, &valuePtr) + + if let keyPtr = keyPtr, let valuePtr = valuePtr { + let key = String(cString: keyPtr) + let value = String(cString: valuePtr) + comments[key] = value + } + } + + return comments + } + + /// Update the stream info from the decoder + private func updateStreamInfo() throws { + guard isInitialized, let context = decoderContext else { + throw OggVorbisDecoderError.invalidSetup + } + + var info = OggVorbisStreamInfo() + let result = OggVorbisDecoderGetInfo(context, &info) + + if result != OGGVORBIS_SUCCESS { + throw OggVorbisDecoderError(code: result) + } + + streamInfo.serialNumber = info.serialNumber + streamInfo.pageCount = info.pageCount + streamInfo.totalSamples = info.totalSamples + streamInfo.sampleRate = info.sampleRate + streamInfo.channels = info.channels + streamInfo.bitRate = info.bitRate + streamInfo.nominalBitrate = info.nominalBitrate + streamInfo.minBitrate = info.minBitrate + streamInfo.maxBitrate = info.maxBitrate + streamInfo.blocksize0 = info.blocksize0 + streamInfo.blocksize1 = info.blocksize1 + streamInfo.granulePosition = info.granulePosition + + // Update comments + streamInfo.commentHeader = getAllComments() + } + + /// Convert decoded PCM data to an AudioBuffer + /// - Parameter pcmData: The decoded PCM data + /// - Returns: An AudioBuffer containing the PCM data + func createAudioBuffer(from pcmData: [Float]) -> AudioBuffer { + var audioBuffer = AudioBuffer() + audioBuffer.mNumberChannels = UInt32(streamInfo.channels) + audioBuffer.mDataByteSize = UInt32(pcmData.count * MemoryLayout.size) + + let data = UnsafeMutablePointer.allocate(capacity: pcmData.count) + data.initialize(from: pcmData, count: pcmData.count) + audioBuffer.mData = UnsafeMutableRawPointer(data) + + return audioBuffer + } +} diff --git a/AudioStreaming/Streaming/OggVorbis/README.md b/AudioStreaming/Streaming/OggVorbis/README.md new file mode 100644 index 0000000..5c29ab6 --- /dev/null +++ b/AudioStreaming/Streaming/OggVorbis/README.md @@ -0,0 +1,66 @@ +# Ogg Vorbis Support for AudioStreaming + +This directory contains the implementation of Ogg Vorbis support for the AudioStreaming library. + +## Overview + +The Ogg Vorbis support consists of: + +1. **OggVorbisBridge.h/.c**: C wrapper for the libvorbis/libogg libraries +2. **OggVorbisDecoder.swift**: Swift wrapper for the C bridge +3. **OggVorbisStreamProcessor.swift**: Integration with the AudioStreaming framework + +## Dependencies + +This implementation requires the following external libraries: + +- **libvorbis**: The Vorbis audio decoder +- **libogg**: The Ogg container format library + +These libraries need to be linked to the project. You can install them using a package manager like CocoaPods, Carthage, or Swift Package Manager. + +## Usage + +Ogg Vorbis files are automatically detected and processed by the AudioStreaming library. You can play Ogg Vorbis files the same way you play other audio formats: + +```swift +let player = AudioPlayer() +player.play(url: URL(string: "https://example.com/audio.ogg")!) +``` + +Or for local files: + +```swift +let player = AudioPlayer() +player.play(url: URL(fileURLWithPath: "/path/to/audio.ogg")) +``` + +## Features + +- Streaming playback of remote Ogg Vorbis files +- Local file playback +- Seeking support +- Metadata extraction +- Gapless playback + +## Implementation Details + +The implementation follows these steps: + +1. Detect Ogg Vorbis files by file extension or MIME type +2. Parse Ogg pages and extract Vorbis packets +3. Decode Vorbis audio data to PCM +4. Convert PCM to the format required by AVAudioEngine +5. Handle seeking by resetting the decoder and seeking to the appropriate position + +## Limitations + +- Seeking is not as precise as with other formats due to the nature of Ogg Vorbis streams +- Performance may be lower compared to formats with native Apple support +- Memory usage may be higher due to the need for additional buffers + +## Future Improvements + +- Optimize memory usage +- Improve seeking precision +- Add support for Opus in Ogg containers diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..0fd7dd3 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,24 @@ +{ + "originHash" : "ed4cb443e9471f7f6ffaf62fedbad9b953d8a2b3b7a793ddb8903afe411855fa", + "pins" : [ + { + "identity" : "ogg-binary-xcframework", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sbooth/ogg-binary-xcframework", + "state" : { + "revision" : "c0e822e18738ad913864e98d9614927ac1e9337c", + "version" : "0.1.2" + } + }, + { + "identity" : "vorbis-binary-xcframework", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sbooth/vorbis-binary-xcframework", + "state" : { + "revision" : "842020eabcebe410e698c68545d6597b2d232e51", + "version" : "0.1.2" + } + } + ], + "version" : 3 +} diff --git a/Package.swift b/Package.swift index a658aee..be1abd6 100644 --- a/Package.swift +++ b/Package.swift @@ -1,24 +1,55 @@ -// swift-tools-version:5.9 +// swift-tools-version:5.10 import PackageDescription let package = Package( name: "AudioStreaming", platforms: [ - .iOS(.v12), + .iOS(.v15), .macOS(.v13), .tvOS(.v16) ], products: [ .library( name: "AudioStreaming", - targets: ["AudioStreaming"] + targets: ["AudioCodecs", "AudioStreaming"] ), ], + 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") + ], targets: [ + // C target for audio codec bridges + .target( + name: "AudioCodecs", + dependencies: [ + .product(name: "ogg", package: "ogg-binary-xcframework"), + .product(name: "vorbis", package: "vorbis-binary-xcframework") + ], + path: "AudioCodecs", + publicHeadersPath: "include", + cSettings: [ + .headerSearchPath("."), + .headerSearchPath("include") + ], + linkerSettings: [ + .linkedFramework("AudioToolbox"), + .linkedFramework("Foundation") + ] + ), + + // Main Swift target .target( name: "AudioStreaming", - path: "AudioStreaming" + dependencies: [ + "AudioCodecs", + .product(name: "ogg", package: "ogg-binary-xcframework"), + .product(name: "vorbis", package: "vorbis-binary-xcframework") + ], + path: "AudioStreaming", + exclude: ["AudioStreaming.h", "Streaming/OggVorbis"], + swiftSettings: [] ), .testTarget( name: "AudioStreamingTests",