Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add pedalboard.io.AudioStream support for Linux #368

Merged
merged 22 commits into from
Aug 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e567f3d
pedalboard.io: Add Linux support for AudioStream class
sarmentow Aug 4, 2024
844b5d0
Fix reverb example to use len instead of .frames for SoundFile class
sarmentow Aug 4, 2024
fc92181
Add example for audio monitoring with Pedalboard effects
sarmentow Aug 4, 2024
b0d4e9a
Fix formatting
sarmentow Aug 4, 2024
0c74c66
Add alsa-lib package in wheel builder for static linking
sarmentow Aug 7, 2024
8530a77
Add libasound2-dev dependency for the pre-build on ubuntu-20.04
sarmentow Aug 8, 2024
ee78238
Add libasound2-dev dependency to Linux actions
sarmentow Aug 8, 2024
e868ffc
Comment out "delete existing cache" step.
psobot Aug 8, 2024
daf5edb
Include Linux in AudioStream tests, remove create_stream_fails_on_lin…
sarmentow Aug 8, 2024
cb8c51f
Update test_audio_stream.py
psobot Aug 9, 2024
f8211ed
Update test_audio_stream.py
psobot Aug 9, 2024
8106865
Update test_audio_stream.py
psobot Aug 9, 2024
788e144
Add step to remove libasound before running tests.
psobot Aug 9, 2024
1ddd2c4
Update all.yml
psobot Aug 9, 2024
1e01722
Add empty string handling in AudioStream constructor
sarmentow Aug 12, 2024
2185923
Add snd-dummy kernel module for testing AudioStream on linux
sarmentow Aug 17, 2024
190bc76
Merge branch 'master' into feature
sarmentow Aug 17, 2024
9c38edd
Remove uninstallation of libasound
psobot Aug 22, 2024
9594e25
Handle None audio devices.
psobot Aug 22, 2024
658c1e6
Is the default device name empty?
psobot Aug 22, 2024
52267f1
Return None for an audio device name if the device name is the empty …
psobot Aug 22, 2024
5300ac0
Merge branch 'master' into feature
psobot Aug 22, 2024
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
20 changes: 15 additions & 5 deletions .github/workflows/all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ jobs:
&& sudo apt-get install -y pkg-config libsndfile1 \
libx11-dev libxrandr-dev libxinerama-dev \
libxrender-dev libxcomposite-dev libxcb-xinerama0-dev \
libxcursor-dev libfreetype6 libfreetype6-dev
libxcursor-dev libfreetype6 libfreetype6-dev \
libasound2-dev
# We depend on ccache features that are only present in 4.8.0 and later, but installing from apt-get gives us v3.
- name: Install ccache on Linux
if: runner.os == 'Linux'
Expand Down Expand Up @@ -257,7 +258,8 @@ jobs:
&& sudo apt-get install -y pkg-config libsndfile1 \
libx11-dev libxrandr-dev libxinerama-dev \
libxrender-dev libxcomposite-dev libxcb-xinerama0-dev \
libxcursor-dev libfreetype6 libfreetype6-dev
libxcursor-dev libfreetype6 libfreetype6-dev \
libasound2-dev
# We depend on ccache features that are only present in 4.8.0 and later, but installing from apt-get gives us v3.
- name: Install ccache on Linux
if: runner.os == 'Linux'
Expand Down Expand Up @@ -363,7 +365,8 @@ jobs:
&& sudo apt-get install -y pkg-config libsndfile1 \
libx11-dev libxrandr-dev libxinerama-dev \
libxrender-dev libxcomposite-dev libxcb-xinerama0-dev \
libxcursor-dev libfreetype6 libfreetype6-dev
libxcursor-dev libfreetype6 libfreetype6-dev \
libasound2-dev
# We depend on ccache features that are only present in 4.8.0 and later, but installing from apt-get gives us v3.
- name: Install ccache on Linux
if: runner.os == 'Linux'
Expand Down Expand Up @@ -431,6 +434,11 @@ jobs:
GCS_ASSET_BUCKET_NAME: ${{ secrets.GCS_ASSET_BUCKET_NAME }}
GCS_READER_SERVICE_ACCOUNT_KEY: ${{ secrets.GCS_READER_SERVICE_ACCOUNT_KEY }}
run: python ./tests/download_test_plugins.py
- name: Setup dummy soundcard for testing
if: runner.os == 'Linux'
run: |
sudo apt-get install -y linux-modules-extra-$(uname -r)
sudo modprobe snd-dummy
- name: Run tests
env:
TEST_WORKER_INDEX: ${{ matrix.runner_index }}
Expand Down Expand Up @@ -482,7 +490,8 @@ jobs:
&& sudo apt-get install -y pkg-config libsndfile1 \
libx11-dev libxrandr-dev libxinerama-dev \
libxrender-dev libxcomposite-dev libxcb-xinerama0-dev \
libxcursor-dev libfreetype6 libfreetype6-dev
libxcursor-dev libfreetype6 libfreetype6-dev \
libasound2-dev
# We depend on ccache features that are only present in 4.8.0 and later, but installing from apt-get gives us v3.
- name: Install ccache on Linux
if: runner.os == 'Linux'
Expand Down Expand Up @@ -613,7 +622,8 @@ jobs:
&& sudo apt-get install -y pkg-config libsndfile1 \
libx11-dev libxrandr-dev libxinerama-dev \
libxrender-dev libxcomposite-dev libxcb-xinerama0-dev \
libxcursor-dev libfreetype6 libfreetype6-dev
libxcursor-dev libfreetype6 libfreetype6-dev \
libasound2-dev
# We depend on ccache features that are only present in 4.8.0 and later, but installing from apt-get gives us v3.
- name: Install ccache on Linux
if: runner.os == 'Linux'
Expand Down
4 changes: 2 additions & 2 deletions examples/add_reverb_to_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
def get_num_frames(f: sf.SoundFile) -> int:
# On some platforms and formats, f.frames == -1L.
# Check for this bug and work around it:
if f.frames > 2 ** 32:
if len(f) > 2 ** 32:
f.seek(0)
last_position = f.tell()
while True:
Expand All @@ -45,7 +45,7 @@ def get_num_frames(f: sf.SoundFile) -> int:
else:
last_position = new_position
else:
return f.frames
return len(f)


