1 | #pragma once |
2 | /* |
3 | sokol_audio.h -- cross-platform audio-streaming API |
4 | |
5 | Do this: |
6 | #define SOKOL_IMPL |
7 | before you include this file in *one* C or C++ file to create the |
8 | implementation. |
9 | |
10 | Optionally provide the following defines with your own implementations: |
11 | |
12 | SOKOL_AUDIO_NO_BACKEND - use a dummy backend |
13 | SOKOL_ASSERT(c) - your own assert macro (default: assert(c)) |
14 | SOKOL_LOG(msg) - your own logging function (default: puts(msg)) |
15 | SOKOL_MALLOC(s) - your own malloc() implementation (default: malloc(s)) |
16 | SOKOL_FREE(p) - your own free() implementation (default: free(p)) |
17 | SOKOL_API_DECL - public function declaration prefix (default: extern) |
18 | SOKOL_API_IMPL - public function implementation prefix (default: -) |
19 | |
20 | FEATURE OVERVIEW |
21 | ================ |
22 | You provide a mono- or stereo-stream of 32-bit float samples, which |
23 | Sokol Audio feeds into platform-specific audio backends: |
24 | |
25 | - Windows: WASAPI |
26 | - Linux: ALSA (link with asound) |
27 | - macOS/iOS: CoreAudio (link with AudioToolbox) |
28 | - emscripten: WebAudio with ScriptProcessorNode |
29 | |
30 | Sokol Audio will not do any buffer mixing or volume control, if you have |
31 | multiple independent input streams of sample data you need to perform the |
32 | mixing yourself before forwarding the data to Sokol Audio. |
33 | |
34 | There are two mutually exclusive ways to provide the sample data: |
35 | |
36 | 1. Callback model: You provide a callback function, which will be called |
37 | when Sokol Audio needs new samples. On all platforms except emscripten, |
38 | this function is called from a separate thread. |
39 | 2. Push model: Your code pushes small blocks of sample data from your |
40 | main loop or a thread you created. The pushed data is stored in |
41 | a ring buffer where it is pulled by the backend code when |
42 | needed. |
43 | |
44 | The callback model is preferred because it is the most direct way to |
45 | feed sample data into the audio backends and also has less moving parts |
46 | (there is no ring buffer between your code and the audio backend). |
47 | |
48 | Sometimes it is not possible to generate the audio stream directly in a |
49 | callback function running in a separate thread, for such cases Sokol Audio |
50 | provides the push-model as a convenience. |
51 | |
52 | SOKOL AUDIO AND SOLOUD |
53 | ====================== |
54 | The WASAPI, ALSA and CoreAudio backend code has been taken from the |
55 | SoLoud library (with some modifications, so any bugs in there are most |
56 | likely my fault). If you need a more fully-featured audio solution, check |
57 | out SoLoud, it's excellent: |
58 | |
59 | https://github.com/jarikomppa/soloud |
60 | |
61 | GLOSSARY |
62 | ======== |
63 | - stream buffer: |
64 | The internal audio data buffer, usually provided by the backend API. The |
65 | size of the stream buffer defines the base latency, smaller buffers have |
66 | lower latency but may cause audio glitches. Bigger buffers reduce or |
67 | eliminate glitches, but have a higher base latency. |
68 | |
69 | - stream callback: |
70 | Optional callback function which is called by Sokol Audio when it |
71 | needs new samples. On Windows, macOS/iOS and Linux, this is called in |
72 | a separate thread, on WebAudio, this is called per-frame in the |
73 | browser thread. |
74 | |
75 | - channel: |
76 | A discrete track of audio data, currently 1-channel (mono) and |
77 | 2-channel (stereo) is supported and tested. |
78 | |
79 | - sample: |
80 | The magnitude of an audio signal on one channel at a given time. In |
81 | Sokol Audio, samples are 32-bit float numbers in the range -1.0 to |
82 | +1.0. |
83 | |
84 | - frame: |
85 | The tightly packed set of samples for all channels at a given time. |
86 | For mono 1 frame is 1 sample. For stereo, 1 frame is 2 samples. |
87 | |
88 | - packet: |
89 | In Sokol Audio, a small chunk of audio data that is moved from the |
90 | main thread to the audio streaming thread in order to decouple the |
91 | rate at which the main thread provides new audio data, and the |
92 | streaming thread consuming audio data. |
93 | |
94 | WORKING WITH SOKOL AUDIO |
95 | ======================== |
96 | First call saudio_setup() with your preferred audio playback options. |
97 | In most cases you can stick with the default values, these provide |
98 | a good balance between low-latency and glitch-free playback |
99 | on all audio backends. |
100 | |
101 | If you want to use the callback-model, you need to provide a stream |
102 | callback function in stream_cb, otherwise keep the stream_cb member |
103 | initialized to zero. |
104 | |
105 | Use push model and default playback parameters: |
106 | |
107 | saudio_setup(&(saudio_desc){0}); |
108 | |
109 | Use stream callback model and default playback parameters: |
110 | |
111 | saudio_setup(&(saudio_desc){ |
112 | .stream_cb = my_stream_callback |
113 | }); |
114 | |
115 | The following playback parameters can be provided through the |
116 | saudio_desc struct: |
117 | |
118 | General parameters (both for stream-callback and push-model): |
119 | |
120 | int sample_rate -- the sample rate in Hz, default: 44100 |
121 | int num_channels -- number of channels, default: 1 (mono) |
122 | int buffer_frames -- number of frames in streaming buffer, default: 2048 |
123 | |
124 | Stream callback parameters: |
125 | |
126 | void (*stream_cb)(float* buffer, int num_frames, int num_channels) |
127 | Function pointer to the user-provide stream callback. |
128 | |
129 | Push-model parameters: |
130 | |
131 | int packet_frames -- number of frames in a packet, default: 128 |
132 | int num_packets -- number of packets in ring buffer, default: 64 |
133 | |
134 | The sample_rate and num_channels parameters are only hints for the audio |
135 | backend, it isn't guaranteed that those are the values used for actual |
136 | playback. |
137 | |
138 | To get the actual parameters, call the following functions after |
139 | saudio_setup(): |
140 | |
141 | int saudio_sample_rate(void) |
142 | int saudio_channels(void); |
143 | |
144 | It's unlikely that the number of channels will be different than requested, |
145 | but a different sample rate isn't uncommon. |
146 | |
147 | (NOTE: there's an yet unsolved issue when an audio backend might switch |
148 | to a different sample rate when switching output devices, for instance |
149 | plugging in a bluetooth headset, this case is currently not handled in |
150 | Sokol Audio). |
151 | |
152 | You can check if audio initialization was successfull with |
153 | saudio_isvalid(). If backend initialization failed for some reason |
154 | (for instance when there's no audio device in the machine), this |
155 | will return false. Not checking for success won't do any harm, all |
156 | Sokol Audio function will silently fail when called after initialization |
157 | has failed, so apart from missing audio output, nothing bad will happen. |
158 | |
159 | Before your application exits, you should call |
160 | |
161 | saudio_shutdown(); |
162 | |
163 | This stops the audio thread (on Linux, Windows and macOS/iOS) and |
164 | properly shuts down the audio backend. |
165 | |
166 | THE STREAM CALLBACK MODEL |
167 | ========================= |
168 | To use Sokol Audio in stream-callback-mode, provide a callback function |
169 | like this in the saudio_desc struct when calling saudio_setup(): |
170 | |
171 | void stream_cb(float* buffer, int num_frames, int num_channels) { ... } |
172 | |
173 | The job of the callback function is to fill the *buffer* with 32-bit |
174 | float sample values. |
175 | |
176 | To output silence, fill the buffer with zeros: |
177 | |
178 | void stream_cb(float* buffer, int num_frames, int num_channels) { |
179 | const int num_samples = num_frames * num_channels; |
180 | for (int i = 0; i < num_samples; i++) { |
181 | buffer[i] = 0.0f; |
182 | } |
183 | } |
184 | |
185 | For stereo output (num_channels == 2), the samples for the left |
186 | and right channel are interleaved: |
187 | |
188 | void stream_cb(float* buffer, int num_frames, int num_channels) { |
189 | assert(2 == num_channels); |
190 | for (int i = 0; i < num_frames; i++) { |
191 | buffer[2*i + 0] = ...; // left channel |
192 | buffer[2*i + 1] = ...; // right channel |
193 | } |
194 | } |
195 | |
196 | Please keep in mind that the stream callback function is running in a |
197 | separate thread, if you need to share data with the main thread you need |
198 | to take care yourself to make the access to the shared data thread-safe! |
199 | |
200 | THE PUSH MODEL |
201 | ============== |
202 | To use the push-model for providing audio data, simply don't set (keep |
203 | zero-initialized) the stream_cb field in the saudio_desc struct when |
204 | calling saudio_setup(). |
205 | |
206 | To provide sample data with the push model, call the saudio_push() |
207 | function at regular intervals (for instance once per frame). You can |
208 | call the saudio_expect() function to ask Sokol Audio how much room is |
209 | in the ring buffer, but if you provide a continuous stream of data |
210 | at the right sample rate, saudio_expect() isn't required (it's a simple |
211 | way to sync/throttle your sample generation code with the playback |
212 | rate though). |
213 | |
214 | With saudio_push() you may need to maintain your own intermediate sample |
215 | buffer, since pushing individual sample values isn't very efficient. |
216 | The following example is from the MOD player sample in |
217 | sokol-samples (https://github.com/floooh/sokol-samples): |
218 | |
219 | const int num_frames = saudio_expect(); |
220 | if (num_frames > 0) { |
221 | const int num_samples = num_frames * saudio_channels(); |
222 | read_samples(flt_buf, num_samples); |
223 | saudio_push(flt_buf, num_frames); |
224 | } |
225 | |
226 | Another option is to ignore saudio_expect(), and just push samples as they |
227 | are generated in small batches. In this case you *need* to generate the |
228 | samples at the right sample rate: |
229 | |
230 | The following example is taken from the Tiny Emulators project |
231 | (https://github.com/floooh/chips-test), this is for mono playback, |
232 | so (num_samples == num_frames): |
233 | |
234 | // tick the sound generator |
235 | if (ay38910_tick(&sys->psg)) { |
236 | // new sample is ready |
237 | sys->sample_buffer[sys->sample_pos++] = sys->psg.sample; |
238 | if (sys->sample_pos == sys->num_samples) { |
239 | // new sample packet is ready |
240 | saudio_push(sys->sample_buffer, sys->num_samples); |
241 | sys->sample_pos = 0; |
242 | } |
243 | } |
244 | |
245 | THE WEBAUDIO BACKEND |
246 | ==================== |
247 | The WebAudio backend is currently using a ScriptProcessorNode callback to |
248 | feed the sample data into WebAudio. ScriptProcessorNode has been |
249 | deprecated for a while because it is running from the main thread, with |
250 | the default initialization parameters it works 'pretty well' though. |
251 | Ultimately Sokol Audio will use Audio Worklets, but this requires a few |
252 | more things to fall into place (Audio Worklets implemented everywhere, |
253 | SharedArrayBuffers enabled again, and I need to figure out a 'low-cost' |
254 | solution in terms of implementation effort, since Audio Worklets are |
255 | a lot more complex than ScriptProcessorNode if the audio data needs to come |
256 | from the main thread). |
257 | |
258 | The WebAudio backend is automatically selected when compiling for |
259 | emscripten (__EMSCRIPTEN__ define exists). |
260 | |
261 | https://developers.google.com/web/updates/2017/12/audio-worklet |
262 | https://developers.google.com/web/updates/2018/06/audio-worklet-design-pattern |
263 | |
264 | "Blob URLs": https://www.html5rocks.com/en/tutorials/workers/basics/ |
265 | |
266 | THE COREAUDIO BACKEND |
267 | ===================== |
268 | The CoreAudio backend is selected on macOS and iOS (__APPLE__ is defined). |
269 | Since the CoreAudio API is implemented in C (not Objective-C) the |
270 | implementation part of Sokol Audio can be included into a C source file. |
271 | |
272 | For thread synchronisation, the CoreAudio backend will use the |
273 | pthread_mutex_* functions. |
274 | |
275 | The incoming floating point samples will be directly forwarded to |
276 | CoreAudio without further conversion. |
277 | |
278 | macOS and iOS applications that use Sokol Audio need to link with |
279 | the AudioToolbox framework. |
280 | |
281 | THE WASAPI BACKEND |
282 | ================== |
283 | The WASAPI backend is automatically selected when compiling on Windows |
284 | (_WIN32 is defined). |
285 | |
286 | For thread synchronisation a Win32 critical section is used. |
287 | |
288 | WASAPI may use a different size for its own streaming buffer then requested, |
289 | so the base latency may be slightly bigger. The current backend implementation |
290 | convertes the incoming floating point sample values to signed 16-bit |
291 | integers. |
292 | |
293 | The required Windows system DLLs are linked with #pragma comment(lib, ...), |
294 | so you shouldn't need to add additional linker libs in the build process |
295 | (otherwise this is a bug which should be fixed in sokol_audio.h). |
296 | |
297 | THE ALSA BACKEND |
298 | ================ |
299 | The ALSA backend is automatically selected when compiling on Linux |
300 | ('linux' is defined). |
301 | |
302 | For thread synchronisation, the pthread_mutex_* functions are used. |
303 | |
304 | Samples are directly forwarded to ALSA in 32-bit float format, no |
305 | further conversion is taking place. |
306 | |
307 | You need to link with the 'asound' library, and the <alsa/asoundlib.h> |
308 | header must be present (usually both are installed with some sort |
309 | of ALSA development package). |
310 | |
311 | LICENSE |
312 | ======= |
313 | |
314 | zlib/libpng license |
315 | |
316 | Copyright (c) 2018 Andre Weissflog |
317 | |
318 | This software is provided 'as-is', without any express or implied warranty. |
319 | In no event will the authors be held liable for any damages arising from the |
320 | use of this software. |
321 | |
322 | Permission is granted to anyone to use this software for any purpose, |
323 | including commercial applications, and to alter it and redistribute it |
324 | freely, subject to the following restrictions: |
325 | |
326 | 1. The origin of this software must not be misrepresented; you must not |
327 | claim that you wrote the original software. If you use this software in a |
328 | product, an acknowledgment in the product documentation would be |
329 | appreciated but is not required. |
330 | |
331 | 2. Altered source versions must be plainly marked as such, and must not |
332 | be misrepresented as being the original software. |
333 | |
334 | 3. This notice may not be removed or altered from any source |
335 | distribution. |
336 | */ |
337 | #include <stdint.h> |
338 | #include <stdbool.h> |
339 | |
340 | #ifndef SOKOL_API_DECL |
341 | #define SOKOL_API_DECL extern |
342 | #endif |
343 | |
344 | #ifdef __cplusplus |
345 | extern "C" { |
346 | #endif |
347 | |
348 | typedef struct { |
349 | int sample_rate; /* requested sample rate */ |
350 | int num_channels; /* number of channels, default: 1 (mono) */ |
351 | int buffer_frames; /* number of frames in streaming buffer */ |
352 | int packet_frames; /* number of frames in a packet */ |
353 | int num_packets; /* number of packets in packet queue */ |
354 | void (*stream_cb)(float* buffer, int num_frames, int num_channels); /* optional streaming callback */ |
355 | } saudio_desc; |
356 | |
357 | /* setup sokol-audio */ |
358 | SOKOL_API_DECL void saudio_setup(const saudio_desc* desc); |
359 | /* shutdown sokol-audio */ |
360 | SOKOL_API_DECL void saudio_shutdown(void); |
361 | /* true after setup if audio backend was successfully initialized */ |
362 | SOKOL_API_DECL bool saudio_isvalid(void); |
363 | /* actual sample rate */ |
364 | SOKOL_API_DECL int saudio_sample_rate(void); |
365 | /* actual backend buffer size */ |
366 | SOKOL_API_DECL int saudio_buffer_size(void); |
367 | /* actual number of channels */ |
368 | SOKOL_API_DECL int saudio_channels(void); |
369 | /* get current number of frames to fill packet queue */ |
370 | SOKOL_API_DECL int saudio_expect(void); |
371 | /* push sample frames from main thread, returns number of frames actually pushed */ |
372 | SOKOL_API_DECL int saudio_push(const float* frames, int num_frames); |
373 | |
374 | #ifdef __cplusplus |
375 | } /* extern "C" */ |
376 | #endif |
377 | |
378 | /*--- IMPLEMENTATION ---------------------------------------------------------*/ |
379 | #ifdef SOKOL_IMPL |
380 | #include <string.h> /* memset, memcpy */ |
381 | |
382 | #ifdef _MSC_VER |
383 | #pragma warning(push) |
384 | #pragma warning(disable:4505) /* unreferenced local function has been removed */ |
385 | #endif |
386 | |
387 | #ifndef SOKOL_API_IMPL |
388 | #define SOKOL_API_IMPL |
389 | #endif |
390 | #ifndef SOKOL_DEBUG |
391 | #ifdef _DEBUG |
392 | #define SOKOL_DEBUG (1) |
393 | #endif |
394 | #endif |
395 | #ifndef SOKOL_ASSERT |
396 | #include <assert.h> |
397 | #define SOKOL_ASSERT(c) assert(c) |
398 | #endif |
399 | #ifndef SOKOL_MALLOC |
400 | #include <stdlib.h> |
401 | #define SOKOL_MALLOC(s) malloc(s) |
402 | #define SOKOL_FREE(p) free(p) |
403 | #endif |
404 | #ifndef SOKOL_LOG |
405 | #ifdef SOKOL_DEBUG |
406 | #include <stdio.h> |
407 | #define SOKOL_LOG(s) { SOKOL_ASSERT(s); puts(s); } |
408 | #else |
409 | #define SOKOL_LOG(s) |
410 | #endif |
411 | #endif |
412 | |
413 | #ifndef _SOKOL_PRIVATE |
414 | #if defined(__GNUC__) |
415 | #define _SOKOL_PRIVATE __attribute__((unused)) static |
416 | #else |
417 | #define _SOKOL_PRIVATE static |
418 | #endif |
419 | #endif |
420 | |
421 | #define _saudio_def(val, def) (((val) == 0) ? (def) : (val)) |
422 | #define _saudio_def_flt(val, def) (((val) == 0.0f) ? (def) : (val)) |
423 | |
424 | /*--- implementation-private constants ---------------------------------------*/ |
425 | #define _SAUDIO_DEFAULT_SAMPLE_RATE (44100) |
426 | #define _SAUDIO_DEFAULT_BUFFER_FRAMES (2048) |
427 | #define _SAUDIO_DEFAULT_PACKET_FRAMES (128) |
428 | #define _SAUDIO_DEFAULT_NUM_PACKETS ((_SAUDIO_DEFAULT_BUFFER_FRAMES/_SAUDIO_DEFAULT_PACKET_FRAMES)*4) |
429 | #define _SAUDIO_RING_MAX_SLOTS (128) |
430 | |
431 | /*--- mutex wrappers ---------------------------------------------------------*/ |
432 | #if defined(__APPLE__) || defined(linux) |
433 | #include "pthread.h" |
434 | static pthread_mutex_t _saudio_mutex; |
435 | |
436 | _SOKOL_PRIVATE void _saudio_mutex_init(void) { |
437 | pthread_mutexattr_t attr; |
438 | pthread_mutexattr_init(&attr); |
439 | pthread_mutex_init(&_saudio_mutex, &attr); |
440 | } |
441 | |
442 | _SOKOL_PRIVATE void _saudio_mutex_destroy(void) { |
443 | pthread_mutex_destroy(&_saudio_mutex); |
444 | } |
445 | |
446 | _SOKOL_PRIVATE void _saudio_mutex_lock(void) { |
447 | pthread_mutex_lock(&_saudio_mutex); |
448 | } |
449 | |
450 | _SOKOL_PRIVATE void _saudio_mutex_unlock(void) { |
451 | pthread_mutex_unlock(&_saudio_mutex); |
452 | } |
453 | #elif defined(_WIN32) |
454 | #ifndef WIN32_LEAN_AND_MEAN |
455 | #define WIN32_LEAN_AND_MEAN |
456 | #endif |
457 | #include <windows.h> |
458 | #include <synchapi.h> |
459 | #pragma comment (lib, "kernel32.lib") |
460 | #pragma comment (lib, "ole32.lib") |
461 | |
462 | CRITICAL_SECTION _saudio_crit; |
463 | |
464 | _SOKOL_PRIVATE void _saudio_mutex_init(void) { |
465 | InitializeCriticalSection(&_saudio_crit); |
466 | } |
467 | |
468 | _SOKOL_PRIVATE void _saudio_mutex_destroy(void) { |
469 | DeleteCriticalSection(&_saudio_crit); |
470 | } |
471 | |
472 | _SOKOL_PRIVATE void _saudio_mutex_lock(void) { |
473 | EnterCriticalSection(&_saudio_crit); |
474 | } |
475 | |
476 | _SOKOL_PRIVATE void _saudio_mutex_unlock(void) { |
477 | LeaveCriticalSection(&_saudio_crit); |
478 | } |
479 | #else |
480 | _SOKOL_PRIVATE void _saudio_mutex_init(void) { } |
481 | _SOKOL_PRIVATE void _saudio_mutex_destroy(void) { } |
482 | _SOKOL_PRIVATE void _saudio_mutex_lock(void) { } |
483 | _SOKOL_PRIVATE void _saudio_mutex_unlock(void) { } |
484 | #endif |
485 | |
486 | /*--- a ring-buffer queue implementation -------------------------------------*/ |
487 | typedef struct { |
488 | int head; /* next slot to write to */ |
489 | int tail; /* next slot to read from */ |
490 | int num; /* number of slots in queue */ |
491 | int queue[_SAUDIO_RING_MAX_SLOTS]; |
492 | } _saudio_ring; |
493 | |
494 | _SOKOL_PRIVATE uint16_t _saudio_ring_idx(_saudio_ring* ring, int i) { |
495 | return (uint16_t) (i % ring->num); |
496 | } |
497 | |
498 | _SOKOL_PRIVATE void _saudio_ring_init(_saudio_ring* ring, int num_slots) { |
499 | SOKOL_ASSERT((num_slots + 1) <= _SAUDIO_RING_MAX_SLOTS); |
500 | ring->head = 0; |
501 | ring->tail = 0; |
502 | /* one slot reserved to detect 'full' vs 'empty' */ |
503 | ring->num = num_slots + 1; |
504 | memset(ring->queue, 0, sizeof(ring->queue)); |
505 | } |
506 | |
507 | _SOKOL_PRIVATE bool _saudio_ring_full(_saudio_ring* ring) { |
508 | return _saudio_ring_idx(ring, ring->head + 1) == ring->tail; |
509 | } |
510 | |
511 | _SOKOL_PRIVATE bool _saudio_ring_empty(_saudio_ring* ring) { |
512 | return ring->head == ring->tail; |
513 | } |
514 | |
515 | _SOKOL_PRIVATE int _saudio_ring_count(_saudio_ring* ring) { |
516 | int count; |
517 | if (ring->head >= ring->tail) { |
518 | count = ring->head - ring->tail; |
519 | } |
520 | else { |
521 | count = (ring->head + ring->num) - ring->tail; |
522 | } |
523 | SOKOL_ASSERT((count >= 0) && (count < ring->num)); |
524 | return count; |
525 | } |
526 | |
527 | _SOKOL_PRIVATE void _saudio_ring_enqueue(_saudio_ring* ring, int val) { |
528 | SOKOL_ASSERT(!_saudio_ring_full(ring)); |
529 | ring->queue[ring->head] = val; |
530 | ring->head = _saudio_ring_idx(ring, ring->head + 1); |
531 | } |
532 | |
533 | _SOKOL_PRIVATE int _saudio_ring_dequeue(_saudio_ring* ring) { |
534 | SOKOL_ASSERT(!_saudio_ring_empty(ring)); |
535 | int val = ring->queue[ring->tail]; |
536 | ring->tail = _saudio_ring_idx(ring, ring->tail + 1); |
537 | return val; |
538 | } |
539 | |
540 | /*--- a packet fifo for queueing audio data from main thread ----------------*/ |
541 | typedef struct { |
542 | bool valid; |
543 | int packet_size; /* size of a single packets in bytes(!) */ |
544 | int num_packets; /* number of packet in fifo */ |
545 | uint8_t* base_ptr; /* packet memory chunk base pointer (dynamically allocated) */ |
546 | int cur_packet; /* current write-packet */ |
547 | int cur_offset; /* current byte-offset into current write packet */ |
548 | _saudio_ring read_queue; /* buffers with data, ready to be streamed */ |
549 | _saudio_ring write_queue; /* empty buffers, ready to be pushed to */ |
550 | } _saudio_fifo; |
551 | |
552 | _SOKOL_PRIVATE void _saudio_fifo_init(_saudio_fifo* fifo, int packet_size, int num_packets) { |
553 | /* NOTE: there's a chicken-egg situation during the init phase where the |
554 | streaming thread must be started before the fifo is actually initialized, |
555 | thus the fifo init must already be protected from access by the fifo_read() func. |
556 | */ |
557 | _saudio_mutex_lock(); |
558 | SOKOL_ASSERT((packet_size > 0) && (num_packets > 0)); |
559 | memset(fifo, 0, sizeof(_saudio_fifo)); |
560 | fifo->packet_size = packet_size; |
561 | fifo->num_packets = num_packets; |
562 | fifo->base_ptr = (uint8_t*) SOKOL_MALLOC(packet_size * num_packets); |
563 | SOKOL_ASSERT(fifo->base_ptr); |
564 | fifo->cur_packet = -1; |
565 | fifo->cur_offset = 0; |
566 | _saudio_ring_init(&fifo->read_queue, num_packets); |
567 | _saudio_ring_init(&fifo->write_queue, num_packets); |
568 | for (int i = 0; i < num_packets; i++) { |
569 | _saudio_ring_enqueue(&fifo->write_queue, i); |
570 | } |
571 | SOKOL_ASSERT(_saudio_ring_full(&fifo->write_queue)); |
572 | SOKOL_ASSERT(_saudio_ring_count(&fifo->write_queue) == num_packets); |
573 | SOKOL_ASSERT(_saudio_ring_empty(&fifo->read_queue)); |
574 | SOKOL_ASSERT(_saudio_ring_count(&fifo->read_queue) == 0); |
575 | fifo->valid = true; |
576 | _saudio_mutex_unlock(); |
577 | } |
578 | |
579 | _SOKOL_PRIVATE void _saudio_fifo_shutdown(_saudio_fifo* fifo) { |
580 | SOKOL_ASSERT(fifo->base_ptr); |
581 | SOKOL_FREE(fifo->base_ptr); |
582 | fifo->base_ptr = 0; |
583 | fifo->valid = false; |
584 | } |
585 | |
586 | _SOKOL_PRIVATE int _saudio_fifo_writable_bytes(_saudio_fifo* fifo) { |
587 | _saudio_mutex_lock(); |
588 | int num_bytes = (_saudio_ring_count(&fifo->write_queue) * fifo->packet_size); |
589 | if (fifo->cur_packet != -1) { |
590 | num_bytes += fifo->packet_size - fifo->cur_offset; |
591 | } |
592 | _saudio_mutex_unlock(); |
593 | SOKOL_ASSERT((num_bytes >= 0) && (num_bytes <= (fifo->num_packets * fifo->packet_size))); |
594 | return num_bytes; |
595 | } |
596 | |
597 | /* write new data to the write queue, this is called from main thread */ |
598 | _SOKOL_PRIVATE int _saudio_fifo_write(_saudio_fifo* fifo, const uint8_t* ptr, int num_bytes) { |
599 | /* returns the number of bytes written, this will be smaller then requested |
600 | if the write queue runs full |
601 | */ |
602 | int all_to_copy = num_bytes; |
603 | while (all_to_copy > 0) { |
604 | /* need to grab a new packet? */ |
605 | if (fifo->cur_packet == -1) { |
606 | _saudio_mutex_lock(); |
607 | if (!_saudio_ring_empty(&fifo->write_queue)) { |
608 | fifo->cur_packet = _saudio_ring_dequeue(&fifo->write_queue); |
609 | } |
610 | _saudio_mutex_unlock(); |
611 | SOKOL_ASSERT(fifo->cur_offset == 0); |
612 | } |
613 | /* append data to current write packet */ |
614 | if (fifo->cur_packet != -1) { |
615 | int to_copy = all_to_copy; |
616 | const int max_copy = fifo->packet_size - fifo->cur_offset; |
617 | if (to_copy > max_copy) { |
618 | to_copy = max_copy; |
619 | } |
620 | uint8_t* dst = fifo->base_ptr + fifo->cur_packet * fifo->packet_size + fifo->cur_offset; |
621 | memcpy(dst, ptr, to_copy); |
622 | ptr += to_copy; |
623 | fifo->cur_offset += to_copy; |
624 | all_to_copy -= to_copy; |
625 | SOKOL_ASSERT(fifo->cur_offset <= fifo->packet_size); |
626 | SOKOL_ASSERT(all_to_copy >= 0); |
627 | } |
628 | else { |
629 | /* early out if we're starving */ |
630 | int bytes_copied = num_bytes - all_to_copy; |
631 | SOKOL_ASSERT((bytes_copied >= 0) && (bytes_copied < num_bytes)); |
632 | return bytes_copied; |
633 | } |
634 | /* if write packet is full, push to read queue */ |
635 | if (fifo->cur_offset == fifo->packet_size) { |
636 | _saudio_mutex_lock(); |
637 | _saudio_ring_enqueue(&fifo->read_queue, fifo->cur_packet); |
638 | _saudio_mutex_unlock(); |
639 | fifo->cur_packet = -1; |
640 | fifo->cur_offset = 0; |
641 | } |
642 | } |
643 | SOKOL_ASSERT(all_to_copy == 0); |
644 | return num_bytes; |
645 | } |
646 | |
647 | /* read queued data, this is called form the stream callback (maybe separate thread) */ |
648 | _SOKOL_PRIVATE int _saudio_fifo_read(_saudio_fifo* fifo, uint8_t* ptr, int num_bytes) { |
649 | /* NOTE: fifo_read might be called before the fifo is properly initialized */ |
650 | _saudio_mutex_lock(); |
651 | int num_bytes_copied = 0; |
652 | if (fifo->valid) { |
653 | SOKOL_ASSERT(0 == (num_bytes % fifo->packet_size)); |
654 | SOKOL_ASSERT(num_bytes <= (fifo->packet_size * fifo->num_packets)); |
655 | const int num_packets_needed = num_bytes / fifo->packet_size; |
656 | uint8_t* dst = ptr; |
657 | /* either pull a full buffer worth of data, or nothing */ |
658 | if (_saudio_ring_count(&fifo->read_queue) >= num_packets_needed) { |
659 | for (int i = 0; i < num_packets_needed; i++) { |
660 | int packet_index = _saudio_ring_dequeue(&fifo->read_queue); |
661 | _saudio_ring_enqueue(&fifo->write_queue, packet_index); |
662 | const uint8_t* src = fifo->base_ptr + packet_index * fifo->packet_size; |
663 | memcpy(dst, src, fifo->packet_size); |
664 | dst += fifo->packet_size; |
665 | num_bytes_copied += fifo->packet_size; |
666 | } |
667 | SOKOL_ASSERT(num_bytes == num_bytes_copied); |
668 | } |
669 | } |
670 | _saudio_mutex_unlock(); |
671 | return num_bytes_copied; |
672 | } |
673 | |
674 | /* sokol-audio state */ |
675 | typedef struct { |
676 | bool valid; |
677 | void (*stream_cb)(float* buffer, int num_frames, int num_channels); |
678 | int sample_rate; /* sample rate */ |
679 | int buffer_frames; /* number of frames in streaming buffer */ |
680 | int bytes_per_frame; /* filled by backend */ |
681 | int packet_frames; /* number of frames in a packet */ |
682 | int num_packets; /* number of packets in packet queue */ |
683 | int num_channels; /* actual number of channels */ |
684 | saudio_desc desc; |
685 | _saudio_fifo fifo; |
686 | } _saudio_state; |
687 | static _saudio_state _saudio; |
688 | |
689 | /*=== DUMMY BACKEND ==========================================================*/ |
690 | #if defined(SOKOL_AUDIO_NO_BACKEND) |
691 | _SOKOL_PRIVATE bool _saudio_backend_init(void) { return false; }; |
692 | _SOKOL_PRIVATE void _saudio_backend_shutdown(void) { }; |
693 | |
694 | /*=== COREAUDIO BACKEND ======================================================*/ |
695 | #elif defined(__APPLE__) |
696 | #include <AudioToolbox/AudioToolbox.h> |
697 | |
698 | static AudioQueueRef _saudio_ca_audio_queue; |
699 | |
700 | /* NOTE: the buffer data callback is called on a separate thread! */ |
701 | _SOKOL_PRIVATE void _sapp_ca_callback(void* user_data, AudioQueueRef queue, AudioQueueBufferRef buffer) { |
702 | if (_saudio.stream_cb) { |
703 | const int num_frames = buffer->mAudioDataByteSize / _saudio.bytes_per_frame; |
704 | const int num_channels = _saudio.num_channels; |
705 | _saudio.stream_cb((float*)buffer->mAudioData, num_frames, num_channels); |
706 | } |
707 | else { |
708 | uint8_t* ptr = (uint8_t*)buffer->mAudioData; |
709 | int num_bytes = (int) buffer->mAudioDataByteSize; |
710 | if (0 == _saudio_fifo_read(&_saudio.fifo, ptr, num_bytes)) { |
711 | /* not enough read data available, fill the entire buffer with silence */ |
712 | memset(ptr, 0, num_bytes); |
713 | } |
714 | } |
715 | AudioQueueEnqueueBuffer(queue, buffer, 0, NULL); |
716 | } |
717 | |
718 | _SOKOL_PRIVATE bool _saudio_backend_init(void) { |
719 | SOKOL_ASSERT(0 == _saudio_ca_audio_queue); |
720 | |
721 | /* create an audio queue with fp32 samples */ |
722 | AudioStreamBasicDescription fmt; |
723 | memset(&fmt, 0, sizeof(fmt)); |
724 | fmt.mSampleRate = (Float64) _saudio.sample_rate; |
725 | fmt.mFormatID = kAudioFormatLinearPCM; |
726 | fmt.mFormatFlags = kLinearPCMFormatFlagIsFloat | kAudioFormatFlagIsPacked; |
727 | fmt.mFramesPerPacket = 1; |
728 | fmt.mChannelsPerFrame = _saudio.num_channels; |
729 | fmt.mBytesPerFrame = sizeof(float) * _saudio.num_channels; |
730 | fmt.mBytesPerPacket = fmt.mBytesPerFrame; |
731 | fmt.mBitsPerChannel = 32; |
732 | OSStatus res = AudioQueueNewOutput(&fmt, _sapp_ca_callback, 0, NULL, NULL, 0, &_saudio_ca_audio_queue); |
733 | SOKOL_ASSERT((res == 0) && _saudio_ca_audio_queue); |
734 | |
735 | /* create 2 audio buffers */ |
736 | for (int i = 0; i < 2; i++) { |
737 | AudioQueueBufferRef buf = NULL; |
738 | const uint32_t buf_byte_size = _saudio.buffer_frames * fmt.mBytesPerFrame; |
739 | res = AudioQueueAllocateBuffer(_saudio_ca_audio_queue, buf_byte_size, &buf); |
740 | SOKOL_ASSERT((res == 0) && buf); |
741 | buf->mAudioDataByteSize = buf_byte_size; |
742 | memset(buf->mAudioData, 0, buf->mAudioDataByteSize); |
743 | AudioQueueEnqueueBuffer(_saudio_ca_audio_queue, buf, 0, NULL); |
744 | } |
745 | |
746 | /* init or modify actual playback parameters */ |
747 | _saudio.bytes_per_frame = fmt.mBytesPerFrame; |
748 | |
749 | /* ...and start playback */ |
750 | res = AudioQueueStart(_saudio_ca_audio_queue, NULL); |
751 | SOKOL_ASSERT(0 == res); |
752 | |
753 | return true; |
754 | } |
755 | |
756 | _SOKOL_PRIVATE void _saudio_backend_shutdown(void) { |
757 | AudioQueueStop(_saudio_ca_audio_queue, true); |
758 | AudioQueueDispose(_saudio_ca_audio_queue, false); |
759 | _saudio_ca_audio_queue = NULL; |
760 | } |
761 | |
762 | /*=== ALSA BACKEND ===========================================================*/ |
763 | #elif defined(linux) |
764 | #define ALSA_PCM_NEW_HW_PARAMS_API |
765 | #include <alsa/asoundlib.h> |
766 | |
767 | typedef struct { |
768 | snd_pcm_t* device; |
769 | float* buffer; |
770 | int buffer_byte_size; |
771 | int buffer_frames; |
772 | pthread_t thread; |
773 | bool thread_stop; |
774 | } _saudio_alsa_state; |
775 | static _saudio_alsa_state _saudio_alsa; |
776 | |
777 | /* the streaming callback runs in a separate thread */ |
778 | _SOKOL_PRIVATE void* _saudio_alsa_cb(void* param) { |
779 | while (!_saudio_alsa.thread_stop) { |
780 | /* snd_pcm_writei() will be blocking until it needs data */ |
781 | int write_res = snd_pcm_writei(_saudio_alsa.device, _saudio_alsa.buffer, _saudio_alsa.buffer_frames); |
782 | if (write_res < 0) { |
783 | /* underrun occurred */ |
784 | snd_pcm_prepare(_saudio_alsa.device); |
785 | } |
786 | else { |
787 | /* fill the streaming buffer with new data */ |
788 | if (_saudio.stream_cb) { |
789 | _saudio.stream_cb(_saudio_alsa.buffer, _saudio_alsa.buffer_frames, _saudio.num_channels); |
790 | } |
791 | else { |
792 | if (0 == _saudio_fifo_read(&_saudio.fifo, (uint8_t*)_saudio_alsa.buffer, _saudio_alsa.buffer_byte_size)) { |
793 | /* not enough read data available, fill the entire buffer with silence */ |
794 | memset(_saudio_alsa.buffer, 0, _saudio_alsa.buffer_byte_size); |
795 | } |
796 | } |
797 | } |
798 | } |
799 | return 0; |
800 | } |
801 | |
802 | _SOKOL_PRIVATE bool _saudio_backend_init(void) { |
803 | memset(&_saudio_alsa, 0, sizeof(_saudio_alsa)); |
804 | int rc = snd_pcm_open(&_saudio_alsa.device, "default" , SND_PCM_STREAM_PLAYBACK, 0); |
805 | if (rc < 0) { |
806 | return false; |
807 | } |
808 | snd_pcm_hw_params_t* params = 0; |
809 | snd_pcm_hw_params_alloca(¶ms); |
810 | snd_pcm_hw_params_any(_saudio_alsa.device, params); |
811 | snd_pcm_hw_params_set_access(_saudio_alsa.device, params, SND_PCM_ACCESS_RW_INTERLEAVED); |
812 | snd_pcm_hw_params_set_channels(_saudio_alsa.device, params, _saudio.num_channels); |
813 | snd_pcm_hw_params_set_buffer_size(_saudio_alsa.device, params, _saudio.buffer_frames); |
814 | if (0 > snd_pcm_hw_params_test_format(_saudio_alsa.device, params, SND_PCM_FORMAT_FLOAT_LE)) { |
815 | goto error; |
816 | } |
817 | else { |
818 | snd_pcm_hw_params_set_format(_saudio_alsa.device, params, SND_PCM_FORMAT_FLOAT_LE); |
819 | } |
820 | unsigned int val = _saudio.sample_rate; |
821 | int dir = 0; |
822 | if (0 > snd_pcm_hw_params_set_rate_near(_saudio_alsa.device, params, &val, &dir)) { |
823 | goto error; |
824 | } |
825 | if (0 > snd_pcm_hw_params(_saudio_alsa.device, params)) { |
826 | goto error; |
827 | } |
828 | |
829 | /* read back actual sample rate and channels */ |
830 | snd_pcm_hw_params_get_rate(params, &val, &dir); |
831 | _saudio.sample_rate = val; |
832 | snd_pcm_hw_params_get_channels(params, &val); |
833 | SOKOL_ASSERT((int)val == _saudio.num_channels); |
834 | _saudio.bytes_per_frame = _saudio.num_channels * sizeof(float); |
835 | |
836 | /* allocate the streaming buffer */ |
837 | _saudio_alsa.buffer_byte_size = _saudio.buffer_frames * _saudio.bytes_per_frame; |
838 | _saudio_alsa.buffer_frames = _saudio.buffer_frames; |
839 | _saudio_alsa.buffer = (float*) SOKOL_MALLOC(_saudio_alsa.buffer_byte_size); |
840 | memset(_saudio_alsa.buffer, 0, _saudio_alsa.buffer_byte_size); |
841 | |
842 | /* create the buffer-streaming start thread */ |
843 | if (0 != pthread_create(&_saudio_alsa.thread, 0, _saudio_alsa_cb, 0)) { |
844 | goto error; |
845 | } |
846 | |
847 | return true; |
848 | error: |
849 | if (_saudio_alsa.device) { |
850 | snd_pcm_close(_saudio_alsa.device); |
851 | _saudio_alsa.device = 0; |
852 | } |
853 | return false; |
854 | }; |
855 | |
856 | _SOKOL_PRIVATE void _saudio_backend_shutdown(void) { |
857 | SOKOL_ASSERT(_saudio_alsa.device); |
858 | _saudio_alsa.thread_stop = true; |
859 | pthread_join(_saudio_alsa.thread, 0); |
860 | snd_pcm_drain(_saudio_alsa.device); |
861 | snd_pcm_close(_saudio_alsa.device); |
862 | SOKOL_FREE(_saudio_alsa.buffer); |
863 | }; |
864 | |
865 | /*=== WASAPI BACKEND =========================================================*/ |
866 | #elif defined(_WIN32) |
867 | #ifndef WIN32_LEAN_AND_MEAN |
868 | #define WIN32_LEAN_AND_MEAN |
869 | #endif |
870 | #ifndef CINTERFACE |
871 | #define CINTERFACE |
872 | #endif |
873 | #ifndef COBJMACROS |
874 | #define COBJMACROS |
875 | #endif |
876 | #ifndef CONST_VTABLE |
877 | #define CONST_VTABLE |
878 | #endif |
879 | #include <mmdeviceapi.h> |
880 | #include <audioclient.h> |
881 | |
882 | static const IID _saudio_IID_IAudioClient = { 0x1cb9ad4c, 0xdbfa, 0x4c32, { 0xb1, 0x78, 0xc2, 0xf5, 0x68, 0xa7, 0x03, 0xb2 } }; |
883 | static const IID _saudio_IID_IMMDeviceEnumerator = { 0xa95664d2, 0x9614, 0x4f35, { 0xa7, 0x46, 0xde, 0x8d, 0xb6, 0x36, 0x17, 0xe6 } }; |
884 | static const CLSID _saudio_CLSID_IMMDeviceEnumerator = { 0xbcde0395, 0xe52f, 0x467c, { 0x8e, 0x3d, 0xc4, 0x57, 0x92, 0x91, 0x69, 0x2e } }; |
885 | static const IID _saudio_IID_IAudioRenderClient = { 0xf294acfc, 0x3146, 0x4483,{ 0xa7, 0xbf, 0xad, 0xdc, 0xa7, 0xc2, 0x60, 0xe2 } }; |
886 | |
887 | /* fix for Visual Studio 2015 SDKs */ |
888 | #ifndef AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM |
889 | #define AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM 0x80000000 |
890 | #endif |
891 | #ifndef AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY |
892 | #define AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY 0x08000000 |
893 | #endif |
894 | |
895 | typedef struct { |
896 | HANDLE thread_handle; |
897 | HANDLE buffer_end_event; |
898 | bool stop; |
899 | UINT32 dst_buffer_frames; |
900 | int src_buffer_frames; |
901 | int src_buffer_byte_size; |
902 | int src_buffer_pos; |
903 | float* src_buffer; |
904 | } _saudio_wasapi_thread_data; |
905 | |
906 | typedef struct { |
907 | IMMDeviceEnumerator* device_enumerator; |
908 | IMMDevice* device; |
909 | IAudioClient* audio_client; |
910 | IAudioRenderClient* render_client; |
911 | int si16_bytes_per_frame; |
912 | _saudio_wasapi_thread_data thread; |
913 | } _saudio_wasapi_state; |
914 | static _saudio_wasapi_state _saudio_wasapi; |
915 | |
916 | /* fill intermediate buffer with new data and reset buffer_pos */ |
917 | _SOKOL_PRIVATE void _saudio_wasapi_fill_buffer(void) { |
918 | if (_saudio.stream_cb) { |
919 | _saudio.stream_cb(_saudio_wasapi.thread.src_buffer, _saudio_wasapi.thread.src_buffer_frames, _saudio.num_channels); |
920 | } |
921 | else { |
922 | if (0 == _saudio_fifo_read(&_saudio.fifo, (uint8_t*)_saudio_wasapi.thread.src_buffer, _saudio_wasapi.thread.src_buffer_byte_size)) { |
923 | /* not enough read data available, fill the entire buffer with silence */ |
924 | memset(_saudio_wasapi.thread.src_buffer, 0, _saudio_wasapi.thread.src_buffer_byte_size); |
925 | } |
926 | } |
927 | } |
928 | |
929 | _SOKOL_PRIVATE void _saudio_wasapi_submit_buffer(UINT32 num_frames) { |
930 | BYTE* wasapi_buffer = 0; |
931 | if (FAILED(IAudioRenderClient_GetBuffer(_saudio_wasapi.render_client, num_frames, &wasapi_buffer))) { |
932 | return; |
933 | } |
934 | SOKOL_ASSERT(wasapi_buffer); |
935 | |
936 | /* convert float samples to int16_t, refill float buffer if needed */ |
937 | const int num_samples = num_frames * _saudio.num_channels; |
938 | int16_t* dst = (int16_t*) wasapi_buffer; |
939 | uint32_t buffer_pos = _saudio_wasapi.thread.src_buffer_pos; |
940 | const uint32_t buffer_float_size = _saudio_wasapi.thread.src_buffer_byte_size / sizeof(float); |
941 | float* src = _saudio_wasapi.thread.src_buffer; |
942 | for (int i = 0; i < num_samples; i++) { |
943 | if (0 == buffer_pos) { |
944 | _saudio_wasapi_fill_buffer(); |
945 | } |
946 | dst[i] = (int16_t) (src[buffer_pos] * 0x7FFF); |
947 | buffer_pos += 1; |
948 | if (buffer_pos == buffer_float_size) { |
949 | buffer_pos = 0; |
950 | } |
951 | } |
952 | _saudio_wasapi.thread.src_buffer_pos = buffer_pos; |
953 | |
954 | IAudioRenderClient_ReleaseBuffer(_saudio_wasapi.render_client, num_frames, 0); |
955 | } |
956 | |
957 | _SOKOL_PRIVATE DWORD WINAPI _saudio_wasapi_thread_fn(LPVOID param) { |
958 | (void)param; |
959 | _saudio_wasapi_submit_buffer(_saudio_wasapi.thread.src_buffer_frames); |
960 | IAudioClient_Start(_saudio_wasapi.audio_client); |
961 | while (!_saudio_wasapi.thread.stop) { |
962 | WaitForSingleObject(_saudio_wasapi.thread.buffer_end_event, INFINITE); |
963 | UINT32 padding = 0; |
964 | if (FAILED(IAudioClient_GetCurrentPadding(_saudio_wasapi.audio_client, &padding))) { |
965 | continue; |
966 | } |
967 | SOKOL_ASSERT(_saudio_wasapi.thread.dst_buffer_frames >= (int)padding); |
968 | UINT32 num_frames = _saudio_wasapi.thread.dst_buffer_frames - padding; |
969 | if (num_frames > 0) { |
970 | _saudio_wasapi_submit_buffer(num_frames); |
971 | } |
972 | } |
973 | return 0; |
974 | } |
975 | |
976 | _SOKOL_PRIVATE void _saudio_wasapi_release(void) { |
977 | if (_saudio_wasapi.thread.src_buffer) { |
978 | SOKOL_FREE(_saudio_wasapi.thread.src_buffer); |
979 | _saudio_wasapi.thread.src_buffer = 0; |
980 | } |
981 | if (_saudio_wasapi.render_client) { |
982 | IAudioRenderClient_Release(_saudio_wasapi.render_client); |
983 | _saudio_wasapi.render_client = 0; |
984 | } |
985 | if (_saudio_wasapi.audio_client) { |
986 | IAudioClient_Release(_saudio_wasapi.audio_client); |
987 | _saudio_wasapi.audio_client = 0; |
988 | } |
989 | if (_saudio_wasapi.device) { |
990 | IMMDevice_Release(_saudio_wasapi.device); |
991 | _saudio_wasapi.device = 0; |
992 | } |
993 | if (_saudio_wasapi.device_enumerator) { |
994 | IMMDeviceEnumerator_Release(_saudio_wasapi.device_enumerator); |
995 | _saudio_wasapi.device_enumerator = 0; |
996 | } |
997 | if (0 != _saudio_wasapi.thread.buffer_end_event) { |
998 | CloseHandle(_saudio_wasapi.thread.buffer_end_event); |
999 | _saudio_wasapi.thread.buffer_end_event = 0; |
1000 | } |
1001 | } |
1002 | |
1003 | _SOKOL_PRIVATE bool _saudio_backend_init(void) { |
1004 | memset(&_saudio_wasapi, 0, sizeof(_saudio_wasapi)); |
1005 | if (FAILED(CoInitializeEx(0, COINIT_MULTITHREADED))) { |
1006 | SOKOL_LOG("sokol_audio wasapi: CoInitializeEx failed" ); |
1007 | return false; |
1008 | } |
1009 | _saudio_wasapi.thread.buffer_end_event = CreateEvent(0, FALSE, FALSE, 0); |
1010 | if (0 == _saudio_wasapi.thread.buffer_end_event) { |
1011 | SOKOL_LOG("sokol_audio wasapi: failed to create buffer_end_event" ); |
1012 | goto error; |
1013 | } |
1014 | if (FAILED(CoCreateInstance(&_saudio_CLSID_IMMDeviceEnumerator, |
1015 | 0, CLSCTX_ALL, |
1016 | &_saudio_IID_IMMDeviceEnumerator, |
1017 | (void**)&_saudio_wasapi.device_enumerator))) |
1018 | { |
1019 | SOKOL_LOG("sokol_audio wasapi: failed to create device enumerator" ); |
1020 | goto error; |
1021 | } |
1022 | if (FAILED(IMMDeviceEnumerator_GetDefaultAudioEndpoint(_saudio_wasapi.device_enumerator, |
1023 | eRender, eConsole, |
1024 | &_saudio_wasapi.device))) |
1025 | { |
1026 | SOKOL_LOG("sokol_audio wasapi: GetDefaultAudioEndPoint failed" ); |
1027 | goto error; |
1028 | } |
1029 | if (FAILED(IMMDevice_Activate(_saudio_wasapi.device, |
1030 | &_saudio_IID_IAudioClient, |
1031 | CLSCTX_ALL, 0, |
1032 | (void**)&_saudio_wasapi.audio_client))) |
1033 | { |
1034 | SOKOL_LOG("sokol_audio wasapi: device activate failed" ); |
1035 | goto error; |
1036 | } |
1037 | WAVEFORMATEX fmt; |
1038 | memset(&fmt, 0, sizeof(fmt)); |
1039 | fmt.nChannels = (WORD) _saudio.num_channels; |
1040 | fmt.nSamplesPerSec = _saudio.sample_rate; |
1041 | fmt.wFormatTag = WAVE_FORMAT_PCM; |
1042 | fmt.wBitsPerSample = 16; |
1043 | fmt.nBlockAlign = (fmt.nChannels * fmt.wBitsPerSample) / 8; |
1044 | fmt.nAvgBytesPerSec = fmt.nSamplesPerSec * fmt.nBlockAlign; |
1045 | REFERENCE_TIME dur = (REFERENCE_TIME) |
1046 | (((double)_saudio.buffer_frames) / (((double)_saudio.sample_rate) * (1.0/10000000.0))); |
1047 | if (FAILED(IAudioClient_Initialize(_saudio_wasapi.audio_client, |
1048 | AUDCLNT_SHAREMODE_SHARED, |
1049 | AUDCLNT_STREAMFLAGS_EVENTCALLBACK|AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM|AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY, |
1050 | dur, 0, &fmt, 0))) |
1051 | { |
1052 | SOKOL_LOG("sokol_audio wasapi: audio client initialize failed" ); |
1053 | goto error; |
1054 | } |
1055 | if (FAILED(IAudioClient_GetBufferSize(_saudio_wasapi.audio_client, &_saudio_wasapi.thread.dst_buffer_frames))) { |
1056 | SOKOL_LOG("sokol_audio wasapi: audio client get buffer size failed" ); |
1057 | goto error; |
1058 | } |
1059 | if (FAILED(IAudioClient_GetService(_saudio_wasapi.audio_client, |
1060 | &_saudio_IID_IAudioRenderClient, |
1061 | (void**)&_saudio_wasapi.render_client))) |
1062 | { |
1063 | SOKOL_LOG("sokol_audio wasapi: audio client GetService failed" ); |
1064 | goto error; |
1065 | } |
1066 | if (FAILED(IAudioClient_SetEventHandle(_saudio_wasapi.audio_client, _saudio_wasapi.thread.buffer_end_event))) { |
1067 | SOKOL_LOG("sokol_audio wasapi: audio client SetEventHandle failed" ); |
1068 | goto error; |
1069 | } |
1070 | _saudio_wasapi.si16_bytes_per_frame = _saudio.num_channels * sizeof(int16_t); |
1071 | _saudio.bytes_per_frame = _saudio.num_channels * sizeof(float); |
1072 | _saudio_wasapi.thread.src_buffer_frames = _saudio.buffer_frames; |
1073 | _saudio_wasapi.thread.src_buffer_byte_size = _saudio_wasapi.thread.src_buffer_frames * _saudio.bytes_per_frame; |
1074 | |
1075 | /* allocate an intermediate buffer for sample format conversion */ |
1076 | _saudio_wasapi.thread.src_buffer = (float*) SOKOL_MALLOC(_saudio_wasapi.thread.src_buffer_byte_size); |
1077 | SOKOL_ASSERT(_saudio_wasapi.thread.src_buffer); |
1078 | |
1079 | /* create streaming thread */ |
1080 | _saudio_wasapi.thread.thread_handle = CreateThread(NULL, 0, _saudio_wasapi_thread_fn, 0, 0, 0); |
1081 | if (0 == _saudio_wasapi.thread.thread_handle) { |
1082 | SOKOL_LOG("sokol_audio wasapi: CreateThread failed" ); |
1083 | goto error; |
1084 | } |
1085 | |
1086 | return true; |
1087 | |
1088 | error: |
1089 | _saudio_wasapi_release(); |
1090 | return false; |
1091 | } |
1092 | |
1093 | _SOKOL_PRIVATE void _saudio_backend_shutdown(void) { |
1094 | if (_saudio_wasapi.thread.thread_handle) { |
1095 | _saudio_wasapi.thread.stop = true; |
1096 | SetEvent(_saudio_wasapi.thread.buffer_end_event); |
1097 | WaitForSingleObject(_saudio_wasapi.thread.thread_handle, INFINITE); |
1098 | CloseHandle(_saudio_wasapi.thread.thread_handle); |
1099 | _saudio_wasapi.thread.thread_handle = 0; |
1100 | } |
1101 | if (_saudio_wasapi.audio_client) { |
1102 | IAudioClient_Stop(_saudio_wasapi.audio_client); |
1103 | } |
1104 | _saudio_wasapi_release(); |
1105 | CoUninitialize(); |
1106 | } |
1107 | |
1108 | /*=== EMSCRIPTEN BACKEND =====================================================*/ |
1109 | |
1110 | #elif defined(__EMSCRIPTEN__) |
1111 | #include <emscripten/emscripten.h> |
1112 | |
1113 | static uint8_t* _saudio_emsc_buffer; |
1114 | |
1115 | EMSCRIPTEN_KEEPALIVE int _saudio_emsc_pull(int num_frames) { |
1116 | SOKOL_ASSERT(_saudio_emsc_buffer); |
1117 | if (num_frames == _saudio.buffer_frames) { |
1118 | if (_saudio.stream_cb) { |
1119 | _saudio.stream_cb((float*)_saudio_emsc_buffer, num_frames, _saudio.num_channels); |
1120 | } |
1121 | else { |
1122 | const int num_bytes = num_frames * _saudio.bytes_per_frame; |
1123 | if (0 == _saudio_fifo_read(&_saudio.fifo, _saudio_emsc_buffer, num_bytes)) { |
1124 | /* not enough read data available, fill the entire buffer with silence */ |
1125 | memset(_saudio_emsc_buffer, 0, num_bytes); |
1126 | } |
1127 | } |
1128 | int res = (int) _saudio_emsc_buffer; |
1129 | return res; |
1130 | } |
1131 | else { |
1132 | return 0; |
1133 | } |
1134 | } |
1135 | |
1136 | /* setup the WebAudio context and attach a ScriptProcessorNode */ |
1137 | EM_JS(int, _saudio_js_init, (int sample_rate, int num_channels, int buffer_size), { |
1138 | Module._saudio_context = null; |
1139 | Module._saudio_node = null; |
1140 | if (typeof AudioContext !== 'undefined') { |
1141 | Module._saudio_context = new AudioContext({ |
1142 | sampleRate: sample_rate, |
1143 | latencyHint: 'interactive', |
1144 | }); |
1145 | console.log('sokol_audio.h: created AudioContext'); |
1146 | } |
1147 | else if (typeof webkitAudioContext !== 'undefined') { |
1148 | Module._saudio_context = new webkitAudioContext({ |
1149 | sampleRate: sample_rate, |
1150 | latencyHint: 'interactive', |
1151 | }); |
1152 | console.log('sokol_audio.h: created webkitAudioContext'); |
1153 | } |
1154 | else { |
1155 | Module._saudio_context = null; |
1156 | console.log('sokol_audio.h: no WebAudio support'); |
1157 | } |
1158 | if (Module._saudio_context) { |
1159 | console.log('sokol_audio.h: sample rate ', Module._saudio_context.sampleRate); |
1160 | Module._saudio_node = Module._saudio_context.createScriptProcessor(buffer_size, 0, num_channels); |
1161 | Module._saudio_node.onaudioprocess = function pump_audio(event) { |
1162 | var num_frames = event.outputBuffer.length; |
1163 | var ptr = __saudio_emsc_pull(num_frames); |
1164 | if (ptr) { |
1165 | var num_channels = event.outputBuffer.numberOfChannels; |
1166 | for (var chn = 0; chn < num_channels; chn++) { |
1167 | var chan = event.outputBuffer.getChannelData(chn); |
1168 | for (var i = 0; i < num_frames; i++) { |
1169 | chan[i] = HEAPF32[(ptr>>2) + ((num_channels*i)+chn)] |
1170 | } |
1171 | } |
1172 | } |
1173 | }; |
1174 | Module._saudio_node.connect(Module._saudio_context.destination); |
1175 | |
1176 | // in some browsers, WebAudio needs to be activated on a user action |
1177 | var resume_webaudio = function() { |
1178 | if (Module._saudio_context) { |
1179 | if (Module._saudio_context.state === 'suspended') { |
1180 | Module._saudio_context.resume(); |
1181 | } |
1182 | } |
1183 | }; |
1184 | document.addEventListener('click', resume_webaudio, {once:true}); |
1185 | document.addEventListener('touchstart', resume_webaudio, {once:true}); |
1186 | document.addEventListener('keydown', resume_webaudio, {once:true}); |
1187 | return 1; |
1188 | } |
1189 | else { |
1190 | return 0; |
1191 | } |
1192 | }); |
1193 | |
1194 | /* get the actual sample rate back from the WebAudio context */ |
1195 | EM_JS(int, _saudio_js_sample_rate, (), { |
1196 | if (Module._saudio_context) { |
1197 | return Module._saudio_context.sampleRate; |
1198 | } |
1199 | else { |
1200 | return 0; |
1201 | } |
1202 | }); |
1203 | |
1204 | /* get the actual buffer size in number of frames */ |
1205 | EM_JS(int, _saudio_js_buffer_frames, (), { |
1206 | if (Module._saudio_node) { |
1207 | return Module._saudio_node.bufferSize; |
1208 | } |
1209 | else { |
1210 | return 0; |
1211 | } |
1212 | }); |
1213 | |
1214 | _SOKOL_PRIVATE bool _saudio_backend_init(void) { |
1215 | if (_saudio_js_init(_saudio.sample_rate, _saudio.num_channels, _saudio.buffer_frames)) { |
1216 | _saudio.bytes_per_frame = sizeof(float) * _saudio.num_channels; |
1217 | _saudio.sample_rate = _saudio_js_sample_rate(); |
1218 | _saudio.buffer_frames = _saudio_js_buffer_frames(); |
1219 | const int buf_size = _saudio.buffer_frames * _saudio.bytes_per_frame; |
1220 | _saudio_emsc_buffer = SOKOL_MALLOC(buf_size); |
1221 | return true; |
1222 | } |
1223 | else { |
1224 | return false; |
1225 | } |
1226 | } |
1227 | |
1228 | _SOKOL_PRIVATE void _saudio_backend_shutdown(void) { |
1229 | /* on HTML5, there's always a 'hard exit' without warning, |
1230 | so nothing useful to do here |
1231 | */ |
1232 | } |
1233 | |
1234 | #else /* dummy backend */ |
1235 | _SOKOL_PRIVATE bool _saudio_backend_init(void) { return false; }; |
1236 | _SOKOL_PRIVATE void _saudio_backend_shutdown(void) { }; |
1237 | #endif |
1238 | |
1239 | /*=== PUBLIC API FUNCTIONS ===================================================*/ |
1240 | SOKOL_API_IMPL void saudio_setup(const saudio_desc* desc) { |
1241 | SOKOL_ASSERT(!_saudio.valid); |
1242 | SOKOL_ASSERT(desc); |
1243 | memset(&_saudio, 0, sizeof(_saudio)); |
1244 | _saudio.desc = *desc; |
1245 | _saudio.stream_cb = desc->stream_cb; |
1246 | _saudio.sample_rate = _saudio_def(_saudio.desc.sample_rate, _SAUDIO_DEFAULT_SAMPLE_RATE); |
1247 | _saudio.buffer_frames = _saudio_def(_saudio.desc.buffer_frames, _SAUDIO_DEFAULT_BUFFER_FRAMES); |
1248 | _saudio.packet_frames = _saudio_def(_saudio.desc.packet_frames, _SAUDIO_DEFAULT_PACKET_FRAMES); |
1249 | _saudio.num_packets = _saudio_def(_saudio.desc.num_packets, _SAUDIO_DEFAULT_NUM_PACKETS); |
1250 | _saudio.num_channels = _saudio_def(_saudio.desc.num_channels, 1); |
1251 | _saudio_mutex_init(); |
1252 | if (_saudio_backend_init()) { |
1253 | SOKOL_ASSERT(0 == (_saudio.buffer_frames % _saudio.packet_frames)); |
1254 | SOKOL_ASSERT(_saudio.bytes_per_frame > 0); |
1255 | _saudio_fifo_init(&_saudio.fifo, _saudio.packet_frames * _saudio.bytes_per_frame, _saudio.num_packets); |
1256 | _saudio.valid = true; |
1257 | } |
1258 | } |
1259 | |
1260 | SOKOL_API_IMPL void saudio_shutdown(void) { |
1261 | if (_saudio.valid) { |
1262 | _saudio_backend_shutdown(); |
1263 | _saudio_fifo_shutdown(&_saudio.fifo); |
1264 | _saudio.valid = false; |
1265 | } |
1266 | _saudio_mutex_destroy(); |
1267 | } |
1268 | |
1269 | SOKOL_API_IMPL bool saudio_isvalid(void) { |
1270 | return _saudio.valid; |
1271 | } |
1272 | |
1273 | SOKOL_API_IMPL int saudio_sample_rate(void) { |
1274 | return _saudio.sample_rate; |
1275 | } |
1276 | |
1277 | SOKOL_API_IMPL int saudio_buffer_frames(void) { |
1278 | return _saudio.buffer_frames; |
1279 | } |
1280 | |
1281 | SOKOL_API_IMPL int saudio_channels(void) { |
1282 | return _saudio.num_channels; |
1283 | } |
1284 | |
1285 | SOKOL_API_IMPL int saudio_expect(void) { |
1286 | if (_saudio.valid) { |
1287 | const int num_frames = _saudio_fifo_writable_bytes(&_saudio.fifo) / _saudio.bytes_per_frame; |
1288 | return num_frames; |
1289 | } |
1290 | else { |
1291 | return 0; |
1292 | } |
1293 | } |
1294 | |
1295 | SOKOL_API_IMPL int saudio_push(const float* frames, int num_frames) { |
1296 | SOKOL_ASSERT(frames && (num_frames > 0)); |
1297 | if (_saudio.valid) { |
1298 | const int num_bytes = num_frames * _saudio.bytes_per_frame; |
1299 | const int num_written = _saudio_fifo_write(&_saudio.fifo, (const uint8_t*)frames, num_bytes); |
1300 | return num_written / _saudio.bytes_per_frame; |
1301 | } |
1302 | else { |
1303 | return 0; |
1304 | } |
1305 | } |
1306 | |
1307 | #undef _saudio_def |
1308 | #undef _saudio_def_flt |
1309 | |
1310 | #ifdef _MSC_VER |
1311 | #pragma warning(pop) |
1312 | #endif |
1313 | |
1314 | #endif /* SOKOL_IMPL */ |
1315 | |