mirror of
https://github.com/libsdl-org/SDL.git
synced 2025-05-17 18:28:28 +00:00
audio: Add channel remapping to SDL_AudioSpec and SDL_AudioStream.
Fixes #8367.
This commit is contained in:
parent
0367f1af19
commit
16e7fdc4f2
12 changed files with 254 additions and 193 deletions
|
@ -43,20 +43,41 @@
|
||||||
* if you aren't reading from a file) as a basic means to load sound data into
|
* if you aren't reading from a file) as a basic means to load sound data into
|
||||||
* your program.
|
* your program.
|
||||||
*
|
*
|
||||||
* For multi-channel audio, data is interleaved (one sample for each channel,
|
* ## Channel layouts as SDL expects them
|
||||||
* then repeat). The SDL channel order is:
|
|
||||||
*
|
*
|
||||||
* - Stereo: FL, FR
|
* Abbreviations:
|
||||||
* - 2.1 surround: FL, FR, LFE
|
*
|
||||||
* - Quad: FL, FR, BL, BR
|
* - FRONT = single mono speaker
|
||||||
* - 4.1 surround: FL, FR, LFE, BL, BR
|
* - FL = front left speaker
|
||||||
* - 5.1 surround: FL, FR, FC, LFE, SL, SR (last two can also be BL BR)
|
* - FR = front right speaker
|
||||||
* - 6.1 surround: FL, FR, FC, LFE, BC, SL, SR
|
* - FC = front center speaker
|
||||||
* - 7.1 surround: FL, FR, FC, LFE, BL, BR, SL, SR
|
* - BL = back left speaker
|
||||||
|
* - BR = back right speaker
|
||||||
|
* - SR = surround right speaker
|
||||||
|
* - SL = surround left speaker
|
||||||
|
* - BC = back center speaker
|
||||||
|
* - LFE = low-frequency speaker
|
||||||
|
*
|
||||||
|
* These are listed in the order they are laid out in
|
||||||
|
* memory, so "FL, FR" means "the front left speaker is
|
||||||
|
* laid out in memory first, then the front right, then
|
||||||
|
* it repeats for the next audio frame".
|
||||||
|
*
|
||||||
|
* - 1 channel (mono) layout: FRONT
|
||||||
|
* - 2 channels (stereo) layout: FL, FR
|
||||||
|
* - 3 channels (2.1) layout: FL, FR, LFE
|
||||||
|
* - 4 channels (quad) layout: FL, FR, BL, BR
|
||||||
|
* - 5 channels (4.1) layout: FL, FR, LFE, BL, BR
|
||||||
|
* - 6 channels (5.1) layout: FL, FR, FC, LFE, BL, BR (last two can also be BL, BR)
|
||||||
|
* - 7 channels (6.1) layout: FL, FR, FC, LFE, BC, SL, SR
|
||||||
|
* - 8 channels (7.1) layout: FL, FR, FC, LFE, BL, BR, SL, SR
|
||||||
*
|
*
|
||||||
* This is the same order as DirectSound expects, but applied to all
|
* This is the same order as DirectSound expects, but applied to all
|
||||||
* platforms; SDL will swizzle the channels as necessary if a platform expects
|
* platforms; SDL will swizzle the channels as necessary if a platform expects
|
||||||
* something different.
|
* something different.
|
||||||
|
*
|
||||||
|
* SDL_AudioStream can also be provided a channel map to change this ordering
|
||||||
|
* to whatever is necessary, in other audio processing scenarios.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef SDL_audio_h_
|
#ifndef SDL_audio_h_
|
||||||
|
@ -280,6 +301,18 @@ typedef Uint32 SDL_AudioDeviceID;
|
||||||
*/
|
*/
|
||||||
#define SDL_AUDIO_DEVICE_DEFAULT_RECORDING ((SDL_AudioDeviceID) 0xFFFFFFFE)
|
#define SDL_AUDIO_DEVICE_DEFAULT_RECORDING ((SDL_AudioDeviceID) 0xFFFFFFFE)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum channels that an SDL_AudioSpec channel map can handle.
|
||||||
|
*
|
||||||
|
* This is (currently) double the number of channels that SDL supports,
|
||||||
|
* to allow for future expansion while maintaining binary compatibility.
|
||||||
|
*
|
||||||
|
* \since This macro is available since SDL 3.0.0.
|
||||||
|
*
|
||||||
|
* \sa SDL_AudioSpec
|
||||||
|
*/
|
||||||
|
#define SDL_MAX_CHANNEL_MAP_SIZE 16
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format specifier for audio data.
|
* Format specifier for audio data.
|
||||||
*
|
*
|
||||||
|
@ -292,6 +325,8 @@ typedef struct SDL_AudioSpec
|
||||||
SDL_AudioFormat format; /**< Audio data format */
|
SDL_AudioFormat format; /**< Audio data format */
|
||||||
int channels; /**< Number of channels: 1 mono, 2 stereo, etc */
|
int channels; /**< Number of channels: 1 mono, 2 stereo, etc */
|
||||||
int freq; /**< sample rate: sample frames per second */
|
int freq; /**< sample rate: sample frames per second */
|
||||||
|
SDL_bool use_channel_map; /**< If SDL_FALSE, ignore `channel_map` and use default order. */
|
||||||
|
Uint8 channel_map[SDL_MAX_CHANNEL_MAP_SIZE]; /**< `channels` items of channel order. */
|
||||||
} SDL_AudioSpec;
|
} SDL_AudioSpec;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -318,6 +353,7 @@ typedef struct SDL_AudioSpec
|
||||||
* when it doesn't have the complete buffer available.
|
* when it doesn't have the complete buffer available.
|
||||||
* - It can handle incoming data in any variable size.
|
* - It can handle incoming data in any variable size.
|
||||||
* - It can handle input/output format changes on the fly.
|
* - It can handle input/output format changes on the fly.
|
||||||
|
* - It can remap audio channels between inputs and outputs.
|
||||||
* - You push data as you have it, and pull it when you need it
|
* - You push data as you have it, and pull it when you need it
|
||||||
* - It can also function as a basic audio data queue even if you just have
|
* - It can also function as a basic audio data queue even if you just have
|
||||||
* sound that needs to pass from one place to another.
|
* sound that needs to pass from one place to another.
|
||||||
|
|
|
@ -249,6 +249,16 @@ static void UpdateAudioStreamFormatsPhysical(SDL_AudioDevice *device)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SDL_bool SDL_AudioSpecsEqual(const SDL_AudioSpec *a, const SDL_AudioSpec *b)
|
||||||
|
{
|
||||||
|
if ((a->format != b->format) || (a->channels != b->channels) || (a->freq != b->freq) || (a->use_channel_map != b->use_channel_map)) {
|
||||||
|
return SDL_FALSE;
|
||||||
|
} else if (a->use_channel_map && (SDL_memcmp(a->channel_map, b->channel_map, sizeof (a->channel_map[0]) * a->channels) != 0)) {
|
||||||
|
return SDL_FALSE;
|
||||||
|
}
|
||||||
|
return SDL_TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Zombie device implementation...
|
// Zombie device implementation...
|
||||||
|
|
||||||
|
@ -632,11 +642,13 @@ SDL_AudioDevice *SDL_AddAudioDevice(SDL_bool recording, const char *name, const
|
||||||
const int default_freq = recording ? DEFAULT_AUDIO_RECORDING_FREQUENCY : DEFAULT_AUDIO_PLAYBACK_FREQUENCY;
|
const int default_freq = recording ? DEFAULT_AUDIO_RECORDING_FREQUENCY : DEFAULT_AUDIO_PLAYBACK_FREQUENCY;
|
||||||
|
|
||||||
SDL_AudioSpec spec;
|
SDL_AudioSpec spec;
|
||||||
|
SDL_zero(spec);
|
||||||
if (!inspec) {
|
if (!inspec) {
|
||||||
spec.format = default_format;
|
spec.format = default_format;
|
||||||
spec.channels = default_channels;
|
spec.channels = default_channels;
|
||||||
spec.freq = default_freq;
|
spec.freq = default_freq;
|
||||||
} else {
|
} else {
|
||||||
|
SDL_assert(!inspec->use_channel_map); // backends shouldn't set a channel map here! Set it when opening the device!
|
||||||
spec.format = (inspec->format != 0) ? inspec->format : default_format;
|
spec.format = (inspec->format != 0) ? inspec->format : default_format;
|
||||||
spec.channels = (inspec->channels != 0) ? inspec->channels : default_channels;
|
spec.channels = (inspec->channels != 0) ? inspec->channels : default_channels;
|
||||||
spec.freq = (inspec->freq != 0) ? inspec->freq : default_freq;
|
spec.freq = (inspec->freq != 0) ? inspec->freq : default_freq;
|
||||||
|
@ -1089,7 +1101,7 @@ SDL_bool SDL_PlaybackAudioThreadIterate(SDL_AudioDevice *device)
|
||||||
SDL_AudioStream *stream = logdev->bound_streams;
|
SDL_AudioStream *stream = logdev->bound_streams;
|
||||||
|
|
||||||
// We should have updated this elsewhere if the format changed!
|
// We should have updated this elsewhere if the format changed!
|
||||||
SDL_assert(AUDIO_SPECS_EQUAL(stream->dst_spec, device->spec));
|
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_GetAudioStreamData(stream, device_buffer, buffer_size);
|
||||||
if (br < 0) { // Probably OOM. Kill the audio device; the whole thing is likely dying soon anyhow.
|
if (br < 0) { // Probably OOM. Kill the audio device; the whole thing is likely dying soon anyhow.
|
||||||
|
@ -1106,9 +1118,8 @@ SDL_bool SDL_PlaybackAudioThreadIterate(SDL_AudioDevice *device)
|
||||||
|
|
||||||
SDL_assert(work_buffer_size <= device->work_buffer_size);
|
SDL_assert(work_buffer_size <= device->work_buffer_size);
|
||||||
|
|
||||||
|
SDL_copyp(&outspec, &device->spec);
|
||||||
outspec.format = SDL_AUDIO_F32;
|
outspec.format = SDL_AUDIO_F32;
|
||||||
outspec.channels = device->spec.channels;
|
|
||||||
outspec.freq = device->spec.freq;
|
|
||||||
|
|
||||||
SDL_memset(final_mix_buffer, '\0', work_buffer_size); // start with silence.
|
SDL_memset(final_mix_buffer, '\0', work_buffer_size); // start with silence.
|
||||||
|
|
||||||
|
@ -1126,7 +1137,7 @@ SDL_bool SDL_PlaybackAudioThreadIterate(SDL_AudioDevice *device)
|
||||||
|
|
||||||
for (SDL_AudioStream *stream = logdev->bound_streams; stream; stream = stream->next_binding) {
|
for (SDL_AudioStream *stream = logdev->bound_streams; stream; stream = stream->next_binding) {
|
||||||
// We should have updated this elsewhere if the format changed!
|
// We should have updated this elsewhere if the format changed!
|
||||||
SDL_assert(AUDIO_SPECS_EQUAL(stream->dst_spec, outspec));
|
SDL_assert(SDL_AudioSpecsEqual(&stream->dst_spec, &outspec));
|
||||||
|
|
||||||
/* this will hold a lock on `stream` while getting. We don't explicitly lock the streams
|
/* this will hold a lock on `stream` while getting. We don't explicitly lock the streams
|
||||||
for iterating here because the binding linked list can only change while the device lock is held.
|
for iterating here because the binding linked list can only change while the device lock is held.
|
||||||
|
@ -1150,8 +1161,8 @@ SDL_bool SDL_PlaybackAudioThreadIterate(SDL_AudioDevice *device)
|
||||||
|
|
||||||
if (((Uint8 *) final_mix_buffer) != device_buffer) {
|
if (((Uint8 *) final_mix_buffer) != device_buffer) {
|
||||||
// !!! FIXME: we can't promise the device buf is aligned/padded for SIMD.
|
// !!! 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, device_buffer, device->spec.format, device->spec.channels, device->work_buffer);
|
//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, device->work_buffer, device->spec.format, device->spec.channels, 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);
|
||||||
SDL_memcpy(device_buffer, device->work_buffer, buffer_size);
|
SDL_memcpy(device_buffer, device->work_buffer, buffer_size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1242,13 +1253,12 @@ SDL_bool SDL_RecordingAudioThreadIterate(SDL_AudioDevice *device)
|
||||||
if (logdev->postmix) {
|
if (logdev->postmix) {
|
||||||
// move to float format.
|
// move to float format.
|
||||||
SDL_AudioSpec outspec;
|
SDL_AudioSpec outspec;
|
||||||
|
SDL_copyp(&outspec, &device->spec);
|
||||||
outspec.format = SDL_AUDIO_F32;
|
outspec.format = SDL_AUDIO_F32;
|
||||||
outspec.channels = device->spec.channels;
|
|
||||||
outspec.freq = device->spec.freq;
|
|
||||||
output_buffer = device->postmix_buffer;
|
output_buffer = device->postmix_buffer;
|
||||||
const int frames = br / SDL_AUDIO_FRAMESIZE(device->spec);
|
const int frames = br / SDL_AUDIO_FRAMESIZE(device->spec);
|
||||||
br = frames * SDL_AUDIO_FRAMESIZE(outspec);
|
br = frames * SDL_AUDIO_FRAMESIZE(outspec);
|
||||||
ConvertAudio(frames, device->work_buffer, device->spec.format, outspec.channels, device->postmix_buffer, SDL_AUDIO_F32, outspec.channels, NULL);
|
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);
|
logdev->postmix(logdev->postmix_userdata, &outspec, device->postmix_buffer, br);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1606,6 +1616,7 @@ static int OpenPhysicalAudioDevice(SDL_AudioDevice *device, const SDL_AudioSpec
|
||||||
device->spec.format = (SDL_AUDIO_BITSIZE(device->default_spec.format) >= SDL_AUDIO_BITSIZE(spec.format)) ? device->default_spec.format : spec.format;
|
device->spec.format = (SDL_AUDIO_BITSIZE(device->default_spec.format) >= SDL_AUDIO_BITSIZE(spec.format)) ? device->default_spec.format : spec.format;
|
||||||
device->spec.freq = SDL_max(device->default_spec.freq, spec.freq);
|
device->spec.freq = SDL_max(device->default_spec.freq, spec.freq);
|
||||||
device->spec.channels = SDL_max(device->default_spec.channels, spec.channels);
|
device->spec.channels = SDL_max(device->default_spec.channels, spec.channels);
|
||||||
|
device->spec.use_channel_map = SDL_FALSE; // all initial channel map requests are denied, since we might have to change channel counts.
|
||||||
device->sample_frames = GetDefaultSampleFramesFromFreq(device->spec.freq);
|
device->sample_frames = GetDefaultSampleFramesFromFreq(device->spec.freq);
|
||||||
SDL_UpdatedAudioDeviceFormat(device); // start this off sane.
|
SDL_UpdatedAudioDeviceFormat(device); // start this off sane.
|
||||||
|
|
||||||
|
@ -2155,7 +2166,7 @@ void SDL_DefaultAudioDeviceChanged(SDL_AudioDevice *new_default_device)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needs_migration) {
|
if (needs_migration) {
|
||||||
const SDL_bool spec_changed = !AUDIO_SPECS_EQUAL(current_default_device->spec, new_default_device->spec);
|
const SDL_bool spec_changed = !SDL_AudioSpecsEqual(¤t_default_device->spec, &new_default_device->spec);
|
||||||
SDL_LogicalAudioDevice *next = NULL;
|
SDL_LogicalAudioDevice *next = NULL;
|
||||||
for (SDL_LogicalAudioDevice *logdev = current_default_device->logical_devices; logdev; logdev = next) {
|
for (SDL_LogicalAudioDevice *logdev = current_default_device->logical_devices; logdev; logdev = next) {
|
||||||
next = logdev->next;
|
next = logdev->next;
|
||||||
|
@ -2235,7 +2246,7 @@ int SDL_AudioDeviceFormatChangedAlreadyLocked(SDL_AudioDevice *device, const SDL
|
||||||
{
|
{
|
||||||
const int orig_work_buffer_size = device->work_buffer_size;
|
const int orig_work_buffer_size = device->work_buffer_size;
|
||||||
|
|
||||||
if (AUDIO_SPECS_EQUAL(device->spec, *newspec) && new_sample_frames == device->sample_frames) {
|
if (SDL_AudioSpecsEqual(&device->spec, newspec) && (new_sample_frames == device->sample_frames)) {
|
||||||
return 0; // we're already in that format.
|
return 0; // we're already in that format.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,39 +29,6 @@
|
||||||
#define SDL_INT_MAX ((int)(~0u>>1))
|
#define SDL_INT_MAX ((int)(~0u>>1))
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/*
|
|
||||||
* CHANNEL LAYOUTS AS SDL EXPECTS THEM:
|
|
||||||
*
|
|
||||||
* (Even if the platform expects something else later, that
|
|
||||||
* SDL will swizzle between the app and the platform).
|
|
||||||
*
|
|
||||||
* Abbreviations:
|
|
||||||
* - FRONT=single mono speaker
|
|
||||||
* - FL=front left speaker
|
|
||||||
* - FR=front right speaker
|
|
||||||
* - FC=front center speaker
|
|
||||||
* - BL=back left speaker
|
|
||||||
* - BR=back right speaker
|
|
||||||
* - SR=surround right speaker
|
|
||||||
* - SL=surround left speaker
|
|
||||||
* - BC=back center speaker
|
|
||||||
* - LFE=low-frequency speaker
|
|
||||||
*
|
|
||||||
* These are listed in the order they are laid out in
|
|
||||||
* memory, so "FL+FR" means "the front left speaker is
|
|
||||||
* laid out in memory first, then the front right, then
|
|
||||||
* it repeats for the next audio frame".
|
|
||||||
*
|
|
||||||
* 1 channel (mono) layout: FRONT
|
|
||||||
* 2 channels (stereo) layout: FL+FR
|
|
||||||
* 3 channels (2.1) layout: FL+FR+LFE
|
|
||||||
* 4 channels (quad) layout: FL+FR+BL+BR
|
|
||||||
* 5 channels (4.1) layout: FL+FR+LFE+BL+BR
|
|
||||||
* 6 channels (5.1) layout: FL+FR+FC+LFE+BL+BR
|
|
||||||
* 7 channels (6.1) layout: FL+FR+FC+LFE+BC+SL+SR
|
|
||||||
* 8 channels (7.1) layout: FL+FR+FC+LFE+BL+BR+SL+SR
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifdef SDL_SSE3_INTRINSICS
|
#ifdef SDL_SSE3_INTRINSICS
|
||||||
// Convert from stereo to mono. Average left and right.
|
// Convert from stereo to mono. Average left and right.
|
||||||
static void SDL_TARGETING("sse3") SDL_ConvertStereoToMono_SSE3(float *dst, const float *src, int num_frames)
|
static void SDL_TARGETING("sse3") SDL_ConvertStereoToMono_SSE3(float *dst, const float *src, int num_frames)
|
||||||
|
@ -157,6 +124,68 @@ static SDL_bool SDL_IsSupportedChannelCount(const int channels)
|
||||||
return ((channels >= 1) && (channels <= 8));
|
return ((channels >= 1) && (channels <= 8));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SDL_bool SDL_ChannelMapIsBogus(const Uint8 *map, int channels)
|
||||||
|
{
|
||||||
|
if (map) {
|
||||||
|
for (int i = 0; i < channels; i++) {
|
||||||
|
if (map[i] >= ((Uint8) channels)) {
|
||||||
|
return SDL_TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return SDL_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_bool SDL_ChannelMapIsDefault(const Uint8 *map, int channels)
|
||||||
|
{
|
||||||
|
if (map) {
|
||||||
|
for (int i = 0; i < channels; i++) {
|
||||||
|
if (map[i] != i) {
|
||||||
|
return SDL_FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return SDL_TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swizzle audio channels. src and dst can be the same pointer. It does not change the buffer size.
|
||||||
|
static void SwizzleAudio(const int num_frames, void *dst, const void *src, int channels, const Uint8 *map, int bitsize)
|
||||||
|
{
|
||||||
|
#define CHANNEL_SWIZZLE(bits) { \
|
||||||
|
Uint##bits *tdst = (Uint##bits *) dst; /* treat as UintX; we only care about moving bits and not the type here. */ \
|
||||||
|
const Uint##bits *tsrc = (const Uint##bits *) src; \
|
||||||
|
if (src != dst) { /* don't need to copy to a temporary frame first. */ \
|
||||||
|
for (int i = 0; i < num_frames; i++, tsrc += channels, tdst += channels) { \
|
||||||
|
for (int ch = 0; ch < channels; ch++) { \
|
||||||
|
tdst[ch] = tsrc[map[ch]]; \
|
||||||
|
} \
|
||||||
|
} \
|
||||||
|
} else { \
|
||||||
|
Uint##bits tmp[SDL_MAX_CHANNEL_MAP_SIZE]; \
|
||||||
|
SDL_assert(SDL_arraysize(tmp) >= channels); \
|
||||||
|
for (int i = 0; i < num_frames; i++, tsrc += channels, tdst += channels) { \
|
||||||
|
for (int ch = 0; ch < channels; ch++) { \
|
||||||
|
tmp[ch] = tsrc[map[ch]]; \
|
||||||
|
} \
|
||||||
|
for (int ch = 0; ch < channels; ch++) { \
|
||||||
|
tdst[ch] = tmp[ch]; \
|
||||||
|
} \
|
||||||
|
} \
|
||||||
|
} \
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (bitsize) {
|
||||||
|
case 8: CHANNEL_SWIZZLE(8); break;
|
||||||
|
case 16: CHANNEL_SWIZZLE(16); break;
|
||||||
|
case 32: CHANNEL_SWIZZLE(32); break;
|
||||||
|
// we don't currently have int64 or double audio datatypes, so no `case 64` for now.
|
||||||
|
default: SDL_assert(!"Unsupported audio datatype size"); break;
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef CHANNEL_SWIZZLE
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// This does type and channel conversions _but not resampling_ (resampling happens in SDL_AudioStream).
|
// This does type and channel conversions _but not resampling_ (resampling happens in SDL_AudioStream).
|
||||||
// This does not check parameter validity, (beyond asserts), it expects you did that already!
|
// This does not check parameter validity, (beyond asserts), it expects you did that already!
|
||||||
// All of this has to function as if src==dst==scratch (conversion in-place), but as a convenience
|
// All of this has to function as if src==dst==scratch (conversion in-place), but as a convenience
|
||||||
|
@ -164,8 +193,10 @@ static SDL_bool SDL_IsSupportedChannelCount(const int channels)
|
||||||
//
|
//
|
||||||
// The scratch buffer must be able to store `num_frames * CalculateMaxSampleFrameSize(src_format, src_channels, dst_format, dst_channels)` bytes.
|
// 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.
|
// If the scratch buffer is NULL, this restriction applies to the output buffer instead.
|
||||||
void ConvertAudio(int num_frames, const void *src, SDL_AudioFormat src_format, int src_channels,
|
void ConvertAudio(int num_frames,
|
||||||
void *dst, SDL_AudioFormat dst_format, int dst_channels, void* scratch)
|
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)
|
||||||
{
|
{
|
||||||
SDL_assert(src != NULL);
|
SDL_assert(src != NULL);
|
||||||
SDL_assert(dst != NULL);
|
SDL_assert(dst != NULL);
|
||||||
|
@ -188,11 +219,13 @@ void ConvertAudio(int num_frames, const void *src, SDL_AudioFormat src_format, i
|
||||||
const int dst_sample_frame_size = (dst_bitsize / 8) * dst_channels;
|
const int dst_sample_frame_size = (dst_bitsize / 8) * dst_channels;
|
||||||
|
|
||||||
/* Type conversion goes like this now:
|
/* Type conversion goes like this now:
|
||||||
|
- swizzle through source channel map to "standard" layout.
|
||||||
- byteswap to CPU native format first if necessary.
|
- byteswap to CPU native format first if necessary.
|
||||||
- convert to native Float32 if necessary.
|
- convert to native Float32 if necessary.
|
||||||
- change channel count if necessary.
|
- change channel count if necessary.
|
||||||
- convert to final data format.
|
- convert to final data format.
|
||||||
- byteswap back to foreign format if necessary.
|
- byteswap back to foreign format if necessary.
|
||||||
|
- swizzle through dest channel map from "standard" layout.
|
||||||
|
|
||||||
The expectation is we can process data faster in float32
|
The expectation is we can process data faster in float32
|
||||||
(possibly with SIMD), and making several passes over the same
|
(possibly with SIMD), and making several passes over the same
|
||||||
|
@ -201,11 +234,20 @@ void ConvertAudio(int num_frames, const void *src, SDL_AudioFormat src_format, i
|
||||||
(script-generated) custom converters for every data type and
|
(script-generated) custom converters for every data type and
|
||||||
it was a bloat on SDL compile times and final library size. */
|
it was a bloat on SDL compile times and final library size. */
|
||||||
|
|
||||||
|
// swizzle input to "standard" format if necessary.
|
||||||
|
if (src_map) {
|
||||||
|
void* buf = scratch ? scratch : dst; // use scratch if available, since it has to be big enough to hold src, unless it's NULL, then dst has to be.
|
||||||
|
SwizzleAudio(num_frames, buf, src, src_channels, src_map, src_bitsize);
|
||||||
|
src = buf;
|
||||||
|
}
|
||||||
|
|
||||||
// see if we can skip float conversion entirely.
|
// see if we can skip float conversion entirely.
|
||||||
if (src_channels == dst_channels) {
|
if (src_channels == dst_channels) {
|
||||||
if (src_format == dst_format) {
|
if (src_format == dst_format) {
|
||||||
// nothing to do, we're already in the right format, just copy it over if necessary.
|
// nothing to do, we're already in the right format, just copy it over if necessary.
|
||||||
if (src != dst) {
|
if (dst_map) {
|
||||||
|
SwizzleAudio(num_frames, dst, src, dst_channels, dst_map, dst_bitsize);
|
||||||
|
} else if (src != dst) {
|
||||||
SDL_memcpy(dst, src, num_frames * dst_sample_frame_size);
|
SDL_memcpy(dst, src, num_frames * dst_sample_frame_size);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
@ -213,7 +255,11 @@ void ConvertAudio(int num_frames, const void *src, SDL_AudioFormat src_format, i
|
||||||
|
|
||||||
// just a byteswap needed?
|
// just a byteswap needed?
|
||||||
if ((src_format ^ dst_format) == SDL_AUDIO_MASK_BIG_ENDIAN) {
|
if ((src_format ^ dst_format) == SDL_AUDIO_MASK_BIG_ENDIAN) {
|
||||||
ConvertAudioSwapEndian(dst, src, num_frames * src_channels, src_bitsize);
|
if (dst_map) { // do this first, in case we duplicate channels, we can avoid an extra copy if src != dst.
|
||||||
|
SwizzleAudio(num_frames, dst, src, dst_channels, dst_map, dst_bitsize);
|
||||||
|
src = dst;
|
||||||
|
}
|
||||||
|
ConvertAudioSwapEndian(dst, src, num_frames * dst_channels, dst_bitsize);
|
||||||
return; // all done.
|
return; // all done.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -275,6 +321,10 @@ void ConvertAudio(int num_frames, const void *src, SDL_AudioFormat src_format, i
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_assert(src == dst); // if we got here, we _had_ to have done _something_. Otherwise, we should have memcpy'd!
|
SDL_assert(src == dst); // if we got here, we _had_ to have done _something_. Otherwise, we should have memcpy'd!
|
||||||
|
|
||||||
|
if (dst_map) {
|
||||||
|
SwizzleAudio(num_frames, dst, src, dst_channels, dst_map, dst_bitsize);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate the largest frame size needed to convert between the two formats.
|
// Calculate the largest frame size needed to convert between the two formats.
|
||||||
|
@ -304,7 +354,7 @@ static Sint64 GetAudioStreamResampleRate(SDL_AudioStream* stream, int src_freq,
|
||||||
|
|
||||||
static int UpdateAudioStreamInputSpec(SDL_AudioStream *stream, const SDL_AudioSpec *spec)
|
static int UpdateAudioStreamInputSpec(SDL_AudioStream *stream, const SDL_AudioSpec *spec)
|
||||||
{
|
{
|
||||||
if (AUDIO_SPECS_EQUAL(stream->input_spec, *spec)) {
|
if (SDL_AudioSpecsEqual(&stream->input_spec, spec)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -451,6 +501,8 @@ int SDL_SetAudioStreamFormat(SDL_AudioStream *stream, const SDL_AudioSpec *src_s
|
||||||
return SDL_SetError("Source rate is too low");
|
return SDL_SetError("Source rate is too low");
|
||||||
} else if (src_spec->freq > max_freq) {
|
} else if (src_spec->freq > max_freq) {
|
||||||
return SDL_SetError("Source rate is too high");
|
return SDL_SetError("Source rate is too high");
|
||||||
|
} else if (src_spec->use_channel_map && SDL_ChannelMapIsBogus(src_spec->channel_map, src_spec->channels)) {
|
||||||
|
return SDL_SetError("Source channel map is invalid");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -465,6 +517,8 @@ int SDL_SetAudioStreamFormat(SDL_AudioStream *stream, const SDL_AudioSpec *src_s
|
||||||
return SDL_SetError("Destination rate is too low");
|
return SDL_SetError("Destination rate is too low");
|
||||||
} else if (dst_spec->freq > max_freq) {
|
} else if (dst_spec->freq > max_freq) {
|
||||||
return SDL_SetError("Destination rate is too high");
|
return SDL_SetError("Destination rate is too high");
|
||||||
|
} else if (dst_spec->use_channel_map && SDL_ChannelMapIsBogus(dst_spec->channel_map, dst_spec->channels)) {
|
||||||
|
return SDL_SetError("Destination channel map is invalid");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -481,11 +535,20 @@ int SDL_SetAudioStreamFormat(SDL_AudioStream *stream, const SDL_AudioSpec *src_s
|
||||||
|
|
||||||
if (src_spec) {
|
if (src_spec) {
|
||||||
SDL_copyp(&stream->src_spec, src_spec);
|
SDL_copyp(&stream->src_spec, src_spec);
|
||||||
|
if (src_spec->use_channel_map && SDL_ChannelMapIsDefault(src_spec->channel_map, src_spec->channels)) {
|
||||||
|
stream->src_spec.use_channel_map = SDL_FALSE; // turn off the channel map, as this is just unnecessary work.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dst_spec) {
|
if (dst_spec) {
|
||||||
SDL_copyp(&stream->dst_spec, dst_spec);
|
SDL_copyp(&stream->dst_spec, dst_spec);
|
||||||
|
if (dst_spec->use_channel_map && !stream->src_spec.use_channel_map && SDL_ChannelMapIsDefault(dst_spec->channel_map, dst_spec->channels)) {
|
||||||
|
stream->dst_spec.use_channel_map = SDL_FALSE; // turn off the channel map, as this is just unnecessary work.
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// !!! FIXME: decide if the source and dest channel maps would swizzle us back to the starting order and just turn them both off.
|
||||||
|
// !!! FIXME: (but in this case, you can only do it if the channel count isn't changing, because source order is important to that.)
|
||||||
|
|
||||||
SDL_UnlockMutex(stream->lock);
|
SDL_UnlockMutex(stream->lock);
|
||||||
|
|
||||||
|
@ -766,6 +829,7 @@ static int GetAudioStreamDataInternal(SDL_AudioStream *stream, void *buf, int ou
|
||||||
|
|
||||||
const SDL_AudioFormat dst_format = dst_spec->format;
|
const SDL_AudioFormat dst_format = dst_spec->format;
|
||||||
const int dst_channels = dst_spec->channels;
|
const int dst_channels = dst_spec->channels;
|
||||||
|
const Uint8 *dst_map = dst_spec->use_channel_map ? dst_spec->channel_map : NULL;
|
||||||
|
|
||||||
const int max_frame_size = CalculateMaxFrameSize(src_format, src_channels, dst_format, dst_channels);
|
const int max_frame_size = CalculateMaxFrameSize(src_format, src_channels, dst_format, dst_channels);
|
||||||
const Sint64 resample_rate = GetAudioStreamResampleRate(stream, src_spec->freq, stream->resample_offset);
|
const Sint64 resample_rate = GetAudioStreamResampleRate(stream, src_spec->freq, stream->resample_offset);
|
||||||
|
@ -789,7 +853,7 @@ static int GetAudioStreamDataInternal(SDL_AudioStream *stream, void *buf, int ou
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SDL_ReadFromAudioQueue(stream->queue, buf, dst_format, dst_channels, 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) != buf) {
|
||||||
return SDL_SetError("Not enough data in queue");
|
return SDL_SetError("Not enough data in queue");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -854,8 +918,9 @@ static int GetAudioStreamDataInternal(SDL_AudioStream *stream, void *buf, int ou
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// (dst channel map is NULL because we'll do the final swizzle on ConvertAudio after resample.)
|
||||||
const Uint8* input_buffer = SDL_ReadFromAudioQueue(stream->queue,
|
const Uint8* input_buffer = SDL_ReadFromAudioQueue(stream->queue,
|
||||||
NULL, resample_format, resample_channels,
|
NULL, resample_format, resample_channels, NULL,
|
||||||
padding_frames, input_frames, padding_frames, work_buffer);
|
padding_frames, input_frames, padding_frames, work_buffer);
|
||||||
|
|
||||||
if (!input_buffer) {
|
if (!input_buffer) {
|
||||||
|
@ -872,8 +937,8 @@ static int GetAudioStreamDataInternal(SDL_AudioStream *stream, void *buf, int ou
|
||||||
(float*) resample_buffer, output_frames,
|
(float*) resample_buffer, output_frames,
|
||||||
resample_rate, &stream->resample_offset);
|
resample_rate, &stream->resample_offset);
|
||||||
|
|
||||||
// Convert to the final format, if necessary
|
// 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, buf, dst_format, dst_channels, work_buffer);
|
ConvertAudio(output_frames, resample_buffer, resample_format, resample_channels, NULL, buf, dst_format, dst_channels, dst_map, work_buffer);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,8 +23,6 @@
|
||||||
#include "SDL_audioqueue.h"
|
#include "SDL_audioqueue.h"
|
||||||
#include "SDL_sysaudio.h"
|
#include "SDL_sysaudio.h"
|
||||||
|
|
||||||
#define AUDIO_SPECS_EQUAL(x, y) (((x).format == (y).format) && ((x).channels == (y).channels) && ((x).freq == (y).freq))
|
|
||||||
|
|
||||||
typedef struct SDL_MemoryPool SDL_MemoryPool;
|
typedef struct SDL_MemoryPool SDL_MemoryPool;
|
||||||
|
|
||||||
struct SDL_MemoryPool
|
struct SDL_MemoryPool
|
||||||
|
@ -285,7 +283,7 @@ void SDL_AddTrackToAudioQueue(SDL_AudioQueue *queue, SDL_AudioTrack *track)
|
||||||
|
|
||||||
if (tail) {
|
if (tail) {
|
||||||
// If the spec has changed, make sure to flush the previous track
|
// If the spec has changed, make sure to flush the previous track
|
||||||
if (!AUDIO_SPECS_EQUAL(tail->spec, track->spec)) {
|
if (!SDL_AudioSpecsEqual(&tail->spec, &track->spec)) {
|
||||||
FlushAudioTrack(tail);
|
FlushAudioTrack(tail);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -319,7 +317,7 @@ int SDL_WriteToAudioQueue(SDL_AudioQueue *queue, const SDL_AudioSpec *spec, cons
|
||||||
SDL_AudioTrack *track = queue->tail;
|
SDL_AudioTrack *track = queue->tail;
|
||||||
|
|
||||||
if (track) {
|
if (track) {
|
||||||
if (!AUDIO_SPECS_EQUAL(track->spec, *spec)) {
|
if (!SDL_AudioSpecsEqual(&track->spec, spec)) {
|
||||||
FlushAudioTrack(track);
|
FlushAudioTrack(track);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -514,7 +512,7 @@ static const Uint8 *PeekIntoAudioQueueFuture(SDL_AudioQueue *queue, Uint8 *data,
|
||||||
}
|
}
|
||||||
|
|
||||||
const Uint8 *SDL_ReadFromAudioQueue(SDL_AudioQueue *queue,
|
const Uint8 *SDL_ReadFromAudioQueue(SDL_AudioQueue *queue,
|
||||||
Uint8 *dst, SDL_AudioFormat dst_format, int dst_channels,
|
Uint8 *dst, SDL_AudioFormat dst_format, int dst_channels, const Uint8 *dst_map,
|
||||||
int past_frames, int present_frames, int future_frames,
|
int past_frames, int present_frames, int future_frames,
|
||||||
Uint8 *scratch)
|
Uint8 *scratch)
|
||||||
{
|
{
|
||||||
|
@ -526,6 +524,7 @@ const Uint8 *SDL_ReadFromAudioQueue(SDL_AudioQueue *queue,
|
||||||
|
|
||||||
SDL_AudioFormat src_format = track->spec.format;
|
SDL_AudioFormat src_format = track->spec.format;
|
||||||
int src_channels = track->spec.channels;
|
int src_channels = track->spec.channels;
|
||||||
|
const Uint8 *src_map = track->spec.use_channel_map ? track->spec.channel_map : NULL;
|
||||||
|
|
||||||
size_t src_frame_size = SDL_AUDIO_BYTESIZE(src_format) * src_channels;
|
size_t src_frame_size = SDL_AUDIO_BYTESIZE(src_format) * src_channels;
|
||||||
size_t dst_frame_size = SDL_AUDIO_BYTESIZE(dst_format) * dst_channels;
|
size_t dst_frame_size = SDL_AUDIO_BYTESIZE(dst_format) * dst_channels;
|
||||||
|
@ -553,7 +552,7 @@ const Uint8 *SDL_ReadFromAudioQueue(SDL_AudioQueue *queue,
|
||||||
// Do we still need to copy/convert the data?
|
// Do we still need to copy/convert the data?
|
||||||
if (dst) {
|
if (dst) {
|
||||||
ConvertAudio(past_frames + present_frames + future_frames, ptr,
|
ConvertAudio(past_frames + present_frames + future_frames, ptr,
|
||||||
src_format, src_channels, dst, dst_format, dst_channels, scratch);
|
src_format, src_channels, src_map, dst, dst_format, dst_channels, dst_map, scratch);
|
||||||
ptr = dst;
|
ptr = dst;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -571,19 +570,19 @@ const Uint8 *SDL_ReadFromAudioQueue(SDL_AudioQueue *queue,
|
||||||
Uint8 *ptr = dst;
|
Uint8 *ptr = dst;
|
||||||
|
|
||||||
if (src_past_bytes) {
|
if (src_past_bytes) {
|
||||||
ConvertAudio(past_frames, PeekIntoAudioQueuePast(queue, scratch, src_past_bytes), src_format, src_channels, dst, dst_format, dst_channels, scratch);
|
ConvertAudio(past_frames, PeekIntoAudioQueuePast(queue, scratch, src_past_bytes), src_format, src_channels, src_map, dst, dst_format, dst_channels, dst_map, scratch);
|
||||||
dst += dst_past_bytes;
|
dst += dst_past_bytes;
|
||||||
scratch += dst_past_bytes;
|
scratch += dst_past_bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (src_present_bytes) {
|
if (src_present_bytes) {
|
||||||
ConvertAudio(present_frames, ReadFromAudioQueue(queue, scratch, src_present_bytes), src_format, src_channels, dst, dst_format, dst_channels, scratch);
|
ConvertAudio(present_frames, ReadFromAudioQueue(queue, scratch, src_present_bytes), src_format, src_channels, src_map, dst, dst_format, dst_channels, dst_map, scratch);
|
||||||
dst += dst_present_bytes;
|
dst += dst_present_bytes;
|
||||||
scratch += dst_present_bytes;
|
scratch += dst_present_bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (src_future_bytes) {
|
if (src_future_bytes) {
|
||||||
ConvertAudio(future_frames, PeekIntoAudioQueueFuture(queue, scratch, src_future_bytes), src_format, src_channels, dst, dst_format, dst_channels, scratch);
|
ConvertAudio(future_frames, PeekIntoAudioQueueFuture(queue, scratch, src_future_bytes), src_format, src_channels, src_map, dst, dst_format, dst_channels, dst_map, scratch);
|
||||||
dst += dst_future_bytes;
|
dst += dst_future_bytes;
|
||||||
scratch += dst_future_bytes;
|
scratch += dst_future_bytes;
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,7 +67,7 @@ void *SDL_BeginAudioQueueIter(SDL_AudioQueue *queue);
|
||||||
size_t SDL_NextAudioQueueIter(SDL_AudioQueue *queue, void **inout_iter, SDL_AudioSpec *out_spec, SDL_bool *out_flushed);
|
size_t SDL_NextAudioQueueIter(SDL_AudioQueue *queue, void **inout_iter, SDL_AudioSpec *out_spec, SDL_bool *out_flushed);
|
||||||
|
|
||||||
const Uint8 *SDL_ReadFromAudioQueue(SDL_AudioQueue *queue,
|
const Uint8 *SDL_ReadFromAudioQueue(SDL_AudioQueue *queue,
|
||||||
Uint8 *dst, SDL_AudioFormat dst_format, int dst_channels,
|
Uint8 *dst, SDL_AudioFormat dst_format, int dst_channels, const Uint8 *dst_map,
|
||||||
int past_frames, int present_frames, int future_frames,
|
int past_frames, int present_frames, int future_frames,
|
||||||
Uint8 *scratch);
|
Uint8 *scratch);
|
||||||
|
|
||||||
|
|
|
@ -48,8 +48,6 @@
|
||||||
#define DEFAULT_AUDIO_RECORDING_CHANNELS 1
|
#define DEFAULT_AUDIO_RECORDING_CHANNELS 1
|
||||||
#define DEFAULT_AUDIO_RECORDING_FREQUENCY 44100
|
#define DEFAULT_AUDIO_RECORDING_FREQUENCY 44100
|
||||||
|
|
||||||
#define AUDIO_SPECS_EQUAL(x, y) (((x).format == (y).format) && ((x).channels == (y).channels) && ((x).freq == (y).freq))
|
|
||||||
|
|
||||||
typedef struct SDL_AudioDevice SDL_AudioDevice;
|
typedef struct SDL_AudioDevice SDL_AudioDevice;
|
||||||
typedef struct SDL_LogicalAudioDevice SDL_LogicalAudioDevice;
|
typedef struct SDL_LogicalAudioDevice SDL_LogicalAudioDevice;
|
||||||
|
|
||||||
|
@ -113,9 +111,19 @@ extern void ConvertAudioToFloat(float *dst, const void *src, int num_samples, SD
|
||||||
extern void ConvertAudioFromFloat(void *dst, const float *src, int num_samples, SDL_AudioFormat dst_fmt);
|
extern void ConvertAudioFromFloat(void *dst, const float *src, int num_samples, SDL_AudioFormat dst_fmt);
|
||||||
extern void ConvertAudioSwapEndian(void* dst, const void* src, int num_samples, int bitsize);
|
extern void ConvertAudioSwapEndian(void* dst, const void* src, int num_samples, int bitsize);
|
||||||
|
|
||||||
|
extern SDL_bool SDL_ChannelMapIsDefault(const Uint8 *map, int channels);
|
||||||
|
extern SDL_bool SDL_ChannelMapIsBogus(const Uint8 *map, int channels);
|
||||||
|
|
||||||
// this gets used from the audio device threads. It has rules, don't use this if you don't know how to use it!
|
// this gets used from the audio device threads. It has rules, don't use this if you don't know how to use it!
|
||||||
extern void ConvertAudio(int num_frames, const void *src, SDL_AudioFormat src_format, int src_channels,
|
extern void ConvertAudio(int num_frames,
|
||||||
void *dst, SDL_AudioFormat dst_format, int dst_channels, void* scratch);
|
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);
|
||||||
|
|
||||||
|
// 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.
|
// 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 OnAudioStreamCreated(SDL_AudioStream *stream);
|
||||||
|
|
|
@ -242,108 +242,22 @@ static const char *get_audio_device(void *handle, const int channels)
|
||||||
return dev->name;
|
return dev->name;
|
||||||
}
|
}
|
||||||
|
|
||||||
// !!! FIXME: is there a channel swizzler in alsalib instead?
|
// Swizzle channels to match SDL defaults.
|
||||||
|
// These are swizzles _from_ SDL's layouts to what ALSA wants.
|
||||||
|
|
||||||
|
// 5.1 swizzle:
|
||||||
// https://bugzilla.libsdl.org/show_bug.cgi?id=110
|
// https://bugzilla.libsdl.org/show_bug.cgi?id=110
|
||||||
// "For Linux ALSA, this is FL-FR-RL-RR-C-LFE
|
// "For Linux ALSA, this is FL-FR-RL-RR-C-LFE
|
||||||
// and for Windows DirectX [and CoreAudio], this is FL-FR-C-LFE-RL-RR"
|
// and for Windows DirectX [and CoreAudio], this is FL-FR-C-LFE-RL-RR"
|
||||||
#define SWIZ6(T) \
|
static const Uint8 swizzle_alsa_channels_6[6] = { 0, 1, 4, 5, 2, 3 };
|
||||||
static void swizzle_alsa_channels_6_##T(void *buffer, const Uint32 bufferlen) \
|
|
||||||
{ \
|
|
||||||
T *ptr = (T *)buffer; \
|
|
||||||
Uint32 i; \
|
|
||||||
for (i = 0; i < bufferlen; i++, ptr += 6) { \
|
|
||||||
T tmp; \
|
|
||||||
tmp = ptr[2]; \
|
|
||||||
ptr[2] = ptr[4]; \
|
|
||||||
ptr[4] = tmp; \
|
|
||||||
tmp = ptr[3]; \
|
|
||||||
ptr[3] = ptr[5]; \
|
|
||||||
ptr[5] = tmp; \
|
|
||||||
} \
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// !!! FIXME: is there a channel swizzler in alsalib instead?
|
|
||||||
// !!! FIXME: this screams for a SIMD shuffle operation.
|
|
||||||
|
|
||||||
|
// 7.1 swizzle:
|
||||||
// https://docs.microsoft.com/en-us/windows-hardware/drivers/audio/mapping-stream-formats-to-speaker-configurations
|
// https://docs.microsoft.com/en-us/windows-hardware/drivers/audio/mapping-stream-formats-to-speaker-configurations
|
||||||
// For Linux ALSA, this appears to be FL-FR-RL-RR-C-LFE-SL-SR
|
// For Linux ALSA, this appears to be FL-FR-RL-RR-C-LFE-SL-SR
|
||||||
// and for Windows DirectX [and CoreAudio], this is FL-FR-C-LFE-SL-SR-RL-RR"
|
// and for Windows DirectX [and CoreAudio], this is FL-FR-C-LFE-SL-SR-RL-RR"
|
||||||
#define SWIZ8(T) \
|
static const Uint8 swizzle_alsa_channels_8[8] = { 0, 1, 6, 7, 2, 3, 4, 5 };
|
||||||
static void swizzle_alsa_channels_8_##T(void *buffer, const Uint32 bufferlen) \
|
|
||||||
{ \
|
|
||||||
T *ptr = (T *)buffer; \
|
|
||||||
Uint32 i; \
|
|
||||||
for (i = 0; i < bufferlen; i++, ptr += 6) { \
|
|
||||||
const T center = ptr[2]; \
|
|
||||||
const T subwoofer = ptr[3]; \
|
|
||||||
const T side_left = ptr[4]; \
|
|
||||||
const T side_right = ptr[5]; \
|
|
||||||
const T rear_left = ptr[6]; \
|
|
||||||
const T rear_right = ptr[7]; \
|
|
||||||
ptr[2] = rear_left; \
|
|
||||||
ptr[3] = rear_right; \
|
|
||||||
ptr[4] = center; \
|
|
||||||
ptr[5] = subwoofer; \
|
|
||||||
ptr[6] = side_left; \
|
|
||||||
ptr[7] = side_right; \
|
|
||||||
} \
|
|
||||||
}
|
|
||||||
|
|
||||||
#define CHANNEL_SWIZZLE(x) \
|
|
||||||
x(Uint64) \
|
|
||||||
x(Uint32) \
|
|
||||||
x(Uint16) \
|
|
||||||
x(Uint8)
|
|
||||||
|
|
||||||
CHANNEL_SWIZZLE(SWIZ6)
|
|
||||||
CHANNEL_SWIZZLE(SWIZ8)
|
|
||||||
|
|
||||||
#undef CHANNEL_SWIZZLE
|
|
||||||
#undef SWIZ6
|
|
||||||
#undef SWIZ8
|
|
||||||
|
|
||||||
// Called right before feeding device->hidden->mixbuf to the hardware. Swizzle
|
|
||||||
// channels from Windows/Mac order to the format alsalib will want.
|
|
||||||
static void swizzle_alsa_channels(SDL_AudioDevice *device, void *buffer, Uint32 bufferlen)
|
|
||||||
{
|
|
||||||
switch (device->spec.channels) {
|
|
||||||
#define CHANSWIZ(chans) \
|
|
||||||
case chans: \
|
|
||||||
switch ((device->spec.format & (0xFF))) { \
|
|
||||||
case 8: \
|
|
||||||
swizzle_alsa_channels_##chans##_Uint8(buffer, bufferlen); \
|
|
||||||
break; \
|
|
||||||
case 16: \
|
|
||||||
swizzle_alsa_channels_##chans##_Uint16(buffer, bufferlen); \
|
|
||||||
break; \
|
|
||||||
case 32: \
|
|
||||||
swizzle_alsa_channels_##chans##_Uint32(buffer, bufferlen); \
|
|
||||||
break; \
|
|
||||||
case 64: \
|
|
||||||
swizzle_alsa_channels_##chans##_Uint64(buffer, bufferlen); \
|
|
||||||
break; \
|
|
||||||
default: \
|
|
||||||
SDL_assert(!"unhandled bitsize"); \
|
|
||||||
break; \
|
|
||||||
} \
|
|
||||||
return;
|
|
||||||
|
|
||||||
CHANSWIZ(6);
|
|
||||||
CHANSWIZ(8);
|
|
||||||
#undef CHANSWIZ
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef SND_CHMAP_API_VERSION
|
|
||||||
// Some devices have the right channel map, no swizzling necessary
|
|
||||||
static void no_swizzle(SDL_AudioDevice *device, void *buffer, Uint32 bufferlen)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
#endif // SND_CHMAP_API_VERSION
|
|
||||||
|
|
||||||
// This function waits until it is possible to write a full sound buffer
|
// This function waits until it is possible to write a full sound buffer
|
||||||
static int ALSA_WaitDevice(SDL_AudioDevice *device)
|
static int ALSA_WaitDevice(SDL_AudioDevice *device)
|
||||||
|
@ -380,8 +294,6 @@ static int ALSA_PlayDevice(SDL_AudioDevice *device, const Uint8 *buffer, int buf
|
||||||
const int frame_size = SDL_AUDIO_FRAMESIZE(device->spec);
|
const int frame_size = SDL_AUDIO_FRAMESIZE(device->spec);
|
||||||
snd_pcm_uframes_t frames_left = (snd_pcm_uframes_t) (buflen / frame_size);
|
snd_pcm_uframes_t frames_left = (snd_pcm_uframes_t) (buflen / frame_size);
|
||||||
|
|
||||||
device->hidden->swizzle_func(device, sample_buf, frames_left);
|
|
||||||
|
|
||||||
while ((frames_left > 0) && !SDL_AtomicGet(&device->shutdown)) {
|
while ((frames_left > 0) && !SDL_AtomicGet(&device->shutdown)) {
|
||||||
const int rc = ALSA_snd_pcm_writei(device->hidden->pcm_handle, sample_buf, frames_left);
|
const int rc = ALSA_snd_pcm_writei(device->hidden->pcm_handle, sample_buf, frames_left);
|
||||||
//SDL_LogInfo(SDL_LOG_CATEGORY_AUDIO, "ALSA PLAYDEVICE: WROTE %d of %d bytes", (rc >= 0) ? ((int) (rc * frame_size)) : rc, (int) (frames_left * frame_size));
|
//SDL_LogInfo(SDL_LOG_CATEGORY_AUDIO, "ALSA PLAYDEVICE: WROTE %d of %d bytes", (rc >= 0) ? ((int) (rc * frame_size)) : rc, (int) (frames_left * frame_size));
|
||||||
|
@ -447,8 +359,6 @@ static int ALSA_RecordDevice(SDL_AudioDevice *device, void *buffer, int buflen)
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
return 0; // go back to WaitDevice and try again.
|
return 0; // go back to WaitDevice and try again.
|
||||||
} else if (rc > 0) {
|
|
||||||
device->hidden->swizzle_func(device, buffer, total_frames - rc);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//SDL_LogInfo(SDL_LOG_CATEGORY_AUDIO, "ALSA: recorded %d bytes", rc * frame_size);
|
//SDL_LogInfo(SDL_LOG_CATEGORY_AUDIO, "ALSA: recorded %d bytes", rc * frame_size);
|
||||||
|
@ -611,23 +521,6 @@ static int ALSA_OpenDevice(SDL_AudioDevice *device)
|
||||||
}
|
}
|
||||||
device->spec.format = test_format;
|
device->spec.format = test_format;
|
||||||
|
|
||||||
// Validate number of channels and determine if swizzling is necessary.
|
|
||||||
// Assume original swizzling, until proven otherwise.
|
|
||||||
device->hidden->swizzle_func = swizzle_alsa_channels;
|
|
||||||
#ifdef SND_CHMAP_API_VERSION
|
|
||||||
snd_pcm_chmap_t *chmap = ALSA_snd_pcm_get_chmap(pcm_handle);
|
|
||||||
if (chmap) {
|
|
||||||
char chmap_str[64];
|
|
||||||
if (ALSA_snd_pcm_chmap_print(chmap, sizeof(chmap_str), chmap_str) > 0) {
|
|
||||||
if (SDL_strcmp("FL FR FC LFE RL RR", chmap_str) == 0 ||
|
|
||||||
SDL_strcmp("FL FR FC LFE SL SR", chmap_str) == 0) {
|
|
||||||
device->hidden->swizzle_func = no_swizzle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
free(chmap); // This should NOT be SDL_free()
|
|
||||||
}
|
|
||||||
#endif // SND_CHMAP_API_VERSION
|
|
||||||
|
|
||||||
// Set the number of channels
|
// Set the number of channels
|
||||||
status = ALSA_snd_pcm_hw_params_set_channels(pcm_handle, hwparams,
|
status = ALSA_snd_pcm_hw_params_set_channels(pcm_handle, hwparams,
|
||||||
device->spec.channels);
|
device->spec.channels);
|
||||||
|
@ -640,6 +533,33 @@ static int ALSA_OpenDevice(SDL_AudioDevice *device)
|
||||||
device->spec.channels = channels;
|
device->spec.channels = channels;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate number of channels and determine if swizzling is necessary.
|
||||||
|
// Assume original swizzling, until proven otherwise.
|
||||||
|
if (channels == 6) {
|
||||||
|
device->spec.use_channel_map = SDL_TRUE;
|
||||||
|
SDL_memcpy(device->spec.channel_map, swizzle_alsa_channels_6, sizeof (device->spec.channel_map[0]) * channels);
|
||||||
|
} else if (channels == 8) {
|
||||||
|
device->spec.use_channel_map = SDL_TRUE;
|
||||||
|
SDL_memcpy(device->spec.channel_map, swizzle_alsa_channels_8, sizeof (device->spec.channel_map[0]) * channels);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef SND_CHMAP_API_VERSION
|
||||||
|
snd_pcm_chmap_t *chmap = ALSA_snd_pcm_get_chmap(pcm_handle);
|
||||||
|
if (chmap) {
|
||||||
|
char chmap_str[64];
|
||||||
|
if (ALSA_snd_pcm_chmap_print(chmap, sizeof(chmap_str), chmap_str) > 0) {
|
||||||
|
if ( (channels == 6) &&
|
||||||
|
((SDL_strcmp("FL FR FC LFE RL RR", chmap_str) == 0) ||
|
||||||
|
(SDL_strcmp("FL FR FC LFE SL SR", chmap_str) == 0)) ) {
|
||||||
|
device->spec.use_channel_map = SDL_FALSE;
|
||||||
|
} else if ((channels == 8) && (SDL_strcmp("FL FR FC LFE SL SR RL RR", chmap_str) == 0)) {
|
||||||
|
device->spec.use_channel_map = SDL_FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(chmap); // This should NOT be SDL_free()
|
||||||
|
}
|
||||||
|
#endif // SND_CHMAP_API_VERSION
|
||||||
|
|
||||||
// Set the audio rate
|
// Set the audio rate
|
||||||
unsigned int rate = device->spec.freq;
|
unsigned int rate = device->spec.freq;
|
||||||
status = ALSA_snd_pcm_hw_params_set_rate_near(pcm_handle, hwparams,
|
status = ALSA_snd_pcm_hw_params_set_rate_near(pcm_handle, hwparams,
|
||||||
|
|
|
@ -789,6 +789,7 @@ static SDL_AudioFormat PulseFormatToSDLFormat(pa_sample_format_t format)
|
||||||
static void AddPulseAudioDevice(const SDL_bool recording, const char *description, const char *name, const uint32_t index, const pa_sample_spec *sample_spec)
|
static void AddPulseAudioDevice(const SDL_bool recording, const char *description, const char *name, const uint32_t index, const pa_sample_spec *sample_spec)
|
||||||
{
|
{
|
||||||
SDL_AudioSpec spec;
|
SDL_AudioSpec spec;
|
||||||
|
SDL_zero(spec);
|
||||||
spec.format = PulseFormatToSDLFormat(sample_spec->format);
|
spec.format = PulseFormatToSDLFormat(sample_spec->format);
|
||||||
spec.channels = sample_spec->channels;
|
spec.channels = sample_spec->channels;
|
||||||
spec.freq = sample_spec->rate;
|
spec.freq = sample_spec->rate;
|
||||||
|
|
|
@ -363,6 +363,7 @@ static void QSA_DetectDevices(SDL_AudioDevice **default_playback, SDL_AudioDevic
|
||||||
|
|
||||||
if (status == EOK) {
|
if (status == EOK) {
|
||||||
SDL_AudioSpec spec;
|
SDL_AudioSpec spec;
|
||||||
|
SDL_zero(spec);
|
||||||
SDL_AudioSpec *pspec = &spec;
|
SDL_AudioSpec *pspec = &spec;
|
||||||
snd_pcm_channel_setup_t csetup;
|
snd_pcm_channel_setup_t csetup;
|
||||||
SDL_zero(csetup);
|
SDL_zero(csetup);
|
||||||
|
|
|
@ -109,6 +109,7 @@ static void queue_audio()
|
||||||
int retval = 0;
|
int retval = 0;
|
||||||
SDL_AudioSpec new_spec;
|
SDL_AudioSpec new_spec;
|
||||||
|
|
||||||
|
SDL_zero(new_spec);
|
||||||
new_spec.format = spec.format;
|
new_spec.format = spec.format;
|
||||||
new_spec.channels = (int) sliders[2].value;
|
new_spec.channels = (int) sliders[2].value;
|
||||||
new_spec.freq = (int) sliders[1].value;
|
new_spec.freq = (int) sliders[1].value;
|
||||||
|
|
|
@ -181,7 +181,7 @@ static int audio_initOpenCloseQuitAudio(void *arg)
|
||||||
SDLTest_AssertCheck(result == 0, "Validate result value; expected: 0 got: %d", result);
|
SDLTest_AssertCheck(result == 0, "Validate result value; expected: 0 got: %d", result);
|
||||||
|
|
||||||
/* Set spec */
|
/* Set spec */
|
||||||
SDL_memset(&desired, 0, sizeof(desired));
|
SDL_zero(desired);
|
||||||
switch (j) {
|
switch (j) {
|
||||||
case 0:
|
case 0:
|
||||||
/* Set standard desired spec */
|
/* Set standard desired spec */
|
||||||
|
@ -272,7 +272,7 @@ static int audio_pauseUnpauseAudio(void *arg)
|
||||||
SDLTest_AssertCheck(result == 0, "Validate result value; expected: 0 got: %d", result);
|
SDLTest_AssertCheck(result == 0, "Validate result value; expected: 0 got: %d", result);
|
||||||
|
|
||||||
/* Set spec */
|
/* Set spec */
|
||||||
SDL_memset(&desired, 0, sizeof(desired));
|
SDL_zero(desired);
|
||||||
switch (j) {
|
switch (j) {
|
||||||
case 0:
|
case 0:
|
||||||
/* Set standard desired spec */
|
/* Set standard desired spec */
|
||||||
|
@ -496,6 +496,9 @@ static int audio_buildAudioStream(void *arg)
|
||||||
SDL_AudioSpec spec2;
|
SDL_AudioSpec spec2;
|
||||||
int i, ii, j, jj, k, kk;
|
int i, ii, j, jj, k, kk;
|
||||||
|
|
||||||
|
SDL_zero(spec1);
|
||||||
|
SDL_zero(spec2);
|
||||||
|
|
||||||
/* Call Quit */
|
/* Call Quit */
|
||||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||||
SDLTest_AssertPass("Call to SDL_QuitSubSystem(SDL_INIT_AUDIO)");
|
SDLTest_AssertPass("Call to SDL_QuitSubSystem(SDL_INIT_AUDIO)");
|
||||||
|
@ -567,6 +570,9 @@ static int audio_buildAudioStreamNegative(void *arg)
|
||||||
int i;
|
int i;
|
||||||
char message[256];
|
char message[256];
|
||||||
|
|
||||||
|
SDL_zero(spec1);
|
||||||
|
SDL_zero(spec2);
|
||||||
|
|
||||||
/* Valid format */
|
/* Valid format */
|
||||||
spec1.format = SDL_AUDIO_S8;
|
spec1.format = SDL_AUDIO_S8;
|
||||||
spec1.channels = 1;
|
spec1.channels = 1;
|
||||||
|
@ -678,6 +684,9 @@ static int audio_convertAudio(void *arg)
|
||||||
char message[128];
|
char message[128];
|
||||||
int i, ii, j, jj, k, kk;
|
int i, ii, j, jj, k, kk;
|
||||||
|
|
||||||
|
SDL_zero(spec1);
|
||||||
|
SDL_zero(spec2);
|
||||||
|
|
||||||
/* Iterate over bitmask that determines which parameters are modified in the conversion */
|
/* Iterate over bitmask that determines which parameters are modified in the conversion */
|
||||||
for (c = 1; c < 8; c++) {
|
for (c = 1; c < 8; c++) {
|
||||||
SDL_strlcpy(message, "Changing:", 128);
|
SDL_strlcpy(message, "Changing:", 128);
|
||||||
|
@ -995,6 +1004,9 @@ static int audio_resampleLoss(void *arg)
|
||||||
double sum_squared_value = 0;
|
double sum_squared_value = 0;
|
||||||
double signal_to_noise = 0;
|
double signal_to_noise = 0;
|
||||||
|
|
||||||
|
SDL_zero(tmpspec1);
|
||||||
|
SDL_zero(tmpspec2);
|
||||||
|
|
||||||
SDLTest_AssertPass("Test resampling of %i s %i Hz %f phase sine wave from sampling rate of %i Hz to %i Hz",
|
SDLTest_AssertPass("Test resampling of %i s %i Hz %f phase sine wave from sampling rate of %i Hz to %i Hz",
|
||||||
spec->time, spec->freq, spec->phase, spec->rate_in, spec->rate_out);
|
spec->time, spec->freq, spec->phase, spec->rate_in, spec->rate_out);
|
||||||
|
|
||||||
|
@ -1147,6 +1159,9 @@ static int audio_convertAccuracy(void *arg)
|
||||||
int tmp_len, dst_len;
|
int tmp_len, dst_len;
|
||||||
int ret;
|
int ret;
|
||||||
|
|
||||||
|
SDL_zero(src_spec);
|
||||||
|
SDL_zero(tmp_spec);
|
||||||
|
|
||||||
SDL_AudioFormat format = formats[i];
|
SDL_AudioFormat format = formats[i];
|
||||||
const char* format_name = format_names[i];
|
const char* format_name = format_names[i];
|
||||||
|
|
||||||
|
@ -1238,6 +1253,10 @@ static int audio_formatChange(void *arg)
|
||||||
double target_signal_to_noise = 75.0;
|
double target_signal_to_noise = 75.0;
|
||||||
int sine_freq = 500;
|
int sine_freq = 500;
|
||||||
|
|
||||||
|
SDL_zero(spec1);
|
||||||
|
SDL_zero(spec2);
|
||||||
|
SDL_zero(spec3);
|
||||||
|
|
||||||
spec1.format = SDL_AUDIO_F32;
|
spec1.format = SDL_AUDIO_F32;
|
||||||
spec1.channels = 1;
|
spec1.channels = 1;
|
||||||
spec1.freq = 20000;
|
spec1.freq = 20000;
|
||||||
|
|
|
@ -1167,7 +1167,7 @@ static AVCodecContext *OpenAudioStream(AVFormatContext *ic, int stream, const AV
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_AudioSpec spec = { SDL_AUDIO_F32, codecpar->ch_layout.nb_channels, codecpar->sample_rate };
|
SDL_AudioSpec spec = { SDL_AUDIO_F32, codecpar->ch_layout.nb_channels, codecpar->sample_rate, SDL_FALSE };
|
||||||
audio = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, NULL, NULL);
|
audio = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, NULL, NULL);
|
||||||
if (audio) {
|
if (audio) {
|
||||||
SDL_ResumeAudioDevice(SDL_GetAudioStreamDevice(audio));
|
SDL_ResumeAudioDevice(SDL_GetAudioStreamDevice(audio));
|
||||||
|
@ -1240,7 +1240,7 @@ static void InterleaveAudio(AVFrame *frame, const SDL_AudioSpec *spec)
|
||||||
static void HandleAudioFrame(AVFrame *frame)
|
static void HandleAudioFrame(AVFrame *frame)
|
||||||
{
|
{
|
||||||
if (audio) {
|
if (audio) {
|
||||||
SDL_AudioSpec spec = { GetAudioFormat(frame->format), frame->ch_layout.nb_channels, frame->sample_rate };
|
SDL_AudioSpec spec = { GetAudioFormat(frame->format), frame->ch_layout.nb_channels, frame->sample_rate, SDL_FALSE };
|
||||||
SDL_SetAudioStreamFormat(audio, &spec, NULL);
|
SDL_SetAudioStreamFormat(audio, &spec, NULL);
|
||||||
|
|
||||||
if (frame->ch_layout.nb_channels > 1 && IsPlanarAudioFormat(frame->format)) {
|
if (frame->ch_layout.nb_channels > 1 && IsPlanarAudioFormat(frame->format)) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue