AtlatestRepositorysigil-audio

sigil-audio / tree / src / caudio-stream.c

1/*
2 * audio-stream.c - Streaming audio sink
3 *
4 * SPSC lockless ring per stream. Producer = any Sigil thread (the one
5 * that calls push-audio-samples). Consumer = sokol_audio's stream
6 * 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 array
10 * 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, and
15 * 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 than
25 * <stdatomic.h> — zig cc's C++ include path on some distros shadows
26 * 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. */
34static inline int atm_cas_release(void *ptr, void *expected, void *desired)
36 return __atomic_compare_exchange_n((void **)ptr, (void **)expected,
37 desired, 0,
38 __ATOMIC_RELEASE, __ATOMIC_RELAXED);
41#define MAX_AUDIO_STREAMS 4
42#define MIN_BUFFER_FRAMES 64
43#define MAX_BUFFER_FRAMES (1 << 20) /* ~24 s at 44.1 kHz stereo */
45typedef 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. */
62static AudioStream *g_streams[MAX_AUDIO_STREAMS];
64static Value stream_type_tag = SIGIL_UNDEFINED;
66/* ------------------------------------------------------------------ */
67/* Helpers */
68/* ------------------------------------------------------------------ */
70static size_t round_up_pow2(size_t n)
72 if (n < 2) return 1;
73 size_t p = 1;
74 while (p < n) p <<= 1;
75 return p;
78static void ensure_stream_type(SigilVM *vm)
80 if (sigil_is_undefined(stream_type_tag)) {
81 stream_type_tag = sigil_intern_symbol(vm, "sigil-audio-stream", 18);
82 }
85static AudioStream *get_stream(SigilVM *vm, Value v)
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);
93static int registry_add(AudioStream *s)
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;
104static void registry_remove(AudioStream *s)
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 }
115static void stream_destructor(void *data)
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);
125/* ------------------------------------------------------------------ */
126/* Audio-thread mix */
127/* ------------------------------------------------------------------ */
129static void mix_one(AudioStream *s, float *out, int num_frames,
130 int out_channels)
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_samples
138 ? 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 }
180void sigil_audio_stream_mix_all(float *out, int num_frames, int out_channels)
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 }
190void sigil_audio_stream_shutdown_all(void)
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 }
201/* ------------------------------------------------------------------ */
202/* Natives */
203/* ------------------------------------------------------------------ */
205/*
206 * (%open-audio-stream channels buffer-frames volume) -> <audio-stream> or #f
207 *
208 * Sigil wrapper applies keyword args. Sample rate is fixed at sokol_audio's
209 * configured rate (44100) — caller must match.
210 */
211static Value native_open_audio_stream(SigilVM *vm, int argc, Value *args)
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));
262/*
263 * (audio-stream? obj) -> boolean
264 */
265static Value native_stream_p(SigilVM *vm, int argc, Value *args)
267 (void)argc;
268 return get_stream(vm, args[0]) ? SIGIL_TRUE : SIGIL_FALSE;
271/*
272 * (push-audio-samples stream pcm-bv frames) -> fixnum
273 *
274 * Non-blocking. Returns frames actually copied (may be < frames).
275 * Returns 0 on a closed stream.
276 */
277static Value native_push_audio_samples(SigilVM *vm, int argc, Value *args)
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));
332/* (audio-stream-room stream) -> fixnum — frames free */
333static Value native_stream_room(SigilVM *vm, int argc, Value *args)
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));
345/* (audio-stream-depth stream) -> fixnum — frames queued */
346static Value native_stream_depth(SigilVM *vm, int argc, Value *args)
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));
357/* (audio-stream-capacity stream) -> fixnum — total ring size in frames */
358static Value native_stream_capacity(SigilVM *vm, int argc, Value *args)
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);
366/* (audio-stream-channels stream) -> fixnum */
367static Value native_stream_channels(SigilVM *vm, int argc, Value *args)
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);
375/* (audio-stream-underruns stream) -> fixnum — cumulative underrun frames */
376static Value native_stream_underruns(SigilVM *vm, int argc, Value *args)
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);
386/* (audio-stream-closed? stream) -> boolean */
387static Value native_stream_closed_p(SigilVM *vm, int argc, Value *args)
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;
395/* (close-audio-stream! stream) — unregister; destructor frees on GC */
396static Value native_close_audio_stream(SigilVM *vm, int argc, Value *args)
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;
408/*
409 * (%audio-stream-drain-for-test out-bv num-frames out-channels) -> fixnum
410 *
411 * Test-only shim: simulates the audio thread by running the mix pass on
412 * a Sigil-provided float32 bytevector. Returns num-frames (nothing fancy
413 * — the caller checks sample values). Treats the bytevector as
414 * zero-initialized each call; the caller is responsible for clearing.
415 * Not exported from the Sigil library — %-prefix signals internal.
416 */
417static Value native_drain_for_test(SigilVM *vm, int argc, Value *args)
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);
440/*
441 * (%bv-f32-ref bv byte-offset) -> flonum
442 *
443 * Test-only: read a float32 from a bytevector at byte-offset, widened to
444 * a flonum. Sigil stdlib doesn't ship a float32 bytevector accessor yet.
445 */
446static Value native_bv_f32_ref(SigilVM *vm, int argc, Value *args)
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);
465/* (set-audio-stream-volume! stream volume) */
466static Value native_set_stream_volume(SigilVM *vm, int argc, Value *args)
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;
486/* ------------------------------------------------------------------ */
487/* Module registration */
488/* ------------------------------------------------------------------ */
490void sigil__register_audio_stream(SigilVM *vm)
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");