diff --git a/AudioCodecs/OggVorbisBridge.c b/AudioCodecs/OggVorbisBridge.c deleted file mode 100644 index 2bf905c..0000000 --- a/AudioCodecs/OggVorbisBridge.c +++ /dev/null @@ -1,505 +0,0 @@ -// -// 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 index bde2f96..0fb8dbd 100644 --- a/AudioCodecs/VorbisFileBridge.c +++ b/AudioCodecs/VorbisFileBridge.c @@ -9,7 +9,8 @@ struct VFRemoteStream { uint8_t *buf; size_t cap, head, tail, size; int eof; - long long pos; + long long pos; // Current read position in the stream + long long total_pushed; // Total bytes pushed into the buffer pthread_mutex_t m; pthread_cond_t cv; }; @@ -93,6 +94,7 @@ void VFStreamPush(VFStreamRef sr, const uint8_t *data, size_t len) { pthread_cond_wait(&s->cv, &s->m); } } + s->total_pushed += (long long)len; pthread_cond_broadcast(&s->cv); pthread_mutex_unlock(&s->m); } @@ -137,10 +139,72 @@ static size_t read_cb(void *ptr, size_t size, size_t nmemb, void *datasrc) { return size ? (got / size) : 0; } -// Seek callback - non-seekable by default +// Seek callback - seek within the ring buffer static int seek_cb(void *datasrc, ogg_int64_t offset, int whence) { - (void)datasrc; (void)offset; (void)whence; - return -1; + struct VFRemoteStream *s = (struct VFRemoteStream *)datasrc; + if (!s) return -1; + + pthread_mutex_lock(&s->m); + + ogg_int64_t new_pos = 0; + switch (whence) { + case SEEK_SET: + new_pos = offset; + break; + case SEEK_CUR: + new_pos = s->pos + offset; + break; + case SEEK_END: + new_pos = s->total_pushed + offset; + break; + default: + pthread_mutex_unlock(&s->m); + return -1; + } + + // Check if the new position is valid (within available data) + if (new_pos < 0 || new_pos > s->total_pushed) { + pthread_mutex_unlock(&s->m); + return -1; // Can't seek outside available data + } + + // Calculate how much data we've already consumed from the buffer + long long already_consumed = s->pos - ((long long)s->total_pushed - (long long)s->size); + + // Calculate the new head position + long long pos_delta = new_pos - s->pos; + + // For forward seeks, we need to have enough data in the buffer + if (pos_delta > 0 && pos_delta > (long long)s->size) { + pthread_mutex_unlock(&s->m); + return -1; // Not enough data in buffer to seek forward + } + + // For backward seeks, check if that data is still in the buffer + if (pos_delta < 0 && (-pos_delta) > already_consumed) { + pthread_mutex_unlock(&s->m); + return -1; // Data has been discarded from buffer + } + + // Adjust head pointer + if (pos_delta >= 0) { + // Forward seek: advance head + s->head = (s->head + pos_delta) % s->cap; + s->size -= (size_t)pos_delta; + } else { + // Backward seek: rewind head + size_t rewind = (size_t)(-pos_delta); + if (s->head >= rewind) { + s->head -= rewind; + } else { + s->head = s->cap - (rewind - s->head); + } + s->size += rewind; + } + + s->pos = new_pos; + pthread_mutex_unlock(&s->m); + return 0; } // Close callback - no-op @@ -165,7 +229,7 @@ int VFOpen(VFStreamRef sr, VFFileRef *out_vf) { ov_callbacks cbs; cbs.read_func = read_cb; - cbs.seek_func = NULL; // non-seekable streaming + cbs.seek_func = NULL; // Non-seekable streaming (seeking handled at Swift level) cbs.close_func = close_cb; cbs.tell_func = tell_cb; @@ -196,6 +260,7 @@ int VFGetInfo(VFFileRef fr, VFStreamInfo *out_info) { out_info->channels = info->channels; out_info->total_pcm_samples = ov_pcm_total(vf, -1); out_info->duration_seconds = ov_time_total(vf, -1); + out_info->bitrate_nominal = info->bitrate_nominal; return 0; } @@ -224,3 +289,22 @@ long VFReadInterleavedFloat(VFFileRef fr, float *dst, int max_frames) { return frames; } + +// Seek to a specific time in seconds +int VFSeekTime(VFFileRef fr, double time_seconds) { + OggVorbis_File *vf = (OggVorbis_File *)fr; + if (!vf) return -1; + + // Use ov_time_seek for time-based seeking + // Returns 0 on success, nonzero on failure + return ov_time_seek(vf, time_seconds); +} + +// Check if the stream is seekable +int VFIsSeekable(VFFileRef fr) { + OggVorbis_File *vf = (OggVorbis_File *)fr; + if (!vf) return 0; + + // Returns nonzero if the stream is seekable + return ov_seekable(vf); +} diff --git a/AudioCodecs/include/AudioCodecs.h b/AudioCodecs/include/AudioCodecs.h index d78f8b8..ee189d9 100644 --- a/AudioCodecs/include/AudioCodecs.h +++ b/AudioCodecs/include/AudioCodecs.h @@ -8,7 +8,6 @@ #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 deleted file mode 100644 index 6df70bd..0000000 --- a/AudioCodecs/include/OggVorbisBridge.h +++ /dev/null @@ -1,81 +0,0 @@ -// -// 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 index 780561d..4dbe272 100644 --- a/AudioCodecs/include/VorbisFileBridge.h +++ b/AudioCodecs/include/VorbisFileBridge.h @@ -18,6 +18,7 @@ typedef struct { int channels; long long total_pcm_samples; // -1 if unknown double duration_seconds; // < 0 if unknown + long bitrate_nominal; // nominal bitrate in bits/sec, or 0 if unknown } VFStreamInfo; // Stream lifecycle @@ -40,6 +41,12 @@ 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); +// Seek to a specific time in seconds; returns 0 on success, <0 on error +int VFSeekTime(VFFileRef vf, double time_seconds); + +// Check if the stream is seekable; returns 1 if seekable, 0 if not +int VFIsSeekable(VFFileRef vf); + #ifdef __cplusplus } #endif diff --git a/AudioPlayer/AudioPlayer.xcodeproj/project.pbxproj b/AudioPlayer/AudioPlayer.xcodeproj/project.pbxproj index fdf08e4..5c72d10 100644 --- a/AudioPlayer/AudioPlayer.xcodeproj/project.pbxproj +++ b/AudioPlayer/AudioPlayer.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ 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 */; }; + 9881693E2EBCDB0100CE7EFF /* hipjazz.ogg in Resources */ = {isa = PBXBuildFile; fileRef = 9881693D2EBCDB0100CE7EFF /* hipjazz.ogg */; }; 989E08E72BF7A4E300599F17 /* PrefersTabNavigationEnvironmentKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 989E08E62BF7A4E300599F17 /* PrefersTabNavigationEnvironmentKey.swift */; }; 98BFB41A2BC97AF800E812C0 /* DisplayLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98BFB4192BC97AF800E812C0 /* DisplayLink.swift */; }; 98BFB41D2BCD7BB800E812C0 /* EqualizerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98BFB41C2BCD7BB800E812C0 /* EqualizerView.swift */; }; @@ -51,6 +52,7 @@ 9816A8BA2BC87BC200AD1299 /* AudioPlayerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerService.swift; sourceTree = ""; }; 984DE9542BDAE59C004B427A /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = ""; }; 984DE9562BDAFC7E004B427A /* AudioPlayerControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerControlsView.swift; sourceTree = ""; }; + 9881693D2EBCDB0100CE7EFF /* hipjazz.ogg */ = {isa = PBXFileReference; lastKnownFileType = file; path = hipjazz.ogg; sourceTree = ""; }; 989E08E62BF7A4E300599F17 /* PrefersTabNavigationEnvironmentKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefersTabNavigationEnvironmentKey.swift; sourceTree = ""; }; 98BFB4192BC97AF800E812C0 /* DisplayLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayLink.swift; sourceTree = ""; }; 98BFB41B2BCAAD8A00E812C0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; @@ -153,6 +155,7 @@ 9816A8B02BC832E100AD1299 /* Resources */ = { isa = PBXGroup; children = ( + 9881693D2EBCDB0100CE7EFF /* hipjazz.ogg */, 9816A8AE2BC832DB00AD1299 /* bensound-jazzyfrenchy.m4a */, 9816A8AF2BC832DC00AD1299 /* hipjazz.wav */, 9816A8AD2BC832DB00AD1299 /* bensound-jazzyfrenchy.mp3 */, @@ -256,6 +259,7 @@ 9806E81C2BC5D12700757370 /* Assets.xcassets in Resources */, 9816A8B12BC8330C00AD1299 /* bensound-jazzyfrenchy.mp3 in Resources */, 9816A8B22BC8330C00AD1299 /* bensound-jazzyfrenchy.m4a in Resources */, + 9881693E2EBCDB0100CE7EFF /* hipjazz.ogg in Resources */, 9816A8B32BC8330C00AD1299 /* hipjazz.wav in Resources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/AudioPlayer/AudioPlayer/Common/AudioContent.swift b/AudioPlayer/AudioPlayer/Common/AudioContent.swift index e3cc460..35e1b0e 100644 --- a/AudioPlayer/AudioPlayer/Common/AudioContent.swift +++ b/AudioPlayer/AudioPlayer/Common/AudioContent.swift @@ -21,6 +21,7 @@ enum AudioContent { case localWave case loopBeatFlac case oggVorbis + case oggVorbisLocal case custom(String) var title: String { @@ -46,7 +47,7 @@ enum AudioContent { case .local: return "Jazzy Frenchy" case .localWave: - return "Local file" + return "Hip Jazz" case .optimized: return "Jazzy Frenchy" case .nonOptimized: @@ -55,6 +56,8 @@ enum AudioContent { return "Beat loop" case .oggVorbis: return "Jazzy Fetchy" + case .oggVorbisLocal: + return "Hip Jazz" case .custom(let url): return url } @@ -79,7 +82,7 @@ enum AudioContent { case .piano: return "Remote mp3" case .remoteWave: - return "wave" + return "Local wav" case .local: return "Music by: bensound.com" case .localWave: @@ -92,6 +95,8 @@ enum AudioContent { return "Remote flac" case .oggVorbis: return "Remote Ogg Vorbis" + case .oggVorbisLocal: + return "Local Ogg Vorbis" case .custom: return "" } @@ -131,6 +136,9 @@ enum AudioContent { 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 .oggVorbisLocal: + let path = Bundle.main.path(forResource: "hipjazz", ofType: "ogg")! + return URL(fileURLWithPath: path) 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 37afe33..9f5a442 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, .oggVorbis] +private let audioTracks: [AudioContent] = [.khruangbin, .piano, .optimized, .nonOptimized, .remoteWave, .local, .localWave, .loopBeatFlac, .oggVorbis, .oggVorbisLocal] private let customStreams: [AudioContent] = [.custom("custom://sinwave")] func audioTracksProvider() -> [AudioPlaylist] { diff --git a/AudioPlayer/AudioPlayer/Resources/hipjazz.ogg b/AudioPlayer/AudioPlayer/Resources/hipjazz.ogg new file mode 100644 index 0000000..e59f7bd Binary files /dev/null and b/AudioPlayer/AudioPlayer/Resources/hipjazz.ogg differ diff --git a/AudioStreaming-Bridging-Header.h b/AudioStreaming-Bridging-Header.h index 155f4ee..dc9a7dc 100644 --- a/AudioStreaming-Bridging-Header.h +++ b/AudioStreaming-Bridging-Header.h @@ -8,6 +8,7 @@ #ifndef AudioStreaming_Bridging_Header_h #define AudioStreaming_Bridging_Header_h -#import +// Bridging header for AudioStreaming +// Add C imports here if needed #endif /* AudioStreaming_Bridging_Header_h */ diff --git a/AudioStreaming/OggVorbis/OggVorbisDecoder.swift b/AudioStreaming/OggVorbis/OggVorbisDecoder.swift deleted file mode 100644 index 9e348f6..0000000 --- a/AudioStreaming/OggVorbis/OggVorbisDecoder.swift +++ /dev/null @@ -1,368 +0,0 @@ -// -// 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 deleted file mode 100644 index 464ff88..0000000 --- a/AudioStreaming/OggVorbis/OggVorbisStreamDataConverter.swift +++ /dev/null @@ -1,49 +0,0 @@ -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 deleted file mode 100644 index 5c29ab6..0000000 --- a/AudioStreaming/OggVorbis/README.md +++ /dev/null @@ -1,66 +0,0 @@ -# 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 index 7bb1617..1fe503e 100644 --- a/AudioStreaming/OggVorbis/VorbisFileDecoder.swift +++ b/AudioStreaming/OggVorbis/VorbisFileDecoder.swift @@ -13,16 +13,12 @@ final class VorbisFileDecoder { private(set) var channels: Int = 0 private(set) var durationSeconds: Double = -1 private(set) var totalPcmSamples: Int64 = -1 + private(set) var nominalBitrate: Int = 0 private(set) var processingFormat: AVAudioFormat? // Thread safety private let decoderLock = NSLock() - // Debug counters - private var totalBytesReceived = 0 - private var totalFramesRead = 0 - private var readCalls = 0 - // Silent frame generation private var silentFrameBuffer: UnsafeMutablePointer? private var silentFrameSize = 0 @@ -33,11 +29,7 @@ final class VorbisFileDecoder { decoderLock.lock() defer { decoderLock.unlock() } - print("VorbisFileDecoder: Creating stream with \(capacityBytes) bytes capacity") stream = VFStreamCreate(capacityBytes) - totalBytesReceived = 0 - totalFramesRead = 0 - readCalls = 0 } /// Clean up resources @@ -54,8 +46,6 @@ final class VorbisFileDecoder { silentFrameBuffer.deallocate() self.silentFrameBuffer = nil } - - print("VorbisFileDecoder: Destroyed decoder") } deinit { @@ -73,14 +63,7 @@ final class VorbisFileDecoder { rawBuf.count > 0, let stream = stream else { return } - let beforeAvailable = VFStreamAvailableBytes(stream) - print("VorbisFileDecoder: Pushing \(rawBuf.count) bytes (buffer has \(beforeAvailable) bytes available)...") - VFStreamPush(stream, base, rawBuf.count) - - totalBytesReceived += rawBuf.count - let afterAvailable = VFStreamAvailableBytes(stream) - print("VorbisFileDecoder: Pushed \(rawBuf.count) bytes (total received: \(totalBytesReceived), buffer now has: \(afterAvailable))") } } @@ -101,7 +84,6 @@ final class VorbisFileDecoder { if let stream = stream { VFStreamMarkEOF(stream) - print("VorbisFileDecoder: Marked EOF") } } @@ -113,16 +95,14 @@ final class VorbisFileDecoder { guard vf == nil, let stream = stream else { return } - print("VorbisFileDecoder: Attempting to open Vorbis file") var outVF: VFFileRef? let rc = VFOpen(stream, &outVF) if rc < 0 { - print("VorbisFileDecoder: Failed to open Vorbis file: \(rc)") + Logger.error("Failed to open Vorbis file", category: .audioRendering) throw NSError(domain: "VorbisFileDecoder", code: Int(rc), - userInfo: [NSLocalizedDescriptionKey: "Failed to open Vorbis file: \(rc)"]) + userInfo: [NSLocalizedDescriptionKey: "Failed to open Vorbis file"]) } - print("VorbisFileDecoder: Successfully opened Vorbis file") vf = outVF // Get stream info @@ -132,8 +112,7 @@ final class VorbisFileDecoder { channels = Int(info.channels) totalPcmSamples = Int64(info.total_pcm_samples) durationSeconds = info.duration_seconds - - print("VorbisFileDecoder: Stream info - Sample rate: \(sampleRate), Channels: \(channels), Duration: \(durationSeconds), Total samples: \(totalPcmSamples)") + nominalBitrate = Int(info.bitrate_nominal) // Create audio format let layoutTag: AudioChannelLayoutTag @@ -158,11 +137,8 @@ final class VorbisFileDecoder { for i in 0.. 0 else { - print("VorbisFileDecoder: Cannot read frames - vf: \(vf != nil), channels: \(buffer.format.channelCount)") return generateSilentFrames(into: buffer, frameCount: frameCount) } // Get float channel data from buffer guard let floatChannelData = buffer.floatChannelData else { - print("VorbisFileDecoder: No float channel data available") return generateSilentFrames(into: buffer, frameCount: frameCount) } @@ -196,24 +168,11 @@ final class VorbisFileDecoder { // Read interleaved frames let framesRead = Int(VFReadInterleavedFloat(vf, tempBuffer, Int32(maxFrames))) - print("VorbisFileDecoder: Read \(framesRead) frames (call #\(readCalls), requested: \(maxFrames))") - // If no frames were read, generate silent frames instead of returning 0 if framesRead <= 0 { - print("VorbisFileDecoder: No frames read, generating silent frames") return generateSilentFrames(into: buffer, frameCount: frameCount) } - // Check for audio data - var maxLevel: Float = 0 - for i in 0.. maxLevel { - maxLevel = level - } - } - print("VorbisFileDecoder: Max audio level in first 20 samples: \(maxLevel)") - // De-interleave into buffer for ch in 0.. Void)? + // MARK: - Constants + + /// Correction factor for Ogg container overhead in bitrate-based duration calculation. + /// Ogg containers add 3-4% overhead (page headers, packet headers, metadata). + /// The nominal bitrate only accounts for audio data, not container overhead. + /// By reducing the bitrate slightly, we increase the calculated duration to match reality. + private let oggContainerOverheadFactor: Double = 0.96 // 4% overhead + + /// Fallback bitrate estimates when nominal bitrate is unavailable + private let fallbackBitrateStereo: Double = 160_000 // 160 kbps for stereo + private let fallbackBitrateMono: Double = 96_000 // 96 kbps for mono + + // MARK: - Properties + private let playerContext: AudioPlayerContext private let rendererContext: AudioRendererContext private let outputAudioFormat: AudioStreamBasicDescription @@ -36,6 +50,9 @@ final class OggVorbisStreamProcessor { private var pcmBuffer: AVAudioPCMBuffer? private let frameCount = 1024 + // Seeking state (currently unused - seeking not fully supported) + // Future enhancement: implement proper seeking for local files + // Debug logging private var totalFramesProcessed = 0 private var dataChunkCount = 0 @@ -56,10 +73,22 @@ final class OggVorbisStreamProcessor { } deinit { + cleanup() + } + + /// Clean up all resources and reset state + func cleanup() { cleanupBuffers() + if let converter = audioConverter { AudioConverterDispose(converter) + audioConverter = nil } + + // Destroy and reset the decoder + vfDecoder.destroy() + isInitialized = false + totalFramesProcessed = 0 } // MARK: - Data Processing @@ -76,7 +105,6 @@ final class OggVorbisStreamProcessor { vfDecoder.create(capacityBytes: 2_097_152) isInitialized = true totalFramesProcessed = 0 - print("OggVorbisStreamProcessor: Initialized with 2MB ring buffer") } vfDecoder.push(data) @@ -254,22 +282,27 @@ final class OggVorbisStreamProcessor { entry.sampleRate = Float(vfDecoder.sampleRate) entry.packetDuration = Double(1) / Double(vfDecoder.sampleRate) - // Set dataPacketOffset for duration calculation (frames = packets since mFramesPerPacket = 1) + // For streaming Ogg files, totalPcmSamples may not be available (returns error code) + // In that case, use bitrate-based duration calculation with container overhead correction if vfDecoder.totalPcmSamples > 0 { + // We have total samples - use packet offset for accurate duration entry.audioStreamState.dataPacketOffset = UInt64(vfDecoder.totalPcmSamples) + } else { + // Streaming - use bitrate for duration estimation + if vfDecoder.nominalBitrate > 0 { + entry.audioStreamState.bitRate = Double(vfDecoder.nominalBitrate) * oggContainerOverheadFactor + } else { + // Fallback: use typical bitrates for Vorbis quality + let estimatedBitrate = vfDecoder.channels == 2 ? fallbackBitrateStereo : fallbackBitrateMono + entry.audioStreamState.bitRate = estimatedBitrate * oggContainerOverheadFactor + } } - - // Bitrate will be estimated from the actual data - // Don't set a fixed value - let it be calculated dynamically entry.audioStreamState.processedDataFormat = true entry.audioStreamState.readyForDecoding = true entry.lock.unlock() // Create audio converter from source format to output format createAudioConverter(from: asbd, to: outputAudioFormat) - - let duration = vfDecoder.totalPcmSamples > 0 ? Double(vfDecoder.totalPcmSamples) / Double(vfDecoder.sampleRate) : 0 - print("OggVorbisStreamProcessor: Format setup - Rate: \(vfDecoder.sampleRate)Hz, Channels: \(vfDecoder.channels), Duration: \(String(format: "%.2f", duration))s, Total samples: \(vfDecoder.totalPcmSamples)") } /// Create audio converter from source format to output format @@ -285,7 +318,7 @@ final class OggVorbisStreamProcessor { let status = AudioConverterNew(&source, &dest, &audioConverter) if status != noErr { - print("OggVorbisStreamProcessor: ERROR - Failed to create AudioConverter: \(status)") + Logger.error("Failed to create AudioConverter", category: .audioRendering) } } @@ -450,31 +483,17 @@ final class OggVorbisStreamProcessor { } /// Process a seek request + /// + /// Seeking is not supported for Ogg Vorbis streams. + /// For HTTP streams, seeking is extremely difficult because: + /// 1. Need to find Ogg page boundaries + /// 2. Need Vorbis headers to initialize decoder + /// 3. Headers are only at the beginning of the file + /// + /// Note: Future enhancement could support seeking in local files + /// by fetching headers and using libvorbisfile's built-in seeking. func processSeek() { - guard let readingEntry = playerContext.audioReadingEntry else { return } - - guard readingEntry.calculatedBitrate() > 0.0 || (playerContext.audioPlayingEntry?.length ?? 0) > 0 else { - return - } - - // Reset the decoder state - isInitialized = false - totalFramesProcessed = 0 - cleanupBuffers() - - // Clear initial bytes to force reinitialization - readingEntry.lock.lock() - readingEntry.audioStreamState.initialOggBytes = nil - readingEntry.audioStreamState.hasAttemptedOggVorbisParse = false - readingEntry.lock.unlock() - - readingEntry.reset() - readingEntry.seek(at: Int(readingEntry.seekRequest.time)) - rendererContext.waitingForDataAfterSeekFrameCount.write { $0 = 0 } - playerContext.setInternalState(to: .waitingForDataAfterSeek) - rendererContext.resetBuffers() - - print("OggVorbisStreamProcessor: Seek processed") + // Seeking not supported - UI should check AudioPlayer.isSeekable } // MARK: - Helper Methods diff --git a/AudioStreaming/Streaming/OggVorbis/OggVorbisDecoder.swift b/AudioStreaming/Streaming/OggVorbis/OggVorbisDecoder.swift deleted file mode 100644 index 2fb91ee..0000000 --- a/AudioStreaming/Streaming/OggVorbis/OggVorbisDecoder.swift +++ /dev/null @@ -1,252 +0,0 @@ -// -// 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 deleted file mode 100644 index 5c29ab6..0000000 --- a/AudioStreaming/Streaming/OggVorbis/README.md +++ /dev/null @@ -1,66 +0,0 @@ -# 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/README.md b/README.md index ea83b00..5c25ec3 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,21 @@ Under the hood `AudioStreaming` uses `AVAudioEngine` and `CoreAudio` for playbac #### Supported audio - Online streaming (Shoutcast/ICY streams) with metadata parsing - AIFF, AIFC, WAVE, CAF, NeXT, ADTS, MPEG Audio Layer 3, AAC audio formats -- M4A +- M4A (optimized and non-optimized) from v1.2.0 +- **Ogg Vorbis** (both local and remote files) ✨ -As of 1.2.0 version, there's support for non-optimized M4A, please report any issues - -Known limitations: +#### Known limitations ~~- As described above non-optimised M4A files are not supported this is a limitation of [AudioFileStream Services](https://developer.apple.com/documentation/audiotoolbox/audio_file_stream_services?language=swift)~~ +**Ogg Vorbis Seeking:** +- Seeking is **not supported** for Ogg Vorbis files in the current release +- This is due to technical challenges with the Ogg container format over HTTP streaming: + - Seeking requires finding precise Ogg page boundaries in the stream + - The Vorbis decoder needs the full headers (identification, comment, and setup packets) to initialize, which are only available at the beginning of the file + - HTTP range requests need to be carefully orchestrated to fetch headers and seek to the correct position +- Your UI can check `player.isSeekable` to determine if seeking is available for the currently playing file +- Future releases may add experimental support for seeking using progressive download or intelligent header caching + # Requirements - iOS 13.0+