spotify / pedalboard

πŸŽ› πŸ”Š A Python library for audio.
https://spotify.github.io/pedalboard
GNU General Public License v3.0
4.96k stars 249 forks source link

Question: AUv3 Support #323

Closed dschiller closed 1 month ago

dschiller commented 1 month ago

Dear Pedalboard Team, is there a possibility to load AUv3 Plug-Ins with Pedalboard ?

I am getting this error message:

Traceback (most recent call last):
  File "/Volumes/K2TB/Work/Coding/Python/PedalboardHost/src/host.py", line 9, in <module>
    plugin = load_plugin('/Users/dirk.schiller/Downloads/Builds/auv3.appex')
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Volumes/K2TB/Work/Coding/Python/PedalboardHost/.venv/lib/python3.12/site-packages/pedalboard/_pedalboard.py", line 796, in load_plugin
    raise ImportError(
ImportError: Failed to load plugin as VST3Plugin or AudioUnitPlugin. Errors were:
    VST3Plugin: Unable to load plugin /Users/dirk.schiller/Downloads/Builds/auv3.appex: unsupported plugin format or load failure.
    AudioUnitPlugin: Unable to load plugin /Users/dirk.schiller/Downloads/Builds/auv3.appex: unsupported plugin format or load failure. macOS requires plugin files to be moved to /Library/Audio/Plug-Ins/Components/ or ~/Library/Audio/Plug-Ins/Components/ before loading.

Also tried to copy the AUv3 Plug-In to the suggested folder, but same issue:

Traceback (most recent call last):
  File "/Volumes/K2TB/Work/Coding/Python/PedalboardHost/src/host.py", line 9, in <module>
    plugin = load_plugin('/Library/Audio/Plug-Ins/Components/auv3.appex')
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Volumes/K2TB/Work/Coding/Python/PedalboardHost/.venv/lib/python3.12/site-packages/pedalboard/_pedalboard.py", line 796, in load_plugin
    raise ImportError(
ImportError: Failed to load plugin as VST3Plugin or AudioUnitPlugin. Errors were:
    VST3Plugin: Unable to load plugin /Library/Audio/Plug-Ins/Components/auv3.appex: unsupported plugin format or load failure.
    AudioUnitPlugin: Unable to load plugin /Library/Audio/Plug-Ins/Components/auv3.appex: unsupported plugin format or load failure.

Maybe AUv3 is not supported yet, but cannot find any explicit information in your documentation.

psobot commented 1 month ago

Hi @dschiller!

AUv3 support is not tested at the moment - the documentation does not explicitly list AUv3 anywhere (although the usage of the generic "Audio Unit" name may suggest that all Audio Units are supported). The JUCE framework that Pedalboard is built on does have support for hosting AUv3 plugins, so enabling support may just be a matter of adding a test plugin to the test suite and changing the code in AudioUnitParser.mm to properly support opening AUv3 bundles.

dschiller commented 1 month ago

Hey Peter, it looks like that the AU Parser already is aware of .appex AUv3 Apple App Extension format at https://github.com/spotify/pedalboard/blob/master/pedalboard/AudioUnitParser.mm#L81

dschiller commented 1 month ago

I created a feature branch and try to build pedalboard locally

git clone https://github.com/spotify/pedalboard.git .
git checkout -b feature/add-auv3
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
pip install -r test-requirements.txt

I needed to download and install JUCE via

curl -LJO https://github.com/juce-framework/JUCE/releases/download/7.0.11/juce-7.0.11-osx.zip
tar -xf juce-7.0.11-osx.zip -o JUCE
rm juce-7.0.11-osx.zip

Other dependencies like Flac also missing:

rm -rf build; python setup.py develop

...
In file included from /Volumes/K2TB/Work/Coding/Python/Pedalboard/feature.add-auv3/pedalboard/juce_overrides/juce_PatchedFLACAudioFormat.cpp:139:
/Volumes/K2TB/Work/Coding/Python/Pedalboard/feature.add-auv3/pedalboard/juce_overrides/../../JUCE/modules/juce_audio_formats/codecs/flac/libFLAC/format.c:57:45: error: use of undeclared identifier 'PACKAGE_VERSION'
FLAC_API const char *FLAC__VERSION_STRING = PACKAGE_VERSION;
                                            ^
/Volumes/K2TB/Work/Coding/Python/Pedalboard/feature.add-auv3/pedalboard/juce_overrides/../../JUCE/modules/juce_audio_formats/codecs/flac/libFLAC/format.c:58:64: error: expected ';' after top level declarator
FLAC_API const char *FLAC__VENDOR_STRING = "reference libFLAC " PACKAGE_VERSION " 20230623";
                                                               ^
                                                               ;
In file included from /Volumes/K2TB/Work/Coding/Python/Pedalboard/feature.add-auv3/pedalboard/juce_overrides/juce_PatchedFLACAudioFormat.cpp:147:
/Volumes/K2TB/Work/Coding/Python/Pedalboard/feature.add-auv3/pedalboard/juce_overrides/../../vendors/libFLAC/metadata_object.c:41:10: fatal error: '../../JUCE/modules/juce_audio_formats/codecs/flac/libFLAC/include/private/metadata.h' file not found
#include "../../JUCE/modules/juce_audio_formats/codecs/flac/libFLAC/include/private/metadata.h"
         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3 errors generated.
error: command '/usr/bin/clang' failed with exit code 1

How do you install the necessary dependencies ?

I checked the .github/workflows/all.yml file but couldn't find anything that installs Flac for example.

Possibly i am using the wrong JUCE version as there is no metadata.h header file in JUCE 7.0.11:

image
dschiller commented 1 month ago

git submodule did the trick.

rm -rf JUCE
git submodule init
git submodule update
Submodule path 'JUCE': checked out 'ddaa09110392a4419fecbb6d3022bede89b7e841'
Submodule path 'vendors/lame': checked out '1f5cc9487284d5950343aa5d4f70de433468070a'
Submodule path 'vendors/libgsm': checked out '98f1708fb5e06a0dfebd58a3b40d610823db9715'
Submodule path 'vendors/pybind11': checked out '19a6b9f4efb569129c878b7f8db09132248fbaa1'
Submodule path 'vendors/rubberband': checked out '2be46b0dffb13273a67396c77bc9278736bb03d2'
dschiller commented 1 month ago

In pedalboard/ExternalPlugin.h it seems typesFound stays empty when using .appex AUv3 with JUCE's findAllTypesForFile():

...
#if JUCE_PLUGINHOST_AU && JUCE_MAC
    if constexpr (std::is_same<ExternalPluginType,
                               juce::AudioUnitPluginFormat>::value) {
      auto identifiers = getAudioUnitIdentifiersFromFile(pluginFileStripped);
      // For each plugin in the identified bundle, scan using its AU identifier:
      for (int i = 0; i < identifiers.size(); i++) {
        std::cout << identifiers[i] << std::endl;
        format.findAllTypesForFile(typesFound, identifiers[i]);
      }
    } else {
      std::cout << pluginFileStripped << std::endl;
      format.findAllTypesForFile(typesFound, pluginFileStripped);
    }
#else
    std::cout << pluginFileStripped << std::endl;
    std::cout << typesFound << std::endl;
    format.findAllTypesForFile(typesFound, pluginFileStripped);
    std::cout << typesFound << std::endl;
#endif

    if (!typesFound.isEmpty()) {
      if (typesFound.size() == 1) {
        foundPluginDescription = *typesFound[0];
      } else if (typesFound.size() > 1) {
        std::string errorMessage =
            "Plugin file " + pathToPluginFile.toStdString() + " contains " +
            std::to_string(typesFound.size()) + " plugins";

        // Use the provided plugin name to disambiguate:
        if (pluginName) {
          for (int i = 0; i < typesFound.size(); i++) {
            if (typesFound[i]->name.toStdString() == *pluginName) {
              foundPluginDescription = *typesFound[i];
              break;
            }
          }

          if (foundPluginDescription.name.isEmpty()) {
            errorMessage += ", and the provided plugin_name \"" + *pluginName +
                            "\" matched no plugins. ";
          }
        } else {
          errorMessage += ". ";
        }

        if (foundPluginDescription.name.isEmpty()) {
          juce::StringArray pluginNames;
          for (int i = 0; i < typesFound.size(); i++) {
            pluginNames.add(typesFound[i]->name);
          }

          errorMessage +=
              ("To open a specific plugin within this file, pass a "
               "\"plugin_name\" parameter with one of the following "
               "values:\n\t\"" +
               pluginNames.joinIntoString("\"\n\t\"").toStdString() + "\"");
          throw std::domain_error(errorMessage);
        }
      }

      reinstantiatePlugin();
    } else {
      std::string errorMessage = "Unable to load plugin " +
                                 pathToPluginFile.toStdString() +
                                 ": unsupported plugin format or load failure.";
...

Do i need to use pluginkit in order to register my AUv3 Plug-In for macOS in order to load it via Pedalboard ?

pluginkit -a /Library/Audio/Plug-Ins/Components/auv3.appex
pluginkit -mv | grep -i modal
   F com.Native.ModalTests.ModalTestsAUv3(0.0.1)    00E61720-45F1-4929-9C98-D5D49D6BF989    2024-05-01 12:47:30 +0000   /Volumes/K2TB/Work/Coding/JUCE/ModalTests/Builds/ModalTests_artefacts/Release/Standalone/ModalTests.app/Contents/PlugIns/ModalTests.appex

Tried that but still typesFound is empty.

dschiller commented 1 month ago

Also added tests at tests/test_external_plugins.py:

...
@pytest.mark.parametrize("plugin_filename", ONE_AVAILABLE_TEST_PLUGIN)
def test_get_plugin_name_from_regular_plugin(plugin_filename: str):
    plugin_path = find_plugin_path(plugin_filename)
    if ".vst3" in plugin_filename:
        names = pedalboard.VST3Plugin.get_plugin_names_for_file(plugin_path)
    elif ".component" in plugin_filename:
        names = pedalboard.AudioUnitPlugin.get_plugin_names_for_file(plugin_path)
    elif ".appex" in plugin_filename:
        names = pedalboard.AudioUnitPlugin.get_plugin_names_for_file(plugin_path)
    else:
        raise ValueError("Plugin does not seem to be a .vst3, .component or .appex.")

    assert len(names) == 1
    assert load_test_plugin(plugin_filename).name == names[0]

@pytest.mark.skipif(
    not AVAILABLE_CONTAINER_EFFECT_PLUGINS_IN_TEST_ENVIRONMENT,
    reason="No plugin containers installed in test environment!",
)
@pytest.mark.parametrize("plugin_filename", AVAILABLE_CONTAINER_EFFECT_PLUGINS_IN_TEST_ENVIRONMENT)
def test_get_plugin_names_from_container(plugin_filename: str):
    plugin_path = find_plugin_path(plugin_filename)
    if ".vst3" in plugin_filename:
        names = pedalboard.VST3Plugin.get_plugin_names_for_file(plugin_path)
    elif ".component" in plugin_filename:
        names = pedalboard.AudioUnitPlugin.get_plugin_names_for_file(plugin_path)
    elif ".appex" in plugin_filename:
        names = pedalboard.AudioUnitPlugin.get_plugin_names_for_file(plugin_path)
    else:
        raise ValueError("Plugin does not seem to be a .vst3, .component or .appex.")

    assert len(names) > 1
...

Of course AUv3 Plug-Ins need to be added to tests/plugins/effect/Darwin and tests/plugins/instrument/Darwin. -> Done

As expected those tests fail now:

pytest tests/test_external_plugins.py                     
================================================================= test session starts =================================================================
platform darwin -- Python 3.12.2, pytest-8.2.0, pluggy-1.5.0
rootdir: /Volumes/K2TB/Work/Coding/Python/Pedalboard/feature.add-auv3
configfile: tox.ini
plugins: cov-5.0.0, mock-3.14.0
collected 0 items / 1 error                                                                                                                           

======================================================================= ERRORS ========================================================================
___________________________________________________ ERROR collecting tests/test_external_plugins.py ___________________________________________________
ImportError while importing test module '/Volumes/K2TB/Work/Coding/Python/Pedalboard/feature.add-auv3/tests/test_external_plugins.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
/opt/homebrew/Cellar/python@3.12/3.12.2_1/Frameworks/Python.framework/Versions/3.12/lib/python3.12/importlib/__init__.py:90: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
tests/test_external_plugins.py:334: in <module>
    for parameter in [k for k, v in get_parameters(path).items() if v.type == float]
tests/test_external_plugins.py:126: in get_parameters
    return load_test_plugin(plugin_filename).parameters
tests/test_external_plugins.py:182: in load_test_plugin
    raise exception
tests/test_external_plugins.py:176: in load_test_plugin
    plugin = pedalboard.load_plugin(plugin_path, *args, **kwargs)
pedalboard/_pedalboard.py:823: in load_plugin
    raise ImportError(
E   ImportError: Failed to load plugin as VST3Plugin or AudioUnitPlugin. Errors were:
E       VST3Plugin: Unable to load plugin /Volumes/K2TB/Work/Coding/Python/Pedalboard/feature.add-auv3/tests/plugins/effect/Darwin/ModalTests.appex: unsupported plugin format or load failure.
E       AudioUnitPlugin: Unable to load plugin /Volumes/K2TB/Work/Coding/Python/Pedalboard/feature.add-auv3/tests/plugins/effect/Darwin/ModalTests.appex: unsupported plugin format or load failure. macOS requires plugin files to be moved to /Library/Audio/Plug-Ins/Components/ or ~/Library/Audio/Plug-Ins/Components/ before loading.
------------------------------------------------------------------- Captured stdout -------------------------------------------------------------------
/Volumes/K2TB/Work/Coding/Python/Pedalboard/feature.add-auv3/tests/plugins/effect/Darwin/ModalTests.appex
/Volumes/K2TB/Work/Coding/Python/Pedalboard/feature.add-auv3/tests/plugins/effect/Darwin/ModalTests.appex
/Volumes/K2TB/Work/Coding/Python/Pedalboard/feature.add-auv3/tests/plugins/effect/Darwin/ModalTests.appex
/Volumes/K2TB/Work/Coding/Python/Pedalboard/feature.add-auv3/tests/plugins/effect/Darwin/ModalTests.appex
/Volumes/K2TB/Work/Coding/Python/Pedalboard/feature.add-auv3/tests/plugins/effect/Darwin/ModalTests.appex
=============================================================== short test summary info ===============================================================
ERROR tests/test_external_plugins.py
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
================================================================== 1 error in 10.50s ==================================================================
dschiller commented 1 month ago

Thank you, Peter, for the fix! When do you regularly create releases?

psobot commented 1 month ago

@dschiller Releases are ad-hoc when there are new features to release; this change is now available in v0.9.5. Thanks for the report!