SDL_mixer: load sounds via libsndfile

From c8582d2b3b34c0b24d92e300856ca5a61ee17d19 Mon Sep 17 00:00:00 2001
From: Fabian Greffrath <[EMAIL REDACTED]>
Date: Sun, 26 Feb 2023 07:45:53 +0100
Subject: [PATCH] load sounds via libsndfile

---
 CMakeLists.txt                  |  34 ++++++
 cmake/FindSndFile.cmake         |  32 +++++
 cmake/SDL3_mixerConfig.cmake.in |   6 +
 src/codecs/load_sndfile.c       | 210 ++++++++++++++++++++++++++++++++
 src/codecs/load_sndfile.h       |  39 ++++++
 src/mixer.c                     |  22 ++--
 6 files changed, 335 insertions(+), 8 deletions(-)
 create mode 100644 cmake/FindSndFile.cmake
 create mode 100644 src/codecs/load_sndfile.c
 create mode 100644 src/codecs/load_sndfile.h

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 4746bdd9..9eb62ba5 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -77,6 +77,9 @@ else()
 endif()
 option(SDL3MIXER_CMD "Support an external music player" ${sdl3mixer_cmd_default})
 
+option(SDL3MIXER_SNDFILE "Support loading sounds via libsndfile" OFF)
+option(SDL3MIXER_SNDFILE_SHARED "Dynamically load libsndfile" "${SDL3MIXER_DEPS_SHARED}")
+
 option(SDL3MIXER_FLAC "Enable FLAC music" ON)
 
 cmake_dependent_option(SDL3MIXER_FLAC_LIBFLAC "Enable FLAC music using libFLAC" ON SDL3MIXER_FLAC OFF)
@@ -210,6 +213,7 @@ set(BUILD_SHARED_LIBS ${SDL3MIXER_BUILD_SHARED_LIBS})
 add_library(SDL3_mixer
     src/codecs/load_aiff.c
     src/codecs/load_voc.c
+    src/codecs/load_sndfile.c
     src/codecs/mp3utils.c
     src/codecs/music_cmd.c
     src/codecs/music_drflac.c
@@ -329,6 +333,35 @@ if(SDL3MIXER_CMD)
     endif()
 endif()
 
+if(SDL3MIXER_SNDFILE)
+    target_compile_definitions(SDL3_mixer PRIVATE LOAD_SNDFILE)
+    if(SDL3MIXER_VENDORED)
+        message(STATUS "Using vendored libsndfile")
+        message(FATAL_ERROR "libsndfile is not vendored.")
+    else()
+        message(STATUS "Using system libsndfile")
+        find_package(SndFile REQUIRED)
+        if(NOT SDL3MIXER_SNDFILE_SHARED)
+            list(APPEND PC_REQUIRES sndfile)
+        endif()
+    endif()
+    if(SDL3MIXER_SNDFILE_SHARED)
+        target_include_directories(SDL3_mixer PRIVATE
+            $<TARGET_PROPERTY:SndFile::sndfile,INCLUDE_DIRECTORIES>
+            $<TARGET_PROPERTY:SndFile::sndfile,INTERFACE_INCLUDE_DIRECTORIES>
+            $<TARGET_PROPERTY:SndFile::sndfile,INTERFACE_SYSTEM_INCLUDE_DIRECTORIES>
+        )
+        target_get_dynamic_library(dynamic_sndfile SndFile::sndfile)
+        message(STATUS "Dynamic libsndfile: ${dynamic_sndfile}")
+        target_compile_definitions(SDL3_mixer PRIVATE "SNDFILE_DYNAMIC=\"${dynamic_sndfile}\"")
+        if(SDL3MIXER_VENDORED)
+            add_dependencies(SDL3_mixer SndFile::sndfile)
+        endif()
+    else()
+        target_link_libraries(SDL3_mixer PRIVATE SndFile::sndfile)
+    endif()
+endif()
+
 if(SDL3MIXER_OGG)
     # libogg is a requirement of libflac, libtremor and libvorbisfile, so only need this library when vendoring
     if(SDL3MIXER_VENDORED)
@@ -906,6 +939,7 @@ if(SDL3MIXER_INSTALL)
             cmake/FindVorbis.cmake
             cmake/Findtremor.cmake
             cmake/Findwavpack.cmake
+            cmake/FindSndFile.cmake
         DESTINATION "${SDL3MIXER_INSTALL_CMAKEDIR}"
         COMPONENT devel
     )