def main():
Expand Down
21 changes: 21 additions & 0 deletions examples/audio_monitoring_with_effects.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from pedalboard import Pedalboard, Compressor, Gain, Reverb
from pedalboard.io import AudioStream

# Open up an audio stream:
stream = AudioStream(
input_device_name=AudioStream.input_device_names[0],
output_device_name=AudioStream.output_device_names[0],
num_input_channels=2,
num_output_channels=2,
allow_feedback=True,
buffer_size=128,
sample_rate=44100,
)

stream.plugins = Pedalboard([
Reverb(wet_level=0.2),
Gain(1.0),
Compressor(),
])

stream.run()
7 changes: 2 additions & 5 deletions pedalboard/JuceHeader.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,12 @@
#pragma once

#include <juce_audio_basics/juce_audio_basics.h>
#include <juce_audio_devices/juce_audio_devices.h>
#include <juce_audio_formats/juce_audio_formats.h>
#include <juce_audio_processors/juce_audio_processors.h>
#include <juce_core/juce_core.h>
#include <juce_data_structures/juce_data_structures.h>
#include <juce_dsp/juce_dsp.h>
#include <juce_events/juce_events.h>
#include <juce_graphics/juce_graphics.h>
#include <juce_gui_basics/juce_gui_basics.h>

