From e567f3d95e8f5d64fe0164972fb2f14709798bd0 Mon Sep 17 00:00:00 2001 From: Pedro Date: Sun, 4 Aug 2024 12:24:50 -0300 Subject: [PATCH 01/20] pedalboard.io: Add Linux support for AudioStream class Problem AudioStream is currently not supported on Linux due to macro definitions in the build script that disable AudioStream functionalities Solution Add the JUCE_MODULE_AVAILABLE_juce_audio_devices macro to ALL_CPPFLAGS, add link flag with the alsa-lib JUCE dependency for interacting with sound devices in Linux, fix audioDeviceIOCallback to support the live audio playback feature Result AudioStream is now supported on Linux as well as Windows and MacOS --- pedalboard/JuceHeader.h | 5 +---- pedalboard/io/AudioStream.h | 24 +++++++++++++----------- setup.py | 4 ++-- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/pedalboard/JuceHeader.h b/pedalboard/JuceHeader.h index 911785e64..b45aa661b 100644 --- a/pedalboard/JuceHeader.h +++ b/pedalboard/JuceHeader.h @@ -31,7 +31,4 @@ #include #include #include - -#ifndef JUCE_LINUX -#include -#endif \ No newline at end of file +#include \ No newline at end of file diff --git a/pedalboard/io/AudioStream.h b/pedalboard/io/AudioStream.h index e2c8fe4dc..7529727d8 100644 --- a/pedalboard/io/AudioStream.h +++ b/pedalboard/io/AudioStream.h @@ -290,7 +290,7 @@ class AudioStream : public std::enable_shared_from_this 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, @@ -314,9 +314,7 @@ class AudioStream : public std::enable_shared_from_this } } } - } - - 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++) { @@ -356,13 +354,12 @@ class AudioStream : public std::enable_shared_from_this 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) @@ -378,6 +375,11 @@ class AudioStream : public std::enable_shared_from_this (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)); + } } } @@ -832,7 +834,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 " diff --git a/setup.py b/setup.py index 1199391b2..195cb505b 100644 --- a/setup.py +++ b/setup.py @@ -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", @@ -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") @@ -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()) @@ -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": From 844b5d0aee9a32066a61fec0dcf68ce5f665c5be Mon Sep 17 00:00:00 2001 From: Pedro Date: Sun, 4 Aug 2024 12:37:56 -0300 Subject: [PATCH 02/20] Fix reverb example to use len instead of .frames for SoundFile class --- examples/add_reverb_to_file.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/add_reverb_to_file.py b/examples/add_reverb_to_file.py index 6a7619310..0a0b16d6b 100644 --- a/examples/add_reverb_to_file.py +++ b/examples/add_reverb_to_file.py @@ -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: @@ -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(): From fc92181f4df433e6fcf186542599fc1c428e96ed Mon Sep 17 00:00:00 2001 From: Pedro Date: Sun, 4 Aug 2024 13:18:45 -0300 Subject: [PATCH 03/20] Add example for audio monitoring with Pedalboard effects --- examples/audio_monitoring_with_effects.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 examples/audio_monitoring_with_effects.py diff --git a/examples/audio_monitoring_with_effects.py b/examples/audio_monitoring_with_effects.py new file mode 100644 index 000000000..feb291d4d --- /dev/null +++ b/examples/audio_monitoring_with_effects.py @@ -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() \ No newline at end of file From b0d4e9a10e2072106eecab767a5117451653cbef Mon Sep 17 00:00:00 2001 From: Pedro Date: Sun, 4 Aug 2024 13:18:45 -0300 Subject: [PATCH 04/20] Fix formatting --- pedalboard/JuceHeader.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pedalboard/JuceHeader.h b/pedalboard/JuceHeader.h index b45aa661b..6a23a03ba 100644 --- a/pedalboard/JuceHeader.h +++ b/pedalboard/JuceHeader.h @@ -23,6 +23,7 @@ #pragma once #include +#include #include #include #include @@ -30,5 +31,4 @@ #include #include #include -#include -#include \ No newline at end of file +#include \ No newline at end of file From 0c74c66479d5eadc68d16ea8c0e9a9fff58610c8 Mon Sep 17 00:00:00 2001 From: Pedro Date: Wed, 7 Aug 2024 20:00:48 -0300 Subject: [PATCH 05/20] Add alsa-lib package in wheel builder for static linking --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 69add7b47..bedc3fcfd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,10 +12,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 libexecinfo-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" From 8530a77bdc6e8bc4dec5315a4fe506b7758d2504 Mon Sep 17 00:00:00 2001 From: Pedro Date: Thu, 8 Aug 2024 13:21:53 -0300 Subject: [PATCH 06/20] Add libasound2-dev dependency for the pre-build on ubuntu-20.04 --- .github/workflows/all.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/all.yml b/.github/workflows/all.yml index 998e414e2..a1fea4318 100644 --- a/.github/workflows/all.yml +++ b/.github/workflows/all.yml @@ -256,7 +256,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' From ee782382f0508980339f6830a6b645323398b314 Mon Sep 17 00:00:00 2001 From: Pedro Date: Thu, 8 Aug 2024 13:28:34 -0300 Subject: [PATCH 07/20] Add libasound2-dev dependency to Linux actions --- .github/workflows/all.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/all.yml b/.github/workflows/all.yml index a1fea4318..46b41b35a 100644 --- a/.github/workflows/all.yml +++ b/.github/workflows/all.yml @@ -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' @@ -363,7 +364,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' @@ -482,7 +484,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' @@ -612,7 +615,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' From e868ffcd1776a4204dc70a448527ccb9bf21db36 Mon Sep 17 00:00:00 2001 From: Peter Sobot Date: Thu, 8 Aug 2024 13:26:59 -0400 Subject: [PATCH 08/20] Comment out "delete existing cache" step. --- .github/workflows/all.yml | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/all.yml b/.github/workflows/all.yml index 46b41b35a..608ab5447 100644 --- a/.github/workflows/all.yml +++ b/.github/workflows/all.yml @@ -132,11 +132,11 @@ jobs: ccache --show-stats --verbose ccache --zero-stats # cat ${{ runner.workspace }}/.ccache_log || true - - name: Delete Existing Cache for ${{ runner.os }} Objects - run: gh cache delete --repo ${{ github.repository }} ${{ steps.cache-objects-restore.outputs.cache-primary-key }} - env: - # This token requires the "repo" scope. - GITHUB_TOKEN: ${{ secrets.ACTION_CLEAR_CACHES_TOKEN }} + # - name: Delete Existing Cache for ${{ runner.os }} Objects + # run: gh cache delete --repo ${{ github.repository }} ${{ steps.cache-objects-restore.outputs.cache-primary-key }} + # env: + # # This token requires the "repo" scope. + # GITHUB_TOKEN: ${{ secrets.ACTION_CLEAR_CACHES_TOKEN }} - name: Save Cached ${{ runner.os }} Objects id: cache-objects-save uses: actions/cache/save@v4 @@ -216,11 +216,11 @@ jobs: ccache --zero-stats # cat ${{ runner.workspace }}/.ccache_log || true # ls -lashR ${{ runner.workspace }}/pedalboard - - name: Delete Existing Cache for ${{ runner.os }} Objects - run: gh cache delete --repo ${{ github.repository }} ${{ steps.cache-objects-restore.outputs.cache-primary-key }} - env: - # This token requires the "repo" scope. - GITHUB_TOKEN: ${{ secrets.ACTION_CLEAR_CACHES_TOKEN }} + # - name: Delete Existing Cache for ${{ runner.os }} Objects + # run: gh cache delete --repo ${{ github.repository }} ${{ steps.cache-objects-restore.outputs.cache-primary-key }} + # env: + # # This token requires the "repo" scope. + # GITHUB_TOKEN: ${{ secrets.ACTION_CLEAR_CACHES_TOKEN }} - name: Save Cached ${{ runner.os }} Objects id: cache-objects-save uses: actions/cache/save@v4 @@ -316,11 +316,11 @@ jobs: ccache --zero-stats # cat ${{ runner.workspace }}/.ccache_log || true # ls -lashR ${{ runner.workspace }} - - name: Delete Existing Cache for ${{ runner.os }} Objects - run: gh cache delete --repo ${{ github.repository }} ${{ steps.cache-objects-restore.outputs.cache-primary-key }} - env: - # This token requires the "repo" scope. - GITHUB_TOKEN: ${{ secrets.ACTION_CLEAR_CACHES_TOKEN }} + # - name: Delete Existing Cache for ${{ runner.os }} Objects + # run: gh cache delete --repo ${{ github.repository }} ${{ steps.cache-objects-restore.outputs.cache-primary-key }} + # env: + # # This token requires the "repo" scope. + # GITHUB_TOKEN: ${{ secrets.ACTION_CLEAR_CACHES_TOKEN }} - name: Save Cached ${{ runner.os }} Objects id: cache-objects-save uses: actions/cache/save@v4 From daf5edb0cde6627a5473c564ca9a8876eea15933 Mon Sep 17 00:00:00 2001 From: Pedro Date: Thu, 8 Aug 2024 16:59:22 -0300 Subject: [PATCH 09/20] Include Linux in AudioStream tests, remove create_stream_fails_on_linux test --- tests/test_audio_stream.py | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/tests/test_audio_stream.py b/tests/test_audio_stream.py index 3a5bb7e47..621784a11 100644 --- a/tests/test_audio_stream.py +++ b/tests/test_audio_stream.py @@ -36,10 +36,11 @@ # 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.") +@pytest.mark.parametrize( + "output_device_name", pedalboard.io.AudioStream.output_device_names +) def test_create_stream(input_device_name: str, output_device_name: str): try: stream = pedalboard.io.AudioStream( @@ -69,8 +70,7 @@ 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.", @@ -94,8 +94,7 @@ 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.", @@ -118,15 +117,16 @@ 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.", ) def test_read_from_stream(): try: - stream = pedalboard.io.AudioStream(pedalboard.io.AudioStream.default_input_device_name) + stream = pedalboard.io.AudioStream( + pedalboard.io.AudioStream.default_input_device_name + ) except Exception as e: if any(substr in str(e) for substr in ACCEPTABLE_ERRORS_ON_CI): raise pytest.skip(str(e)) @@ -141,15 +141,16 @@ 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.", ) def test_read_from_stream_measures_dropped_frames(): try: - stream = pedalboard.io.AudioStream(pedalboard.io.AudioStream.default_input_device_name) + stream = pedalboard.io.AudioStream( + pedalboard.io.AudioStream.default_input_device_name + ) except Exception as e: if any(substr in str(e) for substr in ACCEPTABLE_ERRORS_ON_CI): raise pytest.skip(str(e)) @@ -168,9 +169,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") From cb8c51f74a7e85ca5ace03715ccf8e3e729a3351 Mon Sep 17 00:00:00 2001 From: Peter Sobot Date: Fri, 9 Aug 2024 13:33:20 -0400 Subject: [PATCH 10/20] Update test_audio_stream.py --- tests/test_audio_stream.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_audio_stream.py b/tests/test_audio_stream.py index 621784a11..3759075cc 100644 --- a/tests/test_audio_stream.py +++ b/tests/test_audio_stream.py @@ -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 From f8211ed3956fbebec83f4d2dc5a1b87b8c11ce81 Mon Sep 17 00:00:00 2001 From: Peter Sobot Date: Fri, 9 Aug 2024 14:55:27 -0400 Subject: [PATCH 11/20] Update test_audio_stream.py --- tests/test_audio_stream.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/tests/test_audio_stream.py b/tests/test_audio_stream.py index 3759075cc..8b7d6988a 100644 --- a/tests/test_audio_stream.py +++ b/tests/test_audio_stream.py @@ -37,9 +37,7 @@ # Note: this test may do nothing on CI, because we don't have mock audio devices 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.parametrize("output_device_name", pedalboard.io.AudioStream.output_device_names) def test_create_stream(input_device_name: str, output_device_name: str): try: stream = pedalboard.io.AudioStream( @@ -123,9 +121,7 @@ def test_write_to_stream_without_opening(): ) def test_read_from_stream(): try: - stream = pedalboard.io.AudioStream( - pedalboard.io.AudioStream.default_input_device_name - ) + stream = pedalboard.io.AudioStream(pedalboard.io.AudioStream.default_input_device_name) except Exception as e: if any(substr in str(e) for substr in ACCEPTABLE_ERRORS_ON_CI): raise pytest.skip(str(e)) @@ -147,9 +143,7 @@ def test_read_from_stream(): ) def test_read_from_stream_measures_dropped_frames(): try: - stream = pedalboard.io.AudioStream( - pedalboard.io.AudioStream.default_input_device_name - ) + stream = pedalboard.io.AudioStream(pedalboard.io.AudioStream.default_input_device_name) except Exception as e: if any(substr in str(e) for substr in ACCEPTABLE_ERRORS_ON_CI): raise pytest.skip(str(e)) From 8106865c79d22345883dff766e47635d5654221d Mon Sep 17 00:00:00 2001 From: Peter Sobot Date: Fri, 9 Aug 2024 15:24:40 -0400 Subject: [PATCH 12/20] Update test_audio_stream.py --- tests/test_audio_stream.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_audio_stream.py b/tests/test_audio_stream.py index 8b7d6988a..841184895 100644 --- a/tests/test_audio_stream.py +++ b/tests/test_audio_stream.py @@ -151,6 +151,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) From 788e14460eb72c2395484df157a2342cfefed1fa Mon Sep 17 00:00:00 2001 From: Peter Sobot Date: Fri, 9 Aug 2024 18:42:38 -0400 Subject: [PATCH 13/20] Add step to remove libasound before running tests. --- .github/workflows/all.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/all.yml b/.github/workflows/all.yml index 608ab5447..84387a8b3 100644 --- a/.github/workflows/all.yml +++ b/.github/workflows/all.yml @@ -433,6 +433,10 @@ 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: Remove libasound Before Testing + if: runner.os == 'Linux' + # Tests should still pass if we have no libasound installed. + run: sudo apt-get remove libasound2-dev - name: Run tests env: TEST_WORKER_INDEX: ${{ matrix.runner_index }} From 1ddd2c4a562251bfa47aeda5abf694ce7ac62ba3 Mon Sep 17 00:00:00 2001 From: Peter Sobot Date: Fri, 9 Aug 2024 18:58:13 -0400 Subject: [PATCH 14/20] Update all.yml --- .github/workflows/all.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/all.yml b/.github/workflows/all.yml index 84387a8b3..05d44fe63 100644 --- a/.github/workflows/all.yml +++ b/.github/workflows/all.yml @@ -436,7 +436,7 @@ jobs: - name: Remove libasound Before Testing if: runner.os == 'Linux' # Tests should still pass if we have no libasound installed. - run: sudo apt-get remove libasound2-dev + run: sudo apt-get remove libasound2-dev libasound2 - name: Run tests env: TEST_WORKER_INDEX: ${{ matrix.runner_index }} From 1e0172226a04a78954ea3d45cc39835c2d5c8416 Mon Sep 17 00:00:00 2001 From: Pedro Date: Mon, 12 Aug 2024 23:08:11 +0000 Subject: [PATCH 15/20] Add empty string handling in AudioStream constructor --- pedalboard/io/AudioStream.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pedalboard/io/AudioStream.h b/pedalboard/io/AudioStream.h index 7529727d8..103f7ddab 100644 --- a/pedalboard/io/AudioStream.h +++ b/pedalboard/io/AudioStream.h @@ -81,7 +81,10 @@ class AudioStream : public std::enable_shared_from_this "`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."); } From 218592373c38aae504fade492af8d0731fe68c0a Mon Sep 17 00:00:00 2001 From: Pedro Lins Date: Sat, 17 Aug 2024 10:12:12 -0300 Subject: [PATCH 16/20] Add snd-dummy kernel module for testing AudioStream on linux --- .github/workflows/all.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/all.yml b/.github/workflows/all.yml index 05d44fe63..c58af4c90 100644 --- a/.github/workflows/all.yml +++ b/.github/workflows/all.yml @@ -437,6 +437,11 @@ jobs: if: runner.os == 'Linux' # Tests should still pass if we have no libasound installed. run: sudo apt-get remove libasound2-dev libasound2 + - 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 }} From 9c38eddfb0621313f36d342ad8d3c0fa354afe6c Mon Sep 17 00:00:00 2001 From: Peter Sobot Date: Wed, 21 Aug 2024 20:42:55 -0400 Subject: [PATCH 17/20] Remove uninstallation of libasound --- .github/workflows/all.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/all.yml b/.github/workflows/all.yml index fcabc5135..8392fb552 100644 --- a/.github/workflows/all.yml +++ b/.github/workflows/all.yml @@ -434,10 +434,6 @@ 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: Remove libasound Before Testing - if: runner.os == 'Linux' - # Tests should still pass if we have no libasound installed. - run: sudo apt-get remove libasound2-dev libasound2 - name: Setup dummy soundcard for testing if: runner.os == 'Linux' run: | From 9594e25e1a9e49db0b1dfbd9cc9163a5fd125891 Mon Sep 17 00:00:00 2001 From: Peter Sobot Date: Wed, 21 Aug 2024 20:49:45 -0400 Subject: [PATCH 18/20] Handle None audio devices. --- tests/test_audio_stream.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/tests/test_audio_stream.py b/tests/test_audio_stream.py index 841184895..7c3f648d0 100644 --- a/tests/test_audio_stream.py +++ b/tests/test_audio_stream.py @@ -69,7 +69,10 @@ 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 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", + ( + pedalboard.io.AudioStream.default_output_device_name == "Null Audio Device" + or pedalboard.io.AudioStream.default_output_device_name is None + ), reason="Tests do not work with a null audio device.", ) def test_write_to_stream(): @@ -93,7 +96,10 @@ 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 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", + ( + pedalboard.io.AudioStream.default_output_device_name == "Null Audio Device" + or pedalboard.io.AudioStream.default_output_device_name is None + ), reason="Tests do not work with a null audio device.", ) def test_write_to_stream_without_opening(): @@ -116,7 +122,10 @@ 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 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", + ( + pedalboard.io.AudioStream.default_input_device_name == "Null Audio Device" + or pedalboard.io.AudioStream.default_input_device_name is None + ), reason="Tests do not work with a null audio device.", ) def test_read_from_stream(): @@ -138,7 +147,10 @@ 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 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", + ( + pedalboard.io.AudioStream.default_input_device_name == "Null Audio Device" + or pedalboard.io.AudioStream.default_input_device_name is None + ), reason="Tests do not work with a null audio device.", ) def test_read_from_stream_measures_dropped_frames(): From 658c1e6ce8f0521a6f3a5b7dd43f474b7e3f3ead Mon Sep 17 00:00:00 2001 From: Peter Sobot Date: Thu, 22 Aug 2024 12:08:06 -0400 Subject: [PATCH 19/20] Is the default device name empty? --- tests/test_audio_stream.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_audio_stream.py b/tests/test_audio_stream.py index 7c3f648d0..944c40e21 100644 --- a/tests/test_audio_stream.py +++ b/tests/test_audio_stream.py @@ -71,9 +71,9 @@ def test_create_stream(input_device_name: str, output_device_name: str): @pytest.mark.skipif( ( pedalboard.io.AudioStream.default_output_device_name == "Null Audio Device" - or pedalboard.io.AudioStream.default_output_device_name is None + or not pedalboard.io.AudioStream.default_output_device_name ), - reason="Tests do not work with a null audio device.", + reason="Test requires a working audio device.", ) def test_write_to_stream(): try: @@ -98,9 +98,9 @@ def test_write_to_stream(): @pytest.mark.skipif( ( pedalboard.io.AudioStream.default_output_device_name == "Null Audio Device" - or pedalboard.io.AudioStream.default_output_device_name is None + or not pedalboard.io.AudioStream.default_output_device_name ), - reason="Tests do not work with a null audio device.", + reason="Test requires a working audio device.", ) def test_write_to_stream_without_opening(): try: @@ -124,9 +124,9 @@ def test_write_to_stream_without_opening(): @pytest.mark.skipif( ( pedalboard.io.AudioStream.default_input_device_name == "Null Audio Device" - or pedalboard.io.AudioStream.default_input_device_name is None + or not pedalboard.io.AudioStream.default_input_device_name ), - reason="Tests do not work with a null audio device.", + reason="Test requires a working audio device.", ) def test_read_from_stream(): try: @@ -149,9 +149,9 @@ def test_read_from_stream(): @pytest.mark.skipif( ( pedalboard.io.AudioStream.default_input_device_name == "Null Audio Device" - or pedalboard.io.AudioStream.default_input_device_name is None + or not pedalboard.io.AudioStream.default_input_device_name ), - reason="Tests do not work with a null audio device.", + reason="Test requires a working audio device.", ) def test_read_from_stream_measures_dropped_frames(): try: From 52267f124fa3b0e4fc5bb01eabaa2a3df42c37d4 Mon Sep 17 00:00:00 2001 From: Peter Sobot Date: Thu, 22 Aug 2024 15:26:50 -0400 Subject: [PATCH 20/20] Return None for an audio device name if the device name is the empty string. --- pedalboard/io/AudioStream.h | 10 +++++++--- tests/test_audio_stream.py | 8 ++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/pedalboard/io/AudioStream.h b/pedalboard/io/AudioStream.h index 103f7ddab..3b155fd07 100644 --- a/pedalboard/io/AudioStream.h +++ b/pedalboard/io/AudioStream.h @@ -278,10 +278,14 @@ class AudioStream : public std::enable_shared_from_this 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 {}; diff --git a/tests/test_audio_stream.py b/tests/test_audio_stream.py index 944c40e21..d26847fa2 100644 --- a/tests/test_audio_stream.py +++ b/tests/test_audio_stream.py @@ -71,7 +71,7 @@ def test_create_stream(input_device_name: str, output_device_name: str): @pytest.mark.skipif( ( pedalboard.io.AudioStream.default_output_device_name == "Null Audio Device" - or not pedalboard.io.AudioStream.default_output_device_name + or pedalboard.io.AudioStream.default_output_device_name is None ), reason="Test requires a working audio device.", ) @@ -98,7 +98,7 @@ def test_write_to_stream(): @pytest.mark.skipif( ( pedalboard.io.AudioStream.default_output_device_name == "Null Audio Device" - or not pedalboard.io.AudioStream.default_output_device_name + or pedalboard.io.AudioStream.default_output_device_name is None ), reason="Test requires a working audio device.", ) @@ -124,7 +124,7 @@ def test_write_to_stream_without_opening(): @pytest.mark.skipif( ( pedalboard.io.AudioStream.default_input_device_name == "Null Audio Device" - or not pedalboard.io.AudioStream.default_input_device_name + or pedalboard.io.AudioStream.default_input_device_name is None ), reason="Test requires a working audio device.", ) @@ -149,7 +149,7 @@ def test_read_from_stream(): @pytest.mark.skipif( ( pedalboard.io.AudioStream.default_input_device_name == "Null Audio Device" - or not pedalboard.io.AudioStream.default_input_device_name + or pedalboard.io.AudioStream.default_input_device_name is None ), reason="Test requires a working audio device.", )