AtlatestRepositorysigil-audio

sigil-audio / tree / src / caudio.c

1/*
2 * audio.c - Sigil Audio Module
3 *
4 * Audio playback: a mixer of loaded sounds and one streamed music track over
5 * 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 * CONSTANTS
30 * ============================================================ */
32#define MAX_SOUNDS 64
33#define MAX_PLAYING_SOUNDS 16
34#define STREAM_BUFFER_SAMPLES 4096
36/* ============================================================
37 * DATA STRUCTURES
38 * ============================================================ */
40/* Sound effect - fully loaded into memory */
41typedef 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 */
49typedef 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 */
59typedef 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 STATE
70 * ============================================================ */
72static PlayingSound g_playing_sounds[MAX_PLAYING_SOUNDS];
73static MusicStream g_music = {0};
74static float g_master_volume = 1.0f;
75static bool g_muted = false;
76static bool g_audio_initialized = false;
78/* Type tags for foreign objects */
79static Value sound_type_tag = SIGIL_UNDEFINED;
81/* ============================================================
82 * AUDIO CALLBACK
83 * ============================================================ */
85static void audio_callback(float *buffer, int num_frames, int num_channels)
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 }
197/* ============================================================
198 * HELPER FUNCTIONS
199 * ============================================================ */
201static void ensure_sound_type(SigilVM *vm)
203 if (sigil_is_undefined(sound_type_tag)) {
204 sound_type_tag = sigil_intern_symbol(vm, "sigil-audio-sound", 17);
205 }
208static StudioSound *get_sound(SigilVM *vm, Value v)
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);
216static void sound_destructor(void *data)
218 StudioSound *snd = (StudioSound *)data;
219 if (snd) {
220 free(snd->samples);
221 free(snd);
222 }
225static PlayingSound *find_free_slot(void)
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;
235/* ============================================================
236 * NATIVE FUNCTIONS - SETUP
237 * ============================================================ */
239/*
240 * (audio-setup) - Initialize audio subsystem
241 */
242static Value native_audio_setup(SigilVM *vm, int argc, Value *args)
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 not
251 * an error, as it was not under sokol_audio: the mixer runs with no
252 * output, every play is a no-op, and (audio-initialized?) is still
253 * #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;
272/*
273 * (audio-shutdown) - Shutdown audio subsystem
274 */
275static Value native_audio_shutdown(SigilVM *vm, int argc, Value *args)
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;
298/*
299 * (audio-initialized?) -> boolean
300 */
301static Value native_audio_initialized(SigilVM *vm, int argc, Value *args)
303 (void)vm; (void)argc; (void)args;
304 return g_audio_initialized ? SIGIL_TRUE : SIGIL_FALSE;
307/*
308 * (audio-backend) -> symbol or #f
309 *
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 and
313 * frames are consumed in silence; #f before audio-setup.
314 */
315static Value native_audio_backend(SigilVM *vm, int argc, Value *args)
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));
323/*
324 * (audio-device?) -> boolean
325 *
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 predicate
330 * names the new fact a game may want: whether to bother with sound.
331 */
332static Value native_audio_device(SigilVM *vm, int argc, Value *args)
334 (void)vm; (void)argc; (void)args;
335 return sigil_audio_device_real() ? SIGIL_TRUE : SIGIL_FALSE;
338/* ============================================================
339 * NATIVE FUNCTIONS - SOUNDS
340 * ============================================================ */
342/*
343 * (load-sound path) -> <sound> or #f
344 *
345 * Load an OGG file entirely into memory.
346 */
347static Value native_load_sound(SigilVM *vm, int argc, Value *args)
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));
394/*
395 * (sound? obj) -> boolean
396 */
397static Value native_sound_p(SigilVM *vm, int argc, Value *args)
399 (void)argc;
400 return get_sound(vm, args[0]) ? SIGIL_TRUE : SIGIL_FALSE;
403/*
404 * (play-sound sound [volume] [pan] [loop?]) -> boolean
405 *
406 * Play a sound effect. Returns #t if started, #f if no slots available.
407 */
408static Value native_play_sound(SigilVM *vm, int argc, Value *args)
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;
436/*
437 * (stop-all-sounds) - Stop all playing sound effects
438 */
439static Value native_stop_all_sounds(SigilVM *vm, int argc, Value *args)
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;
450/* ============================================================
451 * NATIVE FUNCTIONS - MUSIC
452 * ============================================================ */
454/*
455 * (play-music path [loop?]) -> boolean
456 *
457 * Start streaming music from an OGG file.
458 */
459static Value native_play_music(SigilVM *vm, int argc, Value *args)
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;
491/*
492 * (stop-music) - Stop music playback
493 */
494static Value native_stop_music(SigilVM *vm, int argc, Value *args)
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;
509/*
510 * (pause-music) - Pause music playback
511 */
512static Value native_pause_music(SigilVM *vm, int argc, Value *args)
514 (void)vm; (void)argc; (void)args;
515 g_music.paused = true;
516 return SIGIL_NIL;
519/*
520 * (resume-music) - Resume music playback
521 */
522static Value native_resume_music(SigilVM *vm, int argc, Value *args)
524 (void)vm; (void)argc; (void)args;
525 g_music.paused = false;
526 return SIGIL_NIL;
529/*
530 * (music-playing?) -> boolean
531 */
532static Value native_music_playing(SigilVM *vm, int argc, Value *args)
534 (void)vm; (void)argc; (void)args;
535 return (g_music.playing && !g_music.paused) ? SIGIL_TRUE : SIGIL_FALSE;
538/*
539 * (set-music-volume volume) - Set music volume (0.0 to 1.0)
540 */
541static Value native_set_music_volume(SigilVM *vm, int argc, Value *args)
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;
556/* ============================================================
557 * NATIVE FUNCTIONS - GLOBAL CONTROL
558 * ============================================================ */
560/*
561 * (set-master-volume volume) - Set master volume (0.0 to 1.0)
562 */
563static Value native_set_master_volume(SigilVM *vm, int argc, Value *args)
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;
578/*
579 * (mute-audio) - Mute all audio
580 */
581static Value native_mute_audio(SigilVM *vm, int argc, Value *args)
583 (void)vm; (void)argc; (void)args;
584 g_muted = true;
585 return SIGIL_NIL;
588/*
589 * (unmute-audio) - Unmute audio
590 */
591static Value native_unmute_audio(SigilVM *vm, int argc, Value *args)
593 (void)vm; (void)argc; (void)args;
594 g_muted = false;
595 return SIGIL_NIL;
598/*
599 * (audio-muted?) -> boolean
600 */
601static Value native_audio_muted(SigilVM *vm, int argc, Value *args)
603 (void)vm; (void)argc; (void)args;
604 return g_muted ? SIGIL_TRUE : SIGIL_FALSE;
607/* ============================================================
608 * MODULE INITIALIZATION
609 * ============================================================ */
611/* OGG encoding (ogg-encode.c) */
612extern void sigil__register_ogg_encode(SigilVM *vm);
614void sigil__init_sigil_audio_module(SigilVM *vm)
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);