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 = the playback device'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
/* Rate changes a stream may have pending at once (a power of two). */46
#define RATE_QUEUE 848
/* A sample above this (after the stream's volume) is sound: -80 dB. */49
#define SILENCE 1e-4f50
/* Frames a loud sample keeps the stream "sounding": a dry run that starts51
* within them cut sound off, an audible gap; one that starts later is a52
* quiet stream running dry, which nobody hears. */53
#define LOUD_HOLD 12855
typedef 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;60
typedef 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 reaches78
* the head it was asked at. SPSC: the producer owns q_head, the audio79
* 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's86
* rate / the device's), fractional position and the frame before the87
* 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. */97
static AudioStream *g_streams[MAX_AUDIO_STREAMS];99
static Value stream_type_tag = SIGIL_UNDEFINED;101
/* ------------------------------------------------------------------ */102
/* Helpers */103
/* ------------------------------------------------------------------ */105
static size_t round_up_pow2(size_t n)106
{107
if (n < 2) return 1;108
size_t p = 1;109
while (p < n) p <<= 1;110
return p;111
}113
static void ensure_stream_type(SigilVM *vm)114
{115
if (sigil_is_undefined(stream_type_tag)) {116
stream_type_tag = sigil_intern_symbol(vm, "sigil-audio-stream", 18);117
}118
}120
static AudioStream *get_stream(SigilVM *vm, Value v)121
{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);126
}128
static int registry_add(AudioStream *s)129
{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;137
}139
static void registry_remove(AudioStream *s)140
{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
}148
}150
static void stream_destructor(void *data)151
{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);158
}160
/* ------------------------------------------------------------------ */161
/* Audio-thread mix */162
/* ------------------------------------------------------------------ */164
/* One stream into the device buffer, additively. At the device's rate the165
* ring is copied sample for sample; below it, upsampled with a 4-tap cubic166
* Hermite over the frame before the read position and the three from it on167
* (the same drain as the web sink's). Starved frames are counted, and split168
* into sounding and idle by the level of what played just before the run. */169
static void mix_one(AudioStream *s, float *out, int num_frames,170
int out_channels)171
{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);270
}272
void sigil_audio_stream_mix_all(float *out, int num_frames, int out_channels)273
{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
}280
}282
void sigil_audio_stream_shutdown_all(void)283
{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
}291
}293
/* ------------------------------------------------------------------ */294
/* Natives */295
/* ------------------------------------------------------------------ */297
/*298
* (%open-audio-stream channels buffer-frames volume) -> <audio-stream> or #f299
*300
* Sigil wrapper applies keyword args. Sample rate is fixed at the device's301
* configured rate (44100) — caller must match.302
*/303
static Value native_open_audio_stream(SigilVM *vm, int argc, Value *args)304
{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));362
}364
/*365
* (audio-stream? obj) -> boolean366
*/367
static Value native_stream_p(SigilVM *vm, int argc, Value *args)368
{369
(void)argc;370
return get_stream(vm, args[0]) ? SIGIL_TRUE : SIGIL_FALSE;371
}373
/*374
* (push-audio-samples stream pcm-bv frames) -> fixnum375
*376
* Non-blocking. Returns frames actually copied (may be < frames).377
* Returns 0 on a closed stream.378
*/379
static Value native_push_audio_samples(SigilVM *vm, int argc, Value *args)380
{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));432
}434
/* (audio-stream-room stream) -> fixnum — frames free */435
static Value native_stream_room(SigilVM *vm, int argc, Value *args)436
{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));445
}447
/* (audio-stream-depth stream) -> fixnum — frames queued */448
static Value native_stream_depth(SigilVM *vm, int argc, Value *args)449
{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));457
}459
/* (audio-stream-capacity stream) -> fixnum — total ring size in frames */460
static Value native_stream_capacity(SigilVM *vm, int argc, Value *args)461
{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);466
}468
/* (audio-stream-channels stream) -> fixnum */469
static Value native_stream_channels(SigilVM *vm, int argc, Value *args)470
{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);475
}477
/* (audio-stream-underruns stream) -> fixnum — cumulative underrun frames */478
static Value native_stream_underruns(SigilVM *vm, int argc, Value *args)479
{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);486
}488
/* (audio-stream-underruns-sounding stream) -> fixnum: the underrun frames489
* that cut off sound (the rest is a quiet stream running dry) */490
static Value native_stream_underruns_sounding(SigilVM *vm, int argc, Value *args)491
{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);498
}500
/* (audio-stream-rate stream) -> fixnum: the rate the producer feeds at */501
static Value native_stream_rate(SigilVM *vm, int argc, Value *args)502
{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);507
}509
/* (%audio-stream-set-rate! stream rate) -> #t | #f510
* 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_QUEUE512
* changes are already pending (the audio thread has not reached them). */513
static Value native_stream_set_rate(SigilVM *vm, int argc, Value *args)514
{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;529
}531
/* (audio-stream-closed? stream) -> boolean */532
static Value native_stream_closed_p(SigilVM *vm, int argc, Value *args)533
{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;538
}540
/* (close-audio-stream! stream) — unregister; destructor frees on GC */541
static Value native_close_audio_stream(SigilVM *vm, int argc, Value *args)542
{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;551
}553
/*554
* (%audio-stream-drain-for-test out-bv num-frames out-channels) -> fixnum555
*556
* Test-only shim: simulates the audio thread by running the mix pass on557
* a Sigil-provided float32 bytevector. Returns num-frames (nothing fancy558
* — the caller checks sample values). Treats the bytevector as559
* zero-initialized each call; the caller is responsible for clearing.560
* Not exported from the Sigil library — %-prefix signals internal.561
*/562
static Value native_drain_for_test(SigilVM *vm, int argc, Value *args)563
{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);583
}585
/*586
* (%bv-f32-ref bv byte-offset) -> flonum587
*588
* Test-only: read a float32 from a bytevector at byte-offset, widened to589
* a flonum. Sigil stdlib doesn't ship a float32 bytevector accessor yet.590
*/591
static Value native_bv_f32_ref(SigilVM *vm, int argc, Value *args)592
{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);608
}610
/* (set-audio-stream-volume! stream volume) */611
static Value native_set_stream_volume(SigilVM *vm, int argc, Value *args)612
{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;629
}631
/* ------------------------------------------------------------------ */632
/* Module registration */633
/* ------------------------------------------------------------------ */635
void sigil__register_audio_stream(SigilVM *vm)636
{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");702
}