Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
3b776c2
Add AudioFile with buggy streaming support.
psobot Mar 5, 2022
2e3ca23
Remove stream functionality and added tests for file reading.
psobot Mar 5, 2022
f706c2b
Move m4a tests to correct directory.
psobot Mar 5, 2022
6cea690
Add tests for empty/unsupported files.
psobot Mar 6, 2022
5fc7431
Add more test files.
psobot Mar 6, 2022
6c0938c
Add ReadableAudioFile and WriteableAudioFile subclasses.
psobot Mar 7, 2022
1e809d9
Fix tests and add integer format support.
psobot Mar 8, 2022
e5da724
Add logging to try to track down StackOverflow on Windows.
psobot Mar 8, 2022
bb62cc0
Simplify write codepath.
psobot Mar 8, 2022
b489d11
Prevent MP3 decoder from being over-eager.
psobot Mar 8, 2022
2736566
Add tests to ensure audio fixtures are read properly.
psobot Mar 8, 2022
694dc73
Prevent crash when reading bad file.
psobot Mar 8, 2022
02b855e
Prevent garbage being returned when reading certain formats.
psobot Mar 8, 2022
9404337
Switch from AIFF to AIFC test fixtures.
psobot Mar 8, 2022
37a611a
Change tolerance for AIFC files.
psobot Mar 8, 2022
28d58a7
Fix tolerance on MP3 for testing.
psobot Mar 8, 2022
862d82a
Try to track down nasty bug on Windows when writing int32.
psobot Mar 9, 2022
215591e
Add bit_depth parameter to writer.
psobot Mar 9, 2022
d870a25
Fix inaccuracy when reading integer-valued input formats.
psobot Mar 9, 2022
6563073
Add read_raw.
psobot Mar 9, 2022
a883693
Add tests for writing unsupported formats.
psobot Mar 9, 2022
83f2dea
Formatting.
psobot Mar 9, 2022
c1e1a5f
Add WriteableAudioFile::quality.
psobot Mar 9, 2022
fc009ef
Improve documentation consistency.
psobot Mar 9, 2022
4bfaf43
Tempered readme speed claims after further testing. [ci skip]
psobot Mar 9, 2022
010f90b
Auto-delete output stream if constructor fails.
psobot Mar 10, 2022
6b066db
Formatting.
psobot Mar 10, 2022
183c7e0
Split AudioFile classes into smaller files.
psobot Mar 10, 2022
11b2e27
Add support for reading seekable file-like objects.
psobot Mar 10, 2022
b185ed5
Formatting.
psobot Mar 10, 2022
b42922a
Fix MP3 tests that fail if passing an unnamed stream.
psobot Mar 10, 2022
d80d0dd
Add support for writing to file-like objects.
psobot Mar 11, 2022
8247fff
Formatting.
psobot Mar 11, 2022
3d9051d
Add missing lambda return type annotations.
psobot Mar 11, 2022
53b91b8
Fix missing GIL around re-raised exceptions.
psobot Mar 11, 2022
39bb632
C++ formatting.
psobot Mar 11, 2022
e4c8b66
Fix Python 3.10 crash when reading bytes from Python.
psobot Mar 11, 2022
0af3ac5
Fix dummy stream filename.
psobot Mar 11, 2022
7b63a3b
Fix size_t on Windows.
psobot Mar 11, 2022
64766ee
Fix flakey exception propagation test.
psobot Mar 11, 2022
656719b
Fix exception propagation test.
psobot Mar 11, 2022
04392d6
Still delete input file if setPosition is called but nothing is written.
psobot Mar 11, 2022
d63368e
Skip constructor deletion test on Windows.
psobot Mar 11, 2022
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
42 changes: 27 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
![PyPI - Downloads](https://img.shields.io/pypi/dm/pedalboard)
![GitHub Repo stars](https://img.shields.io/github/stars/spotify/pedalboard?style=social)

`pedalboard` is a Python library for adding effects to audio. It supports a number of common audio effects out of the box, and also allows the use of [VST3®](https://www.steinberg.net/en/company/technologies/vst3.html) and [Audio Unit](https://en.wikipedia.org/wiki/Audio_Units) plugin formats for third-party effects. It was built by [Spotify's Audio Intelligence Lab](https://research.atspotify.com/audio-intelligence/) to enable using studio-quality audio effects from within Python and TensorFlow.
`pedalboard` is a Python library for manipulating audio: adding effects, reading, writing, and more. It supports a number of common audio effects out of the box, and also allows the use of [VST3®](https://www.steinberg.net/en/company/technologies/vst3.html) and [Audio Unit](https://en.wikipedia.org/wiki/Audio_Units) plugin formats for third-party effects. It was built by [Spotify's Audio Intelligence Lab](https://research.atspotify.com/audio-intelligence/) to enable using studio-quality audio effects from within Python and TensorFlow.

Internally at Spotify, `pedalboard` is used for [data augmentation](https://en.wikipedia.org/wiki/Data_augmentation) to improve machine learning models. `pedalboard` also helps in the process of content creation, making it possible to add effects to audio without using a Digital Audio Workstation.

Expand All @@ -27,11 +27,16 @@ Internally at Spotify, `pedalboard` is used for [data augmentation](https://en.w
- Quality reduction: `Resample`, `Bitcrush`
- Supports VST3® plugins on macOS, Windows, and Linux (`pedalboard.load_plugin`)
- Supports Audio Units on macOS
- Built-in audio I/O utilities (`pedalboard.io.AudioFile`)
- Support for reading AIFF, FLAC, MP3, OGG, and WAV files on all platforms with no dependencies
- Support for writing AIFF, FLAC, OGG, and WAV on all platforms with no dependencies
- Additional support for reading AAC, AC3, WMA, and other formats depending on platform
- Strong thread-safety, memory usage, and speed guarantees
- Releases Python's Global Interpreter Lock (GIL) to allow use of multiple CPU cores
- No need to use `multiprocessing`!
- Even when only using one thread:
- Processes audio up to **300x** faster than [pySoX](https://github.com/rabitt/pysox) for single transforms, and 2-5x faster<sup>[1](https://github.com/iCorv/pedalboard_with_tfdata)</sup> than [SoxBindings](https://github.com/pseeth/soxbindings)
- Reads audio files up to **4x** faster than [`librosa.load`](https://librosa.org/doc/main/generated/librosa.load.html) (in many cases)
- Tested compatibility with TensorFlow - can be used in `tf.data` pipelines!

## Installation
Expand Down Expand Up @@ -88,30 +93,35 @@ to the next in an undesired fashion, try:
### Quick Start

```python
import soundfile as sf
from pedalboard import Pedalboard, Chorus, Reverb
from pedalboard.io import AudioFile

# Read in an audio file:
audio, sample_rate = sf.read('some-file.wav')
# Read in a whole audio file:
with AudioFile('some-file.wav', 'r') as f:
audio = f.read(f.frames)
samplerate = f.samplerate

# Make a Pedalboard object, containing multiple plugins:
board = Pedalboard([Chorus(), Reverb(room_size=0.25)])

# Run the audio through this pedalboard!
effected = board(audio, sample_rate)
effected = board(audio, samplerate)

# Write the audio back as a wav file:
sf.write('./processed-output.wav', effected, sample_rate)
with AudioFile('processed-output.wav', 'w', samplerate) as f:
f.write(effected)
```

### Making a guitar-style pedalboard

```python
import soundfile as sf
# Don't do import *! (It just makes this example smaller)
from pedalboard import *
from pedalboard.io import AudioFile

audio, sample_rate = sf.read('./guitar-input.wav')
with AudioFile('guitar-input.wav', 'r') as f:
audio = f.read(f.frames)
samplerate = f.samplerate

# Make a pretty interesting sounding guitar pedalboard:
board = Pedalboard([
Expand All @@ -133,17 +143,18 @@ board.append(Limiter())
board[0].threshold_db = -40

# Run the audio through this pedalboard!
effected = board(audio, sample_rate)
effected = board(audio, samplerate)

# Write the audio back as a wav file:
sf.write('./guitar-output.wav', effected, sample_rate)
with AudioFile('processed-output.wav', 'w', samplerate) as f:
f.write(effected)
```

### Using VST3® or Audio Unit plugins

```python
import soundfile as sf
from pedalboard import Pedalboard, Reverb, load_plugin
from pedalboard.io import AudioFile

# Load a VST3 or Audio Unit plugin from a known path on disk:
vst = load_plugin("./VSTs/RoughRider3.vst3")
Expand All @@ -160,13 +171,15 @@ print(vst.parameters.keys())
vst.ratio = 15

# Use this VST to process some audio:
audio, sample_rate = sf.read('some-file.wav')
effected = vst(audio, sample_rate)
with AudioFile('some-file.wav', 'r') as f:
audio = f.read(f.frames)
samplerate = f.samplerate
effected = vst(audio, samplerate)

# ...or put this VST into a chain with other plugins:
board = Pedalboard([vst, Reverb()])
# ...and run that pedalboard with the same VST instance!
effected = board(audio, sample_rate)
effected = board(audio, samplerate)
```

### Creating parallel effects chains
Expand All @@ -177,7 +190,6 @@ objects are themselves `Plugin` objects, so you can nest them
as much as you like:

```python
import soundfile as sf
from pedalboard import Pedalboard, Compressor, Delay, Distortion, Gain, PitchShift, Reverb, Mix

passthrough = Gain(gain_db=0)
Expand Down
175 changes: 175 additions & 0 deletions pedalboard/BufferUtils.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/*
* pedalboard
* Copyright 2021 Spotify AB
*
* Licensed under the GNU Public License, Version 3.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.gnu.org/licenses/gpl-3.0.html
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#pragma once
#include "JuceHeader.h"

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

namespace Pedalboard {
enum class ChannelLayout {
Interleaved,
NotInterleaved,
};

template <typename T>
ChannelLayout
detectChannelLayout(const py::array_t<T, py::array::c_style> inputArray) {
py::buffer_info inputInfo = inputArray.request();

if (inputInfo.ndim == 1) {
return ChannelLayout::NotInterleaved;
} else if (inputInfo.ndim == 2) {
// Try to auto-detect the channel layout from the shape
if (inputInfo.shape[1] < inputInfo.shape[0]) {
return ChannelLayout::Interleaved;
} else if (inputInfo.shape[0] < inputInfo.shape[1]) {
return ChannelLayout::NotInterleaved;
} else {
throw std::runtime_error(
"Unable to determine channel layout from shape!");
}
} else {
throw std::runtime_error("Number of input dimensions must be 1 or 2 (got " +
std::to_string(inputInfo.ndim) + ").");
}
}

template <typename T>
juce::AudioBuffer<T>
copyPyArrayIntoJuceBuffer(const py::array_t<T, py::array::c_style> inputArray) {
// Numpy/Librosa convention is (num_samples, num_channels)
py::buffer_info inputInfo = inputArray.request();

unsigned int numChannels = 0;
unsigned int numSamples = 0;
ChannelLayout inputChannelLayout = detectChannelLayout(inputArray);

if (inputInfo.ndim == 1) {
numSamples = inputInfo.shape[0];
numChannels = 1;
} else if (inputInfo.ndim == 2) {
// Try to auto-detect the channel layout from the shape
if (inputInfo.shape[1] < inputInfo.shape[0]) {
numSamples = inputInfo.shape[0];
numChannels = inputInfo.shape[1];
} else if (inputInfo.shape[0] < inputInfo.shape[1]) {
numSamples = inputInfo.shape[1];
numChannels = inputInfo.shape[0];
} else {
throw std::runtime_error("Unable to determine shape of audio input!");
}
} else {
throw std::runtime_error("Number of input dimensions must be 1 or 2 (got " +
std::to_string(inputInfo.ndim) + ").");
}

if (numChannels == 0) {
throw std::runtime_error("No channels passed!");
} else if (numChannels > 2) {
throw std::runtime_error("More than two channels received!");
}

juce::AudioBuffer<T> ioBuffer(numChannels, numSamples);

// Depending on the input channel layout, we need to copy data
// differently. This loop is duplicated here to move the if statement
// outside of the tight loop, as we don't need to re-check that the input
// channel is still the same on every iteration of the loop.
switch (inputChannelLayout) {
case ChannelLayout::Interleaved:
for (unsigned int i = 0; i < numChannels; i++) {
T *channelBuffer = ioBuffer.getWritePointer(i);
// We're de-interleaving the data here, so we can't use copyFrom.
for (unsigned int j = 0; j < numSamples; j++) {
channelBuffer[j] = static_cast<T *>(inputInfo.ptr)[j * numChannels + i];
}
}
break;
case ChannelLayout::NotInterleaved:
for (unsigned int i = 0; i < numChannels; i++) {
ioBuffer.copyFrom(
i, 0, static_cast<T *>(inputInfo.ptr) + (numSamples * i), numSamples);
}
break;
default:
throw std::runtime_error("Internal error: got unexpected channel layout.");
}

return ioBuffer;
}

template <typename T>
py::array_t<T> copyJuceBufferIntoPyArray(const juce::AudioBuffer<T> juceBuffer,
ChannelLayout channelLayout,
int offsetSamples, int ndim = 2) {
unsigned int numChannels = juceBuffer.getNumChannels();
unsigned int numSamples = juceBuffer.getNumSamples();
unsigned int outputSampleCount =
std::max((int)numSamples - (int)offsetSamples, 0);

// TODO: Avoid the need to copy here if offsetSamples is 0!
py::array_t<T> outputArray;
if (ndim == 2) {
switch (channelLayout) {
case ChannelLayout::Interleaved:
outputArray = py::array_t<T>({outputSampleCount, numChannels});
break;
case ChannelLayout::NotInterleaved:
outputArray = py::array_t<T>({numChannels, outputSampleCount});
break;
default:
throw std::runtime_error(
"Internal error: got unexpected channel layout.");
}
} else {
outputArray = py::array_t<T>(outputSampleCount);
}

py::buffer_info outputInfo = outputArray.request();

// Depending on the input channel layout, we need to copy data
// differently. This loop is duplicated here to move the if statement
// outside of the tight loop, as we don't need to re-check that the input
// channel is still the same on every iteration of the loop.
T *outputBasePointer = static_cast<T *>(outputInfo.ptr);

switch (channelLayout) {
case ChannelLayout::Interleaved:
for (unsigned int i = 0; i < numChannels; i++) {
const T *channelBuffer = juceBuffer.getReadPointer(i, offsetSamples);
// We're interleaving the data here, so we can't use copyFrom.
for (unsigned int j = 0; j < outputSampleCount; j++) {
outputBasePointer[j * numChannels + i] = channelBuffer[j];
}
}
break;
case ChannelLayout::NotInterleaved:
for (unsigned int i = 0; i < numChannels; i++) {
const T *channelBuffer = juceBuffer.getReadPointer(i, offsetSamples);
std::copy(channelBuffer, channelBuffer + outputSampleCount,
&outputBasePointer[outputSampleCount * i]);
}
break;
default:
throw std::runtime_error("Internal error: got unexpected channel layout.");
}

return outputArray;
}
} // namespace Pedalboard
26 changes: 26 additions & 0 deletions pedalboard/io/AudioFile.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* pedalboard
* Copyright 2022 Spotify AB
*
* Licensed under the GNU Public License, Version 3.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.gnu.org/licenses/gpl-3.0.html
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#pragma once

namespace Pedalboard {

static constexpr const unsigned int DEFAULT_AUDIO_BUFFER_SIZE_FRAMES = 8192;

class AudioFile {};

} // namespace Pedalboard
Loading