blog

Software screen capture

Developing Audio Applications With JUCE (part 2)

by

Last time we looked at getting a very basic version of an application that can process audio running using the classes provided with the JUCE application framework.

The Scumbler app as we last saw it:

  • Could enumerate the installed audio/MIDI hardware on a computer
  • Let user select which hardware to use
  • Persisted that setup information between invocations
  • Created an AudioProcessorGraph object and an AudioProcessorPlayer to stream blocks of audio samples through
  • Created AudioGraphIOProcessor objects for performing actual audio input and output, which when wired into the filter graph successfully got audio moving through the application.

A lot of work to basically do nothing. This time, we’ll add some meatier stuff — the ability to add and control VST or AU plugins into the filter graph.

 

See other posts tagged “JUCE

Audio Effect Plugins

A brief sidebar on what these plugins are — after writing the last post I realized that I had blown past the issue completely.

If you had walked into a recording studio 20 years ago, you’d find racks of signal processing hardware that the engineers would route different channels through to add different effects like equalization, compressing dynamic range, adding reverberation and echo, etc. The output from this signal processing chain would end up being output to multitrack tape for eventual mixdown to stereo output that could be pressed on CD.

Universal Audio's 1176 Plugin collection

Universal Audio’s 1176 Plugin collection

In the last 20 years, however, more and more studios have moved to a more-or-less fully digital environment; instead of being based around tape, all recording is done using digital audio workstation software running on a powerful modern computer. Somewhere along the way, engineers realized that the racks of hardware could be reduced (with varying degrees of success) to algorithmic recreations of those processors, keeping all processing in the digital domain. Any DAW you’ll find today has the ability to load effects plugins in either Steinberg’s VST format or Apple’s AudioUnit (AU) formats.

Each track of the Scumbler app will be able to load up to 4 plugins before the loop effect and another 4 after the loop.

Useful JUCE Classes

As we saw in Part 1 of this series, JUCE comes with a lot of the code that we need for this task built-in and ready to bake into your application. In order to be able to add support for plugins, we need our app to support these operations:

  • Search the user’s system for any installed plugins
  • List the installed plugins
  • Let the user select a plugin from that list
  • Load the user’s selected plugin into our app’s memory space, ready to process audio
  • Display the plugin’s user interface for modifying control values

KnownPluginList

The KnownPluginList class maintains a list of the plugins that are installed on the user’s system. Additionally, this class knows how to sort itself in a few useful user-selectable ways (e.g., group by manufacturer, group by different plugin types, etc.), and can also populate a JUCE PopupMenu for easy user selection. Like many of the JUCE built-in classes, it can persist itself to and from XML, and being able to store the list of installed plugins saves your users the time of re-scanning their system for plugins every time they launch your app.

