Skip to content

Commit 45178a9

Browse files
authored
feature(Ogg): Adds OggVorbis playback support (#122)
* park * park - working * Adds support for Ogg Vorbis * fixes * fix rateNode issue * Update README to remove M4A limitation note Removed limitation note for non-optimized M4A files. * Update iOS requirement from 13.0 to 15.0 * improvements * removes debug message * improves `isSeekable` property * fix * Refactors project use pure Package.swift and swift test * Refactor GitHub Actions workflow for Swift package testing
1 parent 72028c2 commit 45178a9

36 files changed

Lines changed: 1412 additions & 1378 deletions

File tree

.github/workflows/swift.yml

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,14 @@ on:
1010
- '*'
1111

1212
jobs:
13-
iOS:
14-
name: Test iOS
13+
test:
14+
name: Test Swift Package
1515
runs-on: macOS-latest
1616
env:
1717
DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer
18-
strategy:
19-
matrix:
20-
destination: ["OS=latest,name=iPhone 17"]
2118
steps:
2219
- uses: actions/checkout@v2
23-
- name: iOS - ${{ matrix.destination }}
24-
run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project "AudioStreaming.xcodeproj" -scheme "AudioStreaming" -destination "${{ matrix.destination }}" clean test | xcpretty
20+
- name: Build
21+
run: swift build
22+
- name: Run tests
23+
run: swift test --parallel

.gitignore

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,11 @@ playground.xcworkspace
4141
# Packages/
4242
# Package.pins
4343
# Package.resolved
44-
# *.xcodeproj
44+
*.xcodeproj
4545
#
4646
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
4747
# hence it is not needed unless you have added a package configuration file to your project
48-
# .swiftpm
48+
.swiftpm
4949

5050
.build/
5151

@@ -58,7 +58,7 @@ playground.xcworkspace
5858
# Pods/
5959
#
6060
# Add this line if you want to avoid checking in source code from the Xcode workspace
61-
# *.xcworkspace
61+
*.xcworkspace
6262

6363
# Carthage
6464
#

AudioCodecs/VorbisFileBridge.c

Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
#include "include/VorbisFileBridge.h"
2+
3+
#include <stdlib.h>
4+
#include <string.h>
5+
#include <pthread.h>
6+
#include <vorbis/vorbisfile.h>
7+
8+
struct VFRemoteStream {
9+
uint8_t *buf;
10+
size_t cap, head, tail, size;
11+
int eof;
12+
long long pos; // Current read position in the stream
13+
long long total_pushed; // Total bytes pushed into the buffer
14+
pthread_mutex_t m;
15+
pthread_cond_t cv;
16+
};
17+
18+
// Simple ring buffer write
19+
static size_t rb_write(struct VFRemoteStream *s, const uint8_t *src, size_t len) {
20+
size_t written = 0;
21+
while (written < len) {
22+
size_t free_space = s->cap - s->size;
23+
if (free_space == 0) break;
24+
size_t chunk = s->cap - s->tail;
25+
if (chunk > len - written) chunk = len - written;
26+
if (chunk > free_space) chunk = free_space;
27+
memcpy(s->buf + s->tail, src + written, chunk);
28+
s->tail = (s->tail + chunk) % s->cap;
29+
s->size += chunk;
30+
written += chunk;
31+
}
32+
return written;
33+
}
34+
35+
// Simple ring buffer read
36+
static size_t rb_read(struct VFRemoteStream *s, uint8_t *dst, size_t len) {
37+
size_t read = 0;
38+
while (read < len && s->size > 0) {
39+
size_t chunk = s->cap - s->head;
40+
if (chunk > s->size) chunk = s->size;
41+
if (chunk > len - read) chunk = len - read;
42+
memcpy(dst + read, s->buf + s->head, chunk);
43+
s->head = (s->head + chunk) % s->cap;
44+
s->size -= chunk;
45+
read += chunk;
46+
}
47+
return read;
48+
}
49+
50+
// Create a stream buffer
51+
VFStreamRef VFStreamCreate(size_t capacity_bytes) {
52+
struct VFRemoteStream *s = (struct VFRemoteStream *)calloc(1, sizeof(struct VFRemoteStream));
53+
if (!s) return NULL;
54+
s->buf = (uint8_t *)malloc(capacity_bytes);
55+
if (!s->buf) { free(s); return NULL; }
56+
s->cap = capacity_bytes;
57+
pthread_mutex_init(&s->m, NULL);
58+
pthread_cond_init(&s->cv, NULL);
59+
return s;
60+
}
61+
62+
// Destroy a stream buffer
63+
void VFStreamDestroy(VFStreamRef sr) {
64+
struct VFRemoteStream *s = (struct VFRemoteStream *)sr;
65+
if (!s) return;
66+
pthread_mutex_destroy(&s->m);
67+
pthread_cond_destroy(&s->cv);
68+
free(s->buf);
69+
free(s);
70+
}
71+
72+
// Get available bytes in the buffer
73+
size_t VFStreamAvailableBytes(VFStreamRef sr) {
74+
struct VFRemoteStream *s = (struct VFRemoteStream *)sr;
75+
if (!s) return 0;
76+
pthread_mutex_lock(&s->m);
77+
size_t sz = s->size;
78+
pthread_mutex_unlock(&s->m);
79+
return sz;
80+
}
81+
82+
// Push data into the stream
83+
void VFStreamPush(VFStreamRef sr, const uint8_t *data, size_t len) {
84+
struct VFRemoteStream *s = (struct VFRemoteStream *)sr;
85+
if (!s || !data || len == 0) return;
86+
87+
pthread_mutex_lock(&s->m);
88+
size_t written_total = 0;
89+
while (written_total < len) {
90+
size_t w = rb_write(s, data + written_total, len - written_total);
91+
written_total += w;
92+
if (written_total < len) {
93+
// Buffer full, wait for consumer to read
94+
pthread_cond_wait(&s->cv, &s->m);
95+
}
96+
}
97+
s->total_pushed += (long long)len;
98+
pthread_cond_broadcast(&s->cv);
99+
pthread_mutex_unlock(&s->m);
100+
}
101+
102+
// Mark the stream as EOF
103+
void VFStreamMarkEOF(VFStreamRef sr) {
104+
struct VFRemoteStream *s = (struct VFRemoteStream *)sr;
105+
if (!s) return;
106+
pthread_mutex_lock(&s->m);
107+
s->eof = 1;
108+
pthread_cond_broadcast(&s->cv);
109+
pthread_mutex_unlock(&s->m);
110+
}
111+
112+
// libvorbisfile callbacks
113+
114+
// Read callback for libvorbisfile
115+
static size_t read_cb(void *ptr, size_t size, size_t nmemb, void *datasrc) {
116+
struct VFRemoteStream *s = (struct VFRemoteStream *)datasrc;
117+
size_t want_bytes = size * nmemb;
118+
size_t got = 0;
119+
120+
pthread_mutex_lock(&s->m);
121+
// Read what's available NOW - don't block waiting for more data
122+
while (got < want_bytes && s->size > 0) {
123+
size_t chunk = rb_read(s, (uint8_t *)ptr + got, want_bytes - got);
124+
s->pos += (long long)chunk;
125+
got += chunk;
126+
127+
if (chunk == 0) break;
128+
// Allow producer to push more
129+
pthread_cond_broadcast(&s->cv);
130+
}
131+
132+
// If nothing available and EOF, we're done
133+
if (got == 0 && s->eof) {
134+
// Return 0 to signal EOF to libvorbisfile
135+
}
136+
137+
pthread_mutex_unlock(&s->m);
138+
139+
return size ? (got / size) : 0;
140+
}
141+
142+
// Seek callback - seek within the ring buffer
143+
static int seek_cb(void *datasrc, ogg_int64_t offset, int whence) {
144+
struct VFRemoteStream *s = (struct VFRemoteStream *)datasrc;
145+
if (!s) return -1;
146+
147+
pthread_mutex_lock(&s->m);
148+
149+
ogg_int64_t new_pos = 0;
150+
switch (whence) {
151+
case SEEK_SET:
152+
new_pos = offset;
153+
break;
154+
case SEEK_CUR:
155+
new_pos = s->pos + offset;
156+
break;
157+
case SEEK_END:
158+
new_pos = s->total_pushed + offset;
159+
break;
160+
default:
161+
pthread_mutex_unlock(&s->m);
162+
return -1;
163+
}
164+
165+
// Check if the new position is valid (within available data)
166+
if (new_pos < 0 || new_pos > s->total_pushed) {
167+
pthread_mutex_unlock(&s->m);
168+
return -1; // Can't seek outside available data
169+
}
170+
171+
// Calculate how much data we've already consumed from the buffer
172+
long long already_consumed = s->pos - ((long long)s->total_pushed - (long long)s->size);
173+
174+
// Calculate the new head position
175+
long long pos_delta = new_pos - s->pos;
176+
177+
// For forward seeks, we need to have enough data in the buffer
178+
if (pos_delta > 0 && pos_delta > (long long)s->size) {
179+
pthread_mutex_unlock(&s->m);
180+
return -1; // Not enough data in buffer to seek forward
181+
}
182+
183+
// For backward seeks, check if that data is still in the buffer
184+
if (pos_delta < 0 && (-pos_delta) > already_consumed) {
185+
pthread_mutex_unlock(&s->m);
186+
return -1; // Data has been discarded from buffer
187+
}
188+
189+
// Adjust head pointer
190+
if (pos_delta >= 0) {
191+
// Forward seek: advance head
192+
s->head = (s->head + pos_delta) % s->cap;
193+
s->size -= (size_t)pos_delta;
194+
} else {
195+
// Backward seek: rewind head
196+
size_t rewind = (size_t)(-pos_delta);
197+
if (s->head >= rewind) {
198+
s->head -= rewind;
199+
} else {
200+
s->head = s->cap - (rewind - s->head);
201+
}
202+
s->size += rewind;
203+
}
204+
205+
s->pos = new_pos;
206+
pthread_mutex_unlock(&s->m);
207+
return 0;
208+
}
209+
210+
// Close callback - no-op
211+
static int close_cb(void *datasrc) {
212+
(void)datasrc;
213+
return 0;
214+
}
215+
216+
// Tell callback - return current position
217+
static long tell_cb(void *datasrc) {
218+
struct VFRemoteStream *s = (struct VFRemoteStream *)datasrc;
219+
return (long)s->pos;
220+
}
221+
222+
// Open a vorbis file using callbacks
223+
int VFOpen(VFStreamRef sr, VFFileRef *out_vf) {
224+
struct VFRemoteStream *s = (struct VFRemoteStream *)sr;
225+
if (!s || !out_vf) return -1;
226+
227+
OggVorbis_File *vf = (OggVorbis_File *)malloc(sizeof(OggVorbis_File));
228+
if (!vf) return -1;
229+
230+
ov_callbacks cbs;
231+
cbs.read_func = read_cb;
232+
cbs.seek_func = NULL; // Non-seekable streaming (seeking handled at Swift level)
233+
cbs.close_func = close_cb;
234+
cbs.tell_func = tell_cb;
235+
236+
int rc = ov_open_callbacks((void *)s, vf, NULL, 0, cbs);
237+
if (rc < 0) { free(vf); return rc; }
238+
239+
*out_vf = (VFFileRef)vf;
240+
return 0;
241+
}
242+
243+
// Clear a vorbis file
244+
void VFClear(VFFileRef fr) {
245+
OggVorbis_File *vf = (OggVorbis_File *)fr;
246+
if (!vf) return;
247+
ov_clear(vf);
248+
free(vf);
249+
}
250+
251+
// Get stream info
252+
int VFGetInfo(VFFileRef fr, VFStreamInfo *out_info) {
253+
OggVorbis_File *vf = (OggVorbis_File *)fr;
254+
if (!vf || !out_info) return -1;
255+
256+
vorbis_info const *info = ov_info(vf, -1);
257+
if (!info) return -1;
258+
259+
out_info->sample_rate = info->rate;
260+
out_info->channels = info->channels;
261+
out_info->total_pcm_samples = ov_pcm_total(vf, -1);
262+
out_info->duration_seconds = ov_time_total(vf, -1);
263+
out_info->bitrate_nominal = info->bitrate_nominal;
264+
265+
return 0;
266+
}
267+
268+
// Read deinterleaved float PCM frames
269+
long VFReadFloat(VFFileRef fr, float ***out_pcm, int max_frames) {
270+
OggVorbis_File *vf = (OggVorbis_File *)fr;
271+
if (!vf || !out_pcm || max_frames <= 0) return -1;
272+
273+
int bitstream = 0;
274+
long frames = ov_read_float(vf, out_pcm, max_frames, &bitstream);
275+
276+
// Returns: frames read (0 = EOF, <0 = error)
277+
return frames;
278+
}
279+
280+
// Read interleaved float PCM frames (legacy, less efficient)
281+
long VFReadInterleavedFloat(VFFileRef fr, float *dst, int max_frames) {
282+
OggVorbis_File *vf = (OggVorbis_File *)fr;
283+
if (!vf || !dst || max_frames <= 0) return -1;
284+
285+
int bitstream = 0;
286+
float **pcm = NULL;
287+
long frames = ov_read_float(vf, &pcm, max_frames, &bitstream);
288+
289+
if (frames <= 0) return frames; // 0 EOF, <0 error/hole
290+
291+
vorbis_info const *info = ov_info(vf, -1);
292+
int ch = info->channels;
293+
294+
// Interleave the PCM data
295+
for (long f = 0; f < frames; ++f) {
296+
for (int c = 0; c < ch; ++c) {
297+
dst[f * ch + c] = pcm[c][f];
298+
}
299+
}
300+
301+
return frames;
302+
}
303+
304+
// Seek to a specific time in seconds
305+
int VFSeekTime(VFFileRef fr, double time_seconds) {
306+
OggVorbis_File *vf = (OggVorbis_File *)fr;
307+
if (!vf) return -1;
308+
309+
// Use ov_time_seek for time-based seeking
310+
// Returns 0 on success, nonzero on failure
311+
return ov_time_seek(vf, time_seconds);
312+
}
313+
314+
// Check if the stream is seekable
315+
int VFIsSeekable(VFFileRef fr) {
316+
OggVorbis_File *vf = (OggVorbis_File *)fr;
317+
if (!vf) return 0;
318+
319+
// Returns nonzero if the stream is seekable
320+
return ov_seekable(vf);
321+
}

AudioCodecs/include/AudioCodecs.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//
2+
// AudioCodecs.h
3+
// AudioStreaming
4+
//
5+
// Created on 25/10/2025.
6+
//
7+
8+
#ifndef AudioCodecs_h
9+
#define AudioCodecs_h
10+
11+
#import "VorbisFileBridge.h"
12+
13+
#endif /* AudioCodecs_h */

0 commit comments

Comments
 (0)