#ifndef JUCE_LINUX
#include <juce_audio_devices/juce_audio_devices.h>
#endif
#include <juce_gui_basics/juce_gui_basics.h>
39 changes: 24 additions & 15 deletions pedalboard/io/AudioStream.h
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,10 @@ class AudioStream : public std::enable_shared_from_this<AudioStream>
"`allow_feedback=True` to the AudioStream constructor.");
}

if (!inputDeviceName && !outputDeviceName) {
if ((!inputDeviceName ||
(inputDeviceName.has_value() && inputDeviceName.value().empty())) &&
(!outputDeviceName ||
(outputDeviceName.has_value() && outputDeviceName.value().empty()))) {
throw std::runtime_error("At least one of `input_device_name` or "
"`output_device_name` must be provided.");
}
Expand Down Expand Up @@ -275,10 +278,14 @@ class AudioStream : public std::enable_shared_from_this<AudioStream>
if (auto *type = deviceManager.getCurrentDeviceTypeObject()) {
const auto info = getSetupInfo(setup, isInput);

if (numChannelsNeeded > 0 && info.name.isEmpty())
return {
if (numChannelsNeeded > 0 && info.name.isEmpty()) {
std::string deviceName =
type->getDeviceNames(isInput)[type->getDefaultDeviceIndex(isInput)]
.toStdString()};
.toStdString();
if (!deviceName.empty()) {
return {deviceName};
}
}
}
#endif
return {};
Expand All @@ -290,7 +297,7 @@ class AudioStream : public std::enable_shared_from_this<AudioStream>
float **outputChannelData,
int numOutputChannels, int numSamples) {
// Live processing mode: run the input audio through a Pedalboard object.
if (!playBufferFifo && !recordBufferFifo) {
if (playBufferFifo && recordBufferFifo) {
for (int i = 0; i < numOutputChannels; i++) {
const float *inputChannel = inputChannelData[i % numInputChannels];
std::memcpy((char *)outputChannelData[i], (char *)inputChannel,
Expand All @@ -314,9 +321,7 @@ class AudioStream : public std::enable_shared_from_this<AudioStream>
}
}
}
}

if (recordBufferFifo) {
} else if (recordBufferFifo) {
// If Python wants audio input, then copy the audio into the record
// buffer:
for (int attempt = 0; attempt < 2; attempt++) {
Expand Down Expand Up @@ -356,13 +361,12 @@ class AudioStream : public std::enable_shared_from_this<AudioStream>
break;
}
}
}

for (int i = 0; i < numOutputChannels; i++) {
std::memset((char *)outputChannelData[i], 0, numSamples * sizeof(float));
}
} else if (playBufferFifo) {
for (int i = 0; i < numOutputChannels; i++) {
std::memset((char *)outputChannelData[i], 0,
numSamples * sizeof(float));
}

if (playBufferFifo) {
const auto scope = playBufferFifo->read(numSamples);

if (scope.blockSize1 > 0)
Expand All @@ -378,6 +382,11 @@ class AudioStream : public std::enable_shared_from_this<AudioStream>
(char *)playBuffer->getReadPointer(i, scope.startIndex2),
scope.blockSize2 * sizeof(float));
}
} else {
for (int i = 0; i < numOutputChannels; i++) {
std::memset((char *)outputChannelData[i], 0,
numSamples * sizeof(float));
}
}
}

Expand Down Expand Up @@ -832,7 +841,7 @@ Or use :py:meth:`AudioStream.write` to stream audio in chunks::
#ifdef JUCE_MODULE_AVAILABLE_juce_audio_devices
return stream.getAudioDeviceSetup().bufferSize;
#else
return 0;
return 0;
#endif
},
"The size (in frames) of the buffer used between the audio "
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ build-backend = "setuptools.build_meta"

# See: https://cibuildwheel.readthedocs.io/en/stable/options/#examples
[tool.cibuildwheel.linux]
before-all = "yum install -y libsndfile libX11-devel libXrandr-devel libXinerama-devel libXrender-devel libXcomposite-devel libXinerama-devel libXcursor-devel freetype-devel"
before-all = "yum install -y libsndfile libX11-devel libXrandr-devel libXinerama-devel libXrender-devel libXcomposite-devel libXinerama-devel libXcursor-devel freetype-devel alsa-lib-devel"

