Adds support for Ogg Vorbis
This commit is contained in:
@@ -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];
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#ifndef AudioCodecs_h
|
||||
#define AudioCodecs_h
|
||||
|
||||
#import "OggVorbisBridge.h"
|
||||
#import "VorbisFileBridge.h"
|
||||
|
||||
#endif /* AudioCodecs_h */
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
//
|
||||
// OggVorbisBridge.h
|
||||
// AudioStreaming
|
||||
//
|
||||
// Created on 25/10/2025.
|
||||
//
|
||||
|
||||
#ifndef OggVorbisBridge_h
|
||||
#define OggVorbisBridge_h
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <ogg/ogg.h>
|
||||
#include <vorbis/codec.h>
|
||||
#include <vorbis/vorbisfile.h>
|
||||
|
||||
// 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 */
|
||||
@@ -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
|
||||
|
||||
@@ -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 = "<group>"; };
|
||||
984DE9542BDAE59C004B427A /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; };
|
||||
984DE9562BDAFC7E004B427A /* AudioPlayerControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerControlsView.swift; sourceTree = "<group>"; };
|
||||
9881693D2EBCDB0100CE7EFF /* hipjazz.ogg */ = {isa = PBXFileReference; lastKnownFileType = file; path = hipjazz.ogg; sourceTree = "<group>"; };
|
||||
989E08E62BF7A4E300599F17 /* PrefersTabNavigationEnvironmentKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefersTabNavigationEnvironmentKey.swift; sourceTree = "<group>"; };
|
||||
98BFB4192BC97AF800E812C0 /* DisplayLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayLink.swift; sourceTree = "<group>"; };
|
||||
98BFB41B2BCAAD8A00E812C0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
@@ -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;
|
||||
|
||||
@@ -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)!
|
||||
}
|
||||
|
||||
@@ -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] {
|
||||
|
||||
Binary file not shown.
@@ -8,6 +8,7 @@
|
||||
#ifndef AudioStreaming_Bridging_Header_h
|
||||
#define AudioStreaming_Bridging_Header_h
|
||||
|
||||
#import <AudioCodecs/OggVorbisBridge.h>
|
||||
// Bridging header for AudioStreaming
|
||||
// Add C imports here if needed
|
||||
|
||||
#endif /* AudioStreaming_Bridging_Header_h */
|
||||
|
||||
@@ -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<Float>?
|
||||
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..<totalSamples {
|
||||
output.append(pcm[i])
|
||||
}
|
||||
|
||||
// Free the memory allocated in C
|
||||
pcm.deallocate()
|
||||
|
||||
// Debug: Print first few samples to verify data
|
||||
if output.count > 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..<count {
|
||||
var keyPtr: UnsafePointer<Int8>?
|
||||
var valuePtr: UnsafePointer<Int8>?
|
||||
|
||||
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<Float>.size)
|
||||
|
||||
// Create a buffer for the PCM data
|
||||
let data = UnsafeMutablePointer<Float>.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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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<Float>?
|
||||
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..<silentFrameSize {
|
||||
silentFrameBuffer?[i] = 0.0
|
||||
}
|
||||
|
||||
print("VorbisFileDecoder: Created processing format: \(processingFormat?.description ?? "nil")")
|
||||
print("VorbisFileDecoder: Created silent frame buffer with \(silentFrameSize) samples")
|
||||
} else {
|
||||
print("VorbisFileDecoder: Failed to get stream info")
|
||||
Logger.error("Failed to get stream info", category: .audioRendering)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,16 +151,12 @@ final class VorbisFileDecoder {
|
||||
decoderLock.lock()
|
||||
defer { decoderLock.unlock() }
|
||||
|
||||
readCalls += 1
|
||||
|
||||
guard let vf = vf, buffer.format.channelCount > 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..<min(20, framesRead * channels) {
|
||||
let level = abs(tempBuffer[i])
|
||||
if level > maxLevel {
|
||||
maxLevel = level
|
||||
}
|
||||
}
|
||||
print("VorbisFileDecoder: Max audio level in first 20 samples: \(maxLevel)")
|
||||
|
||||
// De-interleave into buffer
|
||||
for ch in 0..<min(Int(buffer.format.channelCount), channels) {
|
||||
let dst = floatChannelData[ch]
|
||||
@@ -222,9 +181,6 @@ final class VorbisFileDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
totalFramesRead += framesRead
|
||||
print("VorbisFileDecoder: Total frames read: \(totalFramesRead)")
|
||||
|
||||
return framesRead
|
||||
}
|
||||
|
||||
@@ -245,7 +201,6 @@ final class VorbisFileDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
print("VorbisFileDecoder: Generated \(framesToGenerate) silent frames")
|
||||
return framesToGenerate
|
||||
}
|
||||
|
||||
|
||||
@@ -90,6 +90,19 @@ open class AudioPlayer {
|
||||
guard let entry = playingEntry else { return 0 }
|
||||
return entry.framesPlayed
|
||||
}
|
||||
|
||||
/// Indicates whether seeking is supported for the currently playing audio
|
||||
///
|
||||
/// Returns `false` for Ogg Vorbis streams, `true` for other formats
|
||||
public var isSeekable: Bool {
|
||||
playerContext.entriesLock.lock()
|
||||
let playingEntry = playerContext.audioPlayingEntry
|
||||
playerContext.entriesLock.unlock()
|
||||
guard let entry = playingEntry else { return true }
|
||||
|
||||
// Check if this is an Ogg Vorbis file
|
||||
return entry.audioFileHint != kAudioFileOggType
|
||||
}
|
||||
|
||||
public private(set) var customAttachedNodes = [AVAudioNode]()
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@ final class AudioFileStreamProcessor {
|
||||
func closeFileStreamIfNeeded() {
|
||||
if isProcessingOggVorbis {
|
||||
isProcessingOggVorbis = false
|
||||
oggVorbisProcessor.cleanup()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,20 @@ final class OggVorbisStreamProcessor {
|
||||
/// The callback to notify when processing is complete or an error occurs
|
||||
var processorCallback: ((FileStreamProcessorEffect) -> 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
|
||||
|
||||
@@ -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<Float>?
|
||||
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..<Int(samplesDecoded) {
|
||||
for ch in 0..<channels {
|
||||
let value = pcm[i * channels + ch]
|
||||
output.append(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 != OGGVORBIS_SUCCESS {
|
||||
throw OggVorbisDecoderError(code: result)
|
||||
}
|
||||
|
||||
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 != OGGVORBIS_SUCCESS {
|
||||
throw OggVorbisDecoderError(code: result)
|
||||
}
|
||||
|
||||
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..<count {
|
||||
var keyPtr: UnsafePointer<Int8>?
|
||||
var valuePtr: UnsafePointer<Int8>?
|
||||
|
||||
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<Float>.size)
|
||||
|
||||
let data = UnsafeMutablePointer<Float>.allocate(capacity: pcmData.count)
|
||||
data.initialize(from: pcmData, count: pcmData.count)
|
||||
audioBuffer.mData = UnsafeMutableRawPointer(data)
|
||||
|
||||
return audioBuffer
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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+
|
||||
|
||||
Reference in New Issue
Block a user