PFT Essays: The Modulation System

PFT’s modulation system was the biggest single piece of work. It took several months to go from nothing to something semi usable in the UI.

 

Design

 

I started by looking at one of our older plugins: Coercion. Coercion has a very basic modulation setup with four macros that can be applied to any distortion module parameter. At the beginning of the audio callback the values for each macro are recorded in a table that is passed to each processor along with the audio buffer. Each processor then passes that table along to its parameters to calculate their values for this buffer.

 

Coercion’s sliders access the parameter classes directly and set the amount of each modulator to be applied manually. This means adding a modulation in Coercion really doesn’t mean anything. You’re just setting a value in a memory slot that existed the whole time. This also means that the slider components in Coercion only worked with Coercion’s parameters. They’re not reusable at all. The advantage of this is its pretty simple. As long as your requesting the correct parameter ids there isn’t a whole lot of ways to go wrong. The disadvantage is it’s extremely rigid. If you want to change how one piece of this system works you have to change how everything works. If you want a new type of parameter that works slightly differently you need to do a complete rewrite.

 

Tightly coupled code was a theme in Coercion. The place where this was most evident was between the processor and the editor. Every single processor, parameter, model, and modulator value had to be grabbed explicitly by the editor, often in pretty convoluted ways. Everything that was part of the modulation system was separate and had to be handled as a special case (even though most of the sliders you see can be modulated). *Everything* in Coercion was built exclusively for Coercion.

 

Improving on what Coercion had was a major goal for PFT. The whole concept of the plugin is very different. Modulators can be applied to any parameter in the entire plugin including other modulator parameters. Modulators can be added or removed whenever. Filters that are being modulated can be added or removed whenever. Everything needs to work seamlessly across all of the moving parts in the plugin. Each of the sections in the UI need to present modulation controls to the user in different ways. Everything is bigger and better, the modulation system would have to be as well.

 

I started by restricting what information the editor is allowed access to: Only parameters and value trees. This is probably the most important step in simplifying the design. The barrier between the processor and the editor is the most significant one in any normal audio plugin. Making it as simple as possible goes a long ways towards simplifying your overall design.

 

Communicating processor state changes with as few moving parts as possible has the added advantage of forcing the use of very generic components. Coercion passes its modulated parameters around as specialized CrcParameters. This encourages you to write unique classes specifically for them. Not necessarily bad in a simple plugin like Coercion but a huge weakness for something like PFT. Every single parameter in PFT is a juce::RangedAudioParameter (including amount and polarity parameters). This means they all share a couple base component classes for controlling them. The parameters that drive the modulation system and the parameters that drive the filters are all controlled by the same base slider class.

 

While it simplified editor communication it actually made things slightly more complicated in the processor. I didn’t want to allocate all of the modulation parameters up front when a modulated parameter is constructed. It’s unlikely that a user uses even half of the available modulation slots. Each modulation parameter would have to be set and unset as the user adds and removes modulations.

 

The design I came up with revolves around four core classes:

 

ModTableModel: Contains information about which modulators are assigned and what parameters affect their modulation amounts. This is built with and passed to the UI as a ValueTree. Listeners attached to it can be used to build up UI components for editing modulations as well as setting values in the processor.

 

ModTableData: Contains two atomic juce::RangedAudioParameter * for each of the eight modulation slots (four modulators, four macros). One of these parameters is for modulation amount. The other is for polarity. The ModTableData class is the only place the audio thread ever reads for calculating parameter modulations. This is important. Ideally the audio thread should be doing as little work as possible. Building and setting the data is all handled by the message thread. Reading the data and making calculations is the audio thread’s job.

 

ModTableDataSync: receives juce::ValueTree::Listener callbacks from the ModTableModel and fills the ModTableData with parameters from the ParameterStore.

 

ParameterStore: Dynamically creates private juce::RangedAudioParameters to be used within the modulation system. This works on a checkout and return system. When a modulation is added the parameters are checked out from the parameter store. When the modulation is removed the parameters are returned. This means parameters are never deallocated during execution so you don’t have to worry about dangling pointers within ModTableData. At worst the calculation might be made against a stale parameter but that’s not the end of the world and will almost certainly not be noticed. The key advantage to doing things this way is that ParameterStore parameters can be interacted with by the UI the same exact way that regular parameters would be. They’re all the same underlying class and their lifetimes are tied to the plugin itself.

