AtlatestRepositorysigil-audio

sigil-audio / tree / src / caudio.c

1/*
2 * audio.c - Sigil Audio Module
3 *
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 * CONSTANTS
29 * ============================================================ */
31#define MAX_SOUNDS 64
32#define MAX_PLAYING_SOUNDS 16
33#define STREAM_BUFFER_SAMPLES 4096
35/* ============================================================
36 * DATA STRUCTURES
37 * ============================================================ */
39/* Sound effect - fully loaded into memory */
40typedef 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 */
48typedef 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 */
58typedef 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 STATE
69 * ============================================================ */
71static PlayingSound g_playing_sounds[MAX_PLAYING_SOUNDS];
72static MusicStream g_music = {0};
73static float g_master_volume = 1.0f;
74static bool g_muted = false;
75static bool g_audio_initialized = false;
77/* Type tags for foreign objects */
78static Value sound_type_tag = SIGIL_UNDEFINED;
80/* ============================================================
81 * AUDIO CALLBACK
82 * ============================================================ */
84static void audio_callback(float *buffer, int num_frames, int num_channels)
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 }
196/* ============================================================
197 * HELPER FUNCTIONS
198 * ============================================================ */
200static void ensure_sound_type(SigilVM *vm)
202 if (sigil_is_undefined(sound_type_tag)) {
203 sound_type_tag = sigil_intern_symbol(vm, "sigil-audio-sound", 17);
204 }
207static StudioSound *get_sound(SigilVM *vm, Value v)
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);
215static void sound_destructor(void *data)
217 StudioSound *snd = (StudioSound *)data;
218 if (snd) {
219 free(snd->samples);
220 free(snd);
221 }
224static PlayingSound *find_free_slot(void)
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;
234/* ============================================================
235 * NATIVE FUNCTIONS - SETUP
236 * ============================================================ */
238/*
239 * (audio-setup) - Initialize audio subsystem
240 */
241static Value native_audio_setup(SigilVM *vm, int argc, Value *args)
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 = 2048
254 };
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;
271/*
272 * (audio-shutdown) - Shutdown audio subsystem
273 */
274static Value native_audio_shutdown(SigilVM *vm, int argc, Value *args)
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;
297/*
298 * (audio-initialized?) -> boolean
299 */
300static Value native_audio_initialized(SigilVM *vm, int argc, Value *args)
302 (void)vm; (void)argc; (void)args;
303 return g_audio_initialized ? SIGIL_TRUE : SIGIL_FALSE;
306/* ============================================================
307 * NATIVE FUNCTIONS - SOUNDS
308 * ============================================================ */
310/*
311 * (load-sound path) -> <sound> or #f
312 *
313 * Load an OGG file entirely into memory.
314 */
315static Value native_load_sound(SigilVM *vm, int argc, Value *args)
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));
362/*
363 * (sound? obj) -> boolean
364 */
365static Value native_sound_p(SigilVM *vm, int argc, Value *args)
367 (void)argc;
368 return get_sound(vm, args[0]) ? SIGIL_TRUE : SIGIL_FALSE;
371/*
372 * (play-sound sound [volume] [pan] [loop?]) -> boolean
373 *
374 * Play a sound effect. Returns #t if started, #f if no slots available.
375 */
376static Value native_play_sound(SigilVM *vm, int argc, Value *args)
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;
404/*
405 * (stop-all-sounds) - Stop all playing sound effects
406 */
407static Value native_stop_all_sounds(SigilVM *vm, int argc, Value *args)
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;
418/* ============================================================
419 * NATIVE FUNCTIONS - MUSIC
420 * ============================================================ */
422/*
423 * (play-music path [loop?]) -> boolean
424 *
425 * Start streaming music from an OGG file.
426 */
427static Value native_play_music(SigilVM *vm, int argc, Value *args)
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;
459/*
460 * (stop-music) - Stop music playback
461 */
462static Value native_stop_music(SigilVM *vm, int argc, Value *args)
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;
477/*
478 * (pause-music) - Pause music playback
479 */
480static Value native_pause_music(SigilVM *vm, int argc, Value *args)
482 (void)vm; (void)argc; (void)args;
483 g_music.paused = true;
484 return SIGIL_NIL;
487/*
488 * (resume-music) - Resume music playback
489 */
490static Value native_resume_music(SigilVM *vm, int argc, Value *args)
492 (void)vm; (void)argc; (void)args;
493 g_music.paused = false;
494 return SIGIL_NIL;
497/*
498 * (music-playing?) -> boolean
499 */
500static Value native_music_playing(SigilVM *vm, int argc, Value *args)
502 (void)vm; (void)argc; (void)args;
503 return (g_music.playing && !g_music.paused) ? SIGIL_TRUE : SIGIL_FALSE;
506/*
507 * (set-music-volume volume) - Set music volume (0.0 to 1.0)
508 */
509static Value native_set_music_volume(SigilVM *vm, int argc, Value *args)
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;
524/* ============================================================
525 * NATIVE FUNCTIONS - GLOBAL CONTROL
526 * ============================================================ */
528/*
529 * (set-master-volume volume) - Set master volume (0.0 to 1.0)
530 */
531static Value native_set_master_volume(SigilVM *vm, int argc, Value *args)
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;
546/*
547 * (mute-audio) - Mute all audio
548 */
549static Value native_mute_audio(SigilVM *vm, int argc, Value *args)
551 (void)vm; (void)argc; (void)args;
552 g_muted = true;
553 return SIGIL_NIL;
556/*
557 * (unmute-audio) - Unmute audio
558 */
559static Value native_unmute_audio(SigilVM *vm, int argc, Value *args)
561 (void)vm; (void)argc; (void)args;
562 g_muted = false;
563 return SIGIL_NIL;
566/*
567 * (audio-muted?) -> boolean
568 */
569static Value native_audio_muted(SigilVM *vm, int argc, Value *args)
571 (void)vm; (void)argc; (void)args;
572 return g_muted ? SIGIL_TRUE : SIGIL_FALSE;
575/* ============================================================
576 * MODULE INITIALIZATION
577 * ============================================================ */
579/* OGG encoding (ogg-encode.c) */
580extern void sigil__register_ogg_encode(SigilVM *vm);
582void sigil__init_sigil_audio_module(SigilVM *vm)
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);