Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ function(collect_sources)
file(GLOB RUBBERBAND_SOURCES "vendors/rubberband/single/*.cpp")

# Collect general vendor sources
file(GLOB VENDOR_SOURCES "vendors/*.c")
file(GLOB VENDOR_SOURCES "vendors/*.c" "vendors/*.cpp")

# LAME/mpglib sources
file(GLOB LAME_SOURCES "vendors/lame/libmp3lame/*.c" "vendors/lame/libmp3lame/vector/*.c" "vendors/lame/mpglib/*.c")
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,5 +314,6 @@ To cite via BibTeX:
- The `PitchShift` plugin and `time_stretch` functions use [the Rubber Band Library](https://github.com/breakfastquay/rubberband), which is [dual-licensed under a commercial license](https://breakfastquay.com/technology/license.html) and the GPLv2 (or newer). [FFTW](https://www.fftw.org/) is also included to speed up Rubber Band, and [is licensed under the GPLv2 (or newer)](https://www.fftw.org/doc/License-and-Copyright.html).
- The `MP3Compressor` plugin uses [libmp3lame from the LAME project](https://lame.sourceforge.io/), which is [licensed under the LGPLv2](https://github.com/lameproject/lame/blob/master/README) and [upgraded to the GPLv3 for inclusion in this project (as permitted by the LGPLv2)](https://www.gnu.org/licenses/gpl-faq.html#AllCompatibility).
- The `GSMFullRateCompressor` plugin uses [libgsm](http://quut.com/gsm/), which is [licensed under the ISC license](https://github.com/timothytylee/libgsm/blob/master/COPYRIGHT) and [compatible with the GPLv3](https://www.gnu.org/licenses/license-list.en.html#ISC).
- WAV files with formats not natively supported by JUCE (ADPCM, A-law, µ-law, 64-bit float) are decoded using [dr_wav](https://github.com/mackron/dr_libs) by David Reid, which is released into the [public domain](https://unlicense.org/).

_VST is a registered trademark of Steinberg Media Technologies GmbH._
6 changes: 6 additions & 0 deletions pedalboard/io/ReadableAudioFile.h
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,12 @@ class ReadableAudioFile
throw std::runtime_error("I/O operation on a closed file.");

if (reader->usesFloatingPointData) {
if (reader->bitsPerSample > 32) {
throw std::runtime_error(
"This file contains " + std::to_string(reader->bitsPerSample) +
"-bit floating-point audio, which cannot be returned without "
"losing precision. Use read() instead to get 32-bit float data.");
}
return read(numSamples);
} else {
switch (reader->bitsPerSample) {
Expand Down
231 changes: 223 additions & 8 deletions pedalboard/juce_overrides/juce_PatchedWavAudioFormat.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
#include "../JuceHeader.h"
#include "juce_PatchedMP3AudioFormat.h"

// dr_wav for ADPCM decoding (public domain / MIT-0)
#include "dr_wav.h"
#include "dr_wav_config.h"

namespace juce {

/**
Expand All @@ -39,12 +43,179 @@ enum class WavFormatTag : unsigned short {
Extensible = 0xFFFE,
};

/**
* An AudioFormatReader that uses dr_wav to decode audio formats not natively
* supported by JUCE: ADPCM (MS and IMA), A-law, µ-law, and 64-bit float.
* This streams the audio data rather than loading it all into memory.
*/
class DrWavAudioFormatReader : public AudioFormatReader {
public:
DrWavAudioFormatReader(InputStream *stream)
: AudioFormatReader(stream, "dr_wav"), inputStream(stream) {

// Initialize dr_wav with our I/O callbacks
if (!drwav_init(&wav, drwavReadCallback, drwavSeekCallback,
drwavTellCallback, this, nullptr)) {
// Failed to initialize - set invalid state
sampleRate = 0;
numChannels = 0;
lengthInSamples = 0;
return;
}

wavInitialized = true;

// Set up AudioFormatReader properties
sampleRate = static_cast<double>(wav.sampleRate);
numChannels = static_cast<unsigned int>(wav.channels);
lengthInSamples = static_cast<int64>(wav.totalPCMFrameCount);

// For IEEE float formats, report the original bits per sample (32 or 64)
// For other formats (ADPCM, A-law, µ-law), we decode to float32
if (wav.translatedFormatTag == DR_WAVE_FORMAT_IEEE_FLOAT) {
bitsPerSample = static_cast<unsigned int>(wav.fmt.bitsPerSample);
usesFloatingPointData = true;
} else {
// ADPCM, A-law, µ-law are decoded to float32
bitsPerSample = 32;
usesFloatingPointData = true;
}
}

~DrWavAudioFormatReader() override {
if (wavInitialized) {
drwav_uninit(&wav);
}
// Note: inputStream is owned by the base AudioFormatReader class
// and will be deleted in its destructor
}

bool readSamples(int **destChannels, int numDestChannels,
int startOffsetInDestBuffer, int64 startSampleInFile,
int numSamples) override {
if (!wavInitialized || numSamples <= 0) {
return false;
}

// Seek to the requested position if needed
if (startSampleInFile != currentPosition) {
if (!drwav_seek_to_pcm_frame(
&wav, static_cast<drwav_uint64>(startSampleInFile))) {
// Seek failed - fill with zeros
clearSamplesBeyondFile(destChannels, numDestChannels,
startOffsetInDestBuffer, numSamples, 0);
return true;
}
currentPosition = startSampleInFile;
}

// Handle reading before start of file
if (startSampleInFile < 0) {
auto samplesToZero =
static_cast<int>(std::min(-startSampleInFile, (int64)numSamples));
clearSamplesBeyondFile(destChannels, numDestChannels,
startOffsetInDestBuffer, samplesToZero, 0);
startOffsetInDestBuffer += samplesToZero;
numSamples -= samplesToZero;
if (numSamples <= 0)
return true;
startSampleInFile = 0;
currentPosition = 0;
drwav_seek_to_pcm_frame(&wav, 0);
}

// Allocate interleaved buffer for dr_wav output
auto totalChannels = static_cast<int>(wav.channels);
std::vector<float> interleavedBuffer(
static_cast<size_t>(numSamples * totalChannels));

// Read decoded samples
auto framesRead = drwav_read_pcm_frames_f32(
&wav, static_cast<drwav_uint64>(numSamples), interleavedBuffer.data());

currentPosition += static_cast<int64>(framesRead);

// De-interleave into destination channels
auto samplesRead = static_cast<int>(framesRead);
for (int ch = 0; ch < numDestChannels; ++ch) {
if (destChannels[ch] != nullptr) {
auto *dest = reinterpret_cast<float *>(destChannels[ch]) +
startOffsetInDestBuffer;

if (ch < totalChannels) {
// Copy from interleaved source
for (int i = 0; i < samplesRead; ++i) {
dest[i] =
interleavedBuffer[static_cast<size_t>(i * totalChannels + ch)];
}
} else {
// Channel doesn't exist in source - zero fill
std::fill(dest, dest + samplesRead, 0.0f);
}

// Zero any samples beyond what we read
if (samplesRead < numSamples) {
std::fill(dest + samplesRead, dest + numSamples, 0.0f);
}
}
}

return true;
}

private:
drwav wav{};
bool wavInitialized = false;
InputStream *inputStream; // Borrowed reference (owned by base class)
int64 currentPosition = 0;

void clearSamplesBeyondFile(int **destChannels, int numDestChannels,
int startOffset, int numSamples,
int samplesRead) {
for (int ch = 0; ch < numDestChannels; ++ch) {
if (destChannels[ch] != nullptr) {
auto *dest = reinterpret_cast<float *>(destChannels[ch]) + startOffset +
samplesRead;
std::fill(dest, dest + (numSamples - samplesRead), 0.0f);
}
}
}

// dr_wav I/O callbacks - bridge to JUCE InputStream
static size_t drwavReadCallback(void *pUserData, void *pBufferOut,
size_t bytesToRead) {
auto *reader = static_cast<DrWavAudioFormatReader *>(pUserData);
return static_cast<size_t>(
reader->inputStream->read(pBufferOut, static_cast<int>(bytesToRead)));
}

static drwav_bool32 drwavSeekCallback(void *pUserData, int offset,
drwav_seek_origin origin) {
auto *reader = static_cast<DrWavAudioFormatReader *>(pUserData);
int64 newPos;
if (origin == DRWAV_SEEK_SET) {
newPos = offset;
} else if (origin == DRWAV_SEEK_CUR) {
newPos = reader->inputStream->getPosition() + offset;
} else { // DRWAV_SEEK_END
newPos = reader->inputStream->getTotalLength() + offset;
}
return reader->inputStream->setPosition(newPos) ? DRWAV_TRUE : DRWAV_FALSE;
}

static drwav_bool32 drwavTellCallback(void *pUserData, drwav_int64 *pCursor) {
auto *reader = static_cast<DrWavAudioFormatReader *>(pUserData);
*pCursor = static_cast<drwav_int64>(reader->inputStream->getPosition());
return DRWAV_TRUE;
}
};

/**
* A patched version of WavAudioFormat that adds support for WAV files
* containing compressed audio data (e.g.: WAVE_FORMAT_MPEGLAYER3).
*
* These files are valid WAV files that use MP3 compression for the audio
* data, wrapped in a standard RIFF/WAV container.
* containing compressed audio data:
* - WAVE_FORMAT_MPEGLAYER3 (MP3 in WAV container)
* - WAVE_FORMAT_ADPCM (Microsoft ADPCM)
* - WAVE_FORMAT_DVI_ADPCM (IMA ADPCM)
*/
class JUCE_API PatchedWavAudioFormat : public WavAudioFormat {
public:
Expand Down Expand Up @@ -91,6 +262,28 @@ class JUCE_API PatchedWavAudioFormat : public WavAudioFormat {
return createMP3ReaderForWav(sourceStream, chunkEnd, streamStartPos,
deleteStreamIfOpeningFails);

case WavFormatTag::ADPCM:
case WavFormatTag::IMAADPCM:
case WavFormatTag::ALaw:
case WavFormatTag::MuLaw:
return createDrWavReaderForWav(sourceStream, streamStartPos,
deleteStreamIfOpeningFails);

case WavFormatTag::IEEEFloat: {
// JUCE doesn't support 64-bit float WAV, but dr_wav does.
// Read bitsPerSample from fmt chunk to check.
// fmt chunk layout after formatTag: channels(2), sampleRate(4),
// byteRate(4), blockAlign(2), bitsPerSample(2)
sourceStream->skipNextBytes(12);
auto bitsPerSample = (unsigned short)sourceStream->readShort();
if (bitsPerSample == 64) {
return createDrWavReaderForWav(sourceStream, streamStartPos,
deleteStreamIfOpeningFails);
}
// 32-bit float is handled fine by JUCE
return useDefaultReader();
}

default:
// Check for known-but-unsupported formats and throw helpful errors
const char *unsupportedCodecName = getUnsupportedCodecName(format);
Expand Down Expand Up @@ -156,6 +349,32 @@ class JUCE_API PatchedWavAudioFormat : public WavAudioFormat {
return nullptr;
}

/**
* Creates a dr_wav-based reader for WAV files containing compressed audio
* that JUCE doesn't natively support (ADPCM, A-law, µ-law).
* Uses dr_wav to decode the audio data on-the-fly (streaming).
*/
AudioFormatReader *createDrWavReaderForWav(InputStream *sourceStream,
int64 streamStartPos,
bool deleteStreamIfOpeningFails) {
// Reset to start for dr_wav to parse the WAV header
sourceStream->setPosition(streamStartPos);

// Create the streaming reader
auto reader = std::make_unique<DrWavAudioFormatReader>(sourceStream);

// Check if initialization succeeded
if (reader->sampleRate == 0) {
// Failed to initialize - dr_wav couldn't parse the file.
// Note: we don't need to delete the stream here even if
// deleteStreamIfOpeningFails is true, because the reader's destructor
// will handle it via the base class AudioFormatReader.
return nullptr;
}

return reader.release();
}

static constexpr int chunkName(const char *name) noexcept {
return (int)ByteOrder::littleEndianInt(name);
}
Expand All @@ -174,11 +393,7 @@ class JUCE_API PatchedWavAudioFormat : public WavAudioFormat {
// clang-format off
// Format tags from mmreg.h / RFC 2361: https://www.rfc-editor.org/rfc/rfc2361.html
switch (format) {
case 0x0002: return "Microsoft ADPCM";
case 0x0006: return "A-law";
case 0x0007: return "mu-law (u-law)";
case 0x0010: return "OKI ADPCM";
case 0x0011: return "IMA ADPCM (DVI ADPCM)";
case 0x0012: return "MediaSpace ADPCM";
case 0x0013: return "Sierra ADPCM";
case 0x0014: return "G.723 ADPCM";
Expand Down
Binary file added tests/audio/correct/adpcm_ima.wav
Binary file not shown.
Binary file added tests/audio/correct/adpcm_ms.wav
Binary file not shown.
Binary file added tests/audio/correct/alaw.wav
Binary file not shown.
Binary file added tests/audio/correct/float64.wav
Binary file not shown.
Binary file added tests/audio/correct/mulaw.wav
Binary file not shown.
43 changes: 43 additions & 0 deletions tests/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -1310,6 +1310,49 @@ def test_mp3_in_wav_format():
assert np.amax(np.abs(audio)) > 0.1 # Should have actual audio content


Comment thread
psobot marked this conversation as resolved.
@pytest.mark.parametrize(
"filename,expected_dtype",
[
("adpcm_ms.wav", "float32"), # Microsoft ADPCM (format tag 0x0002)
("adpcm_ima.wav", "float32"), # IMA ADPCM (format tag 0x0011)
("alaw.wav", "float32"), # A-law (format tag 0x0006)
("mulaw.wav", "float32"), # µ-law (format tag 0x0007)
("float64.wav", "float64"), # 64-bit float (format tag 0x0003, not supported by JUCE)
],
)
def test_wav_formats_via_drwav(filename: str, expected_dtype: str):
"""
Test reading WAV files with formats not natively supported by JUCE,
decoded via dr_wav: ADPCM, A-law, µ-law, and 64-bit float.

These are valid WAV files using formats common in telephony, older audio
software, embedded systems, and high-precision audio applications.
"""
filepath = os.path.join(os.path.dirname(__file__), "audio", "correct", filename)
with pedalboard.io.AudioFile(filepath) as f:
assert f.samplerate == 44100
assert f.num_channels == 1
assert f.frames >= 44100 # At least 1 second of audio
assert f.file_dtype == expected_dtype

# Read the audio and verify it's not silent
audio = f.read(f.frames)
assert audio.shape[0] == 1
assert np.amax(np.abs(audio)) > 0.01 # Should have actual audio content


def test_float64_read_raw_raises():
"""
Test that read_raw() raises an informative exception for 64-bit float WAV files,
since returning the raw data would lose precision (dr_wav decodes to float32).
"""
filepath = os.path.join(os.path.dirname(__file__), "audio", "correct", "float64.wav")
with pedalboard.io.AudioFile(filepath) as f:
assert f.file_dtype == "float64"
with pytest.raises(RuntimeError, match="64-bit floating-point"):
f.read_raw(1024)


@pytest.mark.parametrize("samplerate", [44100, 32000])
@pytest.mark.parametrize("chunk_size", [1, 2, 16])
@pytest.mark.parametrize("target_samplerate", [44100, 32000, 22050, 1234.56])
Expand Down
Loading
Loading