Replace Apple MainStage with a Custom Max/MSP Implementation (Part 4)
Well, it has been a few months since I wrote the previous articles on my development of a custom alternative to Apple MainStage. After using it for a few months, I started to figure out what else I really needed and as of today, I am comfortable that I have created an update worthy of being called version 2 (or I suppose I should call it version 2012!)
It’s going to take a while to finish this article and I’ve decided to make it visible along the way, otherwise I can’t say how long it will be before I finish it. Of course, having it visible before it’s finished will most likely act as a catalyst to get it finished ASAP.
The key new features of this version are
- Audio mixer
- External audio routing
- Better encapsulation
The one feature that MainStage (and other similar systems) had that was missing in my library was the ability to remember the state of parameters so that the next time a song was loaded, any parameters that were changed in real time while the song was previously loaded would be automatically recalled.
2) Audio mixer
I use a MOTU 828 MkIII + MOTU 8Pre and up until now I was just using MOTU’s CueMix application to do basic volume control. However, CueMix has two flaws: a) no remote control over the mixer and b) no way to process audio through software (e.g, VST effects).
I am now explicitly routing all audio through Max objects. As you will see, there have been some nice benefits to this design. Further, I have not observed any latency issues, something that was of concern to me. The mixer has channel strips with the ability to send signals to VST effects.
3) External audio routing
The new mixer supports my external audio devices (keyboards, synths, guitars, vocals) and those can also be processed with VST effects. This will in fact allow me to get rid of quite a few external stomp pedals.
4) Better encapsulation
Among other things, I have taken the [GenericVST] object and wrapped it inside various new objects that automatically route audio to the desired mixing channel or effects send bus. It is also much easier to configure parameters of a VST and to initiate a VST edit/save cycle.
Consolidated front panel
Here’s a picture of the latest version of my consolidated front panel. You will note that it has changed significantly from the original version I described when I first started this effort. (Click the image to see a larger view)
Apart from all the extra stuff in there (compared to my original version), the key change is that the bpatcher-based panels that represent the control surfaces for some keyboards (Akai and AN1x) as well as the panel that represents the mixer (external, VST and effects) are constructed from more primitive objects at the bottom of which are UI objects whose state can be stored and reloaded centrally.
While Max comes with several mechanisms for managing persistence (save/restore state of a coll, the pattrstorage system, the new dict objects in Max 6), the first was too simplistic, the second was too complicated and the third was not available for Max 5.1.9 and so not evaluated. However, my persistence needs were quite specific. If you turn a dial or change a slider position, directly or via MIDI from a control surface, I want the current value of that dial or slider position to be associated with the currently loaded song so that when the song is reloaded later, the value is restored, and sent out to whatever parameter is being controlled.
This is actually being displayed through a [bpatcher] object, as indicated by the light blue line around it. (That line is only visible when the patcher is in edit presentation mode.) If I drill in (i.e, open up this bpatcher), then one of the items is just the 8 dials.
Similarly, drilling into this bpatcher, we can see that there are in fact 8 separate bpatchers as well as a comment field to which we will return later.
Drilling into any one of these, we find the underlying dial and integer in an object called pdial (persistent dial). This is where the fun starts:
Note that there is also a pslider (persistent slider) object and a plabel (persistent label) object both of which are implemented in the same manner. Note that at its core, the dial can receive values whose name is #2_pdial and it can send its value out as a packed name/value pair to something called #1.
Now, #1 and #2 represent the values of two arguments that are passed into the object when it is instantiated. The first argument (whose value will be the same for all objects used in a given environment) will be the name associated with a [receive] object that will pass all incoming values into a dictionary of some kind. We will see what that dictionary looks like later.
Creating unique names for user interface objects
The second argument is much more interesting. We need a mechanism where we can end up with unique names for different dials or sliders, each of which should have little or no knowledge of what else is there. To cut a long story very short, the solution I chose was to define at every level a name consisting of whatever is passed in from the parent level concatenated with whatever makes sense for the object in question. This is easier to demonstrate than to describe.
The second dial in the AN1x console receives messages addressed to a receiver with the name
Similarly, the 4th channel strip of the MOTU mixer receives messages addressed to
and the lower SEND dial for for the 5th channel strip is addressed by
If you parse that last one, reading right to left, it says that there is a persistent dial in an EffectsSend object which represents the second EFX of a channel strip living in an ExternalChannelStrip, the 5th channel strip contained in the MOTUKeyboardsMixer which lives (along with other things) in the MainMixer patch.
The nice thing about this is that the only objects that have to know this full name are the bottom level persistent dials, sliders and labels themselves.
Connecting with the dictionary
A Dictionary object is inserted into every top level patcher that represents a song. (Remember that each such a patcher contains the MIDI routings and VSTs that are needed to play a particular song). Here’s a view of a Dictionary as contained in a song patcher.
The single parameter is the name that will be used by all persistent objects when they send their values. It can be anything you want. In my system, this name is DHJConsole.
Here’s the contents of the dictionary patcher. (Click to open full view in a separate window)
Let’s go through the steps that occur when you turn a pdial (persistent dial) named AN1x1_AN1xConsole_2_pdial to the value 6
- The pdial sends out a list containing two values (AN1x1_AN1xConsole_2_pdial 6) through a [send DHJConsole] object
- The dictionary receives this list through the [Receive #1] (half-way down on the left hand side). Remember that that #1 will have been replaced with DHJConsole when the dictionary is actually instantiated.
- The text insert is prepended to the list and so the message
insert AN1x1_AN1xConsole_2_pdial 6
is sent into the NVPair.js object.
- The NVPair object creates an entry called AN1x1_AN1xConsole_2_pdial in an associative array and sets its value to 6. This is known as a name/value pair. Note that if there was already such an name in the associative array, then the previous value would just get updated to the new value, 6.
Steps like these occur whenever any persistent object is modified. The NVPair object also has functions that can save and reload all names and values.
When a patcher containing a dictionary is closed, the contents of the associative array are saved to a file whose name is the same as the patcher (but with a different extension). When a patcher containing a dictionary is opened, the contents are reloaded into the associative array and then the following steps occur automatically after the patcher has finished loading any VSTs (those are the only objects that can take quite a few seconds to load).
- The dumpall message is sent to the NVPair
- This message causes the NVPair to iterate through the array, sending out every entry as a name/value pair. This is where the fun starts.
- For each entry, the value will be sent out first and it gets stored in a temporary variable (the [pv value] object)
- The [forward] object is a variant of the [send] object that allows the name to be changed. So the name that will be used is the name that comes out of the NVPair.
- Therefore, the value will be sent to any receiver whose name is the same as the name that came from the NVPair.
- Each name will exist in one single [receive] object corresponding to the persistent object that was created, as described in the “Creating unique names for user interface objects” section above. That is how each individual user interface element is updated.
In upcoming articles, we will talk about audio mixer, routing and encapsulation.