Max Tutorial #12: Finally, Polyphony!

In this tutorial, we’ll finally build our first genuinely polyphonic synthesizer in Max! This tutorial will draw on techniques developed in several of the previous tutorials—particularly Tutorial #11. The first thing to mention is that in order to play a polyphonic synthesizer, you need to be able to press more than one key at once. Therefore, this tutorial really requires the use of a MIDI controller to fully appreciate what we’re doing, since we can only click on one key at a time with the cursor.

As before, we’ll begin with a [kslider] taking input via MIDI from a [notein] object. We’ll also use [attrui] to set the [kslider] object’s display mode to “polyphonic.” We send the output of the [kslider] to a new object called [poly], to which we pass the arguments “8” and “1.” The [poly] object is one of several ways of managing polyphony in Max. It takes MIDI note messages and assigns them to a voice by outputting a voice number from the left outlet. When creating a [poly] object, the first argument specifies the maximum number of voices, and the second (optional) argument indicates whether excess notes (beyond the number of voices specified) should be ignored (“0” or no argument) or if they should “steal” voices (“1”). (If you prefer, you can omit the [kslider] and [attrui] and connect the left and center outlets of [notein] to the left and right inlets of [poly] directly.)

Instead of passing pitch and velocity data separately as we have done previously, we will bundle these values together with the voice number to making polyphonic routing easier. To bundle data into a list format, we can use an object called [pack]. The arguments for [pack] indicate the number of values and data types expected. In this case, we use “0 0 0” as placeholders to indicate three integers: the voice number, the pitch, and the velocity. If we test the output using a message box, when we press a key we can see that the pitch (second number) and velocity (third number) are prefixed by the voice number, which advances each time.

We can use the voice number prefix to route the pitch and velocity information to the appropriate synthesizer voice. The routing object is called [route], with arguments for each expected prefix. With eight voices specified for [poly], we will expect eight different prefixes, corresponding to the numbers 1-8. Once the data has been routed to the appropriate voice (via the outlet corresponding to a prefix), we can unpack the pitch and velocity data using [unpack 0 0]. Once these values are unpacked, we can connect them to our usual synthesizer objects as before.

If we test the synth at this stage, we note that we only hear sound every few notes. The reason is that [poly] routes notes through all eight voices, but we only have an actual synth voice connected to the outlet for the first voice. Consequently, we have to connect synth voices to each outlet of [route]. This is done much more easily if we encapsulate the synth voice. We’ll name it [p synth_voice] and then copy, paste, and connect each to each the [route] object’s outlets. (Note that [route], like [sel] and other objects, has an extra outlet on the right for input values that don’t match any of the arguments. We can ignore this since our number of voices matches our number of arguments.)

Once everything is connected, if we play chords we can finally hear all of the notes at once! However, the sound still has a very rough shape: it starts and stops immediately, producing a choppy, unmusical sound. We’ll finish off this synth by adding a simple attack-release envelope. First, we have to modify the inside of each synth voice. We can do this by modifying one, and then copy and pasting. To open the first synth voice, lock the patch and double-click on the first [p one_synth] subpatch. We don’t have to make any changes to the pitch chain (on the left); we’re just modifying how loudness changes over time, so we’re concerned with the velocity chain (on the right).

First, we’ll add a [routepass] object with an argument of “0.” The [routepass] object is very similar to [route], except that [route] removes the prefix as it passes a message through, whereas [routepass] keeps the prefix as part of the message. This object allows us to distinguish between note off messages (0) and note on messages (any non-zero value). Note off messages will pass through the left inlet, and note on messages will pass through the right. In order to create smooth attack and release ramps, we’ll use the [line~] object as in previous tutorials. The [line~] object takes two arguments to form a ramp: a destination value, and an amount of time to reach that destination. Therefore, we’ll need to pack two values together using [pack 0. 0].

The first value in each [pack] object is the destination (a decimal between 0 and 1), and the second is the duration to reach that destination, corresponding to our attack or release time. We’ll use the [s] and [r] objects discussed in the previous tutorial to circulate attack and release times through all of the voices of the synthesizer. (We’ll call them “attack” and “release.”) Once we’ve made the necessary changes in one voice, we’ll select that subpatch, copy it, and then select all of the other subpatches and click Edit -> Paste Replace. This is a quick way to replace many subpatches from a single template.

The final step is to build an interface for setting the attack and release times. We’ll use integer boxes connected to [s] objects. Now if we change these values, we can hear that the changes are automatically and immediately applied to all voices in the synthesizer, giving us a uniform sound. And just like any other polyphonic synth, not only can we hear all of the notes of a given chord, but with a long release time we can hear an overlap between notes played in sequence.