Skip to content
Open
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
41 changes: 41 additions & 0 deletions pedalboard/process.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#pragma once
#include "JuceHeader.h"

#include <cmath>
#include <pybind11/numpy.h>
#include <pybind11/pybind11.h>

Expand All @@ -29,6 +30,44 @@ namespace py = pybind11;

namespace Pedalboard {

/**
* Check if the given audio buffer looks like it contains integer-valued
* samples (e.g.: raw 16-bit PCM values like -32768 to 32767) rather than
* the expected floating-point samples in the [-1, 1] range.
*
* The heuristic: if EVERY sample is an exact integer (f == std::trunc(f))
* AND at least one sample falls outside [-1, 1], the user almost certainly
* passed unconverted integer data.
*/
inline void
throwIfInputLooksLikeIntegerSamples(const juce::AudioBuffer<float> &buffer) {
bool allInteger = true;
bool anyOutsideUnitRange = false;

for (int c = 0; c < buffer.getNumChannels() && allInteger; c++) {
const float *data = buffer.getReadPointer(c);
for (int s = 0; s < buffer.getNumSamples(); s++) {
float f = data[s];
if (f != std::trunc(f)) {
allInteger = false;
break;
}
if (f < -1.0f || f > 1.0f) {
anyOutsideUnitRange = true;
}
}
}

if (allInteger && anyOutsideUnitRange) {
throw std::domain_error(
"The provided audio data looks like it contains integer samples "
"(all values are whole numbers, with at least one outside the "
"[-1, 1] range). Pedalboard expects floating-point audio samples "
"in the range [-1.0, 1.0]. If your audio is 16-bit integer data, "
"divide by 32768.0; if 32-bit integer, divide by 2147483648.0.");
}
}

inline int process(juce::AudioBuffer<float> &ioBuffer,
juce::dsp::ProcessSpec spec,
const std::vector<std::shared_ptr<Plugin>> &plugins,
Expand Down Expand Up @@ -174,6 +213,8 @@ processFloat32(const py::array_t<float, py::array::c_style> inputArray,
juce::AudioBuffer<float> ioBuffer =
copyPyArrayIntoJuceBuffer(inputArray, {inputChannelLayout});

throwIfInputLooksLikeIntegerSamples(ioBuffer);

if (ioBuffer.getNumChannels() == 0) {
unsigned int numChannels = 0;
unsigned int numSamples = ioBuffer.getNumSamples();
Expand Down
39 changes: 39 additions & 0 deletions tests/test_python_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,42 @@ def test_is_list_like():

with pytest.raises(TypeError):
pb[0] = "not a plugin" # type: ignore


def test_error_on_integer_samples_outside_unit_range(sr=44100):
"""Passing integer-valued samples that exceed [-1, 1] should raise a
descriptive error telling the user to convert to float."""
# Simulate raw 16-bit PCM data cast to float32
int_samples = np.array([-32768, -16384, 0, 16384, 32767], dtype=np.float32)
with pytest.raises(Exception, match="integer samples"):
Pedalboard([Gain(0)]).process(int_samples, sr)


def test_error_on_integer_samples_2d(sr=44100):
"""Same check should trigger for 2-D (multi-channel) arrays."""
int_samples = np.array([[0, 100, 200, -300]], dtype=np.float32)
with pytest.raises(Exception, match="integer samples"):
Pedalboard([Gain(0)]).process(int_samples, sr)


def test_no_error_on_silence(sr=44100):
"""All-zero (silence) is integer-valued but within [-1, 1], so it
must NOT trigger the integer-sample check."""
silence = np.zeros(44100, dtype=np.float32)
output = Pedalboard([Gain(0)]).process(silence, sr)
assert output.shape == silence.shape


def test_no_error_on_normal_float_audio(sr=44100):
"""Normal float audio in [-1, 1] should process without error."""
audio = np.random.uniform(-1.0, 1.0, 44100).astype(np.float32)
output = Pedalboard([Gain(0)]).process(audio, sr)
assert output.shape == audio.shape


def test_no_error_on_quiet_integer_valued_audio(sr=44100):
"""Integer-valued samples that stay within [-1, 1] (e.g. 0, 1, -1)
should NOT trigger the check, to avoid false positives."""
quiet = np.array([0.0, 1.0, -1.0, 0.0, 1.0], dtype=np.float32)
output = Pedalboard([Gain(0)]).process(quiet, sr)
assert output.shape == quiet.shape
Loading