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 = the playback device'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 */
45/* Rate changes a stream may have pending at once (a power of two). */
46#define RATE_QUEUE 8
48/* A sample above this (after the stream's volume) is sound: -80 dB. */
49#define SILENCE 1e-4f
50/* Frames a loud sample keeps the stream "sounding": a dry run that starts
51 * within them cut sound off, an audible gap; one that starts later is a
52 * quiet stream running dry, which nobody hears. */
53#define LOUD_HOLD 128
55typedef struct RateChange {
56 uint64_t at; /* the producer's head (samples) when asked */
57 double ratio; /* stream rate / device rate, in (0, 1] */
58} RateChange;
60typedef struct AudioStream {
61 float *ring; /* capacity_samples floats */
62 size_t capacity_samples;/* power of two, = frames_pow2 * channels */
63 size_t mask; /* capacity_samples - 1 */
64 int channels; /* 1 or 2 */
65 int buffer_frames; /* frames_pow2 (rounded up from request) */
67 uint64_t head; /* producer write index (in samples) */
68 uint64_t tail; /* consumer read index (in samples) */
69 int closed;
70 uint64_t underrun_frames;
71 uint64_t underrun_sounding; /* of those, the frames that cut off sound */
73 float volume; /* single-writer from the producer side;
74 * audio thread reads non-atomically. */
76 /* The rate the producer feeds at (producer side, for audio-stream-rate),
77 * and the pending changes: each lands when the consumer's tail reaches
78 * the head it was asked at. SPSC: the producer owns q_head, the audio
79 * thread q_tail. */
80 int rate;
81 RateChange q[RATE_QUEUE];
82 uint32_t q_head;
83 uint32_t q_tail;
85 /* Drain state, audio thread only: the upsampler's ratio (the stream's
86 * rate / the device's), fractional position and the frame before the
87 * read position; the loudness hold and the current dry run's class. */
88 double ratio;
89 double frac;
90 float y0l, y0r;
91 int loud;
92 int dry;
93 int run_sounding;
94} AudioStream;
96/* Registry of active streams. Entries may be NULL. */
97static AudioStream *g_streams[MAX_AUDIO_STREAMS];
99static Value stream_type_tag = SIGIL_UNDEFINED;
101/* ------------------------------------------------------------------ */
102/* Helpers */
103/* ------------------------------------------------------------------ */
105static size_t round_up_pow2(size_t n)
107 if (n < 2) return 1;
108 size_t p = 1;
109 while (p < n) p <<= 1;
110 return p;
113static void ensure_stream_type(SigilVM *vm)
115 if (sigil_is_undefined(stream_type_tag)) {
116 stream_type_tag = sigil_intern_symbol(vm, "sigil-audio-stream", 18);
117 }
120static AudioStream *get_stream(SigilVM *vm, Value v)
122 if (!sigil_is_foreign(v)) return NULL;
123 ensure_stream_type(vm);
124 if (sigil_foreign_type(v) != stream_type_tag) return NULL;
125 return (AudioStream *)sigil_foreign_data(v);
128static int registry_add(AudioStream *s)
130 for (int i = 0; i < MAX_AUDIO_STREAMS; i++) {
131 AudioStream *expected = NULL;
132 if (atm_cas_release(&g_streams[i], &expected, s)) {
133 return i;
134 }
135 }
136 return -1;
139static void registry_remove(AudioStream *s)
141 for (int i = 0; i < MAX_AUDIO_STREAMS; i++) {
142 AudioStream *cur = ATM_LOAD_REL(&g_streams[i]);
143 if (cur == s) {
144 ATM_STORE_REL(&g_streams[i], (AudioStream *)NULL);
145 return;
146 }
147 }
150static void stream_destructor(void *data)
152 AudioStream *s = (AudioStream *)data;
153 if (!s) return;
154 ATM_STORE_REL(&s->closed, 1);
155 registry_remove(s);
156 free(s->ring);
157 free(s);
160/* ------------------------------------------------------------------ */
161/* Audio-thread mix */
162/* ------------------------------------------------------------------ */
164/* One stream into the device buffer, additively. At the device's rate the
165 * ring is copied sample for sample; below it, upsampled with a 4-tap cubic
166 * Hermite over the frame before the read position and the three from it on
167 * (the same drain as the web sink's). Starved frames are counted, and split
168 * into sounding and idle by the level of what played just before the run. */
169static void mix_one(AudioStream *s, float *out, int num_frames,
170 int out_channels)
172 uint64_t head = ATM_LOAD_ACQ(&s->head);
173 uint64_t tail = ATM_LOAD_REL(&s->tail);
174 const int ch = s->channels;
175 const float *ring = s->ring;
176 const size_t mask = s->mask;
177 uint64_t avail = (head - tail) / (uint64_t)ch; /* frames */
178 float vol = s->volume;
180 double ratio = s->ratio, frac = s->frac;
181 float y0l = s->y0l, y0r = s->y0r;
182 int loud = s->loud, dry = s->dry, run_sounding = s->run_sounding;
183 uint64_t under = 0, under_sounding = 0;
185 for (int f = 0; f < num_frames; f++) {
186 /* a rate change lands when the read position reaches its head */
187 uint32_t qt = s->q_tail;
188 if (qt != ATM_LOAD_ACQ(&s->q_head)) {
189 const RateChange *c = &s->q[qt & (RATE_QUEUE - 1)];
190 if (tail >= c->at) {
191 ratio = c->ratio;
192 if (ratio == 1.0) frac = 0.0;
193 ATM_STORE_REL(&s->q_tail, qt + 1);
194 }
195 }
196 float l = 0.0f, r = 0.0f;
197 int played = 0;
198 if (ratio == 1.0) {
199 if (avail > 0) {
200 y0l = ring[tail & mask];
201 y0r = ch == 2 ? ring[(tail + 1) & mask] : y0l;
202 l = y0l;
203 r = y0r;
204 tail += (uint64_t)ch;
205 avail--;
206 played = 1;
207 }
208 } else if (avail >= 3) {
209 const float *p1 = &ring[tail & mask];
210 const float *p2 = &ring[(tail + (uint64_t)ch) & mask];
211 const float *p3 = &ring[(tail + 2 * (uint64_t)ch) & mask];
212 double t = frac;
213 double a = -0.5 * y0l + 1.5 * p1[0] - 1.5 * p2[0] + 0.5 * p3[0];
214 double b = y0l - 2.5 * p1[0] + 2.0 * p2[0] - 0.5 * p3[0];
215 double c = -0.5 * y0l + 0.5 * p2[0];
216 l = (float)(((a * t + b) * t + c) * t + p1[0]);
217 if (ch == 2) {
218 const float q1 = ring[(tail + 1) & mask];
219 const float q2 = ring[(tail + (uint64_t)ch + 1) & mask];
220 const float q3 = ring[(tail + 2 * (uint64_t)ch + 1) & mask];
221 a = -0.5 * y0r + 1.5 * q1 - 1.5 * q2 + 0.5 * q3;
222 b = y0r - 2.5 * q1 + 2.0 * q2 - 0.5 * q3;
223 c = -0.5 * y0r + 0.5 * q2;
224 r = (float)(((a * t + b) * t + c) * t + q1);
225 } else {
226 r = l;
227 }
228 frac += ratio;
229 while (frac >= 1.0) {
230 y0l = ring[tail & mask];
231 y0r = ch == 2 ? ring[(tail + 1) & mask] : y0l;
232 tail += (uint64_t)ch;
233 avail--;
234 frac -= 1.0;
235 }
236 played = 1;
237 }
238 if (played) {
239 l *= vol;
240 r *= vol;
241 dry = 0;
242 if (l > SILENCE || l < -SILENCE || r > SILENCE || r < -SILENCE) loud = LOUD_HOLD;
243 else if (loud > 0) loud--;
244 if (out_channels >= 2) {
245 out[f * out_channels + 0] += l;
246 out[f * out_channels + 1] += r;
247 } else {
248 out[f] += ch == 2 ? (l + r) * 0.5f : l;
249 }
250 } else {
251 if (!dry) {
252 dry = 1;
253 run_sounding = loud > 0;
254 }
255 under++;
256 if (run_sounding) under_sounding++;
257 }
258 }
260 ATM_STORE_REL(&s->tail, tail);
261 s->ratio = ratio;
262 s->frac = frac;
263 s->y0l = y0l;
264 s->y0r = y0r;
265 s->loud = loud;
266 s->dry = dry;
267 s->run_sounding = run_sounding;
268 if (under) ATM_ADD_RLX(&s->underrun_frames, under);
269 if (under_sounding) ATM_ADD_RLX(&s->underrun_sounding, under_sounding);
272void sigil_audio_stream_mix_all(float *out, int num_frames, int out_channels)
274 for (int i = 0; i < MAX_AUDIO_STREAMS; i++) {
275 AudioStream *s = ATM_LOAD_ACQ(&g_streams[i]);
276 if (!s) continue;
277 if (ATM_LOAD_ACQ(&s->closed)) continue;
278 mix_one(s, out, num_frames, out_channels);
279 }
282void sigil_audio_stream_shutdown_all(void)
284 for (int i = 0; i < MAX_AUDIO_STREAMS; i++) {
285 AudioStream *s = ATM_LOAD_REL(&g_streams[i]);
286 if (s) {
287 ATM_STORE_REL(&s->closed, 1);
288 ATM_STORE_REL(&g_streams[i], (AudioStream *)NULL);
289 }
290 }
293/* ------------------------------------------------------------------ */
294/* Natives */
295/* ------------------------------------------------------------------ */
297/*
298 * (%open-audio-stream channels buffer-frames volume) -> <audio-stream> or #f
299 *
300 * Sigil wrapper applies keyword args. Sample rate is fixed at the device's
301 * configured rate (44100) — caller must match.
302 */
303static Value native_open_audio_stream(SigilVM *vm, int argc, Value *args)
305 if (argc < 3) {
306 sigil__vm_set_error(vm, SIGIL_ERR_ARITY,
307 "%open-audio-stream: channels buffer-frames volume");
308 return SIGIL_FALSE;
309 }
310 int channels = (int)sigil_as_fixnum(args[0]);
311 int buffer_frames = (int)sigil_as_fixnum(args[1]);
312 float volume = (float)sigil_as_flonum(args[2]);
314 if (channels != 1 && channels != 2) {
315 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
316 "open-audio-stream: channels must be 1 or 2");
317 return SIGIL_FALSE;
318 }
319 if (buffer_frames < MIN_BUFFER_FRAMES) buffer_frames = MIN_BUFFER_FRAMES;
320 if (buffer_frames > MAX_BUFFER_FRAMES) buffer_frames = MAX_BUFFER_FRAMES;
322 size_t frames_pow2 = round_up_pow2((size_t)buffer_frames);
323 size_t cap_samples = frames_pow2 * (size_t)channels;
325 AudioStream *s = calloc(1, sizeof(*s));
326 if (!s) return SIGIL_FALSE;
328 s->ring = calloc(cap_samples, sizeof(float));
329 if (!s->ring) { free(s); return SIGIL_FALSE; }
331 s->capacity_samples = cap_samples;
332 s->mask = cap_samples - 1;
333 s->channels = channels;
334 s->buffer_frames = (int)frames_pow2;
335 s->volume = volume;
336 s->head = 0;
337 s->tail = 0;
338 s->closed = 0;
339 s->underrun_frames = 0;
340 s->underrun_sounding = 0;
341 s->rate = SIGIL_AUDIO_DEVICE_RATE;
342 s->q_head = 0;
343 s->q_tail = 0;
344 s->ratio = 1.0;
345 s->frac = 0.0;
346 s->y0l = s->y0r = 0.0f;
347 s->loud = 0;
348 s->dry = 0;
349 s->run_sounding = 0;
351 if (registry_add(s) < 0) {
352 free(s->ring);
353 free(s);
354 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
355 "open-audio-stream: too many active streams");
356 return SIGIL_FALSE;
357 }
359 ensure_stream_type(vm);
360 return sigil_make_foreign(vm, stream_type_tag, s, stream_destructor,
361 sizeof(AudioStream) + cap_samples * sizeof(float));
364/*
365 * (audio-stream? obj) -> boolean
366 */
367static Value native_stream_p(SigilVM *vm, int argc, Value *args)
369 (void)argc;
370 return get_stream(vm, args[0]) ? SIGIL_TRUE : SIGIL_FALSE;
373/*
374 * (push-audio-samples stream pcm-bv frames) -> fixnum
375 *
376 * Non-blocking. Returns frames actually copied (may be < frames).
377 * Returns 0 on a closed stream.
378 */
379static Value native_push_audio_samples(SigilVM *vm, int argc, Value *args)
381 if (argc < 3) {
382 sigil__vm_set_error(vm, SIGIL_ERR_ARITY,
383 "push-audio-samples: stream pcm-bv frames");
384 return sigil_fixnum(0);
385 }
386 AudioStream *s = get_stream(vm, args[0]);
387 if (!s) {
388 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
389 "push-audio-samples: expected audio-stream");
390 return sigil_fixnum(0);
391 }
392 if (!sigil_is_bytevector(args[1])) {
393 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
394 "push-audio-samples: expected bytevector");
395 return sigil_fixnum(0);
396 }
397 int64_t req_frames_i = sigil_as_fixnum(args[2]);
398 if (req_frames_i < 0) req_frames_i = 0;
400 if (ATM_LOAD_ACQ(&s->closed)) {
401 return sigil_fixnum(0);
402 }
404 size_t req_frames = (size_t)req_frames_i;
405 size_t req_samples = req_frames * (size_t)s->channels;
407 /* Bytevector must hold at least req_samples * 4 bytes. */
408 size_t bv_bytes = sigil_bytevector_length(args[1]);
409 if (req_samples * sizeof(float) > bv_bytes) {
410 req_samples = bv_bytes / sizeof(float);
411 req_samples -= req_samples % (size_t)s->channels; /* frame-align */
412 req_frames = req_samples / (size_t)s->channels;
413 }
415 uint64_t head = ATM_LOAD_REL(&s->head);
416 uint64_t tail = ATM_LOAD_ACQ(&s->tail);
417 size_t in_use = (size_t)(head - tail);
418 size_t room = s->capacity_samples - in_use;
420 size_t write_samples = req_samples < room ? req_samples : room;
421 /* Whole-frame alignment. */
422 write_samples -= write_samples % (size_t)s->channels;
424 const float *src = (const float *)sigil_bytevector_data(args[1]);
425 for (size_t i = 0; i < write_samples; i++) {
426 s->ring[(head + i) & s->mask] = src[i];
427 }
429 ATM_STORE_REL(&s->head, head + write_samples);
431 return sigil_fixnum((int64_t)(write_samples / (size_t)s->channels));
434/* (audio-stream-room stream) -> fixnum — frames free */
435static Value native_stream_room(SigilVM *vm, int argc, Value *args)
437 (void)argc;
438 AudioStream *s = get_stream(vm, args[0]);
439 if (!s) return sigil_fixnum(0);
440 uint64_t head = ATM_LOAD_REL(&s->head);
441 uint64_t tail = ATM_LOAD_ACQ(&s->tail);
442 size_t in_use = (size_t)(head - tail);
443 size_t room_samples = s->capacity_samples - in_use;
444 return sigil_fixnum((int64_t)(room_samples / (size_t)s->channels));
447/* (audio-stream-depth stream) -> fixnum — frames queued */
448static Value native_stream_depth(SigilVM *vm, int argc, Value *args)
450 (void)argc;
451 AudioStream *s = get_stream(vm, args[0]);
452 if (!s) return sigil_fixnum(0);
453 uint64_t head = ATM_LOAD_ACQ(&s->head);
454 uint64_t tail = ATM_LOAD_ACQ(&s->tail);
455 size_t in_use = (size_t)(head - tail);
456 return sigil_fixnum((int64_t)(in_use / (size_t)s->channels));
459/* (audio-stream-capacity stream) -> fixnum — total ring size in frames */
460static Value native_stream_capacity(SigilVM *vm, int argc, Value *args)
462 (void)argc;
463 AudioStream *s = get_stream(vm, args[0]);
464 if (!s) return sigil_fixnum(0);
465 return sigil_fixnum((int64_t)s->buffer_frames);
468/* (audio-stream-channels stream) -> fixnum */
469static Value native_stream_channels(SigilVM *vm, int argc, Value *args)
471 (void)argc;
472 AudioStream *s = get_stream(vm, args[0]);
473 if (!s) return sigil_fixnum(0);
474 return sigil_fixnum((int64_t)s->channels);
477/* (audio-stream-underruns stream) -> fixnum — cumulative underrun frames */
478static Value native_stream_underruns(SigilVM *vm, int argc, Value *args)
480 (void)argc;
481 AudioStream *s = get_stream(vm, args[0]);
482 if (!s) return sigil_fixnum(0);
483 uint64_t u = ATM_LOAD_REL(&s->underrun_frames);
484 if (u > (uint64_t)SIGIL_FIXNUM_MAX) u = (uint64_t)SIGIL_FIXNUM_MAX;
485 return sigil_fixnum((int64_t)u);
488/* (audio-stream-underruns-sounding stream) -> fixnum: the underrun frames
489 * that cut off sound (the rest is a quiet stream running dry) */
490static Value native_stream_underruns_sounding(SigilVM *vm, int argc, Value *args)
492 (void)argc;
493 AudioStream *s = get_stream(vm, args[0]);
494 if (!s) return sigil_fixnum(0);
495 uint64_t u = ATM_LOAD_REL(&s->underrun_sounding);
496 if (u > (uint64_t)SIGIL_FIXNUM_MAX) u = (uint64_t)SIGIL_FIXNUM_MAX;
497 return sigil_fixnum((int64_t)u);
500/* (audio-stream-rate stream) -> fixnum: the rate the producer feeds at */
501static Value native_stream_rate(SigilVM *vm, int argc, Value *args)
503 (void)argc;
504 AudioStream *s = get_stream(vm, args[0]);
505 if (!s) return sigil_fixnum(0);
506 return sigil_fixnum((int64_t)s->rate);
509/* (%audio-stream-set-rate! stream rate) -> #t | #f
510 * The producer feeds at RATE from its head on (clamped to the device's rate);
511 * the audio thread switches when it reaches that head. #f when RATE_QUEUE
512 * changes are already pending (the audio thread has not reached them). */
513static Value native_stream_set_rate(SigilVM *vm, int argc, Value *args)
515 (void)argc;
516 AudioStream *s = get_stream(vm, args[0]);
517 if (!s || !sigil_is_fixnum(args[1])) return SIGIL_FALSE;
518 int64_t rate = sigil_as_fixnum(args[1]);
519 if (rate <= 0) return SIGIL_FALSE;
520 if (rate > SIGIL_AUDIO_DEVICE_RATE) rate = SIGIL_AUDIO_DEVICE_RATE;
521 uint32_t qh = s->q_head;
522 if (qh - ATM_LOAD_ACQ(&s->q_tail) >= RATE_QUEUE) return SIGIL_FALSE;
523 RateChange *c = &s->q[qh & (RATE_QUEUE - 1)];
524 c->at = ATM_LOAD_REL(&s->head);
525 c->ratio = rate == SIGIL_AUDIO_DEVICE_RATE ? 1.0 : (double)rate / (double)SIGIL_AUDIO_DEVICE_RATE;
526 ATM_STORE_REL(&s->q_head, qh + 1);
527 s->rate = (int)rate;
528 return SIGIL_TRUE;
531/* (audio-stream-closed? stream) -> boolean */
532static Value native_stream_closed_p(SigilVM *vm, int argc, Value *args)
534 (void)argc;
535 AudioStream *s = get_stream(vm, args[0]);
536 if (!s) return SIGIL_TRUE;
537 return ATM_LOAD_ACQ(&s->closed) ? SIGIL_TRUE : SIGIL_FALSE;
540/* (close-audio-stream! stream) — unregister; destructor frees on GC */
541static Value native_close_audio_stream(SigilVM *vm, int argc, Value *args)
543 (void)argc;
544 AudioStream *s = get_stream(vm, args[0]);
545 if (!s) return SIGIL_NIL;
546 if (ATM_XCHG_ACQREL(&s->closed, 1)) {
547 return SIGIL_NIL;
548 }
549 registry_remove(s);
550 return SIGIL_NIL;
553/*
554 * (%audio-stream-drain-for-test out-bv num-frames out-channels) -> fixnum
555 *
556 * Test-only shim: simulates the audio thread by running the mix pass on
557 * a Sigil-provided float32 bytevector. Returns num-frames (nothing fancy
558 * — the caller checks sample values). Treats the bytevector as
559 * zero-initialized each call; the caller is responsible for clearing.
560 * Not exported from the Sigil library — %-prefix signals internal.
561 */
562static Value native_drain_for_test(SigilVM *vm, int argc, Value *args)
564 if (argc < 3 || !sigil_is_bytevector(args[0])) {
565 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
566 "%audio-stream-drain-for-test: out-bv frames channels");
567 return sigil_fixnum(0);
568 }
569 float *out = (float *)sigil_bytevector_data(args[0]);
570 size_t bv_bytes = sigil_bytevector_length(args[0]);
571 int num_frames = (int)sigil_as_fixnum(args[1]);
572 int out_channels = (int)sigil_as_fixnum(args[2]);
574 size_t need_bytes = (size_t)num_frames * (size_t)out_channels * sizeof(float);
575 if (need_bytes > bv_bytes) {
576 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
577 "%audio-stream-drain-for-test: bytevector too small");
578 return sigil_fixnum(0);
579 }
580 memset(out, 0, need_bytes);
581 sigil_audio_stream_mix_all(out, num_frames, out_channels);
582 return sigil_fixnum((int64_t)num_frames);
585/*
586 * (%bv-f32-ref bv byte-offset) -> flonum
587 *
588 * Test-only: read a float32 from a bytevector at byte-offset, widened to
589 * a flonum. Sigil stdlib doesn't ship a float32 bytevector accessor yet.
590 */
591static Value native_bv_f32_ref(SigilVM *vm, int argc, Value *args)
593 if (argc < 2 || !sigil_is_bytevector(args[0])) {
594 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
595 "%bv-f32-ref: bv offset");
596 return sigil_flonum(0.0);
597 }
598 int64_t off = sigil_as_fixnum(args[1]);
599 size_t bv_bytes = sigil_bytevector_length(args[0]);
600 if (off < 0 || (size_t)off + sizeof(float) > bv_bytes) {
601 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
602 "%bv-f32-ref: offset out of bounds");
603 return sigil_flonum(0.0);
604 }
605 float f;
606 memcpy(&f, sigil_bytevector_data(args[0]) + off, sizeof(f));
607 return sigil_flonum((double)f);
610/* (set-audio-stream-volume! stream volume) */
611static Value native_set_stream_volume(SigilVM *vm, int argc, Value *args)
613 if (argc < 2) {
614 sigil__vm_set_error(vm, SIGIL_ERR_ARITY,
615 "set-audio-stream-volume!: stream volume");
616 return SIGIL_NIL;
617 }
618 AudioStream *s = get_stream(vm, args[0]);
619 if (!s) {
620 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
621 "set-audio-stream-volume!: expected audio-stream");
622 return SIGIL_NIL;
623 }
624 float v = (float)sigil_as_flonum(args[1]);
625 if (v < 0.0f) v = 0.0f;
626 if (v > 1.0f) v = 1.0f;
627 s->volume = v; /* audio thread reads non-atomically; single-writer producer */
628 return SIGIL_NIL;
631/* ------------------------------------------------------------------ */
632/* Module registration */
633/* ------------------------------------------------------------------ */
635void sigil__register_audio_stream(SigilVM *vm)
637 sigil_module_register_native(vm, "%open-audio-stream",
638 native_open_audio_stream, SIGIL_ARITY_EXACT(3),
639 "Open a streaming audio sink (internal)");
640 sigil_module_register_native(vm, "audio-stream?",
641 native_stream_p, SIGIL_ARITY_EXACT(1),
642 "Check if object is an audio-stream");
643 sigil_module_register_native(vm, "push-audio-samples",
644 native_push_audio_samples, SIGIL_ARITY_EXACT(3),
645 "Push float32 PCM frames to a stream; returns frames accepted");
646 sigil_module_register_native(vm, "audio-stream-room",
647 native_stream_room, SIGIL_ARITY_EXACT(1),
648 "Frames free in the stream ring");
649 sigil_module_register_native(vm, "audio-stream-depth",
650 native_stream_depth, SIGIL_ARITY_EXACT(1),
651 "Frames queued, not yet consumed");
652 sigil_module_register_native(vm, "audio-stream-capacity",
653 native_stream_capacity, SIGIL_ARITY_EXACT(1),
654 "Total ring size in frames (power of two)");
655 sigil_module_register_native(vm, "audio-stream-channels",
656 native_stream_channels, SIGIL_ARITY_EXACT(1),
657 "Stream channel count");
658 sigil_module_register_native(vm, "audio-stream-underruns",
659 native_stream_underruns, SIGIL_ARITY_EXACT(1),
660 "Cumulative underrun frames (debug)");
661 sigil_module_register_native(vm, "audio-stream-underruns-sounding",
662 native_stream_underruns_sounding, SIGIL_ARITY_EXACT(1),
663 "Underrun frames that cut off sound");
664 sigil_module_register_native(vm, "audio-stream-rate",
665 native_stream_rate, SIGIL_ARITY_EXACT(1),
666 "The rate the producer feeds the stream at");
667 sigil_module_register_native(vm, "%audio-stream-set-rate!",
668 native_stream_set_rate, SIGIL_ARITY_EXACT(2),
669 "Set the stream's rate from its head on (internal)");
670 sigil_module_register_native(vm, "audio-stream-closed?",
671 native_stream_closed_p, SIGIL_ARITY_EXACT(1),
672 "Is the stream closed?");
673 sigil_module_register_native(vm, "close-audio-stream!",
674 native_close_audio_stream, SIGIL_ARITY_EXACT(1),
675 "Close an audio stream (idempotent)");
676 sigil_module_register_native(vm, "set-audio-stream-volume!",
677 native_set_stream_volume, SIGIL_ARITY_EXACT(2),
678 "Set per-stream volume (0.0-1.0)");
679 sigil_module_register_native(vm, "%audio-stream-drain-for-test",
680 native_drain_for_test, SIGIL_ARITY_EXACT(3),
681 "Internal: simulate audio-thread drain into a bytevector");
682 sigil_module_register_native(vm, "%bv-f32-ref",
683 native_bv_f32_ref, SIGIL_ARITY_EXACT(2),
684 "Internal: read float32 from bytevector at byte offset");
686 sigil_module_export(vm, "%open-audio-stream");
687 sigil_module_export(vm, "audio-stream?");
688 sigil_module_export(vm, "push-audio-samples");
689 sigil_module_export(vm, "audio-stream-room");
690 sigil_module_export(vm, "audio-stream-depth");
691 sigil_module_export(vm, "audio-stream-capacity");
692 sigil_module_export(vm, "audio-stream-channels");
693 sigil_module_export(vm, "audio-stream-underruns");
694 sigil_module_export(vm, "audio-stream-underruns-sounding");
695 sigil_module_export(vm, "audio-stream-rate");
696 sigil_module_export(vm, "%audio-stream-set-rate!");
697 sigil_module_export(vm, "audio-stream-closed?");
698 sigil_module_export(vm, "close-audio-stream!");
699 sigil_module_export(vm, "set-audio-stream-volume!");
700 sigil_module_export(vm, "%audio-stream-drain-for-test");
701 sigil_module_export(vm, "%bv-f32-ref");