Skip to content

Commit

Permalink
audio: Redesigned audio conversion code for SDL3.
Browse files Browse the repository at this point in the history
- SDL_AudioCVT is gone, even internally.
- libsamplerate is gone (I suspect our resampler is finally Good Enough).
- Cleanups and improvements to audio conversion interfaces.
- SDL_AudioStream can change its input/output format/rate/channels on the fly!
  • Loading branch information
icculus committed Apr 27, 2023
1 parent 44bec9c commit e5a6c24
Show file tree
Hide file tree
Showing 20 changed files with 1,370 additions and 2,305 deletions.
3 changes: 0 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -411,8 +411,6 @@ set_option(SDL_PULSEAUDIO "Use PulseAudio" ${UNIX_SYS})
dep_option(SDL_PULSEAUDIO_SHARED "Dynamically load PulseAudio support" ON "SDL_PULSEAUDIO" OFF)
set_option(SDL_SNDIO "Support the sndio audio API" ${UNIX_SYS})
dep_option(SDL_SNDIO_SHARED "Dynamically load the sndio audio API" ON "SDL_SNDIO" OFF)
set_option(SDL_LIBSAMPLERATE "Use libsamplerate for audio rate conversion" ${UNIX_SYS})
dep_option(SDL_LIBSAMPLERATE_SHARED "Dynamically load libsamplerate" ON "SDL_LIBSAMPLERATE" OFF)
set_option(SDL_RPATH "Use an rpath when linking SDL" ${UNIX_SYS})
set_option(SDL_CLOCK_GETTIME "Use clock_gettime() instead of gettimeofday()" ${SDL_CLOCK_GETTIME_ENABLED_BY_DEFAULT})
set_option(SDL_X11 "Use X11 video driver" ${UNIX_SYS})
Expand Down Expand Up @@ -2909,7 +2907,6 @@ if(HAVE_VULKAN AND NOT SDL_LOADSO)
endif()

# Platform-independent options
CheckLibSampleRate()

if(SDL_VIDEO)
if(SDL_OFFSCREEN AND SDL_VIDEO_OPENGL_EGL)
Expand Down
80 changes: 49 additions & 31 deletions build-scripts/gen_audio_channel_conversion.c
Original file line number Diff line number Diff line change
Expand Up @@ -261,27 +261,43 @@ static void write_converter(const int fromchans, const int tochans)
}
}

printf("static void SDLCALL\n"
"SDL_Convert%sTo%s(SDL_AudioCVT *cvt, SDL_AudioFormat format)\n"
"{\n", remove_dots(fromstr), remove_dots(tostr));

if (convert_backwards) { /* must convert backwards when growing the output in-place. */
printf(" float *dst = ((float *) (cvt->buf + ((cvt->len_cvt / %d) * %d))) - %d;\n", fromchans, tochans, tochans);
printf(" const float *src = ((const float *) (cvt->buf + cvt->len_cvt)) - %d;\n", fromchans);
} else {
printf(" float *dst = (float *) cvt->buf;\n");
printf(" const float *src = dst;\n");
}
printf("static void SDL_Convert%sTo%s(float *dst, const float *src, int num_frames)\n{\n", remove_dots(fromstr), remove_dots(tostr));

printf(" int i;\n"
"\n"
" LOG_DEBUG_CONVERT(\"%s\", \"%s\");\n"
" SDL_assert(format == AUDIO_F32SYS);\n"
" LOG_DEBUG_AUDIO_CONVERT(\"%s\", \"%s\");\n"
"\n", lowercase(fromstr), lowercase(tostr));

