diff --git a/THIRD_PARTY_NOTICES.txt b/THIRD_PARTY_NOTICES.txt
index c37f58cc5..f67913cc3 100644
--- a/THIRD_PARTY_NOTICES.txt
+++ b/THIRD_PARTY_NOTICES.txt
@@ -38,6 +38,10 @@ Components
23. Microsoft Visual C++ Runtime
24. libjpeg-turbo
25. LodePNG
+ 26. Opus (libopus)
+ 27. libogg
+ 28. libvorbis
+ 29. dr_libs (dr_wav, dr_flac, dr_mp3)
==============================================================================
1. FFmpeg
@@ -884,6 +888,196 @@ freely, subject to the following restrictions:
3. This notice may not be removed or altered from any source
distribution.
+==============================================================================
+26. Opus (libopus)
+==============================================================================
+
+License: BSD 3-Clause License
+Homepage: https://opus-codec.org
+Used in: Compiled into flm (Opus audio decoding), libopus 1.6.1 built as a static
+ library.
+
+Copyright 2001-2023 Xiph.Org, Skype Limited, Octasic,
+ Jean-Marc Valin, Timothy B. Terriberry,
+ CSIRO, Gregory Maxwell, Mark Borgerding,
+ Erik de Castro Lopo, Mozilla, Amazon
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+- Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+- Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+- Neither the name of Internet Society, IETF or IETF Trust, nor the
+names of specific contributors, may be used to endorse or promote
+products derived from this software without specific prior written
+permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+Opus is subject to the royalty-free patent licenses which are
+specified at:
+
+Xiph.Org Foundation:
+https://datatracker.ietf.org/ipr/1524/
+
+Microsoft Corporation:
+https://datatracker.ietf.org/ipr/1914/
+
+Broadcom Corporation:
+https://datatracker.ietf.org/ipr/1526/
+
+==============================================================================
+27. libogg
+==============================================================================
+
+License: BSD 3-Clause License
+Homepage: https://xiph.org/ogg/
+Used in: Compiled into flm (Ogg container for Vorbis decoding), libogg 1.3.6 built
+ as a static library.
+
+Copyright (c) 2002, Xiph.org Foundation
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+- Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+- Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+- Neither the name of the Xiph.org Foundation nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION
+OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+==============================================================================
+28. libvorbis
+==============================================================================
+
+License: BSD 3-Clause License
+Homepage: https://xiph.org/vorbis/
+Used in: Compiled into flm (Ogg Vorbis audio decoding), libvorbis/libvorbisfile
+ 1.3.7 built as static libraries.
+
+Copyright (c) 2002-2020 Xiph.org Foundation
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+- Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+- Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+- Neither the name of the Xiph.org Foundation nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION
+OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+==============================================================================
+29. dr_libs (dr_wav, dr_flac, dr_mp3)
+==============================================================================
+
+License: Public Domain (Unlicense) or MIT No Attribution (MIT-0), at the
+ licensee's choice
+Homepage: https://github.com/mackron/dr_libs
+Used in: Compiled into flm (WAV/AIFF, FLAC and MP3 audio decoding), vendored in
+ src/include/dr_libs from commit dfe83776 (2026-08-31), which includes
+ hardening fixes made after the dr_wav 0.14.5, dr_flac 0.13.3 and
+ dr_mp3 0.7.3 releases.
+
+This software is available as a choice of the following licenses. Choose
+whichever you prefer.
+
+===============================================================================
+ALTERNATIVE 1 - Public Domain (www.unlicense.org)
+===============================================================================
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
+software, either in source code form or as a compiled binary, for any purpose,
+commercial or non-commercial, and by any means.
+
+In jurisdictions that recognize copyright laws, the author or authors of this
+software dedicate any and all copyright interest in the software to the public
+domain. We make this dedication for the benefit of the public at large and to
+the detriment of our heirs and successors. We intend this dedication to be an
+overt act of relinquishment in perpetuity of all present and future rights to
+this software under copyright law.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+For more information, please refer to
+
+===============================================================================
+ALTERNATIVE 2 - MIT No Attribution
+===============================================================================
+Copyright 2020 David Reid
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
==============================================================================
Full license texts
==============================================================================
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index bb75b7f11..1ea4e97a8 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -294,6 +294,8 @@ add_subdirectory(${CMAKE_SOURCE_DIR}/../third_party/tokenizers-cpp
# JPEG/PNG decoding for ImageReader: static libjpeg-turbo + vendored lodepng.
include(${CMAKE_SOURCE_DIR}/cmake/FlmImageCodecs.cmake)
+# Audio decoding for AudioReader: static libopus/libogg/libvorbis + vendored dr_libs.
+include(${CMAKE_SOURCE_DIR}/cmake/FlmAudioCodecs.cmake)
# ———————————————————————————————————————————————
# Gather your sources
@@ -749,7 +751,7 @@ endif()
target_link_libraries(flm PRIVATE
tokenizers_cpp
)
-target_link_libraries(flm PRIVATE flm_image_codecs)
+target_link_libraries(flm PRIVATE flm_image_codecs flm_audio_codecs)
if(WIN32)
target_link_libraries(flm PRIVATE
diff --git a/src/cmake/FlmAudioCodecs.cmake b/src/cmake/FlmAudioCodecs.cmake
new file mode 100644
index 000000000..cd74dd7e9
--- /dev/null
+++ b/src/cmake/FlmAudioCodecs.cmake
@@ -0,0 +1,100 @@
+# Audio codecs for AudioReader (src/common/audio/audio_decoder.cpp).
+#
+# - libopus 1.6.1, libogg 1.3.6 and libvorbis 1.3.7 (all BSD-3-Clause), built
+# as static libraries through ExternalProject from pinned release tarballs.
+# libvorbis finds libogg through the shared install prefix.
+# - dr_wav/dr_flac/dr_mp3 (public domain or MIT-0), vendored header-only in
+# include/dr_libs and compiled into audio_decoder.cpp.
+#
+# Defines the INTERFACE target flm_audio_codecs. Link it from anything that
+# compiles audio_decoder.cpp.
+include_guard(GLOBAL)
+include(ExternalProject)
+
+set(_flm_audio_prefix ${CMAKE_BINARY_DIR}/audio-codecs)
+if(MSVC)
+ set(_flm_lib_prefix "")
+ set(_flm_lib_suffix ".lib")
+else()
+ set(_flm_lib_prefix "lib")
+ set(_flm_lib_suffix ".a")
+endif()
+foreach(_lib opus ogg vorbis vorbisfile)
+ set(_flm_${_lib}_lib ${_flm_audio_prefix}/lib/${_flm_lib_prefix}${_lib}${_flm_lib_suffix})
+endforeach()
+
+set(_flm_audio_args
+ -DCMAKE_INSTALL_PREFIX=
+ -DCMAKE_INSTALL_LIBDIR=lib
+ -DCMAKE_PREFIX_PATH=${_flm_audio_prefix}
+ -DCMAKE_BUILD_TYPE=Release
+ -DCMAKE_POSITION_INDEPENDENT_CODE=ON
+ -DBUILD_SHARED_LIBS=OFF
+ -DBUILD_TESTING=OFF)
+if(CMAKE_C_COMPILER)
+ list(APPEND _flm_audio_args -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER})
+endif()
+
+# Short working directories keep MSBuild under Windows' 260-char MAX_PATH on
+# CI runners with deep work directories (see FlmImageCodecs.cmake). `urls` is
+# a list of mirrors tried in order; the SHA-256 pins the exact tarball.
+function(_flm_audio_project name urls sha256 byproduct)
+ set(_work ${CMAKE_BINARY_DIR}/ac/${name})
+ ExternalProject_Add(flm_${name}
+ URL ${urls}
+ URL_HASH SHA256=${sha256}
+ DOWNLOAD_EXTRACT_TIMESTAMP TRUE
+ EXCLUDE_FROM_ALL TRUE
+ PREFIX ${_work}
+ DOWNLOAD_DIR ${_work}/dl
+ SOURCE_DIR ${_work}/s
+ BINARY_DIR ${_work}/b
+ STAMP_DIR ${_work}/st
+ TMP_DIR ${_work}/t
+ INSTALL_DIR ${_flm_audio_prefix}
+ CMAKE_ARGS ${_flm_audio_args} ${ARGN}
+ BUILD_BYPRODUCTS ${byproduct})
+endfunction()
+
+_flm_audio_project(opus
+ "https://downloads.xiph.org/releases/opus/opus-1.6.1.tar.gz;https://ftp.osuosl.org/pub/xiph/releases/opus/opus-1.6.1.tar.gz"
+ 6ffcb593207be92584df15b32466ed64bbec99109f007c82205f0194572411a1
+ ${_flm_opus_lib}
+ -DOPUS_BUILD_PROGRAMS=OFF
+ -DOPUS_INSTALL_PKG_CONFIG_MODULE=OFF
+ -DOPUS_INSTALL_CMAKE_CONFIG_MODULE=OFF)
+_flm_audio_project(ogg
+ "https://github.com/xiph/ogg/releases/download/v1.3.6/libogg-1.3.6.tar.gz;https://downloads.xiph.org/releases/ogg/libogg-1.3.6.tar.gz;https://ftp.osuosl.org/pub/xiph/releases/ogg/libogg-1.3.6.tar.gz"
+ 83e6704730683d004d20e21b8f7f55dcb3383cdf84c0daedf30bde175f774638
+ ${_flm_ogg_lib}
+ -DINSTALL_DOCS=OFF)
+# libvorbis 1.3.7 declares cmake_minimum_required(VERSION 2.8.12), which
+# CMake 4 rejects; let it configure under 3.5 policies (older CMake ignores
+# this variable).
+_flm_audio_project(vorbis
+ "https://github.com/xiph/vorbis/releases/download/v1.3.7/libvorbis-1.3.7.tar.gz;https://downloads.xiph.org/releases/vorbis/libvorbis-1.3.7.tar.gz;https://ftp.osuosl.org/pub/xiph/releases/vorbis/libvorbis-1.3.7.tar.gz"
+ 0e982409a9c3fc82ee06e08205b1355e5c6aa4c36bca58146ef399621b0ce5ab
+ "${_flm_vorbis_lib};${_flm_vorbisfile_lib}"
+ -DCMAKE_POLICY_VERSION_MINIMUM=3.5)
+ExternalProject_Add_StepDependencies(flm_vorbis configure flm_ogg)
+
+# The include directory must exist at configure time for the imported targets.
+file(MAKE_DIRECTORY ${_flm_audio_prefix}/include)
+add_library(flm_audio_codecs INTERFACE)
+foreach(_lib vorbisfile vorbis ogg opus)
+ add_library(flm_${_lib}_imported STATIC IMPORTED GLOBAL)
+ set_target_properties(flm_${_lib}_imported PROPERTIES
+ IMPORTED_LOCATION ${_flm_${_lib}_lib}
+ INTERFACE_INCLUDE_DIRECTORIES ${_flm_audio_prefix}/include)
+ target_link_libraries(flm_audio_codecs INTERFACE flm_${_lib}_imported)
+endforeach()
+add_dependencies(flm_vorbisfile_imported flm_vorbis)
+add_dependencies(flm_vorbis_imported flm_vorbis)
+add_dependencies(flm_ogg_imported flm_ogg)
+add_dependencies(flm_opus_imported flm_opus)
+
+# The decoders do wrapping integer arithmetic on corrupt input (e.g. FLAC LPC
+# prediction); make signed overflow defined there instead of undefined.
+get_filename_component(_flm_audio_decoder_src ${CMAKE_CURRENT_LIST_DIR}/../common/audio/audio_decoder.cpp ABSOLUTE)
+set_source_files_properties(${_flm_audio_decoder_src} PROPERTIES
+ COMPILE_OPTIONS "$<$>:-fwrapv>")
diff --git a/src/common/audio/audio_decoder.cpp b/src/common/audio/audio_decoder.cpp
new file mode 100644
index 000000000..56023d32e
--- /dev/null
+++ b/src/common/audio/audio_decoder.cpp
@@ -0,0 +1,778 @@
+/// \file audio_decoder.cpp
+/// \brief Container/codec decoding for AudioReader, built on permissively
+/// licensed decoders: dr_wav/dr_flac/dr_mp3 (public domain or MIT-0),
+/// libvorbis/libogg and libopus (BSD-3-Clause).
+/// \author FastFlowLM Team
+
+#include "audio/audio_decoder.hpp"
+
+#include
+#include
+#include
+#include
+#include
+
+#define DR_WAV_IMPLEMENTATION
+#define DR_WAV_NO_STDIO
+#include "dr_libs/dr_wav.h"
+#define DR_FLAC_IMPLEMENTATION
+#define DR_FLAC_NO_STDIO
+#include "dr_libs/dr_flac.h"
+#define DR_MP3_IMPLEMENTATION
+#define DR_MP3_NO_STDIO
+// Decode in float. Without this dr_mp3 synthesizes 16-bit PCM and converts it,
+// which quantizes quiet recordings audibly (~35 dB SNR on 16 kHz speech).
+#define DR_MP3_FLOAT_OUTPUT
+#include "dr_libs/dr_mp3.h"
+
+#include
+#include
+
+namespace audio {
+namespace {
+
+constexpr size_t kChunkFrames = 65536;
+constexpr int kMaxChannels = 64;
+
+const char* const kUnsupportedHint =
+ "supported formats are WAV, FLAC, MP3, Ogg (Vorbis/Opus/FLAC) and WebM/Matroska (Opus)";
+
+uint16_t le16(const uint8_t* p) { return static_cast(p[0] | (p[1] << 8)); }
+uint32_t le32(const uint8_t* p) {
+ return static_cast(p[0]) | (static_cast(p[1]) << 8) |
+ (static_cast(p[2]) << 16) | (static_cast(p[3]) << 24);
+}
+uint64_t le64(const uint8_t* p) { return le32(p) | (static_cast(le32(p + 4)) << 32); }
+
+bool has_bytes(const uint8_t* data, size_t size, size_t offset, const char* magic, size_t len) {
+ return offset + len <= size && std::memcmp(data + offset, magic, len) == 0;
+}
+
+// Real audio sits between telephony (8 kHz) and 384 kHz. Rejecting rates
+// outside a generous range also bounds what resampling can amplify: a header
+// claiming 1 Hz would otherwise turn a 0.5 MB upload into gigabytes at 16 kHz.
+constexpr int kMinSampleRate = 4000;
+constexpr int kMaxSampleRate = 768000;
+
+bool valid_layout(int sample_rate, int channels) {
+ return sample_rate >= kMinSampleRate && sample_rate <= kMaxSampleRate &&
+ channels > 0 && channels <= kMaxChannels;
+}
+
+// ———————————————————————————————————————————————
+// Format detection
+// ———————————————————————————————————————————————
+enum class Format { Wav, Flac, Mp3, Ogg, Matroska, Mp4, Adts, Unknown };
+
+// Size of a leading ID3v2 tag (0 if none).
+size_t id3v2_size(const uint8_t* data, size_t size) {
+ if (size < 10 || !has_bytes(data, size, 0, "ID3", 3)) {
+ return 0;
+ }
+ const size_t body = (static_cast(data[6] & 0x7F) << 21) | (static_cast(data[7] & 0x7F) << 14) |
+ (static_cast(data[8] & 0x7F) << 7) | static_cast(data[9] & 0x7F);
+ const size_t footer = (data[5] & 0x10) ? 10 : 0;
+ return std::min(size, 10 + body + footer);
+}
+
+Format sniff(const uint8_t* data, size_t size, size_t& offset) {
+ offset = 0;
+ static const uint8_t kW64Riff[16] = {'r', 'i', 'f', 'f', 0x2E, 0x91, 0xCF, 0x11,
+ 0xA5, 0xD6, 0x28, 0xDB, 0x04, 0xC1, 0x00, 0x00};
+ if ((has_bytes(data, size, 0, "RIFF", 4) || has_bytes(data, size, 0, "RF64", 4) ||
+ has_bytes(data, size, 0, "BW64", 4)) && has_bytes(data, size, 8, "WAVE", 4)) {
+ return Format::Wav;
+ }
+ if (has_bytes(data, size, 0, "FORM", 4) &&
+ (has_bytes(data, size, 8, "AIFF", 4) || has_bytes(data, size, 8, "AIFC", 4))) {
+ return Format::Wav;
+ }
+ if (size >= 16 && std::memcmp(data, kW64Riff, 16) == 0) {
+ return Format::Wav;
+ }
+ if (has_bytes(data, size, 0, "OggS", 4)) {
+ return Format::Ogg;
+ }
+ if (has_bytes(data, size, 0, "\x1A\x45\xDF\xA3", 4)) {
+ return Format::Matroska;
+ }
+ if (has_bytes(data, size, 4, "ftyp", 4)) {
+ return Format::Mp4;
+ }
+ offset = id3v2_size(data, size);
+ if (has_bytes(data, size, offset, "fLaC", 4)) {
+ return Format::Flac;
+ }
+ if (offset + 2 <= size && data[offset] == 0xFF && (data[offset + 1] & 0xE0) == 0xE0) {
+ // MPEG audio frame sync; layer bits 00 mean ADTS (AAC).
+ return ((data[offset + 1] >> 1) & 0x03) == 0 ? Format::Adts : Format::Mp3;
+ }
+ if (offset > 0) {
+ offset = 0;
+ return Format::Mp3; // ID3-tagged; let dr_mp3 find the first frame.
+ }
+ return Format::Unknown;
+}
+
+// ———————————————————————————————————————————————
+// dr_libs / libvorbis
+// ———————————————————————————————————————————————
+bool decode_wav(const uint8_t* data, size_t size, DecodedAudio& out, std::string& error) {
+ drwav wav;
+ if (!drwav_init_memory(&wav, data, size, nullptr)) {
+ error = "invalid or unsupported WAV/AIFF file";
+ return false;
+ }
+ out.sample_rate = static_cast(wav.sampleRate);
+ out.channels = static_cast(wav.channels);
+ if (!valid_layout(out.sample_rate, out.channels)) {
+ drwav_uninit(&wav);
+ error = "WAV file has an invalid sample rate or channel count";
+ return false;
+ }
+ // dr_wav maps 8-bit PCM as u8/255*2-1; FFmpeg (and libsndfile) use
+ // (u8-128)/128, so convert those samples here to keep the previous values.
+ const bool u8_pcm = wav.translatedFormatTag == DR_WAVE_FORMAT_PCM && wav.bitsPerSample == 8;
+ std::vector chunk(kChunkFrames * out.channels);
+ std::vector raw(u8_pcm ? kChunkFrames * out.channels : 0);
+ for (;;) {
+ const drwav_uint64 n = u8_pcm ? drwav_read_pcm_frames(&wav, kChunkFrames, raw.data())
+ : drwav_read_pcm_frames_f32(&wav, kChunkFrames, chunk.data());
+ if (n == 0) {
+ break;
+ }
+ if (u8_pcm) {
+ for (size_t i = 0; i < n * out.channels; ++i) {
+ chunk[i] = (static_cast(raw[i]) - 128.0f) / 128.0f;
+ }
+ }
+ out.samples.insert(out.samples.end(), chunk.begin(), chunk.begin() + n * out.channels);
+ }
+ drwav_uninit(&wav);
+ return true;
+}
+
+bool decode_flac(const uint8_t* data, size_t size, DecodedAudio& out, std::string& error) {
+ drflac* flac = drflac_open_memory(data, size, nullptr);
+ if (!flac) {
+ error = "invalid FLAC file";
+ return false;
+ }
+ out.sample_rate = static_cast(flac->sampleRate);
+ out.channels = static_cast(flac->channels);
+ if (!valid_layout(out.sample_rate, out.channels)) {
+ drflac_close(flac);
+ error = "FLAC file has an invalid sample rate or channel count";
+ return false;
+ }
+ std::vector chunk(kChunkFrames * out.channels);
+ for (;;) {
+ const drflac_uint64 n = drflac_read_pcm_frames_f32(flac, kChunkFrames, chunk.data());
+ if (n == 0) {
+ break;
+ }
+ out.samples.insert(out.samples.end(), chunk.begin(), chunk.begin() + n * out.channels);
+ }
+ drflac_close(flac);
+ return true;
+}
+
+bool decode_mp3(const uint8_t* data, size_t size, DecodedAudio& out, std::string& error) {
+ drmp3 mp3;
+ if (!drmp3_init_memory(&mp3, data, size, nullptr)) {
+ error = "invalid MP3 file";
+ return false;
+ }
+ out.sample_rate = static_cast(mp3.sampleRate);
+ out.channels = static_cast(mp3.channels);
+ if (!valid_layout(out.sample_rate, out.channels)) {
+ drmp3_uninit(&mp3);
+ error = "MP3 file has an invalid sample rate or channel count";
+ return false;
+ }
+ std::vector chunk(kChunkFrames * out.channels);
+ for (;;) {
+ const drmp3_uint64 n = drmp3_read_pcm_frames_f32(&mp3, kChunkFrames, chunk.data());
+ if (n == 0) {
+ break;
+ }
+ out.samples.insert(out.samples.end(), chunk.begin(), chunk.begin() + n * out.channels);
+ }
+ drmp3_uninit(&mp3);
+ return true;
+}
+
+// vorbisfile reads through callbacks; serve them from the in-memory file.
+struct MemoryFile {
+ const uint8_t* data;
+ size_t size;
+ size_t pos;
+};
+
+size_t memory_read(void* ptr, size_t size, size_t nmemb, void* source) {
+ auto* f = static_cast(source);
+ if (size == 0) {
+ return 0;
+ }
+ const size_t n = std::min(nmemb, (f->size - f->pos) / size);
+ std::memcpy(ptr, f->data + f->pos, n * size);
+ f->pos += n * size;
+ return n;
+}
+
+int memory_seek(void* source, ogg_int64_t offset, int whence) {
+ auto* f = static_cast(source);
+ int64_t base = 0;
+ if (whence == SEEK_CUR) {
+ base = static_cast(f->pos);
+ } else if (whence == SEEK_END) {
+ base = static_cast(f->size);
+ } else if (whence != SEEK_SET) {
+ return -1;
+ }
+ const int64_t target = base + offset;
+ if (target < 0 || target > static_cast(f->size)) {
+ return -1;
+ }
+ f->pos = static_cast(target);
+ return 0;
+}
+
+long memory_tell(void* source) { return static_cast(static_cast(source)->pos); }
+
+bool decode_vorbis(const uint8_t* data, size_t size, DecodedAudio& out, std::string& error) {
+ MemoryFile file{data, size, 0};
+ const ov_callbacks callbacks{memory_read, memory_seek, nullptr, memory_tell};
+ OggVorbis_File vf;
+ if (ov_open_callbacks(&file, &vf, nullptr, 0, callbacks) < 0) {
+ error = "invalid Ogg Vorbis file";
+ return false;
+ }
+ const vorbis_info* info = ov_info(&vf, -1);
+ out.sample_rate = info ? static_cast(info->rate) : 0;
+ out.channels = info ? info->channels : 0;
+ if (!valid_layout(out.sample_rate, out.channels)) {
+ ov_clear(&vf);
+ error = "Ogg Vorbis file has an invalid sample rate or channel count";
+ return false;
+ }
+ int first_section = -1;
+ for (;;) {
+ float** pcm = nullptr;
+ int section = 0;
+ const long n = ov_read_float(&vf, &pcm, 4096, §ion);
+ if (n == OV_HOLE) {
+ continue; // damaged data; keep going, as FFmpeg does
+ }
+ if (n <= 0) {
+ break;
+ }
+ if (first_section < 0) {
+ first_section = section;
+ } else if (section != first_section) {
+ break; // chained stream: decode the first link only
+ }
+ const size_t base = out.samples.size();
+ out.samples.resize(base + static_cast(n) * out.channels);
+ for (long i = 0; i < n; ++i) {
+ for (int c = 0; c < out.channels; ++c) {
+ out.samples[base + static_cast(i) * out.channels + c] = pcm[c][i];
+ }
+ }
+ }
+ ov_clear(&vf);
+ return true;
+}
+
+// ———————————————————————————————————————————————
+// Opus (RFC 7845 header; packets from Ogg or Matroska)
+// ———————————————————————————————————————————————
+struct OpusPacket {
+ std::vector data;
+ int64_t discard_end = 0; // samples (48 kHz) to drop from the end of this packet's output
+};
+
+bool decode_opus(const std::vector& head, const std::vector& packets,
+ int64_t total_samples, DecodedAudio& out, std::string& error) {
+ if (head.size() < 19 || std::memcmp(head.data(), "OpusHead", 8) != 0 || (head[8] >> 4) != 0) {
+ error = "invalid Opus header";
+ return false;
+ }
+ const int channels = head[9];
+ const int pre_skip = le16(&head[10]);
+ const int16_t gain_q8 = static_cast(le16(&head[16]));
+ const int family = head[18];
+
+ int streams = 1;
+ int coupled = channels > 1 ? 1 : 0;
+ unsigned char mapping[255] = {0, 1};
+ if (family == 0) {
+ if (channels < 1 || channels > 2) {
+ error = "invalid Opus channel count";
+ return false;
+ }
+ } else if (family == 1 || family == 255) {
+ if (channels < 1 || head.size() < 21u + channels) {
+ error = "invalid Opus channel mapping";
+ return false;
+ }
+ streams = head[19];
+ coupled = head[20];
+ std::memcpy(mapping, &head[21], channels);
+ } else {
+ error = "unsupported Opus channel mapping family " + std::to_string(family);
+ return false;
+ }
+
+ int err = 0;
+ std::unique_ptr dec(
+ opus_multistream_decoder_create(48000, channels, streams, coupled, mapping, &err),
+ opus_multistream_decoder_destroy);
+ if (!dec || err != OPUS_OK) {
+ error = std::string("could not create Opus decoder: ") + opus_strerror(err);
+ return false;
+ }
+
+ constexpr int kMaxFrame = 5760; // 120 ms at 48 kHz
+ std::vector pcm(static_cast(kMaxFrame) * channels);
+ for (const OpusPacket& p : packets) {
+ if (p.data.empty() || p.data.size() > static_cast(INT32_MAX)) {
+ continue;
+ }
+ int n = opus_multistream_decode_float(dec.get(), p.data.data(), static_cast(p.data.size()),
+ pcm.data(), kMaxFrame, 0);
+ if (n < 0) {
+ continue; // skip a corrupt packet, as FFmpeg does
+ }
+ n -= static_cast(std::min(std::max(p.discard_end, 0), n));
+ out.samples.insert(out.samples.end(), pcm.begin(), pcm.begin() + static_cast(n) * channels);
+ }
+
+ const size_t skip = std::min(out.samples.size(), static_cast(pre_skip) * channels);
+ out.samples.erase(out.samples.begin(), out.samples.begin() + skip);
+ if (total_samples >= 0 && static_cast(total_samples) * channels < out.samples.size()) {
+ out.samples.resize(static_cast(total_samples) * channels);
+ }
+ if (gain_q8 != 0) {
+ const float gain = static_cast(std::pow(10.0, gain_q8 / (20.0 * 256.0)));
+ for (float& s : out.samples) {
+ s *= gain;
+ }
+ }
+ out.sample_rate = 48000;
+ out.channels = channels;
+ return true;
+}
+
+// ———————————————————————————————————————————————
+// Ogg (RFC 3533): packets of the first logical stream
+// ———————————————————————————————————————————————
+struct OggStream {
+ std::vector> packets;
+ std::vector packet_page; // page on which each packet completes
+ std::vector page_granule; // granule position of each page of the stream
+ int64_t last_granule = -1;
+};
+
+bool read_ogg_stream(const uint8_t* data, size_t size, OggStream& stream) {
+ int64_t serial = -1;
+ std::vector partial;
+ size_t pos = 0;
+ while (pos + 27 <= size) {
+ if (std::memcmp(data + pos, "OggS", 4) != 0 || data[pos + 4] != 0) {
+ break;
+ }
+ const uint8_t header_type = data[pos + 5];
+ const int64_t granule = static_cast(le64(data + pos + 6));
+ const uint32_t page_serial = le32(data + pos + 14);
+ const size_t segments = data[pos + 26];
+ const size_t body = pos + 27 + segments;
+ if (body > size) {
+ break;
+ }
+ size_t body_len = 0;
+ for (size_t i = 0; i < segments; ++i) {
+ body_len += data[pos + 27 + i];
+ }
+ if (body + body_len > size) {
+ break;
+ }
+ if (serial < 0 && (header_type & 0x02)) {
+ serial = page_serial;
+ }
+ if (static_cast(page_serial) == serial) {
+ const size_t page = stream.page_granule.size();
+ stream.page_granule.push_back(granule);
+ size_t off = body;
+ for (size_t i = 0; i < segments; ++i) {
+ const size_t len = data[pos + 27 + i];
+ partial.insert(partial.end(), data + off, data + off + len);
+ off += len;
+ if (len < 255) {
+ stream.packets.push_back(std::move(partial));
+ stream.packet_page.push_back(page);
+ partial.clear();
+ }
+ }
+ if (granule != -1) {
+ stream.last_granule = granule;
+ }
+ }
+ pos = body + body_len;
+ }
+ return !stream.packets.empty();
+}
+
+bool decode_ogg(const uint8_t* data, size_t size, DecodedAudio& out, std::string& error) {
+ OggStream stream;
+ if (!read_ogg_stream(data, size, stream)) {
+ error = "invalid Ogg file";
+ return false;
+ }
+ const std::vector& first = stream.packets.front();
+ if (first.size() >= 7 && std::memcmp(first.data(), "\x01vorbis", 7) == 0) {
+ return decode_vorbis(data, size, out, error);
+ }
+ if (first.size() >= 5 && std::memcmp(first.data(), "\x7F""FLAC", 5) == 0) {
+ return decode_flac(data, size, out, error);
+ }
+ if (first.size() >= 8 && std::memcmp(first.data(), "OpusHead", 8) == 0) {
+ // Packet 0 is OpusHead, packet 1 OpusTags; audio follows. Playback
+ // length (RFC 7845, 4.3-4.5): last granule minus the stream's start
+ // position minus pre-skip, where the start position is the first audio
+ // page's granule minus the samples of the packets completing on it.
+ const int pre_skip = first.size() >= 12 ? le16(&first[10]) : 0;
+ int64_t start = 0;
+ if (stream.packets.size() > 2) {
+ const size_t first_page = stream.packet_page[2];
+ int64_t first_samples = 0;
+ for (size_t i = 2; i < stream.packets.size() && stream.packet_page[i] == first_page; ++i) {
+ const std::vector& pkt = stream.packets[i];
+ const int n = pkt.empty() ? 0 : opus_packet_get_nb_samples(pkt.data(), static_cast(pkt.size()), 48000);
+ first_samples += std::max(n, 0);
+ }
+ const int64_t granule = stream.page_granule[first_page];
+ start = granule >= 0 ? std::max(granule - first_samples, 0) : 0;
+ }
+ const int64_t total = stream.last_granule >= 0 ? stream.last_granule - start - pre_skip : -1;
+
+ std::vector packets;
+ for (size_t i = 2; i < stream.packets.size(); ++i) {
+ packets.push_back({std::move(stream.packets[i]), 0});
+ }
+ return decode_opus(first, packets, total >= 0 ? total : -1, out, error);
+ }
+ error = std::string("unsupported codec in Ogg file; ") + kUnsupportedHint;
+ return false;
+}
+
+// ———————————————————————————————————————————————
+// Matroska / WebM: Opus audio track only
+// ———————————————————————————————————————————————
+namespace ebml {
+constexpr uint32_t kSegment = 0x18538067, kTracks = 0x1654AE6B, kTrackEntry = 0xAE, kTrackNumber = 0xD7,
+ kTrackType = 0x83, kCodecId = 0x86, kCodecPrivate = 0x63A2, kCluster = 0x1F43B675,
+ kSimpleBlock = 0xA3, kBlockGroup = 0xA0, kBlock = 0xA1, kDiscardPadding = 0x75A2;
+
+// Element ID: 1-4 bytes, length marker kept.
+bool read_id(const uint8_t* d, size_t n, size_t& pos, uint32_t& id) {
+ if (pos >= n || d[pos] == 0) {
+ return false;
+ }
+ int len = 1;
+ while (len <= 4 && !(d[pos] & (0x80 >> (len - 1)))) {
+ ++len;
+ }
+ if (len > 4 || pos + len > n) {
+ return false;
+ }
+ id = 0;
+ for (int i = 0; i < len; ++i) {
+ id = (id << 8) | d[pos + i];
+ }
+ pos += len;
+ return true;
+}
+
+// Variable-size integer: 1-8 bytes, length marker removed. `unknown` is set
+// when all value bits are 1 (unknown element size).
+bool read_vint(const uint8_t* d, size_t n, size_t& pos, uint64_t& value, bool* unknown = nullptr, int* length = nullptr) {
+ if (pos >= n || d[pos] == 0) {
+ return false;
+ }
+ int len = 1;
+ while (!(d[pos] & (0x80 >> (len - 1)))) {
+ ++len;
+ }
+ if (pos + len > n) {
+ return false;
+ }
+ value = d[pos] & (0xFF >> len);
+ bool all_ones = value == (0xFFu >> len);
+ for (int i = 1; i < len; ++i) {
+ value = (value << 8) | d[pos + i];
+ all_ones = all_ones && d[pos + i] == 0xFF;
+ }
+ if (unknown) {
+ *unknown = all_ones;
+ }
+ if (length) {
+ *length = len;
+ }
+ pos += len;
+ return true;
+}
+
+uint64_t read_uint(const uint8_t* d, size_t len) {
+ uint64_t v = 0;
+ for (size_t i = 0; i < len && i < 8; ++i) {
+ v = (v << 8) | d[i];
+ }
+ return v;
+}
+
+int64_t read_sint(const uint8_t* d, size_t len) {
+ if (len == 0 || len > 8) {
+ return 0;
+ }
+ uint64_t v = read_uint(d, len);
+ if (d[0] & 0x80) {
+ v |= len == 8 ? 0 : (~uint64_t{0} << (8 * len));
+ }
+ return static_cast(v);
+}
+
+// Split a (Simple)Block payload into frames for `track` (RFC 9559, 10.2).
+bool block_frames(const uint8_t* d, size_t n, uint64_t track, std::vector>& frames) {
+ size_t pos = 0;
+ uint64_t number = 0;
+ if (!read_vint(d, n, pos, number) || pos + 3 > n) {
+ return false;
+ }
+ if (number != track) {
+ return true;
+ }
+ const uint8_t flags = d[pos + 2];
+ pos += 3;
+ const int lacing = (flags >> 1) & 0x03;
+ if (lacing == 0) {
+ frames.emplace_back(d + pos, d + n);
+ return true;
+ }
+ if (pos >= n) {
+ return false;
+ }
+ const size_t count = static_cast(d[pos++]) + 1;
+ std::vector sizes;
+ if (lacing == 1) { // Xiph
+ for (size_t i = 0; i + 1 < count; ++i) {
+ uint64_t s = 0;
+ uint8_t b = 0;
+ do {
+ if (pos >= n) {
+ return false;
+ }
+ b = d[pos++];
+ s += b;
+ } while (b == 255);
+ sizes.push_back(s);
+ }
+ } else if (lacing == 3) { // EBML
+ uint64_t s = 0;
+ if (count > 1) {
+ if (!read_vint(d, n, pos, s)) {
+ return false;
+ }
+ sizes.push_back(s);
+ }
+ for (size_t i = 1; i + 1 < count; ++i) {
+ uint64_t raw = 0;
+ int len = 0;
+ if (!read_vint(d, n, pos, raw, nullptr, &len)) {
+ return false;
+ }
+ const int64_t delta = static_cast(raw) - ((int64_t{1} << (7 * len - 1)) - 1);
+ const int64_t next = static_cast(s) + delta;
+ if (next < 0) {
+ return false;
+ }
+ s = static_cast(next);
+ sizes.push_back(s);
+ }
+ } else { // fixed
+ const size_t each = (n - pos) / count;
+ if (each * count != n - pos) {
+ return false;
+ }
+ sizes.assign(count - 1, each);
+ }
+ for (uint64_t s : sizes) {
+ if (s > n - pos) {
+ return false;
+ }
+ frames.emplace_back(d + pos, d + pos + s);
+ pos += static_cast(s);
+ }
+ frames.emplace_back(d + pos, d + n);
+ return true;
+}
+} // namespace ebml
+
+bool decode_matroska(const uint8_t* data, size_t size, DecodedAudio& out, std::string& error) {
+ struct Track {
+ uint64_t number = 0;
+ uint64_t type = 0;
+ std::string codec;
+ std::vector codec_private;
+ };
+ std::vector