AtlatestRepositorysigil-audio

sigil-audio / tree / src / cminiaudio-impl.c

1/*
2 * miniaudio-impl.c - the audio device, over miniaudio
3 *
4 * The one translation unit that compiles miniaudio (vendor/miniaudio,
5 * unpatched; VENDOR.md has the pin). It exposes three functions to audio.c
6 * (audio-device.h) so the 95k-line header is included exactly once:
7 * open a playback device with a stream callback, close it, name the
8 * backend miniaudio picked.
9 *
10 * Every audio backend library is loaded at run time by miniaudio
11 * (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 this
13 * package links no audio library. Those loads go through
14 * sigil_audio_dl_open (dl-search.c, the resolver shared with
15 * sigil-desktop) instead of dlopen: the define below renames the call in
16 * miniaudio's ma_dlopen, so a Guix or Nix profile's lib/ is searched when
17 * the loader alone finds nothing. A program that also links sigil-desktop
18 * has two copies of the resolver (one of them interposing dlopen for the
19 * whole process); they agree on the table and never conflict, because
20 * 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 by
32 * absolute framework path and need no help. */
33#include <dlfcn.h>
34void *sigil_audio_dl_open(const char *name, int flags);
35#define dlopen sigil_audio_dl_open
36#endif
38/* Device layer only: this package decodes with stb_vorbis and encodes with
39 * the vendored libvorbis; miniaudio's engine, node graph, resource manager,
40 * generators, decoders and encoders are compiled out. */
41#define MA_NO_DECODING
42#define MA_NO_ENCODING
43#define MA_NO_GENERATION
44#define MA_NO_RESOURCE_MANAGER
45#define MA_NO_NODE_GRAPH
46#define MA_NO_ENGINE
47#define MA_API static
48#define MINIAUDIO_IMPLEMENTATION
49#include "miniaudio.h"
51static ma_context g_context;
52static ma_device g_device;
53static int g_context_ready = 0;
54static int g_device_ready = 0;
55static sigil_audio_stream_cb g_stream_cb = NULL;
56static char g_app_name[128];
58static void data_callback(ma_device *device, void *output, const void *input, ma_uint32 frame_count)
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 }
68static void log_callback(void *user, ma_uint32 level, const char *message)
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 }
79/* The name a sound server shows for this stream: the program's own, as
80 * the process sees it, so `pactl list sink-inputs` names the game. */
81static const char *app_name(void)
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#endif
89 if (!name || !*name) name = "sigil-audio";
90 snprintf(g_app_name, sizeof g_app_name, "%s", name);
91 return g_app_name;
94int sigil_audio_device_open(int sample_rate, int channels, int buffer_frames,
95 sigil_audio_stream_cb stream_cb)
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 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 }
137 g_context_ready = 1;
138 if (g_context.backend == ma_backend_null) {
139 /* No backend library was found (or none opened): miniaudio's Null
140 * backend consumes frames in silence. Say so once, out loud, so a
141 * silent game is not a mystery; the trace under SIGIL_AUDIO_VERBOSE
142 * 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; miniaudio
155 * sizes a buffer as periodSizeInFrames x periods, so halve the period
156 * and use two of them to keep the same ~46 ms on ALSA. On PulseAudio
157 * tlength follows the period (1024 frames, 23 ms) and the server's own
158 * 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;
190void sigil_audio_device_close(void)
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;
203const char *sigil_audio_device_backend(void)
205 if (!g_device_ready) return NULL;
206 return ma_get_backend_name(g_context.backend);
209const char *sigil_audio_device_backend_symbol(void)
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 }
225int sigil_audio_device_real(void)
227 return g_device_ready && g_context.backend != ma_backend_null;