AtlatestRepositorysigil-audio
sigil-audio / tree / src / caudio.c
1
/*2
* audio.c - Sigil Audio Module3
*4
* Audio playback: a mixer of loaded sounds and one streamed music track over5
* the playback device in miniaudio-impl.c (miniaudio).6
* Uses stb_vorbis for OGG decoding.7
*8
* Sound effects are loaded entirely into memory.9
* Music is streamed from disk via stb_vorbis.10
*/12
#include "sigil/sigil.h"14
#include <stdio.h>15
#include <stdlib.h>16
#include <string.h>17
#include <math.h>19
/* The playback device (miniaudio, in miniaudio-impl.c) */20
#include "audio-device.h"22
/* stb_vorbis for OGG decoding (implementation in stb_impl.c) */23
#include "stb_vorbis.c"25
/* Streaming audio sink (SPSC ring) */26
#include "audio-stream.h"28
/* ============================================================29
* CONSTANTS30
* ============================================================ */32
#define MAX_SOUNDS 6433
#define MAX_PLAYING_SOUNDS 1634
#define STREAM_BUFFER_SAMPLES 409636
/* ============================================================37
* DATA STRUCTURES38
* ============================================================ */40
/* Sound effect - fully loaded into memory */41
typedef struct {42
float *samples; /* Interleaved stereo samples */43
int num_samples; /* Total samples (frames * channels) */44
int sample_rate;45
int channels;46
} StudioSound;48
/* Playing sound instance */49
typedef struct {50
StudioSound *sound;51
int position; /* Current playback position */52
float volume;53
float pan; /* -1.0 left, 0.0 center, 1.0 right */54
bool playing;55
bool loop;56
} PlayingSound;58
/* Music stream - decoded on the fly */59
typedef struct {60
stb_vorbis *vorbis;61
char *filepath; /* For reopening if looping */62
float volume;63
bool playing;64
bool loop;65
bool paused;66
} MusicStream;68
/* ============================================================69
* GLOBAL STATE70
* ============================================================ */72
static PlayingSound g_playing_sounds[MAX_PLAYING_SOUNDS];73
static MusicStream g_music = {0};74
static float g_master_volume = 1.0f;75
static bool g_muted = false;76
static bool g_audio_initialized = false;78
/* Type tags for foreign objects */79
static Value sound_type_tag = SIGIL_UNDEFINED;81
/* ============================================================82
* AUDIO CALLBACK83
* ============================================================ */85
static void audio_callback(float *buffer, int num_frames, int num_channels)86
{87
/* Clear buffer */88
memset(buffer, 0, num_frames * num_channels * sizeof(float));90
if (g_muted) return;92
float master = g_master_volume;94
/* Mix playing sounds */95
for (int i = 0; i < MAX_PLAYING_SOUNDS; i++) {96
PlayingSound *ps = &g_playing_sounds[i];97
if (!ps->playing || !ps->sound) continue;99
StudioSound *snd = ps->sound;100
float vol = ps->volume * master;102
/* Calculate pan gains */103
float pan = ps->pan;104
float left_gain = vol * (pan <= 0 ? 1.0f : 1.0f - pan);105
float right_gain = vol * (pan >= 0 ? 1.0f : 1.0f + pan);107
for (int f = 0; f < num_frames; f++) {108
if (ps->position >= snd->num_samples / snd->channels) {109
if (ps->loop) {110
ps->position = 0;111
} else {112
ps->playing = false;113
break;114
}115
}117
float left, right;118
if (snd->channels == 1) {119
/* Mono */120
left = right = snd->samples[ps->position];121
} else {122
/* Stereo */123
left = snd->samples[ps->position * 2];124
right = snd->samples[ps->position * 2 + 1];125
}127
if (num_channels >= 2) {128
buffer[f * num_channels] += left * left_gain;129
buffer[f * num_channels + 1] += right * right_gain;130
} else {131
buffer[f] += (left + right) * 0.5f * vol;132
}134
ps->position++;135
}136
}138
/* Mix music stream */139
if (g_music.playing && !g_music.paused && g_music.vorbis) {140
float vol = g_music.volume * master;141
float temp[STREAM_BUFFER_SAMPLES * 2];142
int samples_needed = num_frames;143
int offset = 0;145
while (samples_needed > 0) {146
int to_decode = samples_needed < STREAM_BUFFER_SAMPLES ?147
samples_needed : STREAM_BUFFER_SAMPLES;149
int decoded = stb_vorbis_get_samples_float_interleaved(150
g_music.vorbis, 2, temp, to_decode * 2);152
if (decoded == 0) {153
/* End of file */154
if (g_music.loop && g_music.filepath) {155
/* Reopen and continue */156
stb_vorbis_close(g_music.vorbis);157
int error;158
g_music.vorbis = stb_vorbis_open_filename(159
g_music.filepath, &error, NULL);160
if (!g_music.vorbis) {161
g_music.playing = false;162
break;163
}164
continue;165
} else {166
g_music.playing = false;167
break;168
}169
}171
/* Mix decoded samples */172
for (int f = 0; f < decoded; f++) {173
int buf_idx = (offset + f) * num_channels;174
if (num_channels >= 2) {175
buffer[buf_idx] += temp[f * 2] * vol;176
buffer[buf_idx + 1] += temp[f * 2 + 1] * vol;177
} else {178
buffer[buf_idx] += (temp[f * 2] + temp[f * 2 + 1]) * 0.5f * vol;179
}180
}182
samples_needed -= decoded;183
offset += decoded;184
}185
}187
/* Mix streaming sinks (SPSC ring sources). Additive, silence on under-run. */188
sigil_audio_stream_mix_all(buffer, num_frames, num_channels);190
/* Clamp output */191
for (int i = 0; i < num_frames * num_channels; i++) {192
if (buffer[i] > 1.0f) buffer[i] = 1.0f;193
if (buffer[i] < -1.0f) buffer[i] = -1.0f;194
}195
}197
/* ============================================================198
* HELPER FUNCTIONS199
* ============================================================ */201
static void ensure_sound_type(SigilVM *vm)202
{203
if (sigil_is_undefined(sound_type_tag)) {204
sound_type_tag = sigil_intern_symbol(vm, "sigil-audio-sound", 17);205
}206
}208
static StudioSound *get_sound(SigilVM *vm, Value v)209
{210
if (!sigil_is_foreign(v)) return NULL;211
ensure_sound_type(vm);212
if (sigil_foreign_type(v) != sound_type_tag) return NULL;213
return (StudioSound *)sigil_foreign_data(v);214
}216
static void sound_destructor(void *data)217
{218
StudioSound *snd = (StudioSound *)data;219
if (snd) {220
free(snd->samples);221
free(snd);222
}223
}225
static PlayingSound *find_free_slot(void)226
{227
for (int i = 0; i < MAX_PLAYING_SOUNDS; i++) {228
if (!g_playing_sounds[i].playing) {229
return &g_playing_sounds[i];230
}231
}232
return NULL;233
}235
/* ============================================================236
* NATIVE FUNCTIONS - SETUP237
* ============================================================ */239
/*240
* (audio-setup) - Initialize audio subsystem241
*/242
static Value native_audio_setup(SigilVM *vm, int argc, Value *args)243
{244
(void)vm; (void)argc; (void)args;246
if (g_audio_initialized) {247
return SIGIL_NIL;248
}250
/* No device (a headless runner, no sound server, no ALSA card) is not251
* an error, as it was not under sokol_audio: the mixer runs with no252
* output, every play is a no-op, and (audio-initialized?) is still253
* #t. miniaudio-impl.c has already said why on stderr. */254
if (sigil_audio_device_open(44100, 2, 2048, audio_callback) != 0) {255
fprintf(stderr, "sigil-audio: continuing without an audio device\n");256
}258
/* Clear playing sounds */259
memset(g_playing_sounds, 0, sizeof(g_playing_sounds));261
/* Clear music */262
memset(&g_music, 0, sizeof(g_music));263
g_music.volume = 1.0f;265
g_master_volume = 1.0f;266
g_muted = false;267
g_audio_initialized = true;269
return SIGIL_NIL;270
}272
/*273
* (audio-shutdown) - Shutdown audio subsystem274
*/275
static Value native_audio_shutdown(SigilVM *vm, int argc, Value *args)276
{277
(void)vm; (void)argc; (void)args;279
if (g_audio_initialized) {280
/* Detach any active streaming sinks before tearing down the device */281
sigil_audio_stream_shutdown_all();283
/* Stop music */284
if (g_music.vorbis) {285
stb_vorbis_close(g_music.vorbis);286
g_music.vorbis = NULL;287
}288
free(g_music.filepath);289
g_music.filepath = NULL;291
sigil_audio_device_close();292
g_audio_initialized = false;293
}295
return SIGIL_NIL;296
}298
/*299
* (audio-initialized?) -> boolean300
*/301
static Value native_audio_initialized(SigilVM *vm, int argc, Value *args)302
{303
(void)vm; (void)argc; (void)args;304
return g_audio_initialized ? SIGIL_TRUE : SIGIL_FALSE;305
}307
/*308
* (audio-backend) -> symbol or #f309
*310
* The backend the device runs on: 'pulseaudio (also on a PipeWire desktop,311
* through pipewire-pulse), 'alsa, 'jack, 'wasapi, 'dsound, 'winmm,312
* 'coreaudio, or 'null when no backend library or server was usable and313
* frames are consumed in silence; #f before audio-setup.314
*/315
static Value native_audio_backend(SigilVM *vm, int argc, Value *args)316
{317
(void)argc; (void)args;318
const char *name = sigil_audio_device_backend_symbol();319
if (!name) return SIGIL_FALSE;320
return sigil_intern_symbol(vm, name, strlen(name));321
}323
/*324
* (audio-device?) -> boolean325
*326
* #t when sound can reach a listener: a device is open on a real backend.327
* (audio-initialized?) stays #t after audio-setup whatever the backend,328
* because the mixer and the streams work either way (as under sokol_audio,329
* where an invalid device was silent but initialized); this predicate330
* names the new fact a game may want: whether to bother with sound.331
*/332
static Value native_audio_device(SigilVM *vm, int argc, Value *args)333
{334
(void)vm; (void)argc; (void)args;335
return sigil_audio_device_real() ? SIGIL_TRUE : SIGIL_FALSE;336
}338
/* ============================================================339
* NATIVE FUNCTIONS - SOUNDS340
* ============================================================ */342
/*343
* (load-sound path) -> <sound> or #f344
*345
* Load an OGG file entirely into memory.346
*/347
static Value native_load_sound(SigilVM *vm, int argc, Value *args)348
{349
if (argc < 1 || !sigil_is_string(args[0])) {350
sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "load-sound: expected string path");351
return SIGIL_FALSE;352
}354
SigilString *path_str = (SigilString *)sigil_as_ptr(args[0]);355
const char *path = path_str->data;357
int channels, sample_rate;358
short *raw_samples;359
int num_samples = stb_vorbis_decode_filename(path, &channels, &sample_rate,360
&raw_samples);361
if (num_samples < 0) {362
return SIGIL_FALSE;363
}365
/* Convert to float */366
int total_samples = num_samples * channels;367
float *samples = malloc(total_samples * sizeof(float));368
if (!samples) {369
free(raw_samples);370
return SIGIL_FALSE;371
}373
for (int i = 0; i < total_samples; i++) {374
samples[i] = raw_samples[i] / 32768.0f;375
}376
free(raw_samples);378
StudioSound *snd = malloc(sizeof(StudioSound));379
if (!snd) {380
free(samples);381
return SIGIL_FALSE;382
}384
snd->samples = samples;385
snd->num_samples = total_samples;386
snd->sample_rate = sample_rate;387
snd->channels = channels;389
ensure_sound_type(vm);390
return sigil_make_foreign(vm, sound_type_tag, snd, sound_destructor,391
sizeof(StudioSound) + total_samples * sizeof(float));392
}394
/*395
* (sound? obj) -> boolean396
*/397
static Value native_sound_p(SigilVM *vm, int argc, Value *args)398
{399
(void)argc;400
return get_sound(vm, args[0]) ? SIGIL_TRUE : SIGIL_FALSE;401
}403
/*404
* (play-sound sound [volume] [pan] [loop?]) -> boolean405
*406
* Play a sound effect. Returns #t if started, #f if no slots available.407
*/408
static Value native_play_sound(SigilVM *vm, int argc, Value *args)409
{410
if (argc < 1) {411
sigil__vm_set_error(vm, SIGIL_ERR_ARITY, "play-sound: requires sound argument");412
return SIGIL_FALSE;413
}415
StudioSound *snd = get_sound(vm, args[0]);416
if (!snd) {417
sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "play-sound: expected sound");418
return SIGIL_FALSE;419
}421
PlayingSound *ps = find_free_slot();422
if (!ps) {423
return SIGIL_FALSE; /* No slots available */424
}426
ps->sound = snd;427
ps->position = 0;428
ps->volume = argc > 1 ? (float)sigil_as_flonum(args[1]) : 1.0f;429
ps->pan = argc > 2 ? (float)sigil_as_flonum(args[2]) : 0.0f;430
ps->loop = argc > 3 ? sigil_is_true(args[3]) : false;431
ps->playing = true;433
return SIGIL_TRUE;434
}436
/*437
* (stop-all-sounds) - Stop all playing sound effects438
*/439
static Value native_stop_all_sounds(SigilVM *vm, int argc, Value *args)440
{441
(void)vm; (void)argc; (void)args;443
for (int i = 0; i < MAX_PLAYING_SOUNDS; i++) {444
g_playing_sounds[i].playing = false;445
}447
return SIGIL_NIL;448
}450
/* ============================================================451
* NATIVE FUNCTIONS - MUSIC452
* ============================================================ */454
/*455
* (play-music path [loop?]) -> boolean456
*457
* Start streaming music from an OGG file.458
*/459
static Value native_play_music(SigilVM *vm, int argc, Value *args)460
{461
if (argc < 1 || !sigil_is_string(args[0])) {462
sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "play-music: expected string path");463
return SIGIL_FALSE;464
}466
/* Stop any existing music */467
if (g_music.vorbis) {468
stb_vorbis_close(g_music.vorbis);469
g_music.vorbis = NULL;470
}471
free(g_music.filepath);472
g_music.filepath = NULL;474
SigilString *path_str = (SigilString *)sigil_as_ptr(args[0]);475
const char *path = path_str->data;477
int error;478
g_music.vorbis = stb_vorbis_open_filename(path, &error, NULL);479
if (!g_music.vorbis) {480
return SIGIL_FALSE;481
}483
g_music.filepath = strdup(path);484
g_music.loop = argc > 1 ? sigil_is_true(args[1]) : true;485
g_music.playing = true;486
g_music.paused = false;488
return SIGIL_TRUE;489
}491
/*492
* (stop-music) - Stop music playback493
*/494
static Value native_stop_music(SigilVM *vm, int argc, Value *args)495
{496
(void)vm; (void)argc; (void)args;498
if (g_music.vorbis) {499
stb_vorbis_close(g_music.vorbis);500
g_music.vorbis = NULL;501
}502
free(g_music.filepath);503
g_music.filepath = NULL;504
g_music.playing = false;506
return SIGIL_NIL;507
}509
/*510
* (pause-music) - Pause music playback511
*/512
static Value native_pause_music(SigilVM *vm, int argc, Value *args)513
{514
(void)vm; (void)argc; (void)args;515
g_music.paused = true;516
return SIGIL_NIL;517
}519
/*520
* (resume-music) - Resume music playback521
*/522
static Value native_resume_music(SigilVM *vm, int argc, Value *args)523
{524
(void)vm; (void)argc; (void)args;525
g_music.paused = false;526
return SIGIL_NIL;527
}529
/*530
* (music-playing?) -> boolean531
*/532
static Value native_music_playing(SigilVM *vm, int argc, Value *args)533
{534
(void)vm; (void)argc; (void)args;535
return (g_music.playing && !g_music.paused) ? SIGIL_TRUE : SIGIL_FALSE;536
}538
/*539
* (set-music-volume volume) - Set music volume (0.0 to 1.0)540
*/541
static Value native_set_music_volume(SigilVM *vm, int argc, Value *args)542
{543
if (argc < 1) {544
sigil__vm_set_error(vm, SIGIL_ERR_ARITY, "set-music-volume: requires volume");545
return SIGIL_NIL;546
}548
float vol = (float)sigil_as_flonum(args[0]);549
if (vol < 0.0f) vol = 0.0f;550
if (vol > 1.0f) vol = 1.0f;551
g_music.volume = vol;553
return SIGIL_NIL;554
}556
/* ============================================================557
* NATIVE FUNCTIONS - GLOBAL CONTROL558
* ============================================================ */560
/*561
* (set-master-volume volume) - Set master volume (0.0 to 1.0)562
*/563
static Value native_set_master_volume(SigilVM *vm, int argc, Value *args)564
{565
if (argc < 1) {566
sigil__vm_set_error(vm, SIGIL_ERR_ARITY, "set-master-volume: requires volume");567
return SIGIL_NIL;568
}570
float vol = (float)sigil_as_flonum(args[0]);571
if (vol < 0.0f) vol = 0.0f;572
if (vol > 1.0f) vol = 1.0f;573
g_master_volume = vol;575
return SIGIL_NIL;576
}578
/*579
* (mute-audio) - Mute all audio580
*/581
static Value native_mute_audio(SigilVM *vm, int argc, Value *args)582
{583
(void)vm; (void)argc; (void)args;584
g_muted = true;585
return SIGIL_NIL;586
}588
/*589
* (unmute-audio) - Unmute audio590
*/591
static Value native_unmute_audio(SigilVM *vm, int argc, Value *args)592
{593
(void)vm; (void)argc; (void)args;594
g_muted = false;595
return SIGIL_NIL;596
}598
/*599
* (audio-muted?) -> boolean600
*/601
static Value native_audio_muted(SigilVM *vm, int argc, Value *args)602
{603
(void)vm; (void)argc; (void)args;604
return g_muted ? SIGIL_TRUE : SIGIL_FALSE;605
}607
/* ============================================================608
* MODULE INITIALIZATION609
* ============================================================ */611
/* OGG encoding (ogg-encode.c) */612
extern void sigil__register_ogg_encode(SigilVM *vm);614
void sigil__init_sigil_audio_module(SigilVM *vm)615
{616
SigilModule *module = sigil_begin_module(vm, "(sigil audio)");617
if (!module) return;619
/* Setup/shutdown */620
sigil_module_register_native(vm, "audio-setup", native_audio_setup,621
SIGIL_ARITY_EXACT(0), "Initialize audio");622
sigil_module_register_native(vm, "audio-shutdown", native_audio_shutdown,623
SIGIL_ARITY_EXACT(0), "Shutdown audio");624
sigil_module_register_native(vm, "audio-backend", native_audio_backend,625
SIGIL_ARITY_EXACT(0), "The audio backend in use, as a symbol, or #f");626
sigil_module_register_native(vm, "audio-device?", native_audio_device,627
SIGIL_ARITY_EXACT(0), "Is a real (non-null) audio device open?");628
sigil_module_register_native(vm, "audio-initialized?", native_audio_initialized,629
SIGIL_ARITY_EXACT(0), "Is audio initialized?");631
/* Sound effects */632
sigil_module_register_native(vm, "load-sound", native_load_sound,633
SIGIL_ARITY_EXACT(1), "Load OGG sound into memory");634
sigil_module_register_native(vm, "sound?", native_sound_p,635
SIGIL_ARITY_EXACT(1), "Check if object is a sound");636
sigil_module_register_native(vm, "play-sound", native_play_sound,637
SIGIL_ARITY_RANGE(1, 4), "Play sound effect");638
sigil_module_register_native(vm, "stop-all-sounds", native_stop_all_sounds,639
SIGIL_ARITY_EXACT(0), "Stop all sound effects");641
/* Music streaming */642
sigil_module_register_native(vm, "play-music", native_play_music,643
SIGIL_ARITY_RANGE(1, 2), "Stream music from file");644
sigil_module_register_native(vm, "stop-music", native_stop_music,645
SIGIL_ARITY_EXACT(0), "Stop music");646
sigil_module_register_native(vm, "pause-music", native_pause_music,647
SIGIL_ARITY_EXACT(0), "Pause music");648
sigil_module_register_native(vm, "resume-music", native_resume_music,649
SIGIL_ARITY_EXACT(0), "Resume music");650
sigil_module_register_native(vm, "music-playing?", native_music_playing,651
SIGIL_ARITY_EXACT(0), "Is music playing?");652
sigil_module_register_native(vm, "set-music-volume", native_set_music_volume,653
SIGIL_ARITY_EXACT(1), "Set music volume");655
/* Global control */656
sigil_module_register_native(vm, "set-master-volume", native_set_master_volume,657
SIGIL_ARITY_EXACT(1), "Set master volume");658
sigil_module_register_native(vm, "mute-audio", native_mute_audio,659
SIGIL_ARITY_EXACT(0), "Mute all audio");660
sigil_module_register_native(vm, "unmute-audio", native_unmute_audio,661
SIGIL_ARITY_EXACT(0), "Unmute audio");662
sigil_module_register_native(vm, "audio-muted?", native_audio_muted,663
SIGIL_ARITY_EXACT(0), "Is audio muted?");665
/* Export all */666
sigil_module_export(vm, "audio-setup");667
sigil_module_export(vm, "audio-shutdown");668
sigil_module_export(vm, "audio-initialized?");669
sigil_module_export(vm, "audio-backend");670
sigil_module_export(vm, "audio-device?");671
sigil_module_export(vm, "load-sound");672
sigil_module_export(vm, "sound?");673
sigil_module_export(vm, "play-sound");674
sigil_module_export(vm, "stop-all-sounds");675
sigil_module_export(vm, "play-music");676
sigil_module_export(vm, "stop-music");677
sigil_module_export(vm, "pause-music");678
sigil_module_export(vm, "resume-music");679
sigil_module_export(vm, "music-playing?");680
sigil_module_export(vm, "set-music-volume");681
sigil_module_export(vm, "set-master-volume");682
sigil_module_export(vm, "mute-audio");683
sigil_module_export(vm, "unmute-audio");684
sigil_module_export(vm, "audio-muted?");686
/* OGG encoding */687
sigil__register_ogg_encode(vm);689
/* Streaming sink */690
sigil__register_audio_stream(vm);692
sigil_end_module(vm);693
}