At the time of writing, Moog Music just released their first set of cross-platform audio plugins. The Moogerfooger Effects Bundle is 7 individual effects based on the line of Moogerfooger effect pedals produced between 1998 and 2018. The software effects are built for Windows and Mac as AUv2, VST3 and AAX plugins.
When we set out to build this product, creating an automated pipeline to build and test all seven plugins across each platform and format was incredibly important. We also needed this form of constant, regular testing because the plugins are not 100% standalone products. Since they all share code, even the smallest change requires building and testing all 7 plugins.
This article documents some of the learnings from managing this process over the course of the Moogerfooger Effects Bundle development.
What is Continuous Integration?
Before we get started, it would be good to define Continuous Integration (CI) as we have come to use it. CI is a common practice in software engineering, where a system is built out in order to automatically manage code changes as they are merged into the main code repository.
When there are multiple developers working on the same codebase, each developer may be focused on a narrow aspect of the whole. In some cases, their changes can create unpredicted bugs in seemingly unrelated parts of the codebase. By employing CI, there’s an automated system that makes sure all the products within the codebase still compile and build after each change and that all unit tests still pass. If there’s a failure, the developers are notified and can immediately address the issue.
JUCE, the Projucer and CI
The Moogerfooger Effects Bundle is built using the JUCE C++ framework. If you are not familiar with JUCE, it is a popular cross-platform C++ library for building audio applications. It is also paired with a standalone GUI application called the Projucer that helps the developer manage the application that they are building. It allows the developer to add and remove files, choose which JUCE modules are needed, set build flags, etc. The Projucer can then export the project as an Xcode or Visual Studio project or even a Linux Makefile for platform-specific development.
When we first started researching how to manage JUCE projects within the context of CI, CMake seemed to be a popular tool. CMake is an open-source cross-platform system for generating platform-specific projects, running tests and building applications. In many ways, it does everything that you would normally use the Projucer, Xcode and Visual Studio for. It’s essentially a command-line tool that uses a set of configuration files to do everything. By removing all the GUI applications, we can easily build out scripts to handle all the steps necessary for a proper CI pipeline. In addition, JUCE provides a CMake API to make the entire integration much easier.
CMake as a CI solution
As stated above, CMake is made up of configuration files named CMakeLists.txt
. For each project, there needs to be a CMakeLists.txt
file at its root. For our Moogerfooger Effects Bundle monorepo, that structure looks like this:
Moogerfooger Effects +-- MF-101S | +-- Source/ | +-- CMakeLists.txt +-- MF-102S | +-- Source/ | +-- CMakeLists.txt +-- MF-103S | +-- Source/ | +-- CMakeLists.txt ... +-- SharedCode/ +-- CMakeLists.txt
The root level CMakeLists.txt
adds the plugin directories and then a single cmake
command will generate projects for Xcode or Visual Studio for all 7 plugins and a second
will build the plugins as AU, VST3 and AAX.cmake --build
To have a better idea of what this might look like in practice, the following two commands could generate Xcode projects for all 7 projects and then build them. Here ${config}
is an environment variable for the target configuration, usually either “Debug” or “Release” and ${PWD}
returns an absolute path to the current directory.
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=TRUE \ -DCMAKE_BUILD_TYPE:STRING=${config} \ -S${PWD} \ -B${PWD}/build/${config} \ -G "Xcode" cmake --build build/${config} --config ${config}
At first glance, this seems like the perfect solution but it, unfortunately, came with some downsides.
First, and most importantly, a diminished developer experience for the team. CMake will generate Xcode/Visual Studio projects but they are not the clean, well-organized projects that Projucer exports. The code can be buried in unintuitive folders that don’t represent the project’s actual folder structure and there are many CMake-specific build targets in addition to the developer’s usual “Debug” and “Release.” With a project that already needs a Debug and Release build for each format (AU, VST3, AAX), this gets very noisy. We should also note that some of this can be mitigated through experience and careful configuration.
And secondly, as hinted at above, CMake is a large tool and demands its own kind of expertise. The CMakeLists.txt
are configured using a set of predefined functions and variables and a scripting language that looks and acts a lot of shell scripts. For us, maintaining the CMake configuration system became its own significant workload.
A Simpler Solution
After spending a little over a month using CMake, we began to look for alternatives and it appeared a simpler approach was right under our nose. The Projucer provides a command-line interface (Section 10.4) along with Xcode using xcodebuild
and Visual Studio msbuild
. Why couldn’t we use Projucer to export the projects, use the platform-specific tools for building and run the tests against the generated Debug builds?
With this setup, all of the configuration remains in each Projucer project independently and the developers are able to work in a much cleaner, easier environment. Then when they push their changes to the main repository, the CI runner can replicate the exact same environment as the developer for building and testing.
For example, to generate the Xcode project and build on Mac, where ${config}
is either Release
or Debug
, ${format}
is AU
, VST3
or AAX
and ${plugin}
is the name of both the plugin and its parent folder:
$HOME/JUCE/Projucer.app/Contents/MacOS/Projucer \ --resave "${plugin}/${plugin}.jucer" \ --fix-missing-dependencies xcodebuild build \ -project "${plugin}/Builds/MacOSX/${plugin}.xcodeproj" \ -config ${config} \ -scheme "${plugin} - ${format}_${config}"
And in Windows Powershell, the same thing can be done:
$arguments = '--resave ".\{plugin}\{plugin}.jucer" --fix-missing-dependencies' -f $plugin;
Start-Process -Wait -NoNewWindow -FilePath "C:\JUCE\Projucer.exe" -ArgumentList "$arguments"
& msbuild ".\$plugin\Builds\VisualStudio2019\${plugin}_${format}.vcxproj" /p:Configuration=$config /p:Platform="x64"
To Review:
This post is in no way meant to dissuade you from using CMake with your audio apps but rather offer a proven alternative solution. In order to weigh the two, we will list a few bullets to hightlight the strengths of each.
CMake Advantages:
- Command-line first.
- IDE Independent. You can develop on Sublime, VSCode, etc. and use the same IDE across platforms rather switch between Xcode and/or Visual Studio.
- Shared Configuration. You can configure multiple projects from a single
CMakeLists.txt
file.
Simple Command-Line Advantages:
- Invisible to the developer who is coming from Projucer. (Prettier project layouts, native IDEs, easier GUI configuration, etc.)
- One less technology to manage.