diff --git a/cmake/FindSndFile.cmake b/cmake/FindSndFile.cmake
new file mode 100644
index 00000000..e277c54b
--- /dev/null
+++ b/cmake/FindSndFile.cmake
@@ -0,0 +1,32 @@
+include(FindPackageHandleStandardArgs)
+
+find_library(SndFile_LIBRARY
+    NAMES sndfile sndfile-1
+)
+
+find_path(SndFile_INCLUDE_PATH
+    NAMES sndfile.h
+)
+
+set(SndFile_COMPILE_OPTIONS "" CACHE STRING "Extra compile options of libsndfile")
+
+set(SndFile_LINK_LIBRARIES "" CACHE STRING "Extra link libraries of libsndfile")
+
+set(SndFile_LINK_FLAGS "" CACHE STRING "Extra link flags of libsndfile")
+
+find_package_handle_standard_args(SndFile
+    REQUIRED_VARS SndFile_LIBRARY SndFile_INCLUDE_PATH
+)
+
+if(SndFile_FOUND)
+    if(NOT TARGET SndFile::sndfile)
+        add_library(SndFile::sndfile UNKNOWN IMPORTED)
+        set_target_properties(SndFile::sndfile PROPERTIES
+            IMPORTED_LOCATION "${SndFile_LIBRARY}"
+            INTERFACE_INCLUDE_DIRECTORIES "${SndFile_INCLUDE_PATH}"
+            INTERFACE_COMPILE_OPTIONS "${SndFile_COMPILE_OPTIONS}"
+            INTERFACE_LINK_LIBRARIES "${SndFile_LINK_LIBRARIES}"
+            INTERFACE_LINK_FLAGS "${SndFile_LINK_FLAGS}"
+        )
+    endif()
+endif()
diff --git a/cmake/SDL3_mixerConfig.cmake.in b/cmake/SDL3_mixerConfig.cmake.in
index 8534e78d..f8969ac1 100644
--- a/cmake/SDL3_mixerConfig.cmake.in
+++ b/cmake/SDL3_mixerConfig.cmake.in
@@ -12,6 +12,8 @@ set(SDL3MIXER_VENDORED              @SDL3MIXER_VENDORED@)
 
 set(SDL3MIXER_CMD                   @SDL3MIXER_CMD@)
 
+set(SDL3MIXER_SNDFILE               @SDL3MIXER_SNDFILE@)
+
 set(SDL3MIXER_FLAC_LIBFLAC          @SDL3MIXER_FLAC_LIBFLAC@)
 set(SDL3MIXER_FLAC_DRFLAC           @SDL3MIXER_FLAC_DRFLAC@)
 
@@ -57,6 +59,10 @@ if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/SDL3_mixer-static-targets.cmake")
 
     include(CMakeFindDependencyMacro)
 
+    if(SDL3MIXER_SNDFILE AND NOT SDL3MIXER_VENDORED AND NOT TARGET SndFile::sndfile)
+        find_dependency(SndFile)
+    endif()
+
     if(SDL3MIXER_FLAC_LIBFLAC AND NOT SDL3MIXER_VENDORED AND NOT TARGET FLAC::FLAC)
         find_dependency(FLAC)
     endif()