[[tool.cibuildwheel.overrides]]
# Use apk instead of yum when building on Alpine Linux
# (Note: this is experimental, as most VSTs require glibc and thus Alpine Linux isn't that useful)
select = "*-musllinux*"
before-all = "apk add libsndfile libx11-dev libxrandr-dev libxinerama-dev libxrender-dev libxcomposite-dev libxinerama-dev libxcursor-dev freetype-dev"
before-all = "apk add libsndfile libx11-dev libxrandr-dev libxinerama-dev libxrender-dev libxcomposite-dev libxinerama-dev libxcursor-dev freetype-dev libexecinfo-dev alsa-lib-dev"
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"-DJUCE_MODULE_AVAILABLE_juce_graphics=1",
"-DJUCE_MODULE_AVAILABLE_juce_gui_basics=1",
"-DJUCE_MODULE_AVAILABLE_juce_gui_extra=1",
"-DJUCE_MODULE_AVAILABLE_juce_audio_devices=1",
"-DJUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1",
"-DJUCE_STRICT_REFCOUNTEDPOINTER=1",
"-DJUCE_STANDALONE_APPLICATION=1",
Expand Down Expand Up @@ -260,7 +261,6 @@ def ignore_files_matching(files, *matches):
ALL_CPPFLAGS.append("-flto=thin")
ALL_LINK_ARGS.append("-flto=thin")
ALL_LINK_ARGS.append("-fvisibility=hidden")
ALL_CPPFLAGS.append("-DJUCE_MODULE_AVAILABLE_juce_audio_devices=1")
ALL_CFLAGS += ["-Wno-comment"]
elif platform.system() == "Linux":
ALL_CPPFLAGS.append("-DLINUX=1")
Expand All @@ -272,7 +272,6 @@ def ignore_files_matching(files, *matches):
ALL_CFLAGS += ["-Wno-comment"]
elif platform.system() == "Windows":
ALL_CPPFLAGS.append("-DWINDOWS=1")
ALL_CPPFLAGS.append("-DJUCE_MODULE_AVAILABLE_juce_audio_devices=1")
else:
raise NotImplementedError(
"Not sure how to build JUCE on platform: {}!".format(platform.system())
Expand Down Expand Up @@ -356,6 +355,7 @@ def ignore_files_matching(files, *matches):
include_paths = [flag[2:] for flag in flags]
ALL_INCLUDES += include_paths
ALL_LINK_ARGS += ["-lfreetype"]
ALL_LINK_ARGS += ["-lasound"]

ALL_RESOLVED_SOURCE_PATHS = [str(p.resolve()) for p in ALL_SOURCE_PATHS]
elif platform.system() == "Windows":
Expand Down
52 changes: 27 additions & 25 deletions tests/test_audio_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import platform
import time

import numpy as np
Expand All @@ -36,10 +35,9 @@


# Note: this test may do nothing on CI, because we don't have mock audio devices available.
# This will run on macOS and probably Windows as long as at least one audio device is available.
# This will run on Linux, macOS and probably Windows as long as at least one audio device is available.
@pytest.mark.parametrize("input_device_name", INPUT_DEVICE_NAMES)
@pytest.mark.parametrize("output_device_name", pedalboard.io.AudioStream.output_device_names)
@pytest.mark.skipif(platform.system() == "Linux", reason="AudioStream not supported on Linux yet.")
def test_create_stream(input_device_name: str, output_device_name: str):
try:
stream = pedalboard.io.AudioStream(
Expand Down Expand Up @@ -69,11 +67,13 @@ def test_create_stream(input_device_name: str, output_device_name: str):


# Note: this test may do nothing on CI, because we don't have mock audio devices available.
# This will run on macOS and probably Windows as long as at least one audio device is available.
@pytest.mark.skipif(platform.system() == "Linux", reason="AudioStream not supported on Linux yet.")
# This will run on Linux, macOS and probably Windows as long as at least one audio device is available.
@pytest.mark.skipif(
pedalboard.io.AudioStream.default_output_device_name == "Null Audio Device",
reason="Tests do not work with a null audio device.",
(
pedalboard.io.AudioStream.default_output_device_name == "Null Audio Device"
or pedalboard.io.AudioStream.default_output_device_name is None
),
reason="Test requires a working audio device.",
)
def test_write_to_stream():
try:
Expand All @@ -94,11 +94,13 @@ def test_write_to_stream():


# Note: this test may do nothing on CI, because we don't have mock audio devices available.
# This will run on macOS and probably Windows as long as at least one audio device is available.
@pytest.mark.skipif(platform.system() == "Linux", reason="AudioStream not supported on Linux yet.")
# This will run on Linux, macOS and probably Windows as long as at least one audio device is available.
@pytest.mark.skipif(
pedalboard.io.AudioStream.default_output_device_name == "Null Audio Device",
reason="Tests do not work with a null audio device.",
(
pedalboard.io.AudioStream.default_output_device_name == "Null Audio Device"
or pedalboard.io.AudioStream.default_output_device_name is None
),
reason="Test requires a working audio device.",
)
def test_write_to_stream_without_opening():
try:
Expand All @@ -118,11 +120,13 @@ def test_write_to_stream_without_opening():


# Note: this test may do nothing on CI, because we don't have mock audio devices available.
# This will run on macOS and probably Windows as long as at least one audio device is available.
@pytest.mark.skipif(platform.system() == "Linux", reason="AudioStream not supported on Linux yet.")
# This will run on Linux, macOS and probably Windows as long as at least one audio device is available.
@pytest.mark.skipif(
pedalboard.io.AudioStream.default_output_device_name == "Null Audio Device",
reason="Tests do not work with a null audio device.",
(
pedalboard.io.AudioStream.default_input_device_name == "Null Audio Device"
or pedalboard.io.AudioStream.default_input_device_name is None
),
reason="Test requires a working audio device.",
)
def test_read_from_stream():
try:
Expand All @@ -141,11 +145,13 @@ def test_read_from_stream():


# Note: this test may do nothing on CI, because we don't have mock audio devices available.
# This will run on macOS and probably Windows as long as at least one audio device is available.
@pytest.mark.skipif(platform.system() == "Linux", reason="AudioStream not supported on Linux yet.")
# This will run on Linux, macOS and probably Windows as long as at least one audio device is available.
@pytest.mark.skipif(
pedalboard.io.AudioStream.default_output_device_name == "Null Audio Device",
reason="Tests do not work with a null audio device.",
(
pedalboard.io.AudioStream.default_input_device_name == "Null Audio Device"
or pedalboard.io.AudioStream.default_input_device_name is None
),
reason="Test requires a working audio device.",
)
def test_read_from_stream_measures_dropped_frames():
try:
Expand All @@ -157,6 +163,8 @@ def test_read_from_stream_measures_dropped_frames():

assert stream is not None
with stream:
if stream.sample_rate == 0:
raise pytest.skip("Sample rate of default audio device is 0")
assert stream.running
assert stream.dropped_input_frame_count == 0
time.sleep(5 * stream.buffer_size / stream.sample_rate)
Expand All @@ -168,9 +176,3 @@ def test_read_from_stream_measures_dropped_frames():

# ...but we should still know how many frames were dropped:
assert stream.dropped_input_frame_count == dropped_count


@pytest.mark.skipif(platform.system() != "Linux", reason="Test platform is not Linux.")
def test_create_stream_fails_on_linux():
with pytest.raises(RuntimeError):
pedalboard.io.AudioStream("input", "output")
Loading