diff --git a/.github/workflows/all.yml b/.github/workflows/all.yml index ef9f864d..0730aa30 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' @@ -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' @@ -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' @@ -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 }} @@ -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' @@ -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' diff --git a/examples/add_reverb_to_file.py b/examples/add_reverb_to_file.py index 6a761931..0a0b16d6 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(): diff --git a/examples/audio_monitoring_with_effects.py b/examples/audio_monitoring_with_effects.py new file mode 100644 index 00000000..feb291d4 --- /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 diff --git a/pedalboard/JuceHeader.h b/pedalboard/JuceHeader.h index 911785e6..6a23a03b 100644 --- a/pedalboard/JuceHeader.h +++ b/pedalboard/JuceHeader.h @@ -23,6 +23,7 @@ #pragma once #include +#include #include #include #include @@ -30,8 +31,4 @@ #include #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 e2c8fe4d..3b155fd0 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."); } @@ -275,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 {}; @@ -290,7 +297,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 +321,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 +361,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 +382,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 +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 " diff --git a/pyproject.toml b/pyproject.toml index a5cd8148..c2078580 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/setup.py b/setup.py index a3642763..2e671362 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": diff --git a/tests/test_audio_stream.py b/tests/test_audio_stream.py index 3a5bb7e4..d26847fa 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 @@ -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( @@ -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: @@ -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: @@ -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: @@ -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: @@ -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) @@ -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")