diff --git a/src/codecs/load_sndfile.c b/src/codecs/load_sndfile.c
new file mode 100644
index 00000000..0b53d453
--- /dev/null
+++ b/src/codecs/load_sndfile.c
@@ -0,0 +1,210 @@
+/*
+  SDL_mixer:  An audio mixer library based on the SDL library
+  Copyright (C) 1997-2023 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+
+  This is the source needed to decode a file in any format supported by
+  libsndfile. The only externally-callable function is Mix_LoadSndFile_RW(),
+  which is meant to act as identically to SDL_LoadWAV_RW() as possible.
+
+  This file by Fabian Greffrath (fabian@greffrath.com).
+*/
+
+#include <SDL3/SDL_audio.h>
+
+#include "load_sndfile.h"
+
+#ifdef LOAD_SNDFILE
+
+#include <SDL3/SDL_loadso.h>
+
+#include <sndfile.h>
+
+static SNDFILE* (*SF_sf_open_virtual) (SF_VIRTUAL_IO *sfvirtual, int mode, SF_INFO *sfinfo, void *user_data);
+static int (*SF_sf_close) (SNDFILE *sndfile);
+static sf_count_t (*SF_sf_readf_short) (SNDFILE *sndfile, short *ptr, sf_count_t frames);
+static const char* (*SF_sf_strerror) (SNDFILE *sndfile);
+
+static int SNDFILE_init (void)
+{
+    static int SNDFILE_loaded;
+
+    if (SNDFILE_loaded == 0)
+    {
+#ifdef SNDFILE_DYNAMIC
+        static void *SNDFILE_lib;
+
+        SNDFILE_lib = SDL_LoadObject(SNDFILE_DYNAMIC);
+        if (SNDFILE_lib == NULL) {
+            return -1;
+        }
+
+        /* *INDENT-OFF* */ /* clang-format off */
+        SF_sf_open_virtual = (SNDFILE* (*)(SF_VIRTUAL_IO *sfvirtual, int mode, SF_INFO *sfinfo, void *user_data))SDL_LoadFunction(SNDFILE_lib, "sf_open_virtual");
+        SF_sf_close = (int (*)(SNDFILE *sndfile))SDL_LoadFunction(SNDFILE_lib, "sf_close");
+        SF_sf_readf_short = (sf_count_t(*)(SNDFILE *sndfile, short *ptr, sf_count_t frames))SDL_LoadFunction(SNDFILE_lib, "sf_readf_short");
+        SF_sf_strerror = (const char* (*)(SNDFILE *sndfile))SDL_LoadFunction(SNDFILE_lib, "sf_strerror");
+        /* *INDENT-ON* */ /* clang-format on */
+
+        if (SF_sf_open_virtual == NULL || SF_sf_close == NULL ||
+            SF_sf_readf_short == NULL || SF_sf_strerror == NULL) {
+            SDL_UnloadObject(SNDFILE_lib);
+            SNDFILE_lib = NULL;
+            return -1;
+        }
+#else
+        SF_sf_open_virtual = sf_open_virtual;
+        SF_sf_close = sf_close;
+        SF_sf_readf_short = sf_readf_short;
+        SF_sf_strerror = sf_strerror;
+#endif
+
+        SNDFILE_loaded = 1;
+    }
+
+    return 0;
+}
+
+static sf_count_t sfvio_size(void *user_data)
+{
+    SDL_RWops *RWops = user_data;
+
+    return SDL_RWsize(RWops);
+}
+
+static sf_count_t sfvio_seek(sf_count_t offset, int whence, void *user_data)
+{
+    SDL_RWops *RWops = user_data;
+
+    return SDL_RWseek(RWops, offset, whence);
+}
+
+static sf_count_t sfvio_read(void *ptr, sf_count_t count, void *user_data)
+{
+    SDL_RWops *RWops = user_data;
+
+    return SDL_RWread(RWops, ptr, count);
+}
+
+static sf_count_t sfvio_tell(void *user_data)
+{
+    SDL_RWops *RWops = user_data;
+
+    return SDL_RWtell(RWops);
+}
+
+SDL_AudioSpec *Mix_LoadSndFile_RW (SDL_RWops *src, int freesrc,
+        SDL_AudioSpec *spec, Uint8 **audio_buf, Uint32 *audio_len)
+{
+    SNDFILE *sndfile = NULL;
+    SF_INFO sfinfo;
+    SF_VIRTUAL_IO sfvio =
+    {
+        sfvio_size,
+        sfvio_seek,
+        sfvio_read,
+        NULL,
+        sfvio_tell
+    };
+    Uint32 len;
+    short *buf = NULL;
+
+    int was_error = 1;
+
+    if (src == NULL || spec == NULL ||
+        audio_buf == NULL || audio_len == NULL) {
+        goto done;
+    }
+
+    *audio_buf = NULL;
+    *audio_len = 0;
+    SDL_memset(spec, 0, sizeof(*spec));
+
+    if (SNDFILE_init() != 0) {
+        goto done;
+    }
+
+    SDL_memset(&sfinfo, 0, sizeof(sfinfo));
+
+    sndfile = SF_sf_open_virtual(&sfvio, SFM_READ, &sfinfo, src);
+
+    if (sndfile == NULL) {
+        Mix_SetError("sf_open_virtual: %s", SF_sf_strerror(sndfile));
+        goto done;
+    }
+
+    if (sfinfo.frames <= 0) {
+        Mix_SetError("Invalid number of frames: %ld", (long)sfinfo.frames);
+        goto done;
+    }
+
+    if (sfinfo.channels <= 0) {
+        Mix_SetError("Invalid number of channels: %d", sfinfo.channels);
+        goto done;
+    }
+
+    len = sfinfo.frames * sfinfo.channels * sizeof(short);
+    buf = SDL_malloc(len);
+
+    if (buf == NULL) {
+        Mix_OutOfMemory();
+        goto done;
+    }
+
+    if (SF_sf_readf_short(sndfile, buf, sfinfo.frames) < sfinfo.frames) {
+        SDL_free(buf);
+        Mix_SetError("sf_readf_short: %s", SF_sf_strerror(sndfile));
+        goto done;
+    }
+
+    was_error = 0;
+
+    spec->channels = sfinfo.channels;
+    spec->freq = sfinfo.samplerate;
+    spec->format = AUDIO_S16;
+
+    *audio_buf = (Uint8 *)buf;
+    *audio_len = len;
+
+    if (freesrc && src) {
+        SDL_RWclose(src);
+    }
+
+done:
+    if (sndfile) {
+        SF_sf_close(sndfile);
+    }
+
+    if (was_error) {
+        spec = NULL;
+    }
+
+    return spec;
+}
+
+#else
+
+SDL_AudioSpec *Mix_LoadSndFile_RW (SDL_RWops *src, int freesrc,
+        SDL_AudioSpec *spec, Uint8 **audio_buf, Uint32 *audio_len)
+{
+    return NULL;
+}
+
+#endif
+
+/* vi: set ts=4 sw=4 expandtab: */
diff --git a/src/codecs/load_sndfile.h b/src/codecs/load_sndfile.h
new file mode 100644
index 00000000..6f5e6020
--- /dev/null
+++ b/src/codecs/load_sndfile.h
@@ -0,0 +1,39 @@
+/*
+  SDL_mixer:  An audio mixer library based on the SDL library
+  Copyright (C) 1997-2023 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+
+  This is the source needed to decode a file in any format supported by
+  libsndfile. The only externally-callable function is Mix_LoadSndFile_RW(),
+  which is meant to act as identically to SDL_LoadWAV_RW() as possible.
+
+  This file by Fabian Greffrath (fabian@greffrath.com).
+*/
+
+#ifndef LOAD_SNDFILE_H
+#define LOAD_SNDFILE_H
+
+#include <SDL3/SDL_mixer.h>
+
+/* Don't call this directly; use Mix_LoadWAV_RW() for now. */
+SDL_AudioSpec *Mix_LoadSndFile_RW (SDL_RWops *src, int freesrc,
+        SDL_AudioSpec *spec, Uint8 **audio_buf, Uint32 *audio_len);
+
+#endif
+
+/* vi: set ts=4 sw=4 expandtab: */
diff --git a/src/mixer.c b/src/mixer.c
index 8ac43f45..0af6ed7a 100644
--- a/src/mixer.c
+++ b/src/mixer.c
@@ -26,6 +26,7 @@
 #include "music.h"
 #include "load_aiff.h"
 #include "load_voc.h"
