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 | |
26 | typedef struct SDL_MemoryPool SDL_MemoryPool; |
27 | |
28 | struct SDL_MemoryPool |
29 | { |
30 | void *free_blocks; |
31 | size_t block_size; |
32 | size_t num_free; |
33 | size_t max_free; |
34 | }; |
35 | |
36 | struct 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 | |
54 | struct 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 |
68 | static 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 |
74 | static 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 |
87 | static 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 |
99 | static 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. |
114 | static 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 |
124 | static 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 | |
141 | void 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 | |
152 | SDL_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 | |
171 | static 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 | |
178 | void 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 | |
193 | static void FlushAudioTrack(SDL_AudioTrack *track) |
194 | { |
195 | track->flushed = true; |
196 | } |
197 | |
198 | void SDL_FlushAudioQueue(SDL_AudioQueue *queue) |
199 | { |
200 | SDL_AudioTrack *track = queue->tail; |
201 | |
202 | if (track) { |
203 | FlushAudioTrack(track); |
204 | } |
205 | } |
206 | |
207 | void 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 | |
231 | SDL_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 | |
262 | static 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 | |
269 | static 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 | |
290 | void 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 | |
308 | static 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 | |
321 | bool 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 | |
368 | void *SDL_BeginAudioQueueIter(SDL_AudioQueue *queue) |
369 | { |
370 | return queue->head; |
371 | } |
372 | |
373 | size_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 | |
410 | static 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 | |
430 | static 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 | |
445 | static 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 | |
489 | static 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 | |
525 | const 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 | |
604 | size_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 | |
627 | bool 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 | |