AtlatestRepositorysigil-audio
sigil-audio / tree / src / caudio.c
1
/*2
* audio.c - Sigil Audio Module3
*4
* Wraps sokol_audio.h to provide audio playback capabilities.5
* Uses stb_vorbis for OGG decoding.6
*7
* Sound effects are loaded entirely into memory.8
* Music is streamed from disk via stb_vorbis.9
*/11
#include "sigil/sigil.h"13
#include <stdio.h>14
#include <stdlib.h>15
#include <string.h>16
#include <math.h>18
/* Sokol headers (implementation is in sokol.c) */19
#include "sokol_audio.h"21
/* stb_vorbis for OGG decoding (implementation in stb_impl.c) */22
#include "stb_vorbis.c"24
/* Streaming audio sink (SPSC ring) */25
#include "audio-stream.h"27
/* ============================================================28
* CONSTANTS29
* ============================================================ */31
#define MAX_SOUNDS 6432
#define MAX_PLAYING_SOUNDS 1633
#define STREAM_BUFFER_SAMPLES 409635
/* ============================================================36
* DATA STRUCTURES37
* ============================================================ */39
/* Sound effect - fully loaded into memory */40
typedef struct {41
float *samples; /* Interleaved stereo samples */42
int num_samples; /* Total samples (frames * channels) */43
int sample_rate;44
int channels;45
} StudioSound;47
/* Playing sound instance */48
typedef struct {49
StudioSound *sound;50
int position; /* Current playback position */51
float volume;52
float pan; /* -1.0 left, 0.0 center, 1.0 right */53
bool playing;54
bool loop;55
} PlayingSound;57
/* Music stream - decoded on the fly */58
typedef struct {59
stb_vorbis *vorbis;60
char *filepath; /* For reopening if looping */61
float volume;62
bool playing;63
bool loop;64
bool paused;65
} MusicStream;67
/* ============================================================68
* GLOBAL STATE69
* ============================================================ */71
static PlayingSound g_playing_sounds[MAX_PLAYING_SOUNDS];72
static MusicStream g_music = {0};73
static float g_master_volume = 1.0f;74
static bool g_muted = false;75
static bool g_audio_initialized = false;77
/* Type tags for foreign objects */78
static Value sound_type_tag = SIGIL_UNDEFINED;80
/* ============================================================81
* AUDIO CALLBACK82
* ============================================================ */84
static void audio_callback(float *buffer, int num_frames, int num_channels)85
{86
/* Clear buffer */87
memset(buffer, 0, num_frames * num_channels * sizeof(float));89
if (g_muted) return;91
float master = g_master_volume;93
/* Mix playing sounds */94
for (int i = 0; i < MAX_PLAYING_SOUNDS; i++) {95
PlayingSound *ps = &g_playing_sounds[i];96
if (!ps->playing || !ps->sound) continue;98
StudioSound *snd = ps->sound;99
float vol = ps->volume * master;101
/* Calculate pan gains */102
float pan = ps->pan;103
float left_gain = vol * (pan <= 0 ? 1.0f : 1.0f - pan);104
float right_gain = vol * (pan >= 0 ? 1.0f : 1.0f + pan);106
for (int f = 0; f < num_frames; f++) {107
if (ps->position >= snd->num_samples / snd->channels) {108
if (ps->loop) {109
ps->position = 0;110
} else {111
ps->playing = false;112
break;113
}114
}116
float left, right;117
if (snd->channels == 1) {118
/* Mono */119
left = right = snd->samples[ps->position];120
} else {121
/* Stereo */122
left = snd->samples[ps->position * 2];123
right = snd->samples[ps->position * 2 + 1];124
}126
if (num_channels >= 2) {127
buffer[f * num_channels] += left * left_gain;128
buffer[f * num_channels + 1] += right * right_gain;129
} else {130
buffer[f] += (left + right) * 0.5f * vol;131
}133
ps->position++;134
}135
}137
/* Mix music stream */138
if (g_music.playing && !g_music.paused && g_music.vorbis) {139
float vol = g_music.volume * master;140
float temp[STREAM_BUFFER_SAMPLES * 2];141
int samples_needed = num_frames;142
int offset = 0;144
while (samples_needed > 0) {145
int to_decode = samples_needed < STREAM_BUFFER_SAMPLES ?146
samples_needed : STREAM_BUFFER_SAMPLES;148
int decoded = stb_vorbis_get_samples_float_interleaved(149
g_music.vorbis, 2, temp, to_decode * 2);151
if (decoded == 0) {152
/* End of file */153
if (g_music.loop && g_music.filepath) {154
/* Reopen and continue */155
stb_vorbis_close(g_music.vorbis);156
int error;157
g_music.vorbis = stb_vorbis_open_filename(158
g_music.filepath, &error, NULL);159
if (!g_music.vorbis) {160
g_music.playing = false;161
break;162
}163
continue;164
} else {165
g_music.playing = false;166
break;167
}168
}170
/* Mix decoded samples */171
for (int f = 0; f < decoded; f++) {172
int buf_idx = (offset + f) * num_channels;173
if (num_channels >= 2) {174
buffer[buf_idx] += temp[f * 2] * vol;175
buffer[buf_idx + 1] += temp[f * 2 + 1] * vol;176
} else {177
buffer[buf_idx] += (temp[f * 2] + temp[f * 2 + 1]) * 0.5f * vol;178
}179
}181
samples_needed -= decoded;182
offset += decoded;183
}184
}186
/* Mix streaming sinks (SPSC ring sources). Additive, silence on under-run. */187
sigil_audio_stream_mix_all(buffer, num_frames, num_channels);189
/* Clamp output */190
for (int i = 0; i < num_frames * num_channels; i++) {191
if (buffer[i] > 1.0f) buffer[i] = 1.0f;192
if (buffer[i] < -1.0f) buffer[i] = -1.0f;193
}194
}196
/* ============================================================197
* HELPER FUNCTIONS198
* ============================================================ */200
static void ensure_sound_type(SigilVM *vm)201
{202
if (sigil_is_undefined(sound_type_tag)) {203
sound_type_tag = sigil_intern_symbol(vm, "sigil-audio-sound", 17);204
}205
}207
static StudioSound *get_sound(SigilVM *vm, Value v)208
{209
if (!sigil_is_foreign(v)) return NULL;210
ensure_sound_type(vm);211
if (sigil_foreign_type(v) != sound_type_tag) return NULL;212
return (StudioSound *)sigil_foreign_data(v);213
}215
static void sound_destructor(void *data)216
{217
StudioSound *snd = (StudioSound *)data;218
if (snd) {219
free(snd->samples);220
free(snd);221
}222
}224
static PlayingSound *find_free_slot(void)225
{226
for (int i = 0; i < MAX_PLAYING_SOUNDS; i++) {227
if (!g_playing_sounds[i].playing) {228
return &g_playing_sounds[i];229
}230
}231
return NULL;232
}234
/* ============================================================235
* NATIVE FUNCTIONS - SETUP236
* ============================================================ */238
/*239
* (audio-setup) - Initialize audio subsystem240
*/241
static Value native_audio_setup(SigilVM *vm, int argc, Value *args)242
{243
(void)vm; (void)argc; (void)args;245
if (g_audio_initialized) {246
return SIGIL_NIL;247
}249
saudio_desc desc = {250
.stream_cb = audio_callback,251
.num_channels = 2,252
.sample_rate = 44100,253
.buffer_frames = 2048254
};255
saudio_setup(&desc);257
/* Clear playing sounds */258
memset(g_playing_sounds, 0, sizeof(g_playing_sounds));260
/* Clear music */261
memset(&g_music, 0, sizeof(g_music));262
g_music.volume = 1.0f;264
g_master_volume = 1.0f;265
g_muted = false;266
g_audio_initialized = true;268
return SIGIL_NIL;269
}271
/*272
* (audio-shutdown) - Shutdown audio subsystem273
*/274
static Value native_audio_shutdown(SigilVM *vm, int argc, Value *args)275
{276
(void)vm; (void)argc; (void)args;278
if (g_audio_initialized) {279
/* Detach any active streaming sinks before tearing down the device */280
sigil_audio_stream_shutdown_all();282
/* Stop music */283
if (g_music.vorbis) {284
stb_vorbis_close(g_music.vorbis);285
g_music.vorbis = NULL;286
}287
free(g_music.filepath);288
g_music.filepath = NULL;290
saudio_shutdown();291
g_audio_initialized = false;292
}294
return SIGIL_NIL;295
}297
/*298
* (audio-initialized?) -> boolean299
*/300
static Value native_audio_initialized(SigilVM *vm, int argc, Value *args)301
{302
(void)vm; (void)argc; (void)args;303
return g_audio_initialized ? SIGIL_TRUE : SIGIL_FALSE;304
}306
/* ============================================================307
* NATIVE FUNCTIONS - SOUNDS308
* ============================================================ */310
/*311
* (load-sound path) -> <sound> or #f312
*313
* Load an OGG file entirely into memory.314
*/315
static Value native_load_sound(SigilVM *vm, int argc, Value *args)316
{317
if (argc < 1 || !sigil_is_string(args[0])) {318
sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "load-sound: expected string path");319
return SIGIL_FALSE;320
}322
SigilString *path_str = (SigilString *)sigil_as_ptr(args[0]);323
const char *path = path_str->data;325
int channels, sample_rate;326
short *raw_samples;327
int num_samples = stb_vorbis_decode_filename(path, &channels, &sample_rate,328
&raw_samples);329
if (num_samples < 0) {330
return SIGIL_FALSE;331
}333
/* Convert to float */334
int total_samples = num_samples * channels;335
float *samples = malloc(total_samples * sizeof(float));336
if (!samples) {337
free(raw_samples);338
return SIGIL_FALSE;339
}341
for (int i = 0; i < total_samples; i++) {342
samples[i] = raw_samples[i] / 32768.0f;343
}344
free(raw_samples);346
StudioSound *snd = malloc(sizeof(StudioSound));347
if (!snd) {348
free(samples);349
return SIGIL_FALSE;350
}352
snd->samples = samples;353
snd->num_samples = total_samples;354
snd->sample_rate = sample_rate;355
snd->channels = channels;357
ensure_sound_type(vm);358
return sigil_make_foreign(vm, sound_type_tag, snd, sound_destructor,359
sizeof(StudioSound) + total_samples * sizeof(float));360
}362
/*363
* (sound? obj) -> boolean364
*/365
static Value native_sound_p(SigilVM *vm, int argc, Value *args)366
{367
(void)argc;368
return get_sound(vm, args[0]) ? SIGIL_TRUE : SIGIL_FALSE;369
}371
/*372
* (play-sound sound [volume] [pan] [loop?]) -> boolean373
*374
* Play a sound effect. Returns #t if started, #f if no slots available.375
*/376
static Value native_play_sound(SigilVM *vm, int argc, Value *args)377
{378
if (argc < 1) {379
sigil__vm_set_error(vm, SIGIL_ERR_ARITY, "play-sound: requires sound argument");380
return SIGIL_FALSE;381
}383
StudioSound *snd = get_sound(vm, args[0]);384
if (!snd) {385
sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "play-sound: expected sound");386
return SIGIL_FALSE;387
}389
PlayingSound *ps = find_free_slot();390
if (!ps) {391
return SIGIL_FALSE; /* No slots available */392
}394
ps->sound = snd;395
ps->position = 0;396
ps->volume = argc > 1 ? (float)sigil_as_flonum(args[1]) : 1.0f;397
ps->pan = argc > 2 ? (float)sigil_as_flonum(args[2]) : 0.0f;398
ps->loop = argc > 3 ? sigil_is_true(args[3]) : false;399
ps->playing = true;401
return SIGIL_TRUE;402
}404
/*405
* (stop-all-sounds) - Stop all playing sound effects406
*/407
static Value native_stop_all_sounds(SigilVM *vm, int argc, Value *args)408
{409
(void)vm; (void)argc; (void)args;411
for (int i = 0; i < MAX_PLAYING_SOUNDS; i++) {412
g_playing_sounds[i].playing = false;413
}415
return SIGIL_NIL;416
}418
/* ============================================================419
* NATIVE FUNCTIONS - MUSIC420
* ============================================================ */422
/*423
* (play-music path [loop?]) -> boolean424
*425
* Start streaming music from an OGG file.426
*/427
static Value native_play_music(SigilVM *vm, int argc, Value *args)428
{429
if (argc < 1 || !sigil_is_string(args[0])) {430
sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "play-music: expected string path");431
return SIGIL_FALSE;432
}434
/* Stop any existing music */435
if (g_music.vorbis) {436
stb_vorbis_close(g_music.vorbis);437
g_music.vorbis = NULL;438
}439
free(g_music.filepath);440
g_music.filepath = NULL;442
SigilString *path_str = (SigilString *)sigil_as_ptr(args[0]);443
const char *path = path_str->data;445
int error;446
g_music.vorbis = stb_vorbis_open_filename(path, &error, NULL);447
if (!g_music.vorbis) {448
return SIGIL_FALSE;449
}451
g_music.filepath = strdup(path);452
g_music.loop = argc > 1 ? sigil_is_true(args[1]) : true;453
g_music.playing = true;454
g_music.paused = false;456
return SIGIL_TRUE;457
}459
/*460
* (stop-music) - Stop music playback461
*/462
static Value native_stop_music(SigilVM *vm, int argc, Value *args)463
{464
(void)vm; (void)argc; (void)args;466
if (g_music.vorbis) {467
stb_vorbis_close(g_music.vorbis);468
g_music.vorbis = NULL;469
}470
free(g_music.filepath);471
g_music.filepath = NULL;472
g_music.playing = false;474
return SIGIL_NIL;475
}477
/*478
* (pause-music) - Pause music playback479
*/480
static Value native_pause_music(SigilVM *vm, int argc, Value *args)481
{482
(void)vm; (void)argc; (void)args;483
g_music.paused = true;484
return SIGIL_NIL;485
}487
/*488
* (resume-music) - Resume music playback489
*/490
static Value native_resume_music(SigilVM *vm, int argc, Value *args)491
{492
(void)vm; (void)argc; (void)args;493
g_music.paused = false;494
return SIGIL_NIL;495
}497
/*498
* (music-playing?) -> boolean499
*/500
static Value native_music_playing(SigilVM *vm, int argc, Value *args)501
{502
(void)vm; (void)argc; (void)args;503
return (g_music.playing && !g_music.paused) ? SIGIL_TRUE : SIGIL_FALSE;504
}506
/*507
* (set-music-volume volume) - Set music volume (0.0 to 1.0)508
*/509
static Value native_set_music_volume(SigilVM *vm, int argc, Value *args)510
{511
if (argc < 1) {512
sigil__vm_set_error(vm, SIGIL_ERR_ARITY, "set-music-volume: requires volume");513
return SIGIL_NIL;514
}516
float vol = (float)sigil_as_flonum(args[0]);517
if (vol < 0.0f) vol = 0.0f;518
if (vol > 1.0f) vol = 1.0f;519
g_music.volume = vol;521
return SIGIL_NIL;522
}524
/* ============================================================525
* NATIVE FUNCTIONS - GLOBAL CONTROL526
* ============================================================ */528
/*529
* (set-master-volume volume) - Set master volume (0.0 to 1.0)530
*/531
static Value native_set_master_volume(SigilVM *vm, int argc, Value *args)532
{533
if (argc < 1) {534
sigil__vm_set_error(vm, SIGIL_ERR_ARITY, "set-master-volume: requires volume");535
return SIGIL_NIL;536
}538
float vol = (float)sigil_as_flonum(args[0]);539
if (vol < 0.0f) vol = 0.0f;540
if (vol > 1.0f) vol = 1.0f;541
g_master_volume = vol;543
return SIGIL_NIL;544
}546
/*547
* (mute-audio) - Mute all audio548
*/549
static Value native_mute_audio(SigilVM *vm, int argc, Value *args)550
{551
(void)vm; (void)argc; (void)args;552
g_muted = true;553
return SIGIL_NIL;554
}556
/*557
* (unmute-audio) - Unmute audio558
*/559
static Value native_unmute_audio(SigilVM *vm, int argc, Value *args)560
{561
(void)vm; (void)argc; (void)args;562
g_muted = false;563
return SIGIL_NIL;564
}566
/*567
* (audio-muted?) -> boolean568
*/569
static Value native_audio_muted(SigilVM *vm, int argc, Value *args)570
{571
(void)vm; (void)argc; (void)args;572
return g_muted ? SIGIL_TRUE : SIGIL_FALSE;573
}575
/* ============================================================576
* MODULE INITIALIZATION577
* ============================================================ */579
/* OGG encoding (ogg-encode.c) */580
extern void sigil__register_ogg_encode(SigilVM *vm);582
void sigil__init_sigil_audio_module(SigilVM *vm)583
{584
SigilModule *module = sigil_begin_module(vm, "(sigil audio)");585
if (!module) return;587
/* Setup/shutdown */588
sigil_module_register_native(vm, "audio-setup", native_audio_setup,589
SIGIL_ARITY_EXACT(0), "Initialize audio");590
sigil_module_register_native(vm, "audio-shutdown", native_audio_shutdown,591
SIGIL_ARITY_EXACT(0), "Shutdown audio");592
sigil_module_register_native(vm, "audio-initialized?", native_audio_initialized,593
SIGIL_ARITY_EXACT(0), "Is audio initialized?");595
/* Sound effects */596
sigil_module_register_native(vm, "load-sound", native_load_sound,597
SIGIL_ARITY_EXACT(1), "Load OGG sound into memory");598
sigil_module_register_native(vm, "sound?", native_sound_p,599
SIGIL_ARITY_EXACT(1), "Check if object is a sound");600
sigil_module_register_native(vm, "play-sound", native_play_sound,601
SIGIL_ARITY_RANGE(1, 4), "Play sound effect");602
sigil_module_register_native(vm, "stop-all-sounds", native_stop_all_sounds,603
SIGIL_ARITY_EXACT(0), "Stop all sound effects");605
/* Music streaming */606
sigil_module_register_native(vm, "play-music", native_play_music,607
SIGIL_ARITY_RANGE(1, 2), "Stream music from file");608
sigil_module_register_native(vm, "stop-music", native_stop_music,609
SIGIL_ARITY_EXACT(0), "Stop music");610
sigil_module_register_native(vm, "pause-music", native_pause_music,611
SIGIL_ARITY_EXACT(0), "Pause music");612
sigil_module_register_native(vm, "resume-music", native_resume_music,613
SIGIL_ARITY_EXACT(0), "Resume music");614
sigil_module_register_native(vm, "music-playing?", native_music_playing,615
SIGIL_ARITY_EXACT(0), "Is music playing?");616
sigil_module_register_native(vm, "set-music-volume", native_set_music_volume,617
SIGIL_ARITY_EXACT(1), "Set music volume");619
/* Global control */620
sigil_module_register_native(vm, "set-master-volume", native_set_master_volume,621
SIGIL_ARITY_EXACT(1), "Set master volume");622
sigil_module_register_native(vm, "mute-audio", native_mute_audio,623
SIGIL_ARITY_EXACT(0), "Mute all audio");624
sigil_module_register_native(vm, "unmute-audio", native_unmute_audio,625
SIGIL_ARITY_EXACT(0), "Unmute audio");626
sigil_module_register_native(vm, "audio-muted?", native_audio_muted,627
SIGIL_ARITY_EXACT(0), "Is audio muted?");629
/* Export all */630
sigil_module_export(vm, "audio-setup");631
sigil_module_export(vm, "audio-shutdown");632
sigil_module_export(vm, "audio-initialized?");633
sigil_module_export(vm, "load-sound");634
sigil_module_export(vm, "sound?");635
sigil_module_export(vm, "play-sound");636
sigil_module_export(vm, "stop-all-sounds");637
sigil_module_export(vm, "play-music");638
sigil_module_export(vm, "stop-music");639
sigil_module_export(vm, "pause-music");640
sigil_module_export(vm, "resume-music");641
sigil_module_export(vm, "music-playing?");642
sigil_module_export(vm, "set-music-volume");643
sigil_module_export(vm, "set-master-volume");644
sigil_module_export(vm, "mute-audio");645
sigil_module_export(vm, "unmute-audio");646
sigil_module_export(vm, "audio-muted?");648
/* OGG encoding */649
sigil__register_ogg_encode(vm);651
/* Streaming sink */652
sigil__register_audio_stream(vm);654
sigil_end_module(vm);655
}