+#include "load_sndfile.h"
 
 #define MIX_INTERNAL_EFFECT__
 #include "effects_internal.h"
@@ -808,14 +809,19 @@ Mix_Chunk *Mix_LoadWAV_RW(SDL_RWops *src, int freesrc)
     /* Seek backwards for compatibility with older loaders */
     SDL_RWseek(src, -4, SDL_RW_SEEK_CUR);
 
-    if (SDL_memcmp(magic, "WAVE", 4) == 0 || SDL_memcmp(magic, "RIFF", 4) == 0) {
-        loaded = SDL_LoadWAV_RW(src, freesrc, &wavespec, (Uint8 **)&chunk->abuf, &chunk->alen);
-    } else if (SDL_memcmp(magic, "FORM", 4) == 0) {
-        loaded = Mix_LoadAIFF_RW(src, freesrc, &wavespec, (Uint8 **)&chunk->abuf, &chunk->alen);
-    } else if (SDL_memcmp(magic, "Crea", 4) == 0) {
-        loaded = Mix_LoadVOC_RW(src, freesrc, &wavespec, (Uint8 **)&chunk->abuf, &chunk->alen);
-    } else {
-        loaded = Mix_LoadMusic_RW(src, freesrc, &wavespec, (Uint8 **)&chunk->abuf, &chunk->alen);
+    /* First try loading via libsndfile */
+    loaded = Mix_LoadSndFile_RW(src, freesrc, &wavespec, (Uint8 **)&chunk->abuf, &chunk->alen);
+
+    if (!loaded)  {
+        if (SDL_memcmp(magic, "WAVE", 4) == 0 || SDL_memcmp(magic, "RIFF", 4) == 0) {
+            loaded = SDL_LoadWAV_RW(src, freesrc, &wavespec, (Uint8 **)&chunk->abuf, &chunk->alen);
+        } else if (SDL_memcmp(magic, "FORM", 4) == 0) {
+            loaded = Mix_LoadAIFF_RW(src, freesrc, &wavespec, (Uint8 **)&chunk->abuf, &chunk->alen);
+        } else if (SDL_memcmp(magic, "Crea", 4) == 0) {
+            loaded = Mix_LoadVOC_RW(src, freesrc, &wavespec, (Uint8 **)&chunk->abuf, &chunk->alen);
+        } else {
+            loaded = Mix_LoadMusic_RW(src, freesrc, &wavespec, (Uint8 **)&chunk->abuf, &chunk->alen);
+        }
     }
     if (!loaded) {
         /* The individual loaders have closed src if needed */