Commit83728102Recorded20 Sep 2026Repositorysigil-audio

Export audio-backend and audio-device?; SIGIL_AUDIO_BACKEND pins a backend

Message

(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.

Changed
 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(-)
Diff
CHANGELOG.mdmodified
@@ -33,6 +33,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
33
for the test that opens a device (`test/test-device-drain.sgl`, which
34
proves the device thread drains a stream).
35
+36
### Added
+37
+38
- `(audio-backend)`: the backend the device runs on as a symbol
+39
(`'pulseaudio`, also on a PipeWire desktop through pipewire-pulse;
+40
`'alsa`, `'jack`, `'wasapi`, `'dsound`, `'winmm`, `'coreaudio`, or
+41
`'null` when frames are consumed in silence), `#f` before `audio-setup`.
+42
- `(audio-device?)`: `#t` when a real (non-null) device is open, i.e.
+43
when sound can reach a listener. `audio-initialized?` keeps its meaning
+44
(`audio-setup` ran; the mixer and streams work on every backend), as it
+45
had under sokol_audio where an invalid device was silent but
+46
initialized; the new predicate names the new fact.
+47
- `SIGIL_AUDIO_BACKEND=pulseaudio|alsa|jack|wasapi|coreaudio|null` picks
+48
one backend instead of miniaudio's priority walk (tests run the suite
+49
with `null` too; a desktop whose first choice misbehaves can pin one).
+50
- `test/test-device-drain.sgl`: opens the real device and proves the
+51
device thread drains a stream, and checks the two predicates agree.
+52
53
### Removed
54
55
- `vendor/sokol` (sokol_audio.h, sokol_log.h) and `src/c/sokol-audio.c`.
README.mdmodified
@@ -29,7 +29,9 @@ sigil-desktop's, see `src/c/DL-SEARCH-ORIGIN.md`): `SIGIL_LIBRARY_PATH`,
29
the active `guix shell` / `nix-shell`, then the user and system profiles
30
of both. With no backend library anywhere miniaudio's Null backend runs
31
the mixer in silence and one line on stderr says so.
−32
`SIGIL_AUDIO_VERBOSE=1` prints the backend, the device and every
+32
`(audio-backend)` answers the backend as a symbol and `(audio-device?)`
+33
whether it is a real one; `SIGIL_AUDIO_BACKEND=null` (or a backend name)
+34
pins the choice. `SIGIL_AUDIO_VERBOSE=1` prints the backend, the device and every
35
resolver hit. A run manifest for a game on Guix adds `pulseaudio` (the
36
client library, 187 MiB of closure) or `alsa-lib` (3 MiB; untested here, and the ALSA
37
route would go through the machine's ALSA configuration to reach PipeWire) to
src/c/audio-device.hmodified
@@ -19,4 +19,14 @@ void sigil_audio_device_close(void);
19
* "WASAPI", "Core Audio", ...), or NULL when no device is open. */
20
const char *sigil_audio_device_backend(void);
21
+22
/* The backend as a lowercase symbol name for Scheme ("pulseaudio", "alsa",
+23
* "jack", "wasapi", "dsound", "winmm", "coreaudio", "null", "other"), or
+24
* NULL when no device is open. PipeWire desktops answer "pulseaudio"
+25
* (pipewire-pulse): miniaudio has no PipeWire backend of its own. */
+26
const char *sigil_audio_device_backend_symbol(void);
+27
+28
/* 1 when a device is open on a real backend, 0 on the Null backend (frames
+29
* are consumed in silence) or with no device. */
+30
int sigil_audio_device_real(void);
+31
32
#endif /* SIGIL_AUDIO_DEVICE_H */
src/c/audio.cmodified
@@ -304,6 +304,37 @@ static Value native_audio_initialized(SigilVM *vm, int argc, Value *args)
304
return g_audio_initialized ? SIGIL_TRUE : SIGIL_FALSE;
305
}
306
+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
*/
+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
}
+322
+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
*/
+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
}
+337
338
/* ============================================================
339
* NATIVE FUNCTIONS - SOUNDS
340
* ============================================================ */
@@ -590,6 +621,10 @@ void sigil__init_sigil_audio_module(SigilVM *vm)
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?");
630
@@ -631,6 +666,8 @@ void sigil__init_sigil_audio_module(SigilVM *vm)
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");
src/c/miniaudio-impl.cmodified
@@ -106,7 +106,31 @@ int sigil_audio_device_open(int sample_rate, int channels, int buffer_frames,
106
log = &g_log;
107
}
108
cc.pLog = log;
−109
if (ma_context_init(NULL, 0, &cc, &g_context) != MA_SUCCESS) {
+109
/* SIGIL_AUDIO_BACKEND=pulseaudio|alsa|jack|wasapi|coreaudio|null picks one
+110
* backend instead of miniaudio's priority walk: for tests (null is a
+111
* silent device that still consumes frames in real time) and for a
+112
* desktop where the first choice misbehaves. An unknown name is
+113
* reported and ignored. */
+114
ma_backend forced[1];
+115
ma_backend *backends = NULL;
+116
ma_uint32 backend_count = 0;
+117
const char *want = getenv("SIGIL_AUDIO_BACKEND");
+118
if (want && *want) {
+119
static const struct { const char *name; ma_backend backend; } table[] = {
+120
{ "pulseaudio", ma_backend_pulseaudio }, { "alsa", ma_backend_alsa },
+121
{ "jack", ma_backend_jack }, { "wasapi", ma_backend_wasapi },
+122
{ "coreaudio", ma_backend_coreaudio }, { "null", ma_backend_null },
+123
};
+124
size_t i;
+125
for (i = 0; i < sizeof table / sizeof table[0]; i++) {
+126
if (strcmp(want, table[i].name) == 0) {
+127
forced[0] = table[i].backend; backends = forced; backend_count = 1;
+128
break;
+129
}
+130
}
+131
if (!backends) fprintf(stderr, "sigil-audio: SIGIL_AUDIO_BACKEND=%s is not one of pulseaudio, alsa, jack, wasapi, coreaudio, null; ignored\n", want);
+132
}
+133
if (ma_context_init(backends, backend_count, &cc, &g_context) != MA_SUCCESS) {
134
fprintf(stderr, "sigil-audio: no audio backend could be initialised\n");
135
return -1;
136
}
@@ -181,3 +205,24 @@ const char *sigil_audio_device_backend(void)
205
if (!g_device_ready) return NULL;
206
return ma_get_backend_name(g_context.backend);
207
}
+208
+209
const char *sigil_audio_device_backend_symbol(void)
+210
{
+211
if (!g_device_ready) return NULL;
+212
switch (g_context.backend) {
+213
case ma_backend_pulseaudio: return "pulseaudio";
+214
case ma_backend_alsa: return "alsa";
+215
case ma_backend_jack: return "jack";
+216
case ma_backend_wasapi: return "wasapi";
+217
case ma_backend_dsound: return "dsound";
+218
case ma_backend_winmm: return "winmm";
+219
case ma_backend_coreaudio: return "coreaudio";
+220
case ma_backend_null: return "null";
+221
default: return "other";
+222
}
+223
}
+224
+225
int sigil_audio_device_real(void)
+226
{
+227
return g_device_ready && g_context.backend != ma_backend_null;
+228
}
src/sigil/audio.sglmodified
@@ -30,7 +30,7 @@
30
(sigil time))
31
32
;; Native functions exported directly by audio.c:
−33
;; audio-setup, audio-shutdown, audio-initialized?
+33
;; audio-setup, audio-shutdown, audio-initialized?, audio-backend, audio-device?
34
;; load-sound, sound?, play-sound, stop-all-sounds
35
;; play-music, stop-music, pause-music, resume-music
36
;; music-playing?, set-music-volume
test/test-device-drain.sglmodified
@@ -10,6 +10,7 @@
10
11
(import (sigil core)
12
(sigil time)
+13
(sigil process)
14
(sigil audio))
15
16
(define failures 0)
@@ -30,6 +31,19 @@
31
(audio-setup)
32
(check "audio initialized" (audio-initialized?) #t)
33
+34
;; the backend, as the game sees it; SIGIL_AUDIO_BACKEND=null forces the
+35
;; silent backend, and the two predicates must agree about it
+36
(let ((backend (audio-backend)))
+37
(display " backend: ") (display backend) (newline)
+38
(check "audio-backend is a symbol" (symbol? backend) #t)
+39
(check "audio-backend is a known backend"
+40
(and (memq backend '(pulseaudio alsa jack wasapi dsound winmm coreaudio null other)) #t) #t)
+41
(check "audio-device? is #t exactly when the backend is not null"
+42
(audio-device?) (not (eq? backend 'null)))
+43
(let ((forced (getenv "SIGIL_AUDIO_BACKEND")))
+44
(when forced
+45
(check "SIGIL_AUDIO_BACKEND picked the backend" (symbol->string backend) forced))))
+46
47
(let* ((frames 22050) ; half a second at 44100
48
(s (open-audio-stream channels: 2 buffer-frames: 32768))
49
(bv (make-bytevector (* frames 2 4) 0))
@@ -45,6 +59,8 @@
59
(close-audio-stream! s))
60
61
(audio-shutdown)
+62
(check "no backend after shutdown" (audio-backend) #f)
+63
(check "no device after shutdown" (audio-device?) #f)
64
(check "audio shut down" (audio-initialized?) #f)
65
66
(if (= failures 0)