An MC Journey, Part 1
I’d like to start this series of tutorials with a confession: I work slowly.
Knowing that about me probably explains a lot of things, from the rate at which the tutorials I write appear to the rate at which I release recordings, all the way to how long my current MSP performance rig has been around (hint: it’s measured in years). That doesn’t mean that nothing has changed, of course — merely that I’m one of those slow preparation/fast execution people. When I’m experimenting with implementing new algorithms, I’ll set something up, turn it on, and let it run for a looooong time, listening and tweaking along the way. Similarly, the arrival of new software features — even features in Max — don’t send me down the rabbit hole right away; I tend to try to work my way through them at my leisure, a little bit at a time, until I figure out what the little bits are doing and then imagine where they might fit the way I work and think about things or until I notice something unexpected and start the (slow) process of integrating that into my practice.
The arrival of Max’s MC features has been a game-changer for a lot of Max users, who wept with joy at the ability to use things like the deviate message to retune enormous banks of sine waves or patch large and complex audio processes without the previous "subway-map-from-Hades" riot of patchcord connections.
But while the world has filled with new swarms and clouds, there are still people out where who’ve found it a bit daunting, or wondered how they’d begin to make sense of it. (Witness all the beginner videos out there, in case you doubt me.) I’d like to begin a small set of tutorials in a different way – to chart my own path into using some of Max’s MC features, one small piece at a time. If you’re familiar with the tutorials I’ve written, you’re going to recognize the raw materials pretty quickly – they’re things I’ve created earlier (and, occasionally, modified just a bit for these tutorials for reasons that will become apparent). While it’s possible that my way of working doesn’t match your own, I hope that the through-line of investigation remains clear, and that you can find something useful and (I hope) inspiring. Let’s get started.
Taking the Low Road
Longtime Cycling ’74 website readers probably know one thing about me for certain: I love low frequency oscillators (LFOs), and I’ve got the tutorial series out there to prove it. While the LFO’s service in audio as a modulation source is ubiquitous and great, I personally tend to approach them as a slightly more general source for generating and organizing variety. I use them to rescue myself from situation in which abandoning some part of my Max patching to randomness seems or feels wrong because, well, I’m not random.
So you’d be right in imagining that the first time I saw a video tutorial about using the deviate message to retune a huge bank of sine waves, I started wondering about going low instead of going high.
As you know from my LFO tutorial series, I started by creating a simple LFO patch, and then began to investigate what I could do by creating multiple instances of that patch and then modifying them individually and summing/averaging the results to create more complex control structures. When Gen came along and I’d decided to embracing using it, I realized that porting my LFOs to gen~ was really a pretty easy task, and that gen~ allowed me to realize a good deal of efficiency by “ganging” those multiple summer/averaged LFO networks into a single gen~ object.
So I figured that that’s where I’d start: take a gen~ LFO patch and move it into the MC world and see what I could do with MC’s features that would match up with my working methods.
It Starts With A Single LFO
It's not hard to figure out that one of the great features of MC involves the ability to create a wrapper around multiple Max objects that lets you address each instance individually — especially useful when you use some specialized MC features to set and modify those individual instances from a single source. So I decided to take a version of my standard gen~-based LFO and to create a special version of it for use in the MC world (You'll find this patch - single_channelLFO.maxpat - in the folder of patches I've included):
If you're familiar with my basic gen~ LFO, there aren't a lot of surprises for this as a starting point. The main difference is that I've converted the contents of the gen~ object so that each of the parameters is now a signal input rather than the message-based Gen param operator approach I often use. Here's the contents of the basic_MC_LFO.gendsp file I've saved as a separate .gendsp patch and loaded into the gen~ object using the @gen <filename> attribute:
I have made a few revisions to the gen~ LFO here. First, I'm using the @default attributes for the in operators to set up my gen~ patcher so that I've got a standard set of starting parameter values for my LFO:
The frequency of the phasor operator (@default 1 Hz.) that drives everything else
I've added the ability to set the phase of the individual phasor operator (@default 0.) with a floating-point number value between 0. and 1.0. Not sure why I didn't add this feature before, but I figured that individual phase controls might prove useful (and they will... trust me).
Each phasor operator uses a rate operator (@default 1.0) to scale the phasor operator's output for nice long rates of change
A duty cycle setting (default 50%) for square-wave output
A waveform select option via the selector operator.
The output from the phasor operator (adjusted for phase and output-scaled as needed) is used to generate six different output waveforms. The triangle operator does most of the heavy lifting here, using argument settings to output positive and negative-going ramps or triangle waveforms, with the cycle operator (which uses the @index phase attribute to transform the phasor operator's ramp output to a sine wave and a latch operator-driven noise source (triggered by the end of the master phasor's 0. - 1.0 cycle) that produces random outputs.
I also decided to add an output that indicated the point at which the phase-adjusted and output-scaled phasor wrapped to 0. as a second output. I've found over time that I use that feature a lot - usually adding a little MSP patching to generate bang messages from the spike.
Introducing The Fab Four
Moving this patch into the MC world requires nothing more than loading the MC_LFO.gendsp file into the MC version of the MSP gen~ object (mc.gen~) and specifying the number of instances we want to work with using the @chans attribute. In the interests of clarity (and my own predilection for quartets), I'm going to create a patch with 4 different LFOs: mc.gen~ @gen MC_LFO.gendsp @chans 4.
That now means that each individual inlet in the mc.gen~ object now addresses 4 independent inputs, so I'm going to need to add a way to address each of the four instances. It's MC to the rescue once again by means of the mc.sig~ @chans 4 object. In practice, you'll find that a good deal of the work you do in MC involves substituting the MSP objects you already work with with their MC variants. In some cases, some Max UI objects will automatically adjust to the number of output channels they receive. The scope~ object is just such an example of that — connecting scope~ objects to the waveform and trigger outputs automatically convert to multichannel displays (you can click on the dots at the bottom of the display to see which instance you're viewing. They all look the same, at the moment because they're all set to the same default values).
There are a number of ways to send data to the mc.sig~ objects, but I'm going to go with a really simple method, adding an mc.input 4 object to each of the five mc.sig~ @chans 4 objects. After that, providing control for each parameter of each instance of the LFO is just a matter of adding some number boxes to the patch. Here's the result (the multichannel_LFO1.maxpat patcher file_:
Using MC to created a multichannel version of the basic LFO has the additional advantage of simplifying the internals of a any patch that outputs multiple instances of something - you just don't need the mass of internal param and setparam operator-based logic you'd usually rely on (e.g. needing parameters like phase1, phase2, and so on). Even though I am working with 4 independent instances of my LFO, I didn't have to modify the internals of my basic_MC_LFO.gendsp patch at all.
Once I had that hooked up, I started experimenting with changing input values. Setting different phasor operator rates immediately produced changes to the multichannel displays, so I could see that was happening and could select each instance's display separately.
Finally, summing and averaging these multiple LFO outputs to produce exotic and interesting waveforms involves nothing more than adding an mc.mixdown~ 1 object and multiplying the result by .25 to scale the output into the standard -1.0 - 1.0 output range. In the case of the trigger outputs, we don't need to scale at all - one mc.mixdown~ 1 object does the trick. And, as long as I've got four separate triggers based on the phase-adjusted and scaled phasor operators, I might as well pull them out to use separately. How do I unpacke 'em? Yep, that's right - an mc.unpack~ 4 object. You can see the results in the multichannel_MIDI.maxpat file.
Spread the Love
After I got my patch to the point where I could access all the parameters, I thought I'd check some of the interesting ways to interact with MC objects that were available to me. I'd certainly seen and heard about the deviate message, but was wondering about what else there might be. It was time to click on the magnifying glass in my patcher window to fire up Max's in-app search. Sure enough, I found what I was looking for in the Generating a Range of Values for All Wrapper Instances section of the MC documentation. The idea of distributing parameters across a specified range was much more compelling to me than deviation, so I thought I'd go with that.
I'd found what I was looking for but um... I wasn't quite sure that I understood it. So I went for my usual Plan B when consulting documentation: Create a Max patch that lets you explore the thing you're having trouble making sense of, and play with it until you do. Since I was dealing with numbers distributed over a range. I created the spreading.maxpat patch in the distribution to do just that. I could input specific values in the range of 0. - 1.0 or scroll the integer number box, and watch how several variations of the spread messages behaved, and - most importantly, seem them next to one another (Sometimes, a value didn't do anything and, once I got the hang of it, I could easily see why). Here's the patch in action:
There were two features of this work that surprised me a little. First, I think I would prefer to use different varieties of the spread message for different functions. Here are some examples:
For phase offsets, I want the lower bound clamped to zero (no phase adjustment) and to keep the fourth value below 1.0, since that's really the same thing as a phase of zero).
I'd like to do my range setting by specifying the upper and lower bound when it comes to multipliers for my phasor operators
Duty cycles for square waves should never use zero or one for values.
I'll use different flavors of the spread message for each situation — choose the tool that fits the task.
The second insight is something I'll save for later. It knocked my work completely sideways and sent me right down the rabbit hole. But more on that later....
There were two features of this work that surprised me a little. First, I think I would prefer to use different varieties of the spread message for different functions. Here are some examples:
For phase offsets, I want the lower bound clamped to zero (no phase adjustment) and to keep the fourth value below 1.0, since that's really the same thing as a phase of zero).
I'd like to do my range setting by specifying the upper and lower bound when it comes to multipliers for my phasor operators
Duty cycles for square waves should never use zero or one for values.
I'll use different flavors of the spread message for each situation — choose the tool that fits the task.The second insight is something I'll save for later. It knocked my work completely sideways and sent me right down the rabbit hole. But more on that later....
Okay - What Now?
Hey - I think I've won the prize for going the longest in an MC tutorial without making any noise! But that's okay with me - I'm trying to get things sorted out in terms of UI/parameter control and figuring out the spread messages.. It's time to hook up something to make a little noise. I think I'll use this as the basis for a step sequencer (those are also close to my heart, as you may have noticed or read). Here's how it'll work:
I've got four independent LFO waveforms, each of which has its own trigger output, as well. I'm going to sum and average all four waveforms to created the waveform to sample to derive MIDI note numbers.
I've got four independent trigger outputs, as well. I'm going to create a way to use one or more of them to serve as the latch for waveform sampling. I'd like to be able to arbitrarily apply any trigger (or group of them) to the latching/sampling, and to create a four-output version for my MC step sequencer.t
I think I'd like to be able to add some sync to my individual LFO outputs, while I'm at it. That might require a little thought, but it's a feature I want.
There are a lot of places I could go with this basic design from there - grab the individual waveforms and use them to drive filters, use the summed exotic audio output for pitch for an oscillator, and so on. But I'm going to keep it simple:
I'll add some very simple sample/hold-based parameter acquisition to get me the component parts for a MIDI note message (pitch/velocity/duration),
Take my MIDI message patching and multiply that by four (one voice in honor of each of the LFOs) to get a four-voice patch
I'm going to borrow a little scale-mapping patching from a random grid sequencer Max for Live device I wrote a tutorial on a while back to add some scale/modal rectitude to the proceedings.
And I do need to add that synchronization to the patch while I'm at it.
Taking (and Making Notes)
As things stand, I've got some really interesting summed waveform action - the output of the mc.mixdown~ 1 and *~ .25 object pair. I've also got four independent triggers (one for each phase-adjusted and scaled phasor operator in the MC_LFO.gendsp patch. The nice feature of my exceptionally busy summed waveform is that all I have to do to get very different MIDI message results is to sample the waveform at the slower rates of the phasor operators - as the triggers cluster or overlap, I'll be grabbing the same note or one nearby. Widely spaced phasor speeds will sample from the same material at wide intervals. And, should I desire it, the results can be made periodic by judicious sets of parameters for rate, phase, and multiplier (especially in situations where I'm using the same waveform in all four sets of LFOs. After some trial and error, this patch falls well into the kind of patching I generally enjoy — something whose adjustable variety implies pattern and intention rather than randomness (and your personal mileage may vary here, but on the bright side, this can be coaxed into staggering randomness with only a little effort).
Assembling the MIDI note messages is handled by the ez-map patcher. Each instance of the subpatcher grabs its input from some combination of three of the four audio outlets from the mc.unpack~ 4 object and from the summed waveform to sample. Here's a look at what's under the hood:
It's a straight-ahead piece of work: three sah~ MSP objects take the summed waveform as input and grab the triggers sent into inlets 2, 3, and 4. Each spike grabs a sample of the summed waveform and scales it for use as a MIDI note number (I'm using a narrow range of notes 24 to 48 and then transposing and scale-mapping that value later on. The audio-rate result is then converted to even data with a snapshot~ 20 object and a change object that filters out repeating triggers.
You'll notice there are two additional objects for the first sample-and-hold patching: I decided as I developed the patch that I found total chromaticism kind of boring, so I thought I might do some scale mapping to use different scales and modes (and my own non-western tunings, although I really don't need anything special to do that, since the tuning tables are done via lookup in another MSP patch).
A while back, I wrote a tutorial that used noise surfaces in Jitter to generate synth controls. In that tutorial, I included some patching to make scale mapping a little easier (you'll find it used in "https://cycling74.com/products/books">my book Step by Step, as well). It's perfect for this patch, so I've taken the liberty of borrowing it and using it here.
Those two extra objects are all I need to use the patching I've cut from the noise tutorial patch - all it does is to increment a lookup table index (the table object in Max counts from one instead of the more usual zero), and use that index to lookup a numerical offset to the pitches we've sampled stored in a table object.
Note: If you're interested in working through the scale mapper patch, I'd suggest firing up and checking out Noise Tutorial 1: Riding Tandem With The Random. You'll find the scale mapping stuff described in the section Scale mapping for fun and profit.
The rest of the MIDI patching is a question of collecting and routing the three outputs from the ez-map subpatcher. I added some octave offsets to add variety to the four voice, and decided that I liked using the buddy object to synchronize the different pitch, velocity, and duration outlets (I don't really need it, but decided that I liked the results when working with extreme ranges. Like I said, I patch and then tweak and listen a bunch).
The only other thing I needed was some reset logic. That involved adding a new inlet to the basic_MC_LFO.gendsp patcher that connected the right inlet of the only phasor operator in our patch (I love how easy loading multiple instances of gen~ patches using mc.gen~ - it makes keeping track of things so much simpler). I renamed the patch MC_LFO.gendsp because that's the version I'll be using from here on out, and loaded that new version into the mc.gen~ object. Sending a non-zero value to all four instances is done by using an mc.target 0 object (zero meaning "send the input to all instances") and another mc.sig~ @chans 4 object to the main patch. Presto - reset!
Here's what it looks like with everything in place in the multichannel_format.maxpat file. I added some pattr objects/a preset object/a pattrstorage bit of preset prep, and the easy-peasy output mapping. It sounds pretty good for a bunch of MC LFOs, I think.
That's it for now. Next time out, I'm going to show you what it was during this patchfest that was the mysterious "other thing" that caught my attention. The resulting journey down the rabbit hole will be the starting point for the next excursion. Stay tuned....
Learn More: See all the articles in this series
by Gregory Taylor on April 13, 2021