AtlatestRepositorysigil-audio
sigil-audio / tree / src / caudio-stream.c
1
/*2
* audio-stream.c - Streaming audio sink3
*4
* SPSC lockless ring per stream. Producer = any Sigil thread (the one5
* that calls push-audio-samples). Consumer = sokol_audio's stream6
* callback thread. The ring stores interleaved float32 samples.7
*8
* Registry is a fixed-size array of active stream pointers. Open /9
* close mutate slots atomically; the audio callback iterates the array10
* under atomic acquire loads. Registry writes are rare (per open/close);11
* reads are per audio buffer. No mutex in the hot path.12
*13
* Capacity is rounded up to a power of two so index-wrap is a bitmask.14
* head/tail are free-running 64-bit counters; depth = head - tail, and15
* wrap of the 64-bit counter is a non-issue at audio rates.16
*/18
#include "audio-stream.h"20
#include <stdint.h>21
#include <stdlib.h>22
#include <string.h>24
/* Use GCC __atomic_* builtins (sigil convention) rather than25
* <stdatomic.h> — zig cc's C++ include path on some distros shadows26
* the C stdatomic header with a C++-only variant. */27
#define ATM_LOAD_ACQ(p) __atomic_load_n((p), __ATOMIC_ACQUIRE)28
#define ATM_LOAD_REL(p) __atomic_load_n((p), __ATOMIC_RELAXED)29
#define ATM_STORE_REL(p, v) __atomic_store_n((p), (v), __ATOMIC_RELEASE)30
#define ATM_STORE_RLX(p, v) __atomic_store_n((p), (v), __ATOMIC_RELAXED)31
#define ATM_ADD_RLX(p, v) __atomic_fetch_add((p), (v), __ATOMIC_RELAXED)32
#define ATM_XCHG_ACQREL(p, v) __atomic_exchange_n((p), (v), __ATOMIC_ACQ_REL)33
/* CAS: returns true on success, writes observed value to *expected on failure. */34
static inline int atm_cas_release(void *ptr, void *expected, void *desired)35
{36
return __atomic_compare_exchange_n((void **)ptr, (void **)expected,37
desired, 0,38
__ATOMIC_RELEASE, __ATOMIC_RELAXED);39
}41
#define MAX_AUDIO_STREAMS 442
#define MIN_BUFFER_FRAMES 6443
#define MAX_BUFFER_FRAMES (1 << 20) /* ~24 s at 44.1 kHz stereo */45
typedef struct AudioStream {46
float *ring; /* capacity_samples floats */47
size_t capacity_samples;/* power of two, = frames_pow2 * channels */48
size_t mask; /* capacity_samples - 1 */49
int channels; /* 1 or 2 */50
int buffer_frames; /* frames_pow2 (rounded up from request) */52
uint64_t head; /* producer write index (in samples) */53
uint64_t tail; /* consumer read index (in samples) */54
int closed;55
uint64_t underrun_frames;57
float volume; /* single-writer from the producer side;58
* audio thread reads non-atomically. */59
} AudioStream;61
/* Registry of active streams. Entries may be NULL. */62
static AudioStream *g_streams[MAX_AUDIO_STREAMS];64
static Value stream_type_tag = SIGIL_UNDEFINED;66
/* ------------------------------------------------------------------ */67
/* Helpers */68
/* ------------------------------------------------------------------ */70
static size_t round_up_pow2(size_t n)71
{72
if (n < 2) return 1;73
size_t p = 1;74
while (p < n) p <<= 1;75
return p;76
}78
static void ensure_stream_type(SigilVM *vm)79
{80
if (sigil_is_undefined(stream_type_tag)) {81
stream_type_tag = sigil_intern_symbol(vm, "sigil-audio-stream", 18);82
}83
}85
static AudioStream *get_stream(SigilVM *vm, Value v)86
{87
if (!sigil_is_foreign(v)) return NULL;88
ensure_stream_type(vm);89
if (sigil_foreign_type(v) != stream_type_tag) return NULL;90
return (AudioStream *)sigil_foreign_data(v);91
}93
static int registry_add(AudioStream *s)94
{95
for (int i = 0; i < MAX_AUDIO_STREAMS; i++) {96
AudioStream *expected = NULL;97
if (atm_cas_release(&g_streams[i], &expected, s)) {98
return i;99
}100
}101
return -1;102
}104
static void registry_remove(AudioStream *s)105
{106
for (int i = 0; i < MAX_AUDIO_STREAMS; i++) {107
AudioStream *cur = ATM_LOAD_REL(&g_streams[i]);108
if (cur == s) {109
ATM_STORE_REL(&g_streams[i], (AudioStream *)NULL);110
return;111
}112
}113
}115
static void stream_destructor(void *data)116
{117
AudioStream *s = (AudioStream *)data;118
if (!s) return;119
ATM_STORE_REL(&s->closed, 1);120
registry_remove(s);121
free(s->ring);122
free(s);123
}125
/* ------------------------------------------------------------------ */126
/* Audio-thread mix */127
/* ------------------------------------------------------------------ */129
static void mix_one(AudioStream *s, float *out, int num_frames,130
int out_channels)131
{132
uint64_t head = ATM_LOAD_ACQ(&s->head);133
uint64_t tail = ATM_LOAD_REL(&s->tail);135
size_t available_samples = (size_t)(head - tail);136
size_t wanted_samples = (size_t)num_frames * (size_t)s->channels;137
size_t drain_samples = available_samples < wanted_samples138
? available_samples : wanted_samples;139
size_t drain_frames = drain_samples / (size_t)s->channels;141
float vol = s->volume;143
if (s->channels == 2 && out_channels >= 2) {144
for (size_t f = 0; f < drain_frames; f++) {145
float l = s->ring[(tail + 0) & s->mask];146
float r = s->ring[(tail + 1) & s->mask];147
out[f * out_channels + 0] += l * vol;148
out[f * out_channels + 1] += r * vol;149
tail += 2;150
}151
} else if (s->channels == 1 && out_channels >= 2) {152
for (size_t f = 0; f < drain_frames; f++) {153
float v = s->ring[tail & s->mask] * vol;154
out[f * out_channels + 0] += v;155
out[f * out_channels + 1] += v;156
tail += 1;157
}158
} else if (s->channels == 1 && out_channels == 1) {159
for (size_t f = 0; f < drain_frames; f++) {160
out[f] += s->ring[tail & s->mask] * vol;161
tail += 1;162
}163
} else { /* channels == 2, out == 1: downmix */164
for (size_t f = 0; f < drain_frames; f++) {165
float l = s->ring[(tail + 0) & s->mask];166
float r = s->ring[(tail + 1) & s->mask];167
out[f] += (l + r) * 0.5f * vol;168
tail += 2;169
}170
}172
ATM_STORE_REL(&s->tail, tail);174
if (drain_frames < (size_t)num_frames) {175
uint64_t missing = (uint64_t)(num_frames - (int)drain_frames);176
ATM_ADD_RLX(&s->underrun_frames, missing);177
}178
}180
void sigil_audio_stream_mix_all(float *out, int num_frames, int out_channels)181
{182
for (int i = 0; i < MAX_AUDIO_STREAMS; i++) {183
AudioStream *s = ATM_LOAD_ACQ(&g_streams[i]);184
if (!s) continue;185
if (ATM_LOAD_ACQ(&s->closed)) continue;186
mix_one(s, out, num_frames, out_channels);187
}188
}190
void sigil_audio_stream_shutdown_all(void)191
{192
for (int i = 0; i < MAX_AUDIO_STREAMS; i++) {193
AudioStream *s = ATM_LOAD_REL(&g_streams[i]);194
if (s) {195
ATM_STORE_REL(&s->closed, 1);196
ATM_STORE_REL(&g_streams[i], (AudioStream *)NULL);197
}198
}199
}201
/* ------------------------------------------------------------------ */202
/* Natives */203
/* ------------------------------------------------------------------ */205
/*206
* (%open-audio-stream channels buffer-frames volume) -> <audio-stream> or #f207
*208
* Sigil wrapper applies keyword args. Sample rate is fixed at sokol_audio's209
* configured rate (44100) — caller must match.210
*/211
static Value native_open_audio_stream(SigilVM *vm, int argc, Value *args)212
{213
if (argc < 3) {214
sigil__vm_set_error(vm, SIGIL_ERR_ARITY,215
"%open-audio-stream: channels buffer-frames volume");216
return SIGIL_FALSE;217
}218
int channels = (int)sigil_as_fixnum(args[0]);219
int buffer_frames = (int)sigil_as_fixnum(args[1]);220
float volume = (float)sigil_as_flonum(args[2]);222
if (channels != 1 && channels != 2) {223
sigil__vm_set_error(vm, SIGIL_ERR_TYPE,224
"open-audio-stream: channels must be 1 or 2");225
return SIGIL_FALSE;226
}227
if (buffer_frames < MIN_BUFFER_FRAMES) buffer_frames = MIN_BUFFER_FRAMES;228
if (buffer_frames > MAX_BUFFER_FRAMES) buffer_frames = MAX_BUFFER_FRAMES;230
size_t frames_pow2 = round_up_pow2((size_t)buffer_frames);231
size_t cap_samples = frames_pow2 * (size_t)channels;233
AudioStream *s = calloc(1, sizeof(*s));234
if (!s) return SIGIL_FALSE;236
s->ring = calloc(cap_samples, sizeof(float));237
if (!s->ring) { free(s); return SIGIL_FALSE; }239
s->capacity_samples = cap_samples;240
s->mask = cap_samples - 1;241
s->channels = channels;242
s->buffer_frames = (int)frames_pow2;243
s->volume = volume;244
s->head = 0;245
s->tail = 0;246
s->closed = 0;247
s->underrun_frames = 0;249
if (registry_add(s) < 0) {250
free(s->ring);251
free(s);252
sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,253
"open-audio-stream: too many active streams");254
return SIGIL_FALSE;255
}257
ensure_stream_type(vm);258
return sigil_make_foreign(vm, stream_type_tag, s, stream_destructor,259
sizeof(AudioStream) + cap_samples * sizeof(float));260
}262
/*263
* (audio-stream? obj) -> boolean264
*/265
static Value native_stream_p(SigilVM *vm, int argc, Value *args)266
{267
(void)argc;268
return get_stream(vm, args[0]) ? SIGIL_TRUE : SIGIL_FALSE;269
}271
/*272
* (push-audio-samples stream pcm-bv frames) -> fixnum273
*274
* Non-blocking. Returns frames actually copied (may be < frames).275
* Returns 0 on a closed stream.276
*/277
static Value native_push_audio_samples(SigilVM *vm, int argc, Value *args)278
{279
if (argc < 3) {280
sigil__vm_set_error(vm, SIGIL_ERR_ARITY,281
"push-audio-samples: stream pcm-bv frames");282
return sigil_fixnum(0);283
}284
AudioStream *s = get_stream(vm, args[0]);285
if (!s) {286
sigil__vm_set_error(vm, SIGIL_ERR_TYPE,287
"push-audio-samples: expected audio-stream");288
return sigil_fixnum(0);289
}290
if (!sigil_is_bytevector(args[1])) {291
sigil__vm_set_error(vm, SIGIL_ERR_TYPE,292
"push-audio-samples: expected bytevector");293
return sigil_fixnum(0);294
}295
int64_t req_frames_i = sigil_as_fixnum(args[2]);296
if (req_frames_i < 0) req_frames_i = 0;298
if (ATM_LOAD_ACQ(&s->closed)) {299
return sigil_fixnum(0);300
}302
size_t req_frames = (size_t)req_frames_i;303
size_t req_samples = req_frames * (size_t)s->channels;305
/* Bytevector must hold at least req_samples * 4 bytes. */306
size_t bv_bytes = sigil_bytevector_length(args[1]);307
if (req_samples * sizeof(float) > bv_bytes) {308
req_samples = bv_bytes / sizeof(float);309
req_samples -= req_samples % (size_t)s->channels; /* frame-align */310
req_frames = req_samples / (size_t)s->channels;311
}313
uint64_t head = ATM_LOAD_REL(&s->head);314
uint64_t tail = ATM_LOAD_ACQ(&s->tail);315
size_t in_use = (size_t)(head - tail);316
size_t room = s->capacity_samples - in_use;318
size_t write_samples = req_samples < room ? req_samples : room;319
/* Whole-frame alignment. */320
write_samples -= write_samples % (size_t)s->channels;322
const float *src = (const float *)sigil_bytevector_data(args[1]);323
for (size_t i = 0; i < write_samples; i++) {324
s->ring[(head + i) & s->mask] = src[i];325
}327
ATM_STORE_REL(&s->head, head + write_samples);329
return sigil_fixnum((int64_t)(write_samples / (size_t)s->channels));330
}332
/* (audio-stream-room stream) -> fixnum — frames free */333
static Value native_stream_room(SigilVM *vm, int argc, Value *args)334
{335
(void)argc;336
AudioStream *s = get_stream(vm, args[0]);337
if (!s) return sigil_fixnum(0);338
uint64_t head = ATM_LOAD_REL(&s->head);339
uint64_t tail = ATM_LOAD_ACQ(&s->tail);340
size_t in_use = (size_t)(head - tail);341
size_t room_samples = s->capacity_samples - in_use;342
return sigil_fixnum((int64_t)(room_samples / (size_t)s->channels));343
}345
/* (audio-stream-depth stream) -> fixnum — frames queued */346
static Value native_stream_depth(SigilVM *vm, int argc, Value *args)347
{348
(void)argc;349
AudioStream *s = get_stream(vm, args[0]);350
if (!s) return sigil_fixnum(0);351
uint64_t head = ATM_LOAD_ACQ(&s->head);352
uint64_t tail = ATM_LOAD_ACQ(&s->tail);353
size_t in_use = (size_t)(head - tail);354
return sigil_fixnum((int64_t)(in_use / (size_t)s->channels));355
}357
/* (audio-stream-capacity stream) -> fixnum — total ring size in frames */358
static Value native_stream_capacity(SigilVM *vm, int argc, Value *args)359
{360
(void)argc;361
AudioStream *s = get_stream(vm, args[0]);362
if (!s) return sigil_fixnum(0);363
return sigil_fixnum((int64_t)s->buffer_frames);364
}366
/* (audio-stream-channels stream) -> fixnum */367
static Value native_stream_channels(SigilVM *vm, int argc, Value *args)368
{369
(void)argc;370
AudioStream *s = get_stream(vm, args[0]);371
if (!s) return sigil_fixnum(0);372
return sigil_fixnum((int64_t)s->channels);373
}375
/* (audio-stream-underruns stream) -> fixnum — cumulative underrun frames */376
static Value native_stream_underruns(SigilVM *vm, int argc, Value *args)377
{378
(void)argc;379
AudioStream *s = get_stream(vm, args[0]);380
if (!s) return sigil_fixnum(0);381
uint64_t u = ATM_LOAD_REL(&s->underrun_frames);382
if (u > (uint64_t)SIGIL_FIXNUM_MAX) u = (uint64_t)SIGIL_FIXNUM_MAX;383
return sigil_fixnum((int64_t)u);384
}386
/* (audio-stream-closed? stream) -> boolean */387
static Value native_stream_closed_p(SigilVM *vm, int argc, Value *args)388
{389
(void)argc;390
AudioStream *s = get_stream(vm, args[0]);391
if (!s) return SIGIL_TRUE;392
return ATM_LOAD_ACQ(&s->closed) ? SIGIL_TRUE : SIGIL_FALSE;393
}395
/* (close-audio-stream! stream) — unregister; destructor frees on GC */396
static Value native_close_audio_stream(SigilVM *vm, int argc, Value *args)397
{398
(void)argc;399
AudioStream *s = get_stream(vm, args[0]);400
if (!s) return SIGIL_NIL;401
if (ATM_XCHG_ACQREL(&s->closed, 1)) {402
return SIGIL_NIL;403
}404
registry_remove(s);405
return SIGIL_NIL;406
}408
/*409
* (%audio-stream-drain-for-test out-bv num-frames out-channels) -> fixnum410
*411
* Test-only shim: simulates the audio thread by running the mix pass on412
* a Sigil-provided float32 bytevector. Returns num-frames (nothing fancy413
* — the caller checks sample values). Treats the bytevector as414
* zero-initialized each call; the caller is responsible for clearing.415
* Not exported from the Sigil library — %-prefix signals internal.416
*/417
static Value native_drain_for_test(SigilVM *vm, int argc, Value *args)418
{419
if (argc < 3 || !sigil_is_bytevector(args[0])) {420
sigil__vm_set_error(vm, SIGIL_ERR_TYPE,421
"%audio-stream-drain-for-test: out-bv frames channels");422
return sigil_fixnum(0);423
}424
float *out = (float *)sigil_bytevector_data(args[0]);425
size_t bv_bytes = sigil_bytevector_length(args[0]);426
int num_frames = (int)sigil_as_fixnum(args[1]);427
int out_channels = (int)sigil_as_fixnum(args[2]);429
size_t need_bytes = (size_t)num_frames * (size_t)out_channels * sizeof(float);430
if (need_bytes > bv_bytes) {431
sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,432
"%audio-stream-drain-for-test: bytevector too small");433
return sigil_fixnum(0);434
}435
memset(out, 0, need_bytes);436
sigil_audio_stream_mix_all(out, num_frames, out_channels);437
return sigil_fixnum((int64_t)num_frames);438
}440
/*441
* (%bv-f32-ref bv byte-offset) -> flonum442
*443
* Test-only: read a float32 from a bytevector at byte-offset, widened to444
* a flonum. Sigil stdlib doesn't ship a float32 bytevector accessor yet.445
*/446
static Value native_bv_f32_ref(SigilVM *vm, int argc, Value *args)447
{448
if (argc < 2 || !sigil_is_bytevector(args[0])) {449
sigil__vm_set_error(vm, SIGIL_ERR_TYPE,450
"%bv-f32-ref: bv offset");451
return sigil_flonum(0.0);452
}453
int64_t off = sigil_as_fixnum(args[1]);454
size_t bv_bytes = sigil_bytevector_length(args[0]);455
if (off < 0 || (size_t)off + sizeof(float) > bv_bytes) {456
sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,457
"%bv-f32-ref: offset out of bounds");458
return sigil_flonum(0.0);459
}460
float f;461
memcpy(&f, sigil_bytevector_data(args[0]) + off, sizeof(f));462
return sigil_flonum((double)f);463
}465
/* (set-audio-stream-volume! stream volume) */466
static Value native_set_stream_volume(SigilVM *vm, int argc, Value *args)467
{468
if (argc < 2) {469
sigil__vm_set_error(vm, SIGIL_ERR_ARITY,470
"set-audio-stream-volume!: stream volume");471
return SIGIL_NIL;472
}473
AudioStream *s = get_stream(vm, args[0]);474
if (!s) {475
sigil__vm_set_error(vm, SIGIL_ERR_TYPE,476
"set-audio-stream-volume!: expected audio-stream");477
return SIGIL_NIL;478
}479
float v = (float)sigil_as_flonum(args[1]);480
if (v < 0.0f) v = 0.0f;481
if (v > 1.0f) v = 1.0f;482
s->volume = v; /* audio thread reads non-atomically; single-writer producer */483
return SIGIL_NIL;484
}486
/* ------------------------------------------------------------------ */487
/* Module registration */488
/* ------------------------------------------------------------------ */490
void sigil__register_audio_stream(SigilVM *vm)491
{492
sigil_module_register_native(vm, "%open-audio-stream",493
native_open_audio_stream, SIGIL_ARITY_EXACT(3),494
"Open a streaming audio sink (internal)");495
sigil_module_register_native(vm, "audio-stream?",496
native_stream_p, SIGIL_ARITY_EXACT(1),497
"Check if object is an audio-stream");498
sigil_module_register_native(vm, "push-audio-samples",499
native_push_audio_samples, SIGIL_ARITY_EXACT(3),500
"Push float32 PCM frames to a stream; returns frames accepted");501
sigil_module_register_native(vm, "audio-stream-room",502
native_stream_room, SIGIL_ARITY_EXACT(1),503
"Frames free in the stream ring");504
sigil_module_register_native(vm, "audio-stream-depth",505
native_stream_depth, SIGIL_ARITY_EXACT(1),506
"Frames queued, not yet consumed");507
sigil_module_register_native(vm, "audio-stream-capacity",508
native_stream_capacity, SIGIL_ARITY_EXACT(1),509
"Total ring size in frames (power of two)");510
sigil_module_register_native(vm, "audio-stream-channels",511
native_stream_channels, SIGIL_ARITY_EXACT(1),512
"Stream channel count");513
sigil_module_register_native(vm, "audio-stream-underruns",514
native_stream_underruns, SIGIL_ARITY_EXACT(1),515
"Cumulative underrun frames (debug)");516
sigil_module_register_native(vm, "audio-stream-closed?",517
native_stream_closed_p, SIGIL_ARITY_EXACT(1),518
"Is the stream closed?");519
sigil_module_register_native(vm, "close-audio-stream!",520
native_close_audio_stream, SIGIL_ARITY_EXACT(1),521
"Close an audio stream (idempotent)");522
sigil_module_register_native(vm, "set-audio-stream-volume!",523
native_set_stream_volume, SIGIL_ARITY_EXACT(2),524
"Set per-stream volume (0.0-1.0)");525
sigil_module_register_native(vm, "%audio-stream-drain-for-test",526
native_drain_for_test, SIGIL_ARITY_EXACT(3),527
"Internal: simulate audio-thread drain into a bytevector");528
sigil_module_register_native(vm, "%bv-f32-ref",529
native_bv_f32_ref, SIGIL_ARITY_EXACT(2),530
"Internal: read float32 from bytevector at byte offset");532
sigil_module_export(vm, "%open-audio-stream");533
sigil_module_export(vm, "audio-stream?");534
sigil_module_export(vm, "push-audio-samples");535
sigil_module_export(vm, "audio-stream-room");536
sigil_module_export(vm, "audio-stream-depth");537
sigil_module_export(vm, "audio-stream-capacity");538
sigil_module_export(vm, "audio-stream-channels");539
sigil_module_export(vm, "audio-stream-underruns");540
sigil_module_export(vm, "audio-stream-closed?");541
sigil_module_export(vm, "close-audio-stream!");542
sigil_module_export(vm, "set-audio-stream-volume!");543
sigil_module_export(vm, "%audio-stream-drain-for-test");544
sigil_module_export(vm, "%bv-f32-ref");545
}