I recently wrote a couple of tutorials for the Cycling '74 website on a subject that's close to my heart - generating and organizing variety when working with Max (I'll bet that you just thought they were about making LFOs and working with the new Max 5 timing features, didn't you?).
All LFO tutorials in this series:
- LFO Tutorial 1: The Zen of the Silent Patch
- LFO Tutorial 2: Making Some Noise
- LFO Tutorial 3: Extending Our Generators
- LFO Tutorial 4: Building Complexity
- LFO Tutorial 5: LFO Child Slight Return
- LFO Tutorial 6: Live If You Want It
- LFO Tutorial 7: Rattle and Hmmm
- LFO Tutorial 8: Valediction
At Expo '74, I gave a talk on this same subject and touched upon these same ideas. One of the interesting exchanges of email following the presentation was with an Expo attendee who expressed some personal frustration about the business of "outsourcing" control in a performance patch.
The first problem is one that was at the heart of my original set of tutorials - how can you use simple non-random processes in Max to add subtle control in ways that seem more "human" than dipping into the deep wells of noise or randomness?
The second follow-on question was a more subtle one that reminded me of something that I hadn't talked about: If you've got a generative process whose output is interesting, how can you work with it in live situations so that you're not merely pushing a button or throwing a switch and having your patch suddenly lurch off the rails?
This short tutorial is about that my personal approach to that question. As we say again and again in the Max world, there's always another way to do it (TAAWTDI, for short). I'd like to share some really simple things that have worked for me that I hope you'll find useful, or that may provide a starting point for your own investigations.
I thought we'd try again to start with a patch that makes no sound - a modification of the LFO patch you may remember from the earlier set of tutorials. This patch provides a single selectable waveform as its output which can be synced to Max's global transport. In the previous tutorials, we spent a lot of time looking at summing groups of this simple building block to create complex streams of data we could map in interesting ways.
I'll start this tutorial with a variation on that basic LFO building block from last time. What's different? This LFO patch uses the Max rslider object and a few basic Max objects to let us set output ranges for our LFO other than -1.0 - 1.0 or some output range that oscillates around a center value of 0. You grab the rslider object and set a range for the output waveform, as shown in this example.
And, as we did in previous tutorials, you can take multiple versions of this LFO and sum them to create interesting new aggregate patterns you can use at audio rate or sample for parameter control (Hint: You might also consider the possibility of multiplying these waveforms together as well as summing them. If you do, the clip~ and pong~ objects will be your friend).
What Lies Between
Now that we have this interesting variation on the LFO that we can duplicate and sum to our heart's content to create new control curves, we face a regular dilemma that anyone who wants to outsource their control in a patch - how can you provide a smooth transition from your hands on the faders to the generative fader rumba your LFO patch produces?
While some people thrive on abrupt tectonic shifts in their patch logic, I'm personally more interested in smooth transitions that make it difficult to tell the point at which I actually transferred patch control to my generative structures. In a way, I think it's similar to my interest in creating generative patches in Max that appear to be intentional - I tend to associate those smooth transitions over time to something like the presence of intention in the same way that I tend to hear abrupt shifts in continuity as being the output of some random process. Your mileage may vary, of course (but I'm writing the tutorial, so we'll assume that this is an interesting idea!).
One of the elegant side effects of working with LFO waveforms in this particular case is that - as anyone who's even looked at the cycle~ help file or MSP Tutorial 2 knows - multiplication of a waveform's output changes its amplitude. Since amplitude is really the range over which a waveform varies around its axis, you can also think of that same multiplication as a way to control the amount of LFO output we can apply to whatever it is we want to modulate.
The patch called LFO-fader tutorial shows this principle in action. It's the same basic LFO patch we just saw with a very few but crucial additions: We're taking the sampled waveform output from the snapshot~ object, multiplying the result by a value between 0. and 1.0, and using the trusty scale object to scale the resulting output.
When you start the patch and move the "amount" slider, you'll see a smooth transition from the center to the full range of the waveform.
If you set the output range of the LFO to something a bit more exotic, you'll get the same smooth fade, but the value will rise (or fall) to the range of the LFO output.
The Zen Slap
So we've got a basic method for managing smooth transitions - we can fade the scaled output of our LFO (or any other process, for that matter) in or out in a smooth way. But a little meditation on the places we might want to use something like this might give us pause:
This patch is great if all of the parameters you want to modify are in the exact center of the total modifiable range - there are some places where it's ideal. But what we might need instead would be a way to introduce changes from our LFOs that would take the current value of a parameter into account.
So I went off to get myself a coffee and sat down and doodled a picture of what I wanted the new section of my patch to do. After some doodling and crossing-out, I suddenly realized that I was really drawing something that looked a little bit like the Max rslider object. I don't know about you, but I often find myself thinking that the solution to the 'next' iteration of my Max programming problem involves some modification of what I've got rather than wondering whether or not there's another way to approach the problem before me.
In this case, my blurry drawing was the equivalent of the Zen slap that leads to enlightenment. In a few minutes, I'd whipped together a subpatcher that did exactly what I wanted, called pan_zoom. Here's what the inside of the subpatcher looks like:
The subpatch takes two inputs - a "current state" of a parameter in the left inlet, and a "mapping range" control in the right inlet. Since I usually work with MIDI input, most of my parameters in my patches will be in the range 0 - 127, that that's what I used for the parameter ranges. The right inlet lets me set the output range - how much of a given value will be added or subtracted from the value sent to the left inlet. The output range will always be in the range of 0 - 64 (i.e., one-half of the total range).
So if my input value is 20 and my mapping range is 0, the output range will be a pair of numbers - 20 and 20. As the output range value increases, the range value will be added to and subtracted from the input value (e.g., an input value of 20 and a mapping range of 5 would output the list "15 25".
Stupid Max Tricks
But aren't there going to be situations where the list output would fall outside of the range 0 - 127? Yes, but this abstraction contains a bit of invisible Max sleight-of-hand to handle the problem. As you may know, you can use the Max Inspector to set minimum and maximum values for number box objects. When you do this, the output of the number box objects is automatically truncated to the values you set. That's what I've done for the input and output integer number boxes to make sure my inputs and outputs stay in range (open the Inspector and see for yourself). I do this quite a bit, particularly in situations where I'd like to make sure that I don't accidentally type 50 into a flonum object attached to a *~ object feeding my dac~.
Here's what my pan_zoom subpatcher looks like from the outside:
I can now scale LFO inputs from any point in my parameter range in a way that provides smooth transitions between the current parameter value, and I can also shift the current parameter value itself and maintain the range of modulation. I'm using the rslider object in the patch for visual display purposes, but the scale object does the real work. I can use this anywhere I'd like to modulate a parameter in my patch. For the sake of simplicity, I've also changed the mapping range input to work with an input value in the range 0 - 127 so that I can use it in connection with a MIDI continuous controller in my patch.
Of course, this idea will work for all kinds of control input sources - random numbers, output from chaos objects, data gathered from other parts of your Max patch itself; all you'd need to do is be mindful of the parameter ranges. One more thing - you could also consider the current output of an LFO *itself* as the "input value" you want to mix with another modulation input.
A Final Word
You'll notice that I haven't told you what you should control with this. I did that on purpose - a picture is not only worth a thousand words, but it also doesn't privilege audio or visual control. Also, to quote my colleague and pal Luke DuBois, "Mapping is everything." It's also where all the fun lies, and I don't want to spoil a second of your fun. Go make something amazing.