AtlatestRepositorysigil-audio
sigil-audio / tree / src / cminiaudio-impl.c
1
/*2
* miniaudio-impl.c - the audio device, over miniaudio3
*4
* The one translation unit that compiles miniaudio (vendor/miniaudio,5
* unpatched; VENDOR.md has the pin). It exposes three functions to audio.c6
* (audio-device.h) so the 95k-line header is included exactly once:7
* open a playback device with a stream callback, close it, name the8
* backend miniaudio picked.9
*10
* Every audio backend library is loaded at run time by miniaudio11
* (libpulse.so.0 first on Linux, then libasound.so.2, then libjack.so.0;12
* ole32/mmdevapi on Windows; the CoreAudio frameworks on macOS), so this13
* package links no audio library. Those loads go through14
* sigil_audio_dl_open (dl-search.c, the resolver shared with15
* sigil-desktop) instead of dlopen: the define below renames the call in16
* miniaudio's ma_dlopen, so a Guix or Nix profile's lib/ is searched when17
* the loader alone finds nothing. A program that also links sigil-desktop18
* has two copies of the resolver (one of them interposing dlopen for the19
* whole process); they agree on the table and never conflict, because20
* only sigil-desktop's copy defines the dlopen symbol.21
*/23
#include "audio-device.h"25
#include <stdio.h>26
#include <stdlib.h>27
#include <string.h>29
#if defined(__linux__)30
/* The resolver (dl-search.c, SIGIL_DL_SEARCH_OPEN=sigil_audio_dl_open):31
* Linux only, like the file itself; Windows and macOS load by name or by32
* absolute framework path and need no help. */33
#include <dlfcn.h>34
void *sigil_audio_dl_open(const char *name, int flags);35
#define dlopen sigil_audio_dl_open36
#endif38
/* Device layer only: this package decodes with stb_vorbis and encodes with39
* the vendored libvorbis; miniaudio's engine, node graph, resource manager,40
* generators, decoders and encoders are compiled out. */41
#define MA_NO_DECODING42
#define MA_NO_ENCODING43
#define MA_NO_GENERATION44
#define MA_NO_RESOURCE_MANAGER45
#define MA_NO_NODE_GRAPH46
#define MA_NO_ENGINE47
#define MA_API static48
#define MINIAUDIO_IMPLEMENTATION49
#include "miniaudio.h"51
static ma_context g_context;52
static ma_device g_device;53
static int g_context_ready = 0;54
static int g_device_ready = 0;55
static sigil_audio_stream_cb g_stream_cb = NULL;56
static char g_app_name[128];58
static void data_callback(ma_device *device, void *output, const void *input, ma_uint32 frame_count)59
{60
(void)input;61
if (g_stream_cb) {62
g_stream_cb((float *)output, (int)frame_count, (int)device->playback.channels);63
} else {64
memset(output, 0, (size_t)frame_count * device->playback.channels * sizeof(float));65
}66
}68
static void log_callback(void *user, ma_uint32 level, const char *message)69
{70
(void)user;71
if (level <= MA_LOG_LEVEL_WARNING || getenv("SIGIL_AUDIO_VERBOSE")) {72
/* most of miniaudio's messages carry no newline */73
size_t n = strlen(message);74
fprintf(stderr, "sigil-audio: miniaudio %s: %s%s", ma_log_level_to_string(level), message,75
(n > 0 && message[n - 1] == '\n') ? "" : "\n");76
}77
}79
/* The name a sound server shows for this stream: the program's own, as80
* the process sees it, so `pactl list sink-inputs` names the game. */81
static const char *app_name(void)82
{83
if (g_app_name[0]) return g_app_name;84
const char *name = NULL;85
#if defined(__GLIBC__)86
extern char *program_invocation_short_name;87
name = program_invocation_short_name;88
#endif89
if (!name || !*name) name = "sigil-audio";90
snprintf(g_app_name, sizeof g_app_name, "%s", name);91
return g_app_name;92
}94
int sigil_audio_device_open(int sample_rate, int channels, int buffer_frames,95
sigil_audio_stream_cb stream_cb)96
{97
if (g_device_ready) return 0;99
ma_context_config cc = ma_context_config_init();100
cc.pulse.pApplicationName = app_name();101
cc.jack.pClientName = app_name();102
ma_log *log = NULL;103
static ma_log g_log;104
if (ma_log_init(NULL, &g_log) == MA_SUCCESS) {105
ma_log_register_callback(&g_log, ma_log_callback_init(log_callback, NULL));106
log = &g_log;107
}108
cc.pLog = log;109
/* SIGIL_AUDIO_BACKEND=pulseaudio|alsa|jack|wasapi|coreaudio|null picks one110
* backend instead of miniaudio's priority walk: for tests (null is a111
* silent device that still consumes frames in real time) and for a112
* desktop where the first choice misbehaves. An unknown name is113
* 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
}137
g_context_ready = 1;138
if (g_context.backend == ma_backend_null) {139
/* No backend library was found (or none opened): miniaudio's Null140
* backend consumes frames in silence. Say so once, out loud, so a141
* silent game is not a mystery; the trace under SIGIL_AUDIO_VERBOSE142
* lists every directory the resolver tried. */143
fprintf(stderr, "sigil-audio: no audio backend could be used "144
"(no libpulse.so.0 / libasound.so.2 / libjack.so.0 on Linux, or no server behind one; "145
"the lines above say which); running silent. "146
"Put one in a profile the resolver searches or in SIGIL_LIBRARY_PATH.\n");147
}149
g_stream_cb = stream_cb;150
ma_device_config dc = ma_device_config_init(ma_device_type_playback);151
dc.playback.format = ma_format_f32;152
dc.playback.channels = (ma_uint32)channels;153
dc.sampleRate = (ma_uint32)sample_rate;154
/* sokol_audio's ALSA buffer was buffer_frames (2048) in total; miniaudio155
* sizes a buffer as periodSizeInFrames x periods, so halve the period156
* and use two of them to keep the same ~46 ms on ALSA. On PulseAudio157
* tlength follows the period (1024 frames, 23 ms) and the server's own158
* buffering sits behind it. */159
dc.periodSizeInFrames = (ma_uint32)(buffer_frames / 2);160
dc.periods = 2;161
dc.dataCallback = data_callback;162
/* the stream callback writes every frame (it clears first) */163
dc.noPreSilencedOutputBuffer = MA_TRUE;164
dc.pulse.pStreamNamePlayback = app_name();165
if (ma_device_init(&g_context, &dc, &g_device) != MA_SUCCESS) {166
fprintf(stderr, "sigil-audio: %s: could not open a playback device\n",167
ma_get_backend_name(g_context.backend));168
ma_context_uninit(&g_context);169
g_context_ready = 0;170
return -1;171
}172
if (ma_device_start(&g_device) != MA_SUCCESS) {173
fprintf(stderr, "sigil-audio: %s: could not start the playback device\n",174
ma_get_backend_name(g_context.backend));175
ma_device_uninit(&g_device);176
ma_context_uninit(&g_context);177
g_context_ready = 0;178
return -1;179
}180
g_device_ready = 1;181
if (getenv("SIGIL_AUDIO_VERBOSE")) {182
fprintf(stderr, "sigil-audio: backend=%s device=\"%s\" format=f32 channels=%u rate=%u period=%u frames\n",183
ma_get_backend_name(g_context.backend), g_device.playback.name,184
g_device.playback.channels, g_device.sampleRate,185
g_device.playback.internalPeriodSizeInFrames);186
}187
return 0;188
}190
void sigil_audio_device_close(void)191
{192
if (g_device_ready) {193
ma_device_uninit(&g_device);194
g_device_ready = 0;195
}196
if (g_context_ready) {197
ma_context_uninit(&g_context);198
g_context_ready = 0;199
}200
g_stream_cb = NULL;201
}203
const char *sigil_audio_device_backend(void)204
{205
if (!g_device_ready) return NULL;206
return ma_get_backend_name(g_context.backend);207
}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
}225
int sigil_audio_device_real(void)226
{227
return g_device_ready && g_context.backend != ma_backend_null;228
}