Your app should create a single instance of this class that’s used throughout. (I"m currently using a global variable for this, which makes me sad every time I see it in the code. Getting rid of globals is on my list of future tasks…) As part of application startup, try to recreate the list from XML (here, the app uses a single XML file for all user preferences):

ScopedPointer<XmlElement> pluginList(userSettings->getXmlValue("pluginList"));
 if (nullptr != pluginList)
 {
    gKnownPlugins.recreateFromXml(*pluginList);
 }

…but if this is the first time your app has run (or if the user has installed new plugins since the last time), you’ll need to be able to scan for installed plugins.

We use the KnownPluginList again later in the app when the user wants to actually load and insert a plugin into the Scumbler:

void PluginSlotComponent::mouseDown(const MouseEvent& e)
{
   // displaying the list of plugins to let the user
   if (e.mods.isPopupMenu())
   {
       // If we are empty, we need to display the list of available plugins so
       // the user can select one.
       // If we have a plugin loaded, we need to instead display a menu to let
       // us delete that plug-in.
       PopupMenu m;
       if (this->IsEmpty())
       {
           // add all of the plugins to the menu
           gKnownPlugins.addToMenu(m, KnownPluginList::defaultOrder);
           // show the menu. On return, r will be the index
           // of the selected item
           const int r = m.show();
           // get a PluginDescription object for the selected plugin
           int pluginIndex = gKnownPlugins.getIndexChosenByMenu(r);
           PluginDescription* pd = gKnownPlugins.getType(pluginIndex);
           if (pd)
           {
              String errorMsg;
              // pass this plugin description down toward the Scumbler object,
              // which will:
              // - Load it
              // - connect its inputs/outputs correctly
              // - let us know if that was done successfully.
              if (tk::kSuccess == fPluginBlock->LoadPluginAtIndex(fIndex,
                 *pd, errorMsg))
              {
                 this->setTooltip(pd->manufacturerName + ": " + pd->name);
                 fPluginName = pd->name;
              }
              else
              {
                 AlertWindow::showMessageBoxAsync(AlertWindow::WarningIcon,
                    "Error Loading Plugin", errorMsg);
              }
          }
      }

PluginListComponent

This class can be displayed in a window and will show all of the plugins currently installed as well as presenting buttons to initiate a scan of the system.

This code:

PluginListWindow::PluginListWindow(MainAppWindow* owner,
 AudioPluginFormatManager& formatManager)
: DocumentWindow("Available Plugins", Colours::white, DocumentWindow::closeButton)
, fOwner(owner)
, fFormatManager(formatManager)
{
 // logic lifted directly from the JUCE sample app 'Audio Plugin Host',
 // rewritten to make me happier stylistically.
 PropertiesFile* userSettings = gAppProperties->getUserSettings();
 const File crashFile(userSettings->getFile().getSiblingFile("CrashedPlugins"));
 this->setContentOwned(new PluginListComponent(fFormatManager,
  gKnownPlugins, crashFile, userSettings), true);
   this->setResizable(true, false);
   this->setResizeLimits(300, 400, 800, 1500);
   this->setTopLeftPosition(60, 60);
   this->restoreWindowStateFromString(userSettings->getValue("listWindowPos"));
   this->setVisible(true);
}

…produces this:

PluginList

PluginDescription

As we saw above in the KnownPluginList section, after populating a menu with the available plugins, after the user selects one, we can retrieve an instance of the PluginDescription class to find out details about that plugin. We'll use that object in just a minute to actually get the plugin loaded, but it's also going to be the eventual mechanism we use when we want to be able to persist Scumbler configurations to and from disk.

AudioPluginFormatManager

The last piece that we’ll look at in this installment is the AudioPluginFormatManager class. Your app should create a single shared instance of this class that’s used to find and load plugins. This class also is the source of my one quibble so far with JUCE’s design — it seems to me that there’s logically a tighter coupling between this class and the KnownPluginList than is expressed in the design. Jules is a clever designer, so I suspect that there’s a reason for this choice that hasn’t made itself obvious to me yet.

Early in your app’s life, be sure to tell this object what formats it needs to support — if you don’t at least tell it to look for the default formats, you’ll scratch your head wondering why your app lies to you and says that you don’t have any plugins installed at all.

   // make sure that the plugin format manager knows to look for AU/VST formats.
   fPluginManager.addDefaultFormats();

When your user selects a plugin and retrieves a PluginDescription object from the KnownPluginList, you can pass that PluginDescription object to your AudioPluginFormatManager object to get the plugin loaded up:

NodeId Scumbler::LoadPlugin(const PluginDescription& description,
  String& errorMessage)
{
   NodeId retval = tk::kInvalidNode;
   AudioPluginInstance* loaded = fPluginManager.createPluginInstance(
       description, errorMessage);
   if (loaded)
   {
      // Add this plugin to the AudioProcessorGraph. It gets connected elsewhere
      // (in PluginBlockComponent.cpp)
      retval = this->AddProcessor(loaded);
   }
   return retval;
}

So, mission accomplished for this milestone — the app can now

  • Search for and display installed AU and VST plugins
  • Display that list in a menu for the user’s selection
  • Load the selected plugin and insert it into the audio filter graph.

Next time… we loop.

+ more