As a Max programmer, I spend quite a lot of time making patches that some people might find a little odd; rather than a large "instrument" that I toil over at great length or "the patch is the piece" outings, I love to make Max patches that don't make any noise or play any movies or create OpenGL scenes. Instead, I make things that are generative - working from the idea that Max is really just about messages, numbers, and lists and how you move them about; I love creating ways of generating and organizing variety, and then exploring what those generative structures do when hooked up to audio or video or other outputs.
Just as there are always several ways to do any given thing in Max, so there are several different ways to end the sentence, "One of the cool new things in Max 5 is...."
One of those things that piqued my interest is the ability to specify time in Max in ways that make so-called "musical" sense; in addition to creating metronomes that send out a bang every 100 milliseconds, you can now work with lots of new and interesting ways to specify time: ISO 8601-format hh:mm:ss.ms-style time, samples, "ticks" and note values. There's a long list of objects you can work with (you can find that list here
, both older Max objects as well as some new objects that help you take advantage of them (timepoint
, and combine
). While lots of people are interested in using the new Max 5 transport
object to arrange and trigger events in time, I'm personally a lot more interested in the ability to synchronize processes in Max using time values that resemble musical note values to create control structures that can be easily time synced.
This may be because I have a deep and abiding love for some of the interesting and esoteric control voltage uses of the LFO (Low Frequency Oscillator) in traditional analog synthesis. I'm thinking of analog modules like this
- a cool set of LFOs that can be combined to achieve interesting effects (Anybody want to sell me a beat-up old AKS Sythi on the cheap? No? And you wonder why I patch...). The great thing about Max is that you can produce the Max equivalent of these great general-purpose modules, and use them in any way you want. In fact, I'll often create patches like this and then spend the next few weeks hooking them up to all the patches I've got sitting around to see what they can do.
This tutorial is about making one of those kinds of modules - a quartet of synchronized LFOs whose outputs I can sample individually for several kinds of data (triggers for waveform start, LFO outputs that I can sample at variably synchronized rates, and a nifty summed waveform I can use for more exotic kinds of control).
Note: The downloadable materials
that accompany this tutorial contain not only the large patch I'm going to talk about making, but a number of the little steps along the way - feel free to take the pieces and tear them apart and make things that are interesting to you
, in addition to following along.
Here's what I usually use to create LFOs in Max.
You might have been expecting to see a cycle~
object here instead of the phasor~
object. While you could certainly use cycle~
objects for your waveform output, I prefer to use the phasor~
- a Max object that outputs a ramp that cycles between the floating point values 0. and 1.0 at a rate you specify.
object will take the output of a phasor~
object and produce a nice sinusoidal waveform with the standard -1.0 to 1.0 output range. The nice thing about using a phasor~
is that I can also easily produce a sawtooth LFO output by adding a pair of objects that change the range of the phasor~
object's output. (For extra credit, you might think about how you could generate a triangle wave using a phasor~
as a source. Yes, I know - you could just slap a triangle~ object below the phasor~
in the patch, but I'm encouraging you to think about signal math. If that seems too easy for you, how would you produce a square
Now that we've got two waveforms, what kinds of useful information can we extract for our use? Since the waveform's output is happening at audio rate, we'll need to sample its output. Personally, I prefer to use the snapshot~
object, which samples a waveform output at whatever millisecond interval I provide as an argument. I'll probably need to do some linear mapping of the resulting numerical output into a more useful range of numbers, but I'll get to that in a minute. I'd also be interested in sending a bang message every time my waveform began its cycle. The example above shows how you can do this using two simple Max objects – a >=~
(greater than/equal to comparison signal object) followed by the handy edge~
object that detects zero to non-zero signal transitions (and vice-versa, too). Beginning Max users might wish to meditate on this pair of objects in the course of their quest for enlightenment.
But there's another good reason to use the phasor~ object to create LFOs in Max, as shown in the left-hand part of the next example.
object takes a phasor~
as an input source, and produces the same kind of output (0. - 1.0) time-scaled by the number specified as an argument or sent to the object's right inlet. In the previous Max 4.x world, this was the only reliable way to synchronize any two LFOs. While there's a new way to do that now (shown on the right-hand side of the patch below), the rate~
object will still be really useful, as you'll soon see.
As I mentioned earlier, Max 5 gives you new ways to specify time in Max - some of which use terminology that is more explicitly "musical”; besides the old school metronomes that send out a bang every 100 milliseconds, you can now work with different “kinds” of time: ISO 8601-format hh:mm:ss.ms-style time, samples, and the time format we'll be using in this tutorial - note values. Since I'm interested in the ability to synchronize processes in Max using time values that resemble musical note values, I’ll use note values – whole notes, half notes, etc. – to set the rate of my LFO. Here's a simple example:
This patch contains the familiar phasor~
object, but instead of using an argument to specify a frequency for the ramp, I've used different arguments to each phasor~
), and I've also added an attribute called (lock
), set to a value of 1 (on). The arguments 1n
) and 2n
) refer to note values - in this case, a whole note and a half note (you can find a complete listing of these note numbers here
object also has a umenu
object connected to it containing a listing of all the possible note values. By selecting a note value from the menu, I can reset the phasor~
on the fly.
But what are the phasor~ objects locked to?
In Max, a central timekeeping object called transport
is used to handle metrical timing. The transport
object not only keeps track of time, but broadcasts information to any Max objects in your patch that are "listening" or expecting timing information. When we use a note value argument with the phasor~
object, we're telling the object that it will need to listen for timing instructions.
While you can add transport objects to your patch (and even have different transport objects running at different rates controlling different sets of Max objects in a single patch), Max 5 includes a handy Global Transport - a default source of timing information that is ready any time you launch Max. You can find the global transport by choosing GlobalTransport from the Max Extras menu
The Global Transport has an on/off switch (the button to the right of the word Activate", a flashing LED indicator, displays bar/beat/unit timing and temp, and a display that shows the current "position" of the transport since you've started it.
NOTE: Since these tutorial patches are time-critical, you should have Overdrive turned on when you run them. If Overdrive is off, you may find that playback gets jerky when the density of notes is high or you perform other tasks on your computer (such as opening another patch).
To see the musically time-synced phasor~ objects in action:
- Click on the ezdac~ object;
- Click on the Global Transport's activate button.
Once the Global Transport is running, you'll see a pair of synchronized waveforms, and you can use the umenu objects to change their relative rates on the fly.
Now that we've got a great way to product perfectly synchronized LFO waveforms, let's go back to our earlier LFO examples and add some additional functionality. Of course, since our locked phasor~ objects are producing beautifully choreographed ramp outputs, they'll also be outputting beautiful sine waves when we use the cos~ object. And while we don't really need the rate~ object to synchronize the phasor~ objects' outputs any more, it's still useful. Suppose we want to produce a nicely synchronized waveform that repeats at a period that's greater than a dotted whole note (1nd, the largest time increment we can specify using note values)? Easy - we can use our old friend the rate~ object to scale the output of the locked phasor~ objects to longer time periods.
And while we're at it, it would also be nice to be able to invert the waveform phase outputs, too. That's an easy one - just multiply the signal by a value of -1.0. The following example patch adds all of these features, and a set of switches that let us configure our output
It's hard to stare at any number of simultaneously synchronized LFOs and not feel the urge to sum those waveforms to produce lovely new shapes, if only for the pleasure of watching them. It's easy enough to do - you just scale the output of each waveform by the reciprocal of the number of waveforms you want to sum (1/2 or .5, 1/3 or .33, etc.) and then add them together.
And for one final touch, I think I'd like to be able to sample the LFO waveform output at a rate which is independent of the waveform itself, but still synced to the "global" rate of the Global transport. In this example patch, I'm sampling the waveform output with a snapshot~ object, and using the scale
object to map the waveform's output data range from -1.0 - 1.0 to a more MIDI-friendly range. That data stream is then sent to the right inlet of an int
object, whose current value will be output any time a bang is sent to the left inlet of the int object. The source of that bang comes from another Max object that can take advantage of the new timing values - the metro
object. I've used a note value argument to set the rate at which the metro object sends bangs rather than the more common number argument (which specifies milliseconds), so the metronome is now also outputting its bangs at a rate derived from the Global Transport.
All of these little component level solutions are used in the giant example patch called LFOur.maxpat - which, as its title implies, contains FOur different and independent LFOs. Although the patch is pretty large, it contains all the little techniques you've seen so far.
You should feel free to open up the LFOur patch and poke around “under the hood.”
Max 5 makes it really easy to create user interfaces using the Presentation Mode
. You can find a good tutorial on it here
, and Andrew Benson has written a great tutorial on creating interfaces
, as well. Working in Max 5, I now find that using the Presentation Layer to prototype interface designs accounts for a lot of my patching time these days; the patch that you see when you launch the LFOur patch is the presentation layer arrangement I decided on. Since I've set this patch to open up in Presentation mode and show you the user interface, you’ll need to do two things:
- Click on the Lock icon in the patcher toolbar to unlock the patch;
- Click on the Presentation Mode icon to display the LFOur patch in patching mode.
(Note: You’ll notice that when you click on the Presentation Mode icon, all the UI objects gracefully glide to their original position in the larger patch. If you’re curious about how to do this in your own patchers, choose Patcher Inspector from the View menu and double click in the Setting column for the Box Animate Time setting. Typing in a large number will create the glide as you toggle between patching and presentation modes).
While you’ll probably be able to recognize parts of the larger patch from the examples above, there are some useful things I’ve added to the patch that may be less obvious:
Each user interface item in the LFOur display in presentation mode includes a hint that describes what the object displays or does, and gives you hints about how to use it. Those hints were added by entering text into the Hint area in each object’s Inspector.
I also thought it’d be useful to be able to create preset combinations of waveforms and rates, so I added support for the pattr family of objects. Although the pattr family of objects are deep and subtle, it was actually quite simple to add this to the LFOur patch in just four steps:
- I opened the Inspector for each and every UI object I wanted to save as part of a preset, and entered a unique name for the Inspector’s Scripting Name setting.
- I added a pattrstorage object to my patch and gave it a name (in this case, LFOs).
- I added an autopattr object to my patch (which has the effect of adding all the objects with scripting names to the pattr registry automatically).
- To make storing and recalling presets super easy, I added a preset object to the patch (and added it to the Presentation layer, of course), opened its Inspector, and entered the name of the pattrstorage object (LFOs) to the value column for the pattrstorage setting. Once I did this, I could use the preset object to load and store presets for my patch.
Of course, this patch doesn't make any noise at all, although it is interesting to play with and restfully instructive to watch. LFOur is useful because of a key idea in Max: data spaces are global; any time you have a send object sending data to a destination named "suitcase", anyreceive object in any Max patch you have open and running with a receive object that specifies "suitcase" as its source will receive and output the message. So I run my LFOur patch alongside whatever Max patch I want to use it with, and as long as I've added receive objects with arguments that correspond to the names of the send objects in the LFOur patch, I've got data to work with. In addition, I've added a few receive objects that will let you control some functions of the LFOur patch from your patch.Here's a listing of the data that my LFOur patch makes available when it is running:
- There are four outputs (named nvA - nvD) that correspond to the note values for LFOs A, B, C, and D. The messages sent correspond to what you see on the control panels for each of the four LFOs (e.g. the output is sent to an r nvA object would be 1n).
- An output named wavelist continually outputs the current value of the scaled integer values (in the range 0 - 127) shown in the moving window display.
- LFOur has a single send~ object that sends the summed output of the four combined LFOs at audio rate to the destination audiowave. The output corresponds to what you see in the oscilloscope output.
- There are four outputs (sampA - sampD) that correspond to the most recently sampled waveform output value (in the range 0-127) driven by the notevalue sampling rate. These values are displayed above the display on the far right.
- There are four outputs (named bangA - bangD), each of which triggers at the start of the waveform's output. Each bang is displayed above the third output display.
The LFOur patch also contains several receive objects you can control from your own Max patch.
- Sending a bang with a destination L4sync will automatically synchronize all four LFOs to their starting point (0.) simultaneously. It is the equivalent of clicking the sync button located below the third output display.
- Sending a zero or one with a destination L4start will toggle audio processing in the LFOur patch.
- Sending a zero or one with a destination L4samp_on will enable or disable sampling the LFO waveform ouputs. When sampling is off, you can use the rightmost display to set the sliders to that the same value will be output every time an LFO starts its waveform and sends a bang message.
- Sending a number in the range 0 - 23 with a destination L4samp will change the note value rate at which values are reported (you might want to copy a umenu object from the original LFOur patch and connect its left outlet to the send object instead of trying to remember what numbers to send). Since the rate at which the LFO waveform’s position is sampled is not tied to the rate at which values are reported, setting different note values for the sampling will produce different sets of values = at very high samples rates [e.g. 64n], the outputs will essentially be in sync with the third waveform display.
Next time, we'll create a simple patch that uses the LFOur patch to make some noise.