1/*
2 Simple DirectMedia Layer
3 Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
4
5 This software is provided 'as-is', without any express or implied
6 warranty. In no event will the authors be held liable for any damages
7 arising from the use of this software.
8
9 Permission is granted to anyone to use this software for any purpose,
10 including commercial applications, and to alter it and redistribute it
11 freely, subject to the following restrictions:
12
13 1. The origin of this software must not be misrepresented; you must not
14 claim that you wrote the original software. If you use this software
15 in a product, an acknowledgment in the product documentation would be
16 appreciated but is not required.
17 2. Altered source versions must be plainly marked as such, and must not be
18 misrepresented as being the original software.
19 3. This notice may not be removed or altered from any source distribution.
20*/
21#include "SDL_internal.h"
22
23#include "SDL_audioqueue.h"
24#include "SDL_sysaudio.h"
25
26typedef struct SDL_MemoryPool SDL_MemoryPool;
27
28struct SDL_MemoryPool
29{
30 void *free_blocks;
31 size_t block_size;
32 size_t num_free;
33 size_t max_free;
34};
35
36struct SDL_AudioTrack
37{
38 SDL_AudioSpec spec;
39 int *chmap;
40 bool flushed;
41 SDL_AudioTrack *next;
42
43 void *userdata;
44 SDL_ReleaseAudioBufferCallback callback;
45
46 Uint8 *data;
47 size_t head;
48 size_t tail;
49 size_t capacity;
50
51 int chmap_storage[SDL_MAX_CHANNELMAP_CHANNELS]; // !!! FIXME: this needs to grow if SDL ever supports more channels. But if it grows, we should probably be more clever about allocations.
52};
53
54struct SDL_AudioQueue
55{
56 SDL_AudioTrack *head;
57 SDL_AudioTrack *tail;
58
59 Uint8 *history_buffer;
60 size_t history_length;
61 size_t history_capacity;
62
63 SDL_MemoryPool track_pool;
64 SDL_MemoryPool chunk_pool;
65};
66
67// Allocate a new block, avoiding checking for ones already in the pool
68static void *AllocNewMemoryPoolBlock(const SDL_MemoryPool *pool)
69{
70 return SDL_malloc(pool->block_size);
71}
72
73// Allocate a new block, first checking if there are any in the pool
74static void *AllocMemoryPoolBlock(SDL_MemoryPool *pool)
75{
76 if (pool->num_free == 0) {
77 return AllocNewMemoryPoolBlock(pool);
78 }
79
80 void *block = pool->free_blocks;
81 pool->free_blocks = *(void **)block;
82 --pool->num_free;
83 return block;
84}
85
86// Free a block, or add it to the pool if there's room
87static void FreeMemoryPoolBlock(SDL_MemoryPool *pool, void *block)
88{
89 if (pool->num_free < pool->max_free) {
90 *(void **)block = pool->free_blocks;
91 pool->free_blocks = block;
92 ++pool->num_free;
93 } else {
94 SDL_free(block);
95 }
96}
97
98// Destroy a pool and all of its blocks
99static void DestroyMemoryPool(SDL_MemoryPool *pool)
100{
101 void *block = pool->free_blocks;
102 pool->free_blocks = NULL;
103 pool->num_free = 0;
104
105 while (block) {
106 void *next = *(void **)block;
107 SDL_free(block);
108 block = next;
109 }
110}
111
112// Keeping a list of free chunks reduces memory allocations,
113// But also increases the amount of work to perform when freeing the track.
114static void InitMemoryPool(SDL_MemoryPool *pool, size_t block_size, size_t max_free)
115{
116 SDL_zerop(pool);
117
118 SDL_assert(block_size >= sizeof(void *));
119 pool->block_size = block_size;
120 pool->max_free = max_free;
121}
122
123// Allocates a number of blocks and adds them to the pool
124static bool ReserveMemoryPoolBlocks(SDL_MemoryPool *pool, size_t num_blocks)
125{
126 for (; num_blocks; --num_blocks) {
127 void *block = AllocNewMemoryPoolBlock(pool);
128
129 if (block == NULL) {
130 return false;
131 }
132
133 *(void **)block = pool->free_blocks;
134 pool->free_blocks = block;
135 ++pool->num_free;
136 }
137
138 return true;
139}
140
141void SDL_DestroyAudioQueue(SDL_AudioQueue *queue)
142{
143 SDL_ClearAudioQueue(queue);
144
145 DestroyMemoryPool(&queue->track_pool);
146 DestroyMemoryPool(&queue->chunk_pool);
147 SDL_aligned_free(queue->history_buffer);
148
149 SDL_free(queue);
150}
151
152SDL_AudioQueue *SDL_CreateAudioQueue(size_t chunk_size)
153{
154 SDL_AudioQueue *queue = (SDL_AudioQueue *)SDL_calloc(1, sizeof(*queue));
155
156 if (!queue) {
157 return NULL;
158 }
159
160 InitMemoryPool(&queue->track_pool, sizeof(SDL_AudioTrack), 8);
161 InitMemoryPool(&queue->chunk_pool, chunk_size, 4);
162
163 if (!ReserveMemoryPoolBlocks(&queue->track_pool, 2)) {
164 SDL_DestroyAudioQueue(queue);
165 return NULL;
166 }
167
168 return queue;
169}
170
171static void DestroyAudioTrack(SDL_AudioQueue *queue, SDL_AudioTrack *track)
172{
173 track->callback(track->userdata, track->data, (int)track->capacity);
174
175 FreeMemoryPoolBlock(&queue->track_pool, track);
176}
177
178void SDL_ClearAudioQueue(SDL_AudioQueue *queue)
179{
180 SDL_AudioTrack *track = queue->head;
181
182 queue->head = NULL;
183 queue->tail = NULL;
184 queue->history_length = 0;
185
186 while (track) {
187 SDL_AudioTrack *next = track->next;
188 DestroyAudioTrack(queue, track);
189 track = next;
190 }
191}
192
193static void FlushAudioTrack(SDL_AudioTrack *track)
194{
195 track->flushed = true;
196}
197
198void SDL_FlushAudioQueue(SDL_AudioQueue *queue)
199{
200 SDL_AudioTrack *track = queue->tail;
201
202 if (track) {
203 FlushAudioTrack(track);
204 }
205}
206
207void SDL_PopAudioQueueHead(SDL_AudioQueue *queue)
208{
209 SDL_AudioTrack *track = queue->head;
210
211 for (;;) {
212 bool flushed = track->flushed;
213
214 SDL_AudioTrack *next = track->next;
215 DestroyAudioTrack(queue, track);
216 track = next;
217
218 if (flushed) {
219 break;
220 }
221 }
222
223 queue->head = track;
224 queue->history_length = 0;
225
226 if (!track) {
227 queue->tail = NULL;
228 }
229}
230
231SDL_AudioTrack *SDL_CreateAudioTrack(
232 SDL_AudioQueue *queue, const SDL_AudioSpec *spec, const int *chmap,
233 Uint8 *data, size_t len, size_t capacity,
234 SDL_ReleaseAudioBufferCallback callback, void *userdata)
235{
236 SDL_AudioTrack *track = (SDL_AudioTrack *)AllocMemoryPoolBlock(&queue->track_pool);
237
238 if (!track) {
239 return NULL;
240 }
241
242 SDL_zerop(track);
243
244 if (chmap) {
245 SDL_assert(SDL_arraysize(track->chmap_storage) >= spec->channels);
246 SDL_memcpy(track->chmap_storage, chmap, sizeof (*chmap) * spec->channels);
247 track->chmap = track->chmap_storage;
248 }
249
250 SDL_copyp(&track->spec, spec);
251
252 track->userdata = userdata;
253 track->callback = callback;
254 track->data = data;
255 track->head = 0;
256 track->tail = len;
257 track->capacity = capacity;
258
259 return track;
260}
261
262static void SDLCALL FreeChunkedAudioBuffer(void *userdata, const void *buf, int len)
263{
264 SDL_AudioQueue *queue = (SDL_AudioQueue *)userdata;
265
266 FreeMemoryPoolBlock(&queue->chunk_pool, (void *)buf);
267}
268
269static SDL_AudioTrack *CreateChunkedAudioTrack(SDL_AudioQueue *queue, const SDL_AudioSpec *spec, const int *chmap)
270{
271 Uint8 *chunk = (Uint8 *)AllocMemoryPoolBlock(&queue->chunk_pool);
272
273 if (!chunk) {
274 return NULL;
275 }
276
277 size_t capacity = queue->chunk_pool.block_size;
278 capacity -= capacity % SDL_AUDIO_FRAMESIZE(*spec);
279
280 SDL_AudioTrack *track = SDL_CreateAudioTrack(queue, spec, chmap, chunk, 0, capacity, FreeChunkedAudioBuffer, queue);
281
282 if (!track) {
283 FreeMemoryPoolBlock(&queue->chunk_pool, chunk);
284 return NULL;
285 }
286
287 return track;
288}
289
290void SDL_AddTrackToAudioQueue(SDL_AudioQueue *queue, SDL_AudioTrack *track)
291{
292 SDL_AudioTrack *tail = queue->tail;
293
294 if (tail) {
295 // If the spec has changed, make sure to flush the previous track
296 if (!SDL_AudioSpecsEqual(&tail->spec, &track->spec, tail->chmap, track->chmap)) {
297 FlushAudioTrack(tail);
298 }
299
300 tail->next = track;
301 } else {
302 queue->head = track;
303 }
304
305 queue->tail = track;
306}
307
308static size_t WriteToAudioTrack(SDL_AudioTrack *track, const Uint8 *data, size_t len)
309{
310 if (track->flushed || track->tail >= track->capacity) {
311 return 0;
312 }
313
314 len = SDL_min(len, track->capacity - track->tail);
315 SDL_memcpy(&track->data[track->tail], data, len);
316 track->tail += len;
317
318 return len;
319}
320
321bool SDL_WriteToAudioQueue(SDL_AudioQueue *queue, const SDL_AudioSpec *spec, const int *chmap, const Uint8 *data, size_t len)
322{
323 if (len == 0) {
324 return true;
325 }
326
327 SDL_AudioTrack *track = queue->tail;
328
329 if (track) {
330 if (!SDL_AudioSpecsEqual(&track->spec, spec, track->chmap, chmap)) {
331 FlushAudioTrack(track);
332 }
333 } else {
334 SDL_assert(!queue->head);
335 track = CreateChunkedAudioTrack(queue, spec, chmap);
336
337 if (!track) {
338 return false;
339 }
340
341 queue->head = track;
342 queue->tail = track;
343 }
344
345 for (;;) {
346 const size_t written = WriteToAudioTrack(track, data, len);
347 data += written;
348 len -= written;
349
350 if (len == 0) {
351 break;
352 }
353
354 SDL_AudioTrack *new_track = CreateChunkedAudioTrack(queue, spec, chmap);
355
356 if (!new_track) {
357 return false;
358 }
359
360 track->next = new_track;
361 queue->tail = new_track;
362 track = new_track;
363 }
364
365 return true;
366}
367
368void *SDL_BeginAudioQueueIter(SDL_AudioQueue *queue)
369{
370 return queue->head;
371}
372
373size_t SDL_NextAudioQueueIter(SDL_AudioQueue *queue, void **inout_iter, SDL_AudioSpec *out_spec, int **out_chmap, bool *out_flushed)
374{
375 SDL_AudioTrack *iter = (SDL_AudioTrack *)(*inout_iter);
376 SDL_assert(iter != NULL);
377
378 SDL_copyp(out_spec, &iter->spec);
379 *out_chmap = iter->chmap;
380
381 bool flushed = false;
382 size_t queued_bytes = 0;
383
384 while (iter) {
385 SDL_AudioTrack *track = iter;
386 iter = iter->next;
387
388 size_t avail = track->tail - track->head;
389
390 if (avail >= SDL_SIZE_MAX - queued_bytes) {
391 queued_bytes = SDL_SIZE_MAX;
392 flushed = false;
393 break;
394 }
395
396 queued_bytes += avail;
397 flushed = track->flushed;
398
399 if (flushed) {
400 break;
401 }
402 }
403
404 *inout_iter = iter;
405 *out_flushed = flushed;
406
407 return queued_bytes;
408}
409
410static const Uint8 *PeekIntoAudioQueuePast(SDL_AudioQueue *queue, Uint8 *data, size_t len)
411{
412 SDL_AudioTrack *track = queue->head;
413
414 if (track->head >= len) {
415 return &track->data[track->head - len];
416 }
417
418 size_t past = len - track->head;
419
420 if (past > queue->history_length) {
421 return NULL;
422 }
423
424 SDL_memcpy(data, &queue->history_buffer[queue->history_length - past], past);
425 SDL_memcpy(&data[past], track->data, track->head);
426
427 return data;
428}
429
430static void UpdateAudioQueueHistory(SDL_AudioQueue *queue,
431 const Uint8 *data, size_t len)
432{
433 Uint8 *history_buffer = queue->history_buffer;
434 size_t history_bytes = queue->history_length;
435
436 if (len >= history_bytes) {
437 SDL_memcpy(history_buffer, &data[len - history_bytes], history_bytes);
438 } else {
439 size_t preserve = history_bytes - len;
440 SDL_memmove(history_buffer, &history_buffer[len], preserve);
441 SDL_memcpy(&history_buffer[preserve], data, len);
442 }
443}
444
445static const Uint8 *ReadFromAudioQueue(SDL_AudioQueue *queue, Uint8 *data, size_t len)
446{
447 SDL_AudioTrack *track = queue->head;
448
449 if (track->tail - track->head >= len) {
450 const Uint8 *ptr = &track->data[track->head];
451 track->head += len;
452 return ptr;
453 }
454
455 size_t total = 0;
456
457 for (;;) {
458 size_t avail = SDL_min(len - total, track->tail - track->head);
459 SDL_memcpy(&data[total], &track->data[track->head], avail);
460 track->head += avail;
461 total += avail;
462
463 if (total == len) {
464 break;
465 }
466
467 if (track->flushed) {
468 SDL_SetError("Reading past end of flushed track");
469 return NULL;
470 }
471
472 SDL_AudioTrack *next = track->next;
473
474 if (!next) {
475 SDL_SetError("Reading past end of incomplete track");
476 return NULL;
477 }
478
479 UpdateAudioQueueHistory(queue, track->data, track->tail);
480
481 queue->head = next;
482 DestroyAudioTrack(queue, track);
483 track = next;
484 }
485
486 return data;
487}
488
489static const Uint8 *PeekIntoAudioQueueFuture(SDL_AudioQueue *queue, Uint8 *data, size_t len)
490{
491 SDL_AudioTrack *track = queue->head;
492
493 if (track->tail - track->head >= len) {
494 return &track->data[track->head];
495 }
496
497 size_t total = 0;
498
499 for (;;) {
500 size_t avail = SDL_min(len - total, track->tail - track->head);
501 SDL_memcpy(&data[total], &track->data[track->head], avail);
502 total += avail;
503
504 if (total == len) {
505 break;
506 }
507
508 if (track->flushed) {
509 // If we have run out of data, fill the rest with silence.
510 SDL_memset(&data[total], SDL_GetSilenceValueForFormat(track->spec.format), len - total);
511 break;
512 }
513
514 track = track->next;
515
516 if (!track) {
517 SDL_SetError("Peeking past end of incomplete track");
518 return NULL;
519 }
520 }
521
522 return data;
523}
524
525const Uint8 *SDL_ReadFromAudioQueue(SDL_AudioQueue *queue,
526 Uint8 *dst, SDL_AudioFormat dst_format, int dst_channels, const int *dst_map,
527 int past_frames, int present_frames, int future_frames,
528 Uint8 *scratch, float gain)
529{
530 SDL_AudioTrack *track = queue->head;
531
532 if (!track) {
533 return NULL;
534 }
535
536 SDL_AudioFormat src_format = track->spec.format;
537 int src_channels = track->spec.channels;
538 const int *src_map = track->chmap;
539
540 size_t src_frame_size = SDL_AUDIO_BYTESIZE(src_format) * src_channels;
541 size_t dst_frame_size = SDL_AUDIO_BYTESIZE(dst_format) * dst_channels;
542
543 size_t src_past_bytes = past_frames * src_frame_size;
544 size_t src_present_bytes = present_frames * src_frame_size;
545 size_t src_future_bytes = future_frames * src_frame_size;
546
547 size_t dst_past_bytes = past_frames * dst_frame_size;
548 size_t dst_present_bytes = present_frames * dst_frame_size;
549 size_t dst_future_bytes = future_frames * dst_frame_size;
550
551 const bool convert = (src_format != dst_format) || (src_channels != dst_channels) || (gain != 1.0f);
552
553 if (convert && !dst) {
554 // The user didn't ask for the data to be copied, but we need to convert it, so store it in the scratch buffer
555 dst = scratch;
556 }
557
558 // Can we get all of the data straight from this track?
559 if ((track->head >= src_past_bytes) && ((track->tail - track->head) >= (src_present_bytes + src_future_bytes))) {
560 const Uint8 *ptr = &track->data[track->head - src_past_bytes];
561 track->head += src_present_bytes;
562
563 // Do we still need to copy/convert the data?
564 if (dst) {
565 ConvertAudio(past_frames + present_frames + future_frames, ptr,
566 src_format, src_channels, src_map, dst, dst_format, dst_channels, dst_map, scratch, gain);
567 ptr = dst;
568 }
569
570 return ptr;
571 }
572
573 if (!dst) {
574 // The user didn't ask for the data to be copied, but we need to, so store it in the scratch buffer
575 dst = scratch;
576 } else if (!convert) {
577 // We are only copying, not converting, so copy straight into the dst buffer
578 scratch = dst;
579 }
580
581 Uint8 *ptr = dst;
582
583 if (src_past_bytes) {
584 ConvertAudio(past_frames, PeekIntoAudioQueuePast(queue, scratch, src_past_bytes), src_format, src_channels, src_map, dst, dst_format, dst_channels, dst_map, scratch, gain);
585 dst += dst_past_bytes;
586 scratch += dst_past_bytes;
587 }
588
589 if (src_present_bytes) {
590 ConvertAudio(present_frames, ReadFromAudioQueue(queue, scratch, src_present_bytes), src_format, src_channels, src_map, dst, dst_format, dst_channels, dst_map, scratch, gain);
591 dst += dst_present_bytes;
592 scratch += dst_present_bytes;
593 }
594
595 if (src_future_bytes) {
596 ConvertAudio(future_frames, PeekIntoAudioQueueFuture(queue, scratch, src_future_bytes), src_format, src_channels, src_map, dst, dst_format, dst_channels, dst_map, scratch, gain);
597 dst += dst_future_bytes;
598 scratch += dst_future_bytes;
599 }
600
601 return ptr;
602}
603
604size_t SDL_GetAudioQueueQueued(SDL_AudioQueue *queue)
605{
606 size_t total = 0;
607 void *iter = SDL_BeginAudioQueueIter(queue);
608
609 while (iter) {
610 SDL_AudioSpec src_spec;
611 int *src_chmap;
612 bool flushed;
613
614 size_t avail = SDL_NextAudioQueueIter(queue, &iter, &src_spec, &src_chmap, &flushed);
615
616 if (avail >= SDL_SIZE_MAX - total) {
617 total = SDL_SIZE_MAX;
618 break;
619 }
620
621 total += avail;
622 }
623
624 return total;
625}
626
627bool SDL_ResetAudioQueueHistory(SDL_AudioQueue *queue, int num_frames)
628{
629 SDL_AudioTrack *track = queue->head;
630
631 if (!track) {
632 return false;
633 }
634
635 size_t length = num_frames * SDL_AUDIO_FRAMESIZE(track->spec);
636 Uint8 *history_buffer = queue->history_buffer;
637
638 if (queue->history_capacity < length) {
639 history_buffer = (Uint8 *)SDL_aligned_alloc(SDL_GetSIMDAlignment(), length);
640 if (!history_buffer) {
641 return false;
642 }
643 SDL_aligned_free(queue->history_buffer);
644 queue->history_buffer = history_buffer;
645 queue->history_capacity = length;
646 }
647
648 queue->history_length = length;
649 SDL_memset(history_buffer, SDL_GetSilenceValueForFormat(track->spec.format), length);
650
651 return true;
652}
653