Managing plugins in Logic Pro X with Python (part 2)

28/11/2019

I spend way too much time making sure I add every new plugin I buy to a plugin manager category, else it gets forgotten and lost in the depths of the plugins by manufacturer section - so I decided to see if I could improve this a little with python.

In the previous post we looked at how Logic Pro's plugin manager stores plugin data. Now we want to do something useful with that data, but first we should scan for which AudioUnits are installed on the system.

Intro

This post is part of a series which will end with some C++ and Python code that can scan for plugins and alert us if a plugin hasn't been added to a category. In the process we'll cover the following: I assume that you're already familiar with the plugin manager and how to use it to create categories. If not, I recommend reading this article for an introduction.

This post will tackle the issue of scanning for installed plugins, which is a prerequisite to doing something useful with the plugin manager data.

AudioUnit metadata

Contained in an AudioUnit package are several pieces of metadata which identify the AudioUnit to a host or other program which is scanning for them. This data includes: The most obvious way of retreving this data from a plugin seems like it would be parsing the Info.plist file contained in the plugin, however this doesn't always contain all the required data.

A more reliable way is to get the data from the compiled code inside the plugin, which involves loading the plugin into a program and then using the API defined by the AudioUnit standard to extract the data. This is one of the things Logic Pro is doing when scanning for plugins, and is the reason why it can take a while.

It's entirely possible to write your own program to interact directly with the plugins and extract the data, however this is a reasonably complex process which has already been solved for us, so instead we'll use the JUCE C++ library to do this.

Using JUCE with Python

So this post is supposed to be about Python, but here we'll use a little C++ to interact with the AudioUnits and then wrap this in Python.

First you need to download JUCE here. Then you can open the Projucer and create a new dynamic library project.

The Projucer is pretty intuitive and should feel familiar to anyone that has used an IDE before. If at any point you find that some part of your project isn't working, you can download a completed version here.

Adding the JUCE code

Create a header and cpp file called interface.h and interface.cpp. These files will contain the code that uses JUCE's plugin scanner implementation to scan for plugins.

So the first thing we'll need is an object that allows us to scan for plugins and retrieve information about the plugins. Add the following to interface.h to define this object:
    #pragma once

    #include "JuceHeader.h"

    class PluginScanner {
    public:
        PluginScanner();

        bool nextPlugin();

        const char* getFileOrIdentifier(int index) const;
        const char* getManufacturer(int index) const;
        const char* getName(int index) const;
        int getNumPlugins() const;

    private:
        KnownPluginList _pluginList;
    };
So our PluginScanner object now provides a way for us to retrieve information about an arbitrary plugin that is installed on our system. Notice that it doesn't provide a method to scan for plugins, it'll do this when it's constructor is called.

Now we need to actually implement this object. Add the following code to interface.cpp:
    #include "interface.h"

    namespace {
        void scanPlugins(PluginDirectoryScanner& scanner) {
            String pluginName;

            while (true) {
                const String nextName = scanner.getNextPluginFileThatWillBeScanned();

                if (!scanner.scanNextFile(true, pluginName)) {
                    break;
                }
            }
        }
    }

    PluginScanner::PluginScanner() {

        AudioUnitPluginFormat aupf;
        PluginDirectoryScanner scanner(_pluginList,
                                    aupf,
                                    aupf.getDefaultLocationsToSearch(),
                                    true,
                                    File());

        scanPlugins(scanner);
    }

    const char* PluginScanner::getFileOrIdentifier(int index) const {
        return static_cast(_pluginList.getType(index)->fileOrIdentifier.toUTF8());
    }

    const char* PluginScanner::getManufacturer(int index) const {
        return static_cast(_pluginList.getType(index)->manufacturerName.toUTF8());
    }

    const char* PluginScanner::getName(int index) const {
        return static_cast(_pluginList.getType(index)->descriptiveName.toUTF8());
    }

    int PluginScanner::getNumPlugins() const {
        return _pluginList.getNumTypes();
    }
Most of what we're doing in this implementation isn't super interesting, the constructor simply creates an AudioUnitPluginFormat object, along with a PluginDirectoryScanner which is given the _pluginList which we want it to populate with detected plugins.

Then we call scanPlugins and use it to iterate through the scanned plugins. After this our _pluginList will be populated, ready for us to call one of our get methods to retrieve information.

Calling C++ from Python

Now that we've written the code to scan for our plugins in C++, we could either write the rest of our progam in C++ as well, or we could call the C++ we've written with Python. In this case we're going for the Python option since many people find it to be a slightly friendlier and more productive language to work with.

Unfortunately Python can't directly call C++ code but it can call C code, so we're going to provide a C interface for the PluginScanner class we created. Add the following code to the bottom of interface.h:
    extern "C" {
        __attribute__((visibility("default")))
        PluginScanner* PluginScanner_new() {
            return new PluginScanner();
        }

        __attribute__((visibility("default")))
        const char* PluginScanner_getFileOrIdentifier(PluginScanner* self, int index) {
            return self->getFileOrIdentifier(index);
        }

        __attribute__((visibility("default")))
        const char* PluginScanner_getManufacturer(PluginScanner* self, int index) {
            return self->getManufacturer(index);
        }

        __attribute__((visibility("default")))
        const char* PluginScanner_getName(PluginScanner* self, int index) {
            return self->getName(index);
        }

        __attribute__((visibility("default")))
        int PluginScanner_getNumPlugins(PluginScanner* self) {
            return self->getNumPlugins();
        }
    }
The above code does several things, the primary one being that we define a function for each method of our PluginScanner class.

For example the function PluginScanner_new is our interface to the constructor, it creates a new instance of the class and returns a pointer to it.

Then a function like PluginScanner_getName can be given the pointer that was created by PluginScanner_new in the form of the "self" variable, and will call the getName method on it using an index we provide.

This is all then decorated with extern "C" to make the functions available to external code as C style functions, and __attribute__((visibility("default"))) which ensures that the functions aren't going to be hidden from external code.

And that's all the C++ we need to write. You can build the project from the Projucer using an IDE of your choice.

In the next post we'll move on to the Python code that contains most of the logic, and then how to schedule it to run every day.