Interactions

 

When the user adds a modulation to a parameter:

 

 – The slot in the ModTableModel is set to assigned.

 

 – The ModTableDataSync gets a callback and requests a new parameter identifier from the parameter store. The returned juce::Identifier is set in the ModTableModel.

 

 – The ModTableDataSync is notified about the new parameter identifier that it set and grabs the juce::RangedAudioParameter * from the parameter store. It then sets the parameter pointer in the ModTableData class.

 

 – The parameter process happens twice. Once for the polarity and once for the amount.

 

Handling the thread boundaries here was the most complex part. I (maybe naively) assumed that a juce::MessageManagerLock would be enough to handle thread synchronization while interacting with the ModTableModel. However, the plugin being loaded and processing audio does not mean that the message loop is running. My false assumption about this lead to the plugin hanging while waiting to acquire a lock on the message thread in some cases. It took me a couple days to realize that the bug I had encountered was actually expected behavior and I needed to rethink some aspects of my design.

 

One related thing that’s worth saying about using juce::ValueTree models in the audio thread: They’re generally only intended for use in the message thread – be careful. If the user triggers a change while the tree is being serialized in the processor you’ll have a collision, for example.

 

In this case I have a std::mutex that protects against concurrent juce::ValueTree access. Additionally, the audio thread itself never touches the juce::ValueTree during the audio callback (important!). It just reads the data in ModTableData, which is set atomically by the ModTableDataSync. This means no locks and no possibility of thread collisions while processing audio. In the rare case that the host deserializes while the user tries to add or remove a modulation the lock prevents anything from going wrong.

Implementation

 

When I was in school I absolutely hated doing UML diagrams. Now I try not to write too much code without them. Breaking out the crayons is the best way I know to organize and understand my ideas. I leaned on diagrams heavily as a visual aid while implementing everything in PFT but especially the modulation system.

 

I use the diagram as kind of a dependency graph to figure out which components in the system I should implement when. Generally, boxes in the diagram with the fewest dependencies are easiest to start with. In PFT specifically, I started with the parameter store. It’s a relatively simple class that only exposes three methods in its interface. It also only interacts with one class in the modulation system. I think implementing functionality from least to most complex is a pretty logical way to go about doing things. The complicated stuff tends to be a combination of the simple stuff anyway.

 

Testing

 

Writing tests sucks. I hate doing it. being said I couldn’t have written the modulation system without them. Each step of the process I wrote up a suite of tests targeting the interactions with the class I was writing. When object A does thing B to object C does the state of object C reflect it? It gets a little complex once you start testing interactions between lots of different components.

 

Adding a modulation to a parameter for example is too complex to capture effectively in unit tests the first time around. Its easy to miss the little things when you’re dealing with so many moving parts. It’s time to use my favorite form of debugging: printf debugging. Log each step of the process you’re testing and compare the output to your sequence diagram. This helped me nail multiple bugs in adding and removing modulations. Once you understand the bug you can write a nice unit test to squash it. I used to avoid doing stuff like printf debugging because it wasn’t ‘professional’. If its stupid but it works then it isn’t stupid.

 

Final Thoughts

 

I like PFT’s modulation system. I like how simple the interface is. I like how self contained it is. Pulling it out and put it in another project is pretty straightforward. I like how elegantly it handles thread boundaries. I like that it’s better than Coercion’s in every single way.

 

I wish some parts of it were more flexible. The current implementation is hard coded to eight modulation slots. This works for PFT but it wouldn’t work well in a more complex plugin. A big synth like Serum 2 has an insane number of possible modulation sources. Each parameter having its own table of tens of modulation entries doesn’t scale well.

 

One thing that I thought about doing but ultimately decided I wasn’t capable of was modulating with SIMD. The concept of setting up all of the parameters in memory so that SIMD operations can be applied seems really cool. The way that PFT is works where modulations are calculated at each parameter is a lot easier to understand but I would assume significantly less performant than some centralized modulation scheme would be. Someday I’ll try it out and write about it.