JUCE x Qt

Qt is used across many different industries, including the world of audio and music production software. As someone who occasionally dabbles in music production, I've always been motivated to keep Qt on the map for these use-cases, and to improve things where we can. To that end, during our latest company hackathon, I decided to explore the world of audio plugins. 

JUCE

JUCE is a widely used open source framework for cross-platform audio application and plug-in development. It provides end-end-to end support for the entire plug-in development workflow from writing DSP audio code to build system integration and packaging — and supports a wide array for plug-in formats, including AU, VST3, and AAX.

It also has a GUI library, but since we're fans of Qt, the goal was to try to integrate JUCE with a Qt-based UI. This would allow re-using all the plug-in specific wrappers and glue code in JUCE, as well as any custom DSP code written using the JUCE helpers, while being able to develop fluid UIs with Qt and QML.

Setting things up

I started by setting up a basic JUCE CMake-based project for an audio plug-in, verifying that I could run it as both a standalone application, as well as a plug-in in hosts such as Logic Pro and Ableton Live. JUCE provides convenient functionality that automatically copies the built plug-ins into the right system locations for being picked up by the host applications, so this was a breeze.

I also downloaded Apple's AU Lab for testing audio unit plug-ins, as this provided a much faster edit-compile-test cycle than launching Logic Pro after each change.

Diving into the code

My initial idea was to replace the entire UI layer with Qt, but it quickly became apparent that JUCE's various plug-in wrappers rely on setting up and owning the root level window/view for each plug-in. This is also where some of the host-specific quirks are handled, which we wanted to keep, so I started looking at embedding Qt into the JUCE UI hierarchy instead. Some quick googling didn't turn up any immediate results, so I concluded that this was not an out of the box feature of JUCE  (foreshadowing), and continued down the rabbit hole.

One function immediately stuck out, Component::addToDesktop(), as it took an optional second argument named nativeWindowToAttachToIt looked like this function was used by JUCE's own code for integrating with the host, so perhaps I could use it to integrate Qt with JUCE? Further diving lead to the factory function Component::createNewPeer() that looked promising. The design of JUCE matches Qt, in that a Component that's backed by a native view/window has a ComponentPeer, similar to how a QWindow may have a QPlatformWindow backing it.

I went ahead and implemented a QtComponentPeer subclass for managing a QWindow, and hooked it up to the audio editor component by waiting until the editor had a native window I could attach to:

void QtAudioProcessorEditor::parentHierarchyChanged()
{
    auto *parent = getParentComponent();
    while (parent) {
        if (auto *windowHandle = parent->getWindowHandle()) {
            qDebug() << "Adding editor to" <<  windowHandle;
            addToDesktop(ComponentPeer::StyleFlags{}, windowHandle);
            return;
        }
        parent = parent->getParentComponent();
    }
}

I was excited to see my test window pop up on screen in all its glory:

image

Something was a bit off with the margins, but we had something up and running! I quickly moved on to testing AU and VST3 plug-ins, and besides an assert from JUCE that I wasn't closing my editor at shutdown things were working surprisingly well.

I wanted to test a somewhat more complicated example, so I ported our beloved wiggly example, which allowed me to implement and test keyboard input as well.

image

It was at this point that I realized that both my margin offset woes, and the assert, was due to the component no longer being part of the component hierarchy. Calling Component::addToDesktop() brought it out of its parent component and made it a top level component, as I should perhaps have predicted. With this new realization I decided to call it a day, and sleep on the problem.

Starting out fresh

A good night's sleep and a few liters of coffee later I was ready to jump back in, and figured there would need to be some way for a Component to observe another component's moves, so we could update our embedded window accordingly. This led me to ComponentMovementWatcher, and finally to some of its consumers, including the embedding components: NSViewComponent, HWNDComponent, and friends.

It seemed I had been too fast to conclude JUCE didn't support this use-case 😅 Looking back, it's even listed on the feature page as one of the prominent features of JUCE. Oh well, I had learned a few things along the way!

With this newfound knowledge, I shifted my approach to implementing an embedding QtComponent for a QWindow , which turned out to be a much simpler task due to the minimal API surface of the ComponentMovementWatcher. This solved both the margin offsets I was seeing earlier, as well as the assert on plug-in shutdown, and worked out of the box for both standalone and VST/AU plug-ins.

Extending the minimal example to use QML worked pretty much out of the box, giving us rotating text! 🥳

(limited to 10fps in this GIF)

Unfortunately the hackathon was drawing to a close at this point, so I had to commit what I had and call it a day.

Next steps

While we did get something up and running in the end, the current solution is not without its kinks even requiring a few tweaks to Qt itself. I'll continue to improve things on this front, but feel free to take the code for a spin if you are adventurous, or just want to use it as inspiration for your own projects!

 


Blog Topics:

Comments