Export audio-backend and audio-device?; SIGIL_AUDIO_BACKEND pins a backend
(audio-backend) answers the backend miniaudio picked as a symbol ('pulseaudio, 'alsa, 'jack, 'wasapi, 'dsound, 'winmm, 'coreaudio, 'null; #f before audio-setup); (audio-device?) is #t when a real device is open. audio-initialized? keeps its meaning (audio-setup ran; the mixer and the streams work on every backend), as under sokol_audio where an invalid device was silent but initialized: code that gated pushes on it keeps working, and the new predicate names the new fact.
SIGIL_AUDIO_BACKEND=<name> makes ma_context_init try that backend only, so the suite runs once as it falls and once forced to null; the drain test checks the symbol is known, that audio-device? is #t exactly when the backend is not null, that a forced name is honoured, and that both answer #f after audio-shutdown. Sabotaged (audio-device? made #t on every backend): the forced-null run failed that check.
CHANGELOG.md | 17 +++++++++++++++++
README.md | 4 +++-
src/c/audio-device.h | 10 ++++++++++
src/c/audio.c | 37 +++++++++++++++++++++++++++++++++++++
src/c/miniaudio-impl.c | 47 ++++++++++++++++++++++++++++++++++++++++++++++-
src/sigil/audio.sgl | 2 +-
test/test-device-drain.sgl | 16 ++++++++++++++++
7 files changed, 130 insertions(+), 3 deletions(-)CHANGELOG.mdmodified
for the test that opens a device (`test/test-device-drain.sgl`, which proves the device thread drains a stream).### Added- `(audio-backend)`: the backend the device runs on as a symbol (`'pulseaudio`, also on a PipeWire desktop through pipewire-pulse; `'alsa`, `'jack`, `'wasapi`, `'dsound`, `'winmm`, `'coreaudio`, or `'null` when frames are consumed in silence), `#f` before `audio-setup`.- `(audio-device?)`: `#t` when a real (non-null) device is open, i.e. when sound can reach a listener. `audio-initialized?` keeps its meaning (`audio-setup` ran; the mixer and streams work on every backend), as it had under sokol_audio where an invalid device was silent but initialized; the new predicate names the new fact.- `SIGIL_AUDIO_BACKEND=pulseaudio|alsa|jack|wasapi|coreaudio|null` picks one backend instead of miniaudio's priority walk (tests run the suite with `null` too; a desktop whose first choice misbehaves can pin one).- `test/test-device-drain.sgl`: opens the real device and proves the device thread drains a stream, and checks the two predicates agree.### Removed- `vendor/sokol` (sokol_audio.h, sokol_log.h) and `src/c/sokol-audio.c`.README.mdmodified
the active `guix shell` / `nix-shell`, then the user and system profilesof both. With no backend library anywhere miniaudio's Null backend runsthe mixer in silence and one line on stderr says so.`SIGIL_AUDIO_VERBOSE=1` prints the backend, the device and every`(audio-backend)` answers the backend as a symbol and `(audio-device?)`whether it is a real one; `SIGIL_AUDIO_BACKEND=null` (or a backend name)pins the choice. `SIGIL_AUDIO_VERBOSE=1` prints the backend, the device and everyresolver hit. A run manifest for a game on Guix adds `pulseaudio` (theclient library, 187 MiB of closure) or `alsa-lib` (3 MiB; untested here, and the ALSAroute would go through the machine's ALSA configuration to reach PipeWire) tosrc/c/audio-device.hmodified
* "WASAPI", "Core Audio", ...), or NULL when no device is open. */const char *sigil_audio_device_backend(void);/* The backend as a lowercase symbol name for Scheme ("pulseaudio", "alsa", * "jack", "wasapi", "dsound", "winmm", "coreaudio", "null", "other"), or * NULL when no device is open. PipeWire desktops answer "pulseaudio" * (pipewire-pulse): miniaudio has no PipeWire backend of its own. */const char *sigil_audio_device_backend_symbol(void);/* 1 when a device is open on a real backend, 0 on the Null backend (frames * are consumed in silence) or with no device. */int sigil_audio_device_real(void);#endif /* SIGIL_AUDIO_DEVICE_H */src/c/audio.cmodified
return g_audio_initialized ? SIGIL_TRUE : SIGIL_FALSE;}/* * (audio-backend) -> symbol or #f * * The backend the device runs on: 'pulseaudio (also on a PipeWire desktop, * through pipewire-pulse), 'alsa, 'jack, 'wasapi, 'dsound, 'winmm, * 'coreaudio, or 'null when no backend library or server was usable and * frames are consumed in silence; #f before audio-setup. */static Value native_audio_backend(SigilVM *vm, int argc, Value *args){ (void)argc; (void)args; const char *name = sigil_audio_device_backend_symbol(); if (!name) return SIGIL_FALSE; return sigil_intern_symbol(vm, name, strlen(name));}/* * (audio-device?) -> boolean * * #t when sound can reach a listener: a device is open on a real backend. * (audio-initialized?) stays #t after audio-setup whatever the backend, * because the mixer and the streams work either way (as under sokol_audio, * where an invalid device was silent but initialized); this predicate * names the new fact a game may want: whether to bother with sound. */static Value native_audio_device(SigilVM *vm, int argc, Value *args){ (void)vm; (void)argc; (void)args; return sigil_audio_device_real() ? SIGIL_TRUE : SIGIL_FALSE;}/* ============================================================ * NATIVE FUNCTIONS - SOUNDS * ============================================================ */ SIGIL_ARITY_EXACT(0), "Initialize audio"); sigil_module_register_native(vm, "audio-shutdown", native_audio_shutdown, SIGIL_ARITY_EXACT(0), "Shutdown audio"); sigil_module_register_native(vm, "audio-backend", native_audio_backend, SIGIL_ARITY_EXACT(0), "The audio backend in use, as a symbol, or #f"); sigil_module_register_native(vm, "audio-device?", native_audio_device, SIGIL_ARITY_EXACT(0), "Is a real (non-null) audio device open?"); sigil_module_register_native(vm, "audio-initialized?", native_audio_initialized, SIGIL_ARITY_EXACT(0), "Is audio initialized?"); sigil_module_export(vm, "audio-setup"); sigil_module_export(vm, "audio-shutdown"); sigil_module_export(vm, "audio-initialized?"); sigil_module_export(vm, "audio-backend"); sigil_module_export(vm, "audio-device?"); sigil_module_export(vm, "load-sound"); sigil_module_export(vm, "sound?"); sigil_module_export(vm, "play-sound");src/c/miniaudio-impl.cmodified
log = &g_log; } cc.pLog = log; if (ma_context_init(NULL, 0, &cc, &g_context) != MA_SUCCESS) { /* SIGIL_AUDIO_BACKEND=pulseaudio|alsa|jack|wasapi|coreaudio|null picks one * backend instead of miniaudio's priority walk: for tests (null is a * silent device that still consumes frames in real time) and for a * desktop where the first choice misbehaves. An unknown name is * reported and ignored. */ ma_backend forced[1]; ma_backend *backends = NULL; ma_uint32 backend_count = 0; const char *want = getenv("SIGIL_AUDIO_BACKEND"); if (want && *want) { static const struct { const char *name; ma_backend backend; } table[] = { { "pulseaudio", ma_backend_pulseaudio }, { "alsa", ma_backend_alsa }, { "jack", ma_backend_jack }, { "wasapi", ma_backend_wasapi }, { "coreaudio", ma_backend_coreaudio }, { "null", ma_backend_null }, }; size_t i; for (i = 0; i < sizeof table / sizeof table[0]; i++) { if (strcmp(want, table[i].name) == 0) { forced[0] = table[i].backend; backends = forced; backend_count = 1; break; } } if (!backends) fprintf(stderr, "sigil-audio: SIGIL_AUDIO_BACKEND=%s is not one of pulseaudio, alsa, jack, wasapi, coreaudio, null; ignored\n", want); } if (ma_context_init(backends, backend_count, &cc, &g_context) != MA_SUCCESS) { fprintf(stderr, "sigil-audio: no audio backend could be initialised\n"); return -1; } if (!g_device_ready) return NULL; return ma_get_backend_name(g_context.backend);}const char *sigil_audio_device_backend_symbol(void){ if (!g_device_ready) return NULL; switch (g_context.backend) { case ma_backend_pulseaudio: return "pulseaudio"; case ma_backend_alsa: return "alsa"; case ma_backend_jack: return "jack"; case ma_backend_wasapi: return "wasapi"; case ma_backend_dsound: return "dsound"; case ma_backend_winmm: return "winmm"; case ma_backend_coreaudio: return "coreaudio"; case ma_backend_null: return "null"; default: return "other"; }}int sigil_audio_device_real(void){ return g_device_ready && g_context.backend != ma_backend_null;}src/sigil/audio.sglmodified
(sigil time)) ;; Native functions exported directly by audio.c: ;; audio-setup, audio-shutdown, audio-initialized? ;; audio-setup, audio-shutdown, audio-initialized?, audio-backend, audio-device? ;; load-sound, sound?, play-sound, stop-all-sounds ;; play-music, stop-music, pause-music, resume-music ;; music-playing?, set-music-volumetest/test-device-drain.sglmodified
(import (sigil core) (sigil time) (sigil process) (sigil audio))(define failures 0)(audio-setup)(check "audio initialized" (audio-initialized?) #t);; the backend, as the game sees it; SIGIL_AUDIO_BACKEND=null forces the;; silent backend, and the two predicates must agree about it(let ((backend (audio-backend))) (display " backend: ") (display backend) (newline) (check "audio-backend is a symbol" (symbol? backend) #t) (check "audio-backend is a known backend" (and (memq backend '(pulseaudio alsa jack wasapi dsound winmm coreaudio null other)) #t) #t) (check "audio-device? is #t exactly when the backend is not null" (audio-device?) (not (eq? backend 'null))) (let ((forced (getenv "SIGIL_AUDIO_BACKEND"))) (when forced (check "SIGIL_AUDIO_BACKEND picked the backend" (symbol->string backend) forced))))(let* ((frames 22050) ; half a second at 44100 (s (open-audio-stream channels: 2 buffer-frames: 32768)) (bv (make-bytevector (* frames 2 4) 0)) (close-audio-stream! s))(audio-shutdown)(check "no backend after shutdown" (audio-backend) #f)(check "no device after shutdown" (audio-device?) #f)(check "audio shut down" (audio-initialized?) #f)(if (= failures 0)