audio: Add gain support to audio streams and logical audio devices.
Fixes #10028.
This commit is contained in:
1 parent
74cc06db1b
commit
2a8f1e11ca
10 files changed
+286
-26
No files matched your search
+51
-9
@@ -1103,7 +1103,7 @@ SDL_bool SDL_PlaybackAudioThreadIterate(SDL_AudioDevice *device)
|
||||
// We should have updated this elsewhere if the format changed!
|
||||
SDL_assert(SDL_AudioSpecsEqual(&stream->dst_spec, &device->spec));
|
||||
|
||||
const int br = SDL_AtomicGet(&logdev->paused) ? 0 : SDL_GetAudioStreamData(stream, device_buffer, buffer_size);
|
||||
const int br = SDL_AtomicGet(&logdev->paused) ? 0 : SDL_GetAudioStreamDataAdjustGain(stream, device_buffer, buffer_size, logdev->gain);
|
||||
if (br < 0) { // Probably OOM. Kill the audio device; the whole thing is likely dying soon anyhow.
|
||||
failed = SDL_TRUE;
|
||||
SDL_memset(device_buffer, device->silence_value, buffer_size); // just supply silence to the device before we die.
|
||||
@@ -1143,7 +1143,7 @@ SDL_bool SDL_PlaybackAudioThreadIterate(SDL_AudioDevice *device)
|
||||
for iterating here because the binding linked list can only change while the device lock is held.
|
||||
(we _do_ lock the stream during binding/unbinding to make sure that two threads can't try to bind
|
||||
the same stream to different devices at the same time, though.) */
|
||||
const int br = SDL_GetAudioStreamData(stream, device->work_buffer, work_buffer_size);
|
||||
const int br = SDL_GetAudioStreamDataAdjustGain(stream, device->work_buffer, work_buffer_size, logdev->gain);
|
||||
if (br < 0) { // Probably OOM. Kill the audio device; the whole thing is likely dying soon anyhow.
|
||||
failed = SDL_TRUE;
|
||||
break;
|
||||
@@ -1161,8 +1161,8 @@ SDL_bool SDL_PlaybackAudioThreadIterate(SDL_AudioDevice *device)
|
||||
|
||||
if (((Uint8 *) final_mix_buffer) != device_buffer) {
|
||||
// !!! FIXME: we can't promise the device buf is aligned/padded for SIMD.
|
||||
//ConvertAudio(needed_samples / device->spec.channels, final_mix_buffer, SDL_AUDIO_F32, device->spec.channels, NULL, device_buffer, device->spec.format, device->spec.channels, NULL, NULL);
|
||||
ConvertAudio(needed_samples / device->spec.channels, final_mix_buffer, SDL_AUDIO_F32, device->spec.channels, NULL, device->work_buffer, device->spec.format, device->spec.channels, NULL, NULL);
|
||||
//ConvertAudio(needed_samples / device->spec.channels, final_mix_buffer, SDL_AUDIO_F32, device->spec.channels, NULL, device_buffer, device->spec.format, device->spec.channels, NULL, NULL, 1.0f);
|
||||
ConvertAudio(needed_samples / device->spec.channels, final_mix_buffer, SDL_AUDIO_F32, device->spec.channels, NULL, device->work_buffer, device->spec.format, device->spec.channels, NULL, NULL, 1.0f);
|
||||
SDL_memcpy(device_buffer, device->work_buffer, buffer_size);
|
||||
}
|
||||
}
|
||||
@@ -1250,7 +1250,7 @@ SDL_bool SDL_RecordingAudioThreadIterate(SDL_AudioDevice *device)
|
||||
void *output_buffer = device->work_buffer;
|
||||
|
||||
// I don't know why someone would want a postmix on a recording device, but we offer it for API consistency.
|
||||
if (logdev->postmix) {
|
||||
if (logdev->postmix || (logdev->gain != 1.0f)) {
|
||||
// move to float format.
|
||||
SDL_AudioSpec outspec;
|
||||
SDL_copyp(&outspec, &device->spec);
|
||||
@@ -1258,13 +1258,15 @@ SDL_bool SDL_RecordingAudioThreadIterate(SDL_AudioDevice *device)
|
||||
output_buffer = device->postmix_buffer;
|
||||
const int frames = br / SDL_AUDIO_FRAMESIZE(device->spec);
|
||||
br = frames * SDL_AUDIO_FRAMESIZE(outspec);
|
||||
ConvertAudio(frames, device->work_buffer, device->spec.format, outspec.channels, NULL, device->postmix_buffer, SDL_AUDIO_F32, outspec.channels, NULL, NULL);
|
||||
logdev->postmix(logdev->postmix_userdata, &outspec, device->postmix_buffer, br);
|
||||
ConvertAudio(frames, device->work_buffer, device->spec.format, outspec.channels, NULL, device->postmix_buffer, SDL_AUDIO_F32, outspec.channels, NULL, NULL, logdev->gain);
|
||||
if (logdev->postmix) {
|
||||
logdev->postmix(logdev->postmix_userdata, &outspec, device->postmix_buffer, br);
|
||||
}
|
||||
}
|
||||
|
||||
for (SDL_AudioStream *stream = logdev->bound_streams; stream; stream = stream->next_binding) {
|
||||
// We should have updated this elsewhere if the format changed!
|
||||
SDL_assert(stream->src_spec.format == (logdev->postmix ? SDL_AUDIO_F32 : device->spec.format));
|
||||
SDL_assert(stream->src_spec.format == ((logdev->postmix || (logdev->gain != 1.0f)) ? SDL_AUDIO_F32 : device->spec.format));
|
||||
SDL_assert(stream->src_spec.channels == device->spec.channels);
|
||||
SDL_assert(stream->src_spec.freq == device->spec.freq);
|
||||
|
||||
@@ -1695,6 +1697,7 @@ SDL_AudioDeviceID SDL_OpenAudioDevice(SDL_AudioDeviceID devid, const SDL_AudioSp
|
||||
SDL_AtomicSet(&logdev->paused, 0);
|
||||
retval = logdev->instance_id = AssignAudioDeviceInstanceId(device->recording, /*islogical=*/SDL_TRUE);
|
||||
logdev->physical_device = device;
|
||||
logdev->gain = 1.0f;
|
||||
logdev->opened_as_default = wants_default;
|
||||
logdev->next = device->logical_devices;
|
||||
if (device->logical_devices) {
|
||||
@@ -1752,6 +1755,44 @@ SDL_bool SDL_AudioDevicePaused(SDL_AudioDeviceID devid)
|
||||
return retval;
|
||||
}
|
||||
|
||||
float SDL_GetAudioDeviceGain(SDL_AudioDeviceID devid)
|
||||
{
|
||||
SDL_AudioDevice *device = NULL;
|
||||
SDL_LogicalAudioDevice *logdev = ObtainLogicalAudioDevice(devid, &device);
|
||||
const float retval = logdev ? logdev->gain : -1.0f;
|
||||
ReleaseAudioDevice(device);
|
||||
return retval;
|
||||
}
|
||||
|
||||
int SDL_SetAudioDeviceGain(SDL_AudioDeviceID devid, float gain)
|
||||
{
|
||||
if (gain < 0.0f) {
|
||||
return SDL_InvalidParamError("gain");
|
||||
}
|
||||
|
||||
SDL_AudioDevice *device = NULL;
|
||||
SDL_LogicalAudioDevice *logdev = ObtainLogicalAudioDevice(devid, &device);
|
||||
int retval = -1;
|
||||
if (logdev) {
|
||||
logdev->gain = gain;
|
||||
if (device->recording) {
|
||||
const SDL_bool need_float32 = (logdev->postmix || logdev->gain != 1.0f);
|
||||
for (SDL_AudioStream *stream = logdev->bound_streams; stream; stream = stream->next_binding) {
|
||||
// set the proper end of the stream to the device's format.
|
||||
// SDL_SetAudioStreamFormat does a ton of validation just to memcpy an audiospec.
|
||||
SDL_LockMutex(stream->lock);
|
||||
stream->src_spec.format = need_float32 ? SDL_AUDIO_F32 : device->spec.format;
|
||||
SDL_UnlockMutex(stream->lock);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateAudioStreamFormatsPhysical(device);
|
||||
retval = 0;
|
||||
}
|
||||
ReleaseAudioDevice(device);
|
||||
return retval;
|
||||
}
|
||||
|
||||
int SDL_SetAudioPostmixCallback(SDL_AudioDeviceID devid, SDL_AudioPostmixCallback callback, void *userdata)
|
||||
{
|
||||
SDL_AudioDevice *device = NULL;
|
||||
@@ -1770,11 +1811,12 @@ int SDL_SetAudioPostmixCallback(SDL_AudioDeviceID devid, SDL_AudioPostmixCallbac
|
||||
logdev->postmix_userdata = userdata;
|
||||
|
||||
if (device->recording) {
|
||||
const SDL_bool need_float32 = (callback || logdev->gain != 1.0f);
|
||||
for (SDL_AudioStream *stream = logdev->bound_streams; stream; stream = stream->next_binding) {
|
||||
// set the proper end of the stream to the device's format.
|
||||
// SDL_SetAudioStreamFormat does a ton of validation just to memcpy an audiospec.
|
||||
SDL_LockMutex(stream->lock);
|
||||
stream->src_spec.format = callback ? SDL_AUDIO_F32 : device->spec.format;
|
||||
stream->src_spec.format = need_float32 ? SDL_AUDIO_F32 : device->spec.format;
|
||||
SDL_UnlockMutex(stream->lock);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,10 +194,14 @@ static void SwizzleAudio(const int num_frames, void *dst, const void *src, int c
|
||||
//
|
||||
// The scratch buffer must be able to store `num_frames * CalculateMaxSampleFrameSize(src_format, src_channels, dst_format, dst_channels)` bytes.
|
||||
// If the scratch buffer is NULL, this restriction applies to the output buffer instead.
|
||||
//
|
||||
// Since this is a convenient point that audio goes through even if it doesn't need format conversion,
|
||||
// we also handle gain adjustment here, so we don't have to make another pass over the data later.
|
||||
// Strictly speaking, this is also a "conversion". :)
|
||||
void ConvertAudio(int num_frames,
|
||||
const void *src, SDL_AudioFormat src_format, int src_channels, const Uint8 *src_map,
|
||||
void *dst, SDL_AudioFormat dst_format, int dst_channels, const Uint8 *dst_map,
|
||||
void* scratch)
|
||||
void* scratch, float gain)
|
||||
{
|
||||
SDL_assert(src != NULL);
|
||||
SDL_assert(dst != NULL);
|
||||
@@ -243,7 +247,7 @@ void ConvertAudio(int num_frames,
|
||||
}
|
||||
|
||||
// see if we can skip float conversion entirely.
|
||||
if (src_channels == dst_channels) {
|
||||
if ((src_channels == dst_channels) && (gain == 1.0f)) {
|
||||
if (src_format == dst_format) {
|
||||
// nothing to do, we're already in the right format, just copy it over if necessary.
|
||||
if (dst_map) {
|
||||
@@ -280,6 +284,23 @@ void ConvertAudio(int num_frames,
|
||||
src = buf;
|
||||
}
|
||||
|
||||
// Gain adjustment
|
||||
if (gain != 1.0f) {
|
||||
float *buf = (float *)(dstconvert ? scratch : dst);
|
||||
const int total_samples = num_frames * src_channels;
|
||||
if (src == buf) {
|
||||
for (int i = 0; i < total_samples; i++) {
|
||||
buf[i] *= gain;
|
||||
}
|
||||
} else {
|
||||
float *fsrc = (float *)src;
|
||||
for (int i = 0; i < total_samples; i++) {
|
||||
buf[i] = fsrc[i] * gain;
|
||||
}
|
||||
}
|
||||
src = buf;
|
||||
}
|
||||
|
||||
// Channel conversion
|
||||
|
||||
if (channelconvert) {
|
||||
@@ -379,6 +400,7 @@ SDL_AudioStream *SDL_CreateAudioStream(const SDL_AudioSpec *src_spec, const SDL_
|
||||
}
|
||||
|
||||
retval->freq_ratio = 1.0f;
|
||||
retval->gain = 1.0f;
|
||||
retval->queue = SDL_CreateAudioQueue(8192);
|
||||
|
||||
if (!retval->queue) {
|
||||
@@ -593,6 +615,35 @@ int SDL_SetAudioStreamFrequencyRatio(SDL_AudioStream *stream, float freq_ratio)
|
||||
return 0;
|
||||
}
|
||||
|
||||
float SDL_GetAudioStreamGain(SDL_AudioStream *stream)
|
||||
{
|
||||
if (!stream) {
|
||||
SDL_InvalidParamError("stream");
|
||||
return -1.0f;
|
||||
}
|
||||
|
||||
SDL_LockMutex(stream->lock);
|
||||
const float gain = stream->gain;
|
||||
SDL_UnlockMutex(stream->lock);
|
||||
|
||||
return gain;
|
||||
}
|
||||
|
||||
int SDL_SetAudioStreamGain(SDL_AudioStream *stream, float gain)
|
||||
{
|
||||
if (!stream) {
|
||||
return SDL_InvalidParamError("stream");
|
||||
} else if (gain < 0.0f) {
|
||||
return SDL_InvalidParamError("gain");
|
||||
}
|
||||
|
||||
SDL_LockMutex(stream->lock);
|
||||
stream->gain = gain;
|
||||
SDL_UnlockMutex(stream->lock);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int CheckAudioStreamIsFullySetup(SDL_AudioStream *stream)
|
||||
{
|
||||
if (stream->src_spec.format == 0) {
|
||||
@@ -820,7 +871,7 @@ static Sint64 GetAudioStreamHead(SDL_AudioStream* stream, SDL_AudioSpec* out_spe
|
||||
|
||||
// You must hold stream->lock and validate your parameters before calling this!
|
||||
// Enough input data MUST be available!
|
||||
static int GetAudioStreamDataInternal(SDL_AudioStream *stream, void *buf, int output_frames)
|
||||
static int GetAudioStreamDataInternal(SDL_AudioStream *stream, void *buf, int output_frames, float gain)
|
||||
{
|
||||
const SDL_AudioSpec* src_spec = &stream->input_spec;
|
||||
const SDL_AudioSpec* dst_spec = &stream->dst_spec;
|
||||
@@ -854,7 +905,7 @@ static int GetAudioStreamDataInternal(SDL_AudioStream *stream, void *buf, int ou
|
||||
}
|
||||
}
|
||||
|
||||
if (SDL_ReadFromAudioQueue(stream->queue, buf, dst_format, dst_channels, dst_map, 0, output_frames, 0, work_buffer) != buf) {
|
||||
if (SDL_ReadFromAudioQueue(stream->queue, buf, dst_format, dst_channels, dst_map, 0, output_frames, 0, work_buffer, gain) != buf) {
|
||||
return SDL_SetError("Not enough data in queue");
|
||||
}
|
||||
|
||||
@@ -919,10 +970,15 @@ static int GetAudioStreamDataInternal(SDL_AudioStream *stream, void *buf, int ou
|
||||
return -1;
|
||||
}
|
||||
|
||||
// adjust gain either before resampling or after, depending on which point has less
|
||||
// samples to process.
|
||||
const float preresample_gain = (input_frames > output_frames) ? 1.0f : gain;
|
||||
const float postresample_gain = (input_frames > output_frames) ? gain : 1.0f;
|
||||
|
||||
// (dst channel map is NULL because we'll do the final swizzle on ConvertAudio after resample.)
|
||||
const Uint8* input_buffer = SDL_ReadFromAudioQueue(stream->queue,
|
||||
NULL, resample_format, resample_channels, NULL,
|
||||
padding_frames, input_frames, padding_frames, work_buffer);
|
||||
padding_frames, input_frames, padding_frames, work_buffer, preresample_gain);
|
||||
|
||||
if (!input_buffer) {
|
||||
return SDL_SetError("Not enough data in queue (resample)");
|
||||
@@ -939,13 +995,13 @@ static int GetAudioStreamDataInternal(SDL_AudioStream *stream, void *buf, int ou
|
||||
resample_rate, &stream->resample_offset);
|
||||
|
||||
// Convert to the final format, if necessary (src channel map is NULL because SDL_ReadFromAudioQueue already handled this).
|
||||
ConvertAudio(output_frames, resample_buffer, resample_format, resample_channels, NULL, buf, dst_format, dst_channels, dst_map, work_buffer);
|
||||
ConvertAudio(output_frames, resample_buffer, resample_format, resample_channels, NULL, buf, dst_format, dst_channels, dst_map, work_buffer, postresample_gain);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// get converted/resampled data from the stream
|
||||
int SDL_GetAudioStreamData(SDL_AudioStream *stream, void *voidbuf, int len)
|
||||
int SDL_GetAudioStreamDataAdjustGain(SDL_AudioStream *stream, void *voidbuf, int len, float extra_gain)
|
||||
{
|
||||
Uint8 *buf = (Uint8 *) voidbuf;
|
||||
|
||||
@@ -970,6 +1026,7 @@ int SDL_GetAudioStreamData(SDL_AudioStream *stream, void *voidbuf, int len)
|
||||
return -1;
|
||||
}
|
||||
|
||||
const float gain = stream->gain * extra_gain;
|
||||
const int dst_frame_size = SDL_AUDIO_FRAMESIZE(stream->dst_spec);
|
||||
|
||||
len -= len % dst_frame_size; // chop off any fractional sample frame.
|
||||
@@ -1029,7 +1086,7 @@ int SDL_GetAudioStreamData(SDL_AudioStream *stream, void *voidbuf, int len)
|
||||
output_frames = SDL_min(output_frames, chunk_size);
|
||||
output_frames = (int) SDL_min(output_frames, available_frames);
|
||||
|
||||
if (GetAudioStreamDataInternal(stream, &buf[total], output_frames) != 0) {
|
||||
if (GetAudioStreamDataInternal(stream, &buf[total], output_frames, gain) != 0) {
|
||||
total = total ? total : -1;
|
||||
break;
|
||||
}
|
||||
@@ -1046,6 +1103,11 @@ int SDL_GetAudioStreamData(SDL_AudioStream *stream, void *voidbuf, int len)
|
||||
return total;
|
||||
}
|
||||
|
||||
int SDL_GetAudioStreamData(SDL_AudioStream *stream, void *voidbuf, int len)
|
||||
{
|
||||
return SDL_GetAudioStreamDataAdjustGain(stream, voidbuf, len, 1.0f);
|
||||
}
|
||||
|
||||
// number of converted/resampled bytes available for output
|
||||
int SDL_GetAudioStreamAvailable(SDL_AudioStream *stream)
|
||||
{
|
||||
|
||||
@@ -514,7 +514,7 @@ static const Uint8 *PeekIntoAudioQueueFuture(SDL_AudioQueue *queue, Uint8 *data,
|
||||
const Uint8 *SDL_ReadFromAudioQueue(SDL_AudioQueue *queue,
|
||||
Uint8 *dst, SDL_AudioFormat dst_format, int dst_channels, const Uint8 *dst_map,
|
||||
int past_frames, int present_frames, int future_frames,
|
||||
Uint8 *scratch)
|
||||
Uint8 *scratch, float gain)
|
||||
{
|
||||
SDL_AudioTrack *track = queue->head;
|
||||
|
||||
@@ -552,7 +552,7 @@ const Uint8 *SDL_ReadFromAudioQueue(SDL_AudioQueue *queue,
|
||||
// Do we still need to copy/convert the data?
|
||||
if (dst) {
|
||||
ConvertAudio(past_frames + present_frames + future_frames, ptr,
|
||||
src_format, src_channels, src_map, dst, dst_format, dst_channels, dst_map, scratch);
|
||||
src_format, src_channels, src_map, dst, dst_format, dst_channels, dst_map, scratch, gain);
|
||||
ptr = dst;
|
||||
}
|
||||
|
||||
@@ -570,19 +570,19 @@ const Uint8 *SDL_ReadFromAudioQueue(SDL_AudioQueue *queue,
|
||||
Uint8 *ptr = dst;
|
||||
|
||||
if (src_past_bytes) {
|
||||
ConvertAudio(past_frames, PeekIntoAudioQueuePast(queue, scratch, src_past_bytes), src_format, src_channels, src_map, dst, dst_format, dst_channels, dst_map, scratch);
|
||||
ConvertAudio(past_frames, PeekIntoAudioQueuePast(queue, scratch, src_past_bytes), src_format, src_channels, src_map, dst, dst_format, dst_channels, dst_map, scratch, gain);
|
||||
dst += dst_past_bytes;
|
||||
scratch += dst_past_bytes;
|
||||
}
|
||||
|
||||
if (src_present_bytes) {
|
||||
ConvertAudio(present_frames, ReadFromAudioQueue(queue, scratch, src_present_bytes), src_format, src_channels, src_map, dst, dst_format, dst_channels, dst_map, scratch);
|
||||
ConvertAudio(present_frames, ReadFromAudioQueue(queue, scratch, src_present_bytes), src_format, src_channels, src_map, dst, dst_format, dst_channels, dst_map, scratch, gain);
|
||||
dst += dst_present_bytes;
|
||||
scratch += dst_present_bytes;
|
||||
}
|
||||
|
||||
if (src_future_bytes) {
|
||||
ConvertAudio(future_frames, PeekIntoAudioQueueFuture(queue, scratch, src_future_bytes), src_format, src_channels, src_map, dst, dst_format, dst_channels, dst_map, scratch);
|
||||
ConvertAudio(future_frames, PeekIntoAudioQueueFuture(queue, scratch, src_future_bytes), src_format, src_channels, src_map, dst, dst_format, dst_channels, dst_map, scratch, gain);
|
||||
dst += dst_future_bytes;
|
||||
scratch += dst_future_bytes;
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ size_t SDL_NextAudioQueueIter(SDL_AudioQueue *queue, void **inout_iter, SDL_Audi
|
||||
const Uint8 *SDL_ReadFromAudioQueue(SDL_AudioQueue *queue,
|
||||
Uint8 *dst, SDL_AudioFormat dst_format, int dst_channels, const Uint8 *dst_map,
|
||||
int past_frames, int present_frames, int future_frames,
|
||||
Uint8 *scratch);
|
||||
Uint8 *scratch, float gain);
|
||||
|
||||
// Get the total number of bytes currently queued
|
||||
size_t SDL_GetAudioQueueQueued(SDL_AudioQueue *queue);
|
||||
|
||||
@@ -118,17 +118,19 @@ extern SDL_bool SDL_ChannelMapIsBogus(const Uint8 *map, int channels);
|
||||
extern void ConvertAudio(int num_frames,
|
||||
const void *src, SDL_AudioFormat src_format, int src_channels, const Uint8 *src_map,
|
||||
void *dst, SDL_AudioFormat dst_format, int dst_channels, const Uint8 *dst_map,
|
||||
void* scratch);
|
||||
void* scratch, float gain);
|
||||
|
||||
// Compare two SDL_AudioSpecs, return SDL_TRUE if they match exactly.
|
||||
// Using SDL_memcmp directly isn't safe, since potential padding (and unused parts of the channel map) might not be initialized.
|
||||
extern SDL_bool SDL_AudioSpecsEqual(const SDL_AudioSpec *a, const SDL_AudioSpec *b);
|
||||
|
||||
|
||||
// Special case to let something in SDL_audiocvt.c access something in SDL_audio.c. Don't use this.
|
||||
extern void OnAudioStreamCreated(SDL_AudioStream *stream);
|
||||
extern void OnAudioStreamDestroy(SDL_AudioStream *stream);
|
||||
|
||||
// This just lets audio playback apply logical device gain at the same time as audiostream gain, so it's one multiplication instead of thousands.
|
||||
extern int SDL_GetAudioStreamDataAdjustGain(SDL_AudioStream *stream, void *voidbuf, int len, float extra_gain);
|
||||
|
||||
typedef struct SDL_AudioDriverImpl
|
||||
{
|
||||
void (*DetectDevices)(SDL_AudioDevice **default_playback, SDL_AudioDevice **default_recording);
|
||||
@@ -196,6 +198,7 @@ struct SDL_AudioStream
|
||||
SDL_AudioSpec src_spec;
|
||||
SDL_AudioSpec dst_spec;
|
||||
float freq_ratio;
|
||||
float gain;
|
||||
|
||||
struct SDL_AudioQueue* queue;
|
||||
|
||||
@@ -230,6 +233,9 @@ struct SDL_LogicalAudioDevice
|
||||
// If whole logical device is paused (process no streams bound to this device).
|
||||
SDL_AtomicInt paused;
|
||||
|
||||
// Volume of the device output.
|
||||
float gain;
|
||||
|
||||
// double-linked list of all audio streams currently bound to this opened device.
|
||||
SDL_AudioStream *bound_streams;
|
||||
|
||||
|
||||
Reference in new issue
Block a user