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