if (convert_backwards) {
if (convert_backwards) { /* must convert backwards when growing the output in-place. */
printf(" /* convert backwards, since output is growing in-place. */\n");
printf(" for (i = cvt->len_cvt / (sizeof (float) * %d); i; i--, src -= %d, dst -= %d) {\n", fromchans, fromchans, tochans);
printf(" src += (num_frames-1)");
if (fromchans != 1) {
printf(" * %d", fromchans);
}
printf(";\n");

printf(" dst += (num_frames-1)");
if (tochans != 1) {
printf(" * %d", tochans);
}
printf(";\n");
printf(" for (i = num_frames");
if (fromchans > 1) {
printf(" * %d", fromchans);
}
printf("; i; i--, ");
if (fromchans == 1) {
printf("src--");
} else {
printf("src -= %d", fromchans);
}
printf(", ");
if (tochans == 1) {
printf("dst--");
} else {
printf("dst -= %d", tochans);
}
printf(") {\n");
fptr = cvtmatrix;
for (i = 0; i < fromchans; i++) {
if (input_channel_used[i] > 1) { /* don't read it from src more than once. */
Expand Down Expand Up @@ -326,7 +342,19 @@ static void write_converter(const int fromchans, const int tochans)

printf(" }\n");
} else {
printf(" for (i = cvt->len_cvt / (sizeof (float) * %d); i; i--, src += %d, dst += %d) {\n", fromchans, fromchans, tochans);
printf(" for (i = num_frames * %d; i; i--, ", fromchans);
if (fromchans == 1) {
printf("src++");
} else {
printf("src += %d", fromchans);
}
printf(", ");
if (tochans == 1) {
printf("dst++");
} else {
printf("dst += %d", tochans);
}
printf(") {\n");

fptr = cvtmatrix;
for (i = 0; i < fromchans; i++) {
Expand Down Expand Up @@ -372,20 +400,7 @@ static void write_converter(const int fromchans, const int tochans)
printf(" }\n");
}

printf("\n");

if ((fromchans > 1) && (tochans > 1)) {
printf(" cvt->len_cvt = (cvt->len_cvt / %d) * %d;\n", fromchans, tochans);
} else if (tochans == 1) {
printf(" cvt->len_cvt = cvt->len_cvt / %d;\n", fromchans);
} else /* if (fromchans == 1) */ {
printf(" cvt->len_cvt = cvt->len_cvt * %d;\n", tochans);
}

printf(" if (cvt->filters[++cvt->filter_index]) {\n"
" cvt->filters[cvt->filter_index] (cvt, format);\n"
" }\n"
"}\n\n");
printf("\n}\n\n");
}

int main(void)
Expand Down Expand Up @@ -416,6 +431,9 @@ int main(void)
"\n"
"/* DO NOT EDIT, THIS FILE WAS GENERATED BY build-scripts/gen_audio_channel_conversion.c */\n"
"\n"
"\n"
"typedef void (*SDL_AudioChannelConverter)(float *dst, const float *src, int num_frames);\n"
"\n"
);

for (ini = 1; ini <= NUM_CHANNELS; ini++) {
Expand All @@ -424,7 +442,7 @@ int main(void)
}
}

printf("static const SDL_AudioFilter channel_converters[%d][%d] = { /* [from][to] */\n", NUM_CHANNELS, NUM_CHANNELS);
printf("static const SDL_AudioChannelConverter channel_converters[%d][%d] = { /* [from][to] */\n", NUM_CHANNELS, NUM_CHANNELS);
for (ini = 1; ini <= NUM_CHANNELS; ini++) {
const char *comma = "";
printf(" {");
Expand Down
47 changes: 0 additions & 47 deletions cmake/sdlchecks.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -240,53 +240,6 @@ macro(CheckSNDIO)
endif()
endmacro()

# Requires:
# - SDL_LIBSAMPLERATE
# Optional:
# - SDL_LIBSAMPLERATE_SHARED opt
# - HAVE_SDL_LOADSO opt
macro(CheckLibSampleRate)
if(SDL_LIBSAMPLERATE)
find_package(SampleRate QUIET)
if(SampleRate_FOUND AND TARGET SampleRate::samplerate)
set(HAVE_LIBSAMPLERATE TRUE)
if(SDL_LIBSAMPLERATE_SHARED)
target_include_directories(sdl-build-options INTERFACE $<TARGET_PROPERTY:SampleRate::samplerate,INTERFACE_INCLUDE_DIRECTORIES>)
if(NOT HAVE_SDL_LOADSO)
message_warn("You must have SDL_LoadObject() support for dynamic libsamplerate loading")
else()
get_property(_samplerate_type TARGET SampleRate::samplerate PROPERTY TYPE)
if(_samplerate_type STREQUAL "SHARED_LIBRARY")
set(HAVE_LIBSAMPLERATE_SHARED TRUE)
if(WIN32)
set(SDL_LIBSAMPLERATE_DYNAMIC "\"$<TARGET_FILE_NAME:SampleRate::samplerate>\"")
else()
set(SDL_LIBSAMPLERATE_DYNAMIC "\"$<TARGET_SONAME_FILE_NAME:SampleRate::samplerate>\"")
endif()
endif()
endif()
else()
target_link_libraries(sdl-build-options INTERFACE SampleRate::samplerate)
endif()
else()
check_include_file(samplerate.h HAVE_LIBSAMPLERATE_H)
if(HAVE_LIBSAMPLERATE_H)
set(HAVE_LIBSAMPLERATE TRUE)
if(SDL_LIBSAMPLERATE_SHARED AND NOT HAVE_SDL_LOADSO)
message_warn("You must have SDL_LoadObject() support for dynamic libsamplerate loading")
endif()
FindLibraryAndSONAME("samplerate")
if(SDL_LIBSAMPLERATE_SHARED AND SAMPLERATE_LIB AND HAVE_SDL_LOADSO)
set(SDL_LIBSAMPLERATE_DYNAMIC "\"${SAMPLERATE_LIB_SONAME}\"")
set(HAVE_LIBSAMPLERATE_SHARED TRUE)
else()
list(APPEND SDL_EXTRA_LIBS samplerate)
endif()
endif()
endif()
endif()
endmacro()

# Requires:
# - n/a
# Optional:
Expand Down
8 changes: 2 additions & 6 deletions docs/README-linux.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Ubuntu 18.04, all available features enabled:

sudo apt-get install build-essential git make \
pkg-config cmake ninja-build gnome-desktop-testing libasound2-dev libpulse-dev \
libaudio-dev libjack-dev libsndio-dev libsamplerate0-dev libx11-dev libxext-dev \
libaudio-dev libjack-dev libsndio-dev libx11-dev libxext-dev \
libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev \
libxkbcommon-dev libdrm-dev libgbm-dev libgl1-mesa-dev libgles2-mesa-dev \
libegl1-mesa-dev libdbus-1-dev libibus-1.0-dev libudev-dev fcitx-libs-dev
Expand All @@ -32,15 +32,11 @@ Fedora 35, all available features enabled:
systemd-devel mesa-libGL-devel libxkbcommon-devel mesa-libGLES-devel \
mesa-libEGL-devel vulkan-devel wayland-devel wayland-protocols-devel \
libdrm-devel mesa-libgbm-devel libusb-devel libdecor-devel \
libsamplerate-devel pipewire-jack-audio-connection-kit-devel \
pipewire-jack-audio-connection-kit-devel \

NOTES:
- The sndio audio target is unavailable on Fedora (but probably not what you
should want to use anyhow).
- libsamplerate0-dev lets SDL optionally link to libresamplerate at runtime
for higher-quality audio resampling. SDL will work without it if the library
is missing, so it's safe to build in support even if the end user doesn't
have this library installed.


Joystick does not work
Expand Down
3 changes: 3 additions & 0 deletions docs/README-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ If you need to convert U16 audio data to a still-supported format at runtime, th
}
```
In SDL2, SDL_AudioStream would convert/resample audio data during input (via SDL_AudioStreamPut). In SDL3, it does this work when requesting audio (via SDL_GetAudioStreamData, which would have been SDL_AudioStreamPut in SDL2. The way you use an AudioStream is roughly the same, just be aware that the workload moved to a different phase.
In SDL2, SDL_AudioStreamAvailable() returns 0 if passed a NULL stream. In SDL3, the equivalent SDL_GetAudioStreamAvailable() call returns -1 and sets an error string, which matches other audiostream APIs' behavior.
The following functions have been renamed:
* SDL_AudioStreamAvailable() => SDL_GetAudioStreamAvailable()
Expand Down
107 changes: 98 additions & 9 deletions include/SDL3/SDL_audio.h
Original file line number Diff line number Diff line change
Expand Up @@ -682,15 +682,15 @@ extern DECLSPEC SDL_AudioSpec *SDLCALL SDL_LoadWAV_RW(SDL_RWops * src,
SDL_LoadWAV_RW(SDL_RWFromFile(file, "rb"),1, spec,audio_buf,audio_len)


/* SDL_AudioStream is a new audio conversion interface.
The benefits vs SDL_AudioCVT:
- it can handle resampling data in chunks without generating
/* SDL_AudioStream is an audio conversion interface.
- It can handle resampling data in chunks without generating
artifacts, 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.
- 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 sound that needs to pass from one place to another.
*/
/* this is opaque to the outside world. */
struct SDL_AudioStream;
struct SDL_AudioStream; /* this is opaque to the outside world. */
typedef struct SDL_AudioStream SDL_AudioStream;

/**
Expand All @@ -704,25 +704,96 @@ typedef struct SDL_AudioStream SDL_AudioStream;
* \param dst_rate The sampling rate of the desired audio output
* \returns 0 on success, or -1 on error.
*
* \threadsafety It is safe to call this function from any thread.
*
* \since This function is available since SDL 3.0.0.
*
* \sa SDL_PutAudioStreamData
* \sa SDL_GetAudioStreamData
* \sa SDL_GetAudioStreamAvailable
* \sa SDL_FlushAudioStream
* \sa SDL_ClearAudioStream
* \sa SDL_ChangeAudioStreamOutput
* \sa SDL_DestroyAudioStream
*/
extern DECLSPEC SDL_AudioStream *SDLCALL SDL_CreateAudioStream(SDL_AudioFormat src_format,
Uint8 src_channels,
int src_channels,
int src_rate,
SDL_AudioFormat dst_format,
Uint8 dst_channels,
int dst_channels,
int dst_rate);


/**
* Query the current format of an audio stream.
*
* \param stream the SDL_AudioStream to query.
* \param src_format Where to store the input audio format; ignored if NULL.
* \param src_channels Where to store the input channel count; ignored if NULL.
* \param src_rate Where to store the input sample rate; ignored if NULL.
* \param dst_format Where to store the output audio format; ignored if NULL.
* \param dst_channels Where to store the output channel count; ignored if NULL.
* \param dst_rate Where to store the output sample rate; ignored if NULL.
* \returns 0 on success, or -1 on error.
*
* \threadsafety It is safe to call this function from any thread, as it
* holds a stream-specific mutex while running.
*
* \since This function is available since SDL 3.0.0.
*/
extern DECLSPEC int SDLCALL SDL_GetAudioStreamFormat(SDL_AudioStream *stream,
SDL_AudioFormat *src_format,
int *src_channels,
int *src_rate,
SDL_AudioFormat *dst_format,
int *dst_channels,
int *dst_rate);

/**
* Change the input and output formats of an audio stream.
*
* Future calls to and SDL_GetAudioStreamAvailable and SDL_GetAudioStreamData
* will reflect the new format, and future calls to SDL_PutAudioStreamData
* must provide data in the new input formats.
*
* \param src_format The format of the audio input
* \param src_channels The number of channels of the audio input
* \param src_rate The sampling rate of the audio input
* \param dst_format The format of the desired audio output
* \param dst_channels The number of channels of the desired audio output
* \param dst_rate The sampling rate of the desired audio output
* \returns 0 on success, or -1 on error.
*
* \threadsafety It is safe to call this function from any thread, as it
* holds a stream-specific mutex while running.
*
* \since This function is available since SDL 3.0.0.
*
* \sa SDL_GetAudioStreamFormat
* \sa SDL_PutAudioStreamData
* \sa SDL_GetAudioStreamData
* \sa SDL_GetAudioStreamAvailable
*/
extern DECLSPEC int SDLCALL SDL_SetAudioStreamFormat(SDL_AudioStream *stream,
SDL_AudioFormat src_format,
int src_channels,
int src_rate,
SDL_AudioFormat dst_format,
int dst_channels,
int dst_rate);

/**
* Add data to be converted/resampled to the stream.
*
* This data must match the format/channels/samplerate specified in
* the latest call to SDL_SetAudioStreamFormat, or the format
* specified when creating the stream if it hasn't been changed.
*
* Note that this call simply queues unconverted data for later.
* This is different than SDL2, where data was converted during the
* Put call and the Get call would just dequeue the
* previously-converted data.
*
* \param stream The stream the audio data is being added to
* \param buf A pointer to the audio data to add
* \param len The number of bytes to write to the stream
Expand All @@ -741,7 +812,17 @@ extern DECLSPEC SDL_AudioStream *SDLCALL SDL_CreateAudioStream(SDL_AudioFormat s
extern DECLSPEC int SDLCALL SDL_PutAudioStreamData(SDL_AudioStream *stream, const void *buf, int len);

/**
* Get converted/resampled data from the stream
* Get converted/resampled data from the stream.
*
* The input/output data format/channels/samplerate is specified when
* creating the stream, and can be changed after creation by calling
* SDL_SetAudioStreamFormat.
*
* Note that any conversion and resampling necessary is done during
* this call, and SDL_PutAudioStreamData simply queues unconverted
* data for later. This is different than SDL2, where that work was
* done while inputting new data to the stream and requesting the
* output just copied the converted data.
*
* \param stream The stream the audio is being requested from
* \param buf A buffer to fill with audio data
Expand All @@ -753,6 +834,7 @@ extern DECLSPEC int SDLCALL SDL_PutAudioStreamData(SDL_AudioStream *stream, cons
* \sa SDL_CreateAudioStream
* \sa SDL_PutAudioStreamData
* \sa SDL_GetAudioStreamAvailable
* \sa SDL_SetAudioStreamFormat
* \sa SDL_FlushAudioStream
* \sa SDL_ClearAudioStream
* \sa SDL_DestroyAudioStream
Expand All @@ -766,6 +848,12 @@ extern DECLSPEC int SDLCALL SDL_GetAudioStreamData(SDL_AudioStream *stream, void
* resample correctly, so this number might be lower than what you expect, or
* even be zero. Add more data or flush the stream if you need the data now.
*
* If the stream has so much data that it would overflow an int, the return
* value is clamped to a maximum value, but no queued data is lost; if there
* are gigabytes of data queued, the app might need to read some of it with
* SDL_GetAudioStreamData before this function's return value is no longer
* clamped.
*
* \param stream The audio stream to query
* \returns the number of converted/resampled bytes available.
*
Expand Down Expand Up @@ -1133,6 +1221,7 @@ extern DECLSPEC void SDLCALL SDL_UnlockAudioDevice(SDL_AudioDeviceID dev);
*/
extern DECLSPEC void SDLCALL SDL_CloseAudioDevice(SDL_AudioDeviceID dev);

/* !!! FIXME: maybe remove this before SDL3's API is locked down. */
/**
* Convert some audio data of one format to another format.
*
Expand Down
Loading

0 comments on commit e5a6c24

Please sign in to comment.