Adds support for Ogg Vorbis

This commit is contained in:
dimitris-c
2025-11-06 18:15:40 +02:00
parent 6b1dc644fe
commit fcba80cef7
20 changed files with 196 additions and 1484 deletions
-505
View File
@@ -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];
}
+89 -5
View File
@@ -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);
}
-1
View File
@@ -8,7 +8,6 @@
#ifndef AudioCodecs_h
#define AudioCodecs_h
#import "OggVorbisBridge.h"
#import "VorbisFileBridge.h"
#endif /* AudioCodecs_h */
-81
View File
@@ -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 */
+7
View File
@@ -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.
+2 -1
View File
@@ -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
}
}
-66
View File
@@ -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
+12 -4
View File
@@ -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+