Along the course of a series of LFO tutorials, I've tried to describe my interest in the humble Low Frequency Oscillator as a generative tool in various contexts - from a general approach (link) to implementing the results as MIDI event generators, adding Ableton modulation mode-like behaviors, and implementing the results as a Max for Live device, and a little discussion about why Max patches are never done.
It's been a while since I wrote them, and I wasn't sure if or when I'd return to the subject. Until now, that is.
This final tutorial in the series has two aims/points of origin:
- I'd like to encourage some of you who are newer users to have a little fun with gen~ - there's a lot of interesting and enjoyable things you can do even if you're not a rocket surgeon who uses the codebox operator as a tool to generate command-line code for other projects (if you are one of those people, this tutorial may not be for you - although there's a little something extra in the patches folder for this tutorial that may amuse you....).
- I got back into doing this work when I started to think using Max alongside the modest Eurorack analog synth setup I use in the studio. In fact, this tutorial is simultaneously the end of this series and the prequel to a series of tutorials about using Max in the Eurorack world - so stay tuned!
For this tutorial, I'll play tour guide for a port my standard LFO patch to gen~, adding what I hope are some helpful digressions along the way. I'll finish up with something that might be of interest even if you aren't a Eurorack person.
This tutorial is a little longer and involves maybe a bit more heavy lifting than a lot of my earlier tutorials - just think of this as my equivalent of a Viking funeral for the LFO tutorial series. Light 'em up!
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
A Place to Start
The starting point for this tutorial is the LFO patch I started working with in LFO Tutorial 4. 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. Here's what the basic LFO patch looks like in its non-Presentation Mode form:
You'll notice that our patch has four subpatches each of which performs a different function:
- The phasor_speed subpatch lets us choose from between two ways to specify the LFO frequency – using numerical frequencies or Max’s more musical ITM note values.
- The wave_select subpatch lets us choose a waveform whose phase and inversion are variable and whose output can be multiplied for timescaling.
- The wave_scale subpatch lets us choose an output for our waveform which falls anywhere within the range of the normal -1.0 - 1.0 we expect an audio waveforms output to be.
- Finally, the modulation_modes subpatch implements parameter modulation in the way that the Ableton Live program does.
Returning to or repurposing older patches often involves deciding what to keep, what to throw away, and what to modify - that will be the case here. Let's start with the phasor_speed subpatch:
- The gen~ environment varies from normal MSP objects in the sense that it has no internal notion of what Max's ITM time specifications are. Since we already have a single phasor~ outputting audio in the range of 0. - 1.0 at a rate we specify, we'll keep that. In fact, we can use it to drive multiple gen~-based LFOs later on, if we wish (hint hint).
- While Ableton's modulation modes are great, I have some more Euroracky approaches in mind for later tutorials - I'll skip that, too.
That leaves us with the two center subpatchers: wave_select and wave_scale. I'll show you what the port from MSP to gen~ looks like by comparing and contrasting the before and after versions.
Making Waves - Commonality and Difference
I think a lot more people might consider using gen~ if they saw how relatively simple it can be to do some fairly complex things without needing to resort to the codebox at every bump in the road. For this tutorial I'm going to show you the wave_select (and wave_scale) parts of the original patch alongside their gen~ equivalents. Let's start with contents of the waves_select subpatch and its gen~ equivalent:
A quick look will suggest that these two patches have quite a lot in common. There are inlets and outlets, objects/operators patched to create waveform outputs, and logic to handle things like inversion and scaling and generating triggers for waveform reset. The main difference involves some minor variation in the way that a few operators in the gen~ environment works.
One of the nice things about working with gen~ is that quite often the operator you need will have nearly the same name as the Max externals you're used to working with. In the case of phase offset and scaling for the input phasor~ for both patches, the logic is exactly the same – we have a +~/+ object used to set the phase offset, and a rate~/rate object which is used to scale the input from the phasor~ input.
Similarly, the part of our patch that does the selection and inversion of the output waveform is nearly exactly the same as the original Max patch, right down to the need to add one to the selector~/selector's input to make the selection.
However, there is one difference between the MSP and gen~ environments that we see here, and that we'll see again: the use of arguments. In MSP we can use an additional second argument to the selector~ object to set its initial state. In the gen~ world, we cannot specify the initial state for the selector operator because inlets are always signals, not messages (It doesn’t make sense to set an initial value, since it will be overridden as soon as audio is on!). Setting the value needs to be done using logic outside of the gen~ object (as we'll see in just a minute, arguments to multi-input operators will often change the number of inlets some gen~ operators have. Stay tuned....).
Let's look at the portion of the two patches that handle the waveform generation:
The part of our past that generates the waveforms looks a little different, but almost all of that difference has a simple explanation - In the MSP world, the triangle~ external takes the 0. - 1.0 input from a phasor~ object and outputs a triangle waveform, a positive, or a negative ramp waveform (depending on the arguments to the triangle~ object). However, in the gen~ world the triangle operator's output is in the range of 0 - 1.0. We can fix that in exactly the same way we set the output for the square wave in the MSP version of the patch - by multiplying the output waveform by two and subtracting one (By the way - since everything in the gen~ world is floating-point, you don't need to add the period as you do with Max. You can, but you don't need to.).
Remember how we talked about arguments to operators behaving differently in the gen~ world than they do in Max? Here's another example: both the MSP and the gen versions of triangle~/triangle include arguments to specify the output, but adding those arguments to the triangle operator in gen~sets the value rather than specifying an initial value. You can tell that this is the case because the triangle operators have no second inlet, as would normally be the case. Note, too that the > operator used for generating square waves also doesn't include an argument - we do this in gen~ because we want to externally set a duty cycle.
Let's take a look at the parts of the waveform generating patch that are different from the original Mac subpatch.
You probably noticed right off that we're using a different method for generating sine waves. That's related to an important feature of the gen~ programming environment: In gen~, we are always doing single-sample calculations to create output rather than processing data in blocks of samples (which we call signal vectors in MSP). That means that it makes sense for some operations to reflect what we usually do when we do DSP with single samples. The gen~ environment includes specific operators that help us calculate things (the samplestoms operator), and gen~ sometimes does things a little differently. The trigonometric functions we commonly use in MSP are an example of that difference - when you're doing single-sample DSP, it's generally more useful to have trigonometric functions that return values in radians rather than floating-point values.
The cos and singen~ operators expect input values in the range of 0 - 2π (radians) and output values in the range of -1. to 1. The trusty MSP cos~ external takes arguments in 0. - 1. range instead, so we'll have to find another method for generating sinusoidal output (Exciting nerdy detail: the Max cosx~ external works just like the cos operator in gen~ - it expects input in the 0. - 2π range). Rather than using cos~ to give us our sine wave, we'll use the cycle operator – an interpolating wavetable oscillator. It’s easy to use for sine waves by adding the @index phase attribute - this lets us use the 0. - 1.0 phasor~ input to specify the phase of the output waveform.
Finally you'll notice a little difference in the way that we specify sample and hold waveforms for our LFO. There is a sample and hold operator in gen~ (helpfully called sah), but I've got a good reason for doing things this way.
Both the MSP and the gen versions of our waveform generator use a noise as the sample source for their output, but in the gen version we use a latch operator in combination with the delta and > operators to generate a trigger at the waveform start. I decided to do that because there's another place in my patch where I'd like to keep track of when a waveform starts besides doing the sampling - generating trigger outputs. If you take a look at the MSP version of the wave_select subpatch, you'll see that we do right at the point where we output the waveform. Using the delta - > operator pair lets me kill two birds with one stone.
A Question of Boundaries
Let's move on to the wave_scale portion of our gen~ port. I'm going to be making some changes here. One of those changes is particularly obvious when you compare the MSP and gen versions of the wave_scale patches:
Where the original patch used a scale and offset method for its calculations, I've opted instead for something much more straight ahead: the gen~ equivalent of the scale~ external in MSP. My reason for doing this isn't just a matter of a love of simplicity: I'm going to be migrating the results for use in the Eurorack world, where I'm much more likely to be doing scales and offsets either as separate gen~ or MSP patches, or actually using Eurorack modules for that purpose. It's certainly a simpler patch - the big difference between the two versions lies in the way that I've configured the rslider objects in both patches, and the Max logic I've built around them (the rsti subpatcher in the MSP patch and the setreset and minmax subpatchers in the gen version of the patch's external logic). I commend study of the differences in that external patch logic (both in terms of the rslider objects' Inspector settings and the logic itself) to the thoroughgoingly curious among you.
The greatly simplified use of the scale operator in the gen version of our patch provides another example of the way that arguments work in the gen~ environment - as with the triangle operator we saw earlier, typing arguments into the operator box sets the values rather than initializes them. In the case of the wave_scale patch, we know that we're always going to be working with input values in the audio range: -1.0 - 1.0. Typing those arguments into the scale operator's box sets the input low and high values, and displays outlets for the remaining three inlets; since the next two inlets are inputs we will use when scaling, we need to leave those arguments off (another way to do the same thing would be to use the constant operator to provide input values, as shown in this example):
Consolidation and Design
Now that we've created versions of the wave_select and wave_scale subpatchers in gen~, it's time to consolidate our design into a single gen~ object we can reuse at will (I am, as you might imagine, a big Max 7 snippet fan). An additional advantage of using gen is that we can take our patch and gang multiples inside of a single gen~ object for greater efficiency. Before we begin, we need to stop for a moment and think about something - the ways that a gen~ patch processes input data.
There are three ways to do this:
- If our gen~ object receives data at signal rate (normal audio-rate input from an oscillator or a constant connected to a sig~ object), that information will be updated for each sample that the gen~ object processes.
- If we connect the output of a UI object or a number box to the inlet of a gen~ object, input information will be updated for each MSP signal vector.
- If we use a param operator in our gen~ patcher and send values to the gen~ object's left inlet as part of a list that contains the param object's name, that information will also be updated for each MSP signal vector.
There are times when sample-based updating is preferable, and times when I simply don't need to update things all that much - in general, I tend to use the param operator in situations where I have a parameter I am not likely to update regularly (e.g. waveforms and inversion), and to use the in operator to create inlets in any situation where I have a doubt as to whether I want to use signal rate or sample rate update. Having the in operator will allow me to make that try things out as I go.
Here's where your design decisions might vary from mine - in fact, I wouldn't be too surprised if you don't open up the enclosed patch and make some mods of your own. Parameters for everything? Audio rate control for everything? It's up to you.
Consolidating the wave_select and wave_scale gen~ patches is pretty much the same procedure you already use for MSP patching: cutting and pasting the contents of the subpatches into a single gen~ object, and then repatching and renumbering the in operators as needed.
For our patcher, here's the resulting single patch:
Once everything in the gen~ edit window looks right, save your contents. When you save the contents of a gen~ patcher, Max 7 saves the contents as a .gendsp file that you can either load into a top-level gen~ external or as an abstraction inside of a gen~ patcher (the resulting patcher needs to be in your search path, or in the same folder as your Max patch). Use the @gen attribute to specify the gen patcher you want to use, making sure you include the .gendsp portion of the filename:
Now that you've got a nicely reusable LFO, you might also want to select the contents of your patch, click on the Save Snippet icon at the bottom of your patcher window, and save the results as a Max Snippet you can reuse any time you want. Just sayin'....
A Word at the End
I'm partial to the lagniappe - that little something extra so beloved by the fine citizens of N'awlins. In their honor, I'll end this tutorial with something that you don't need to use with a Eurorack. I've taken the BasicLFO gen~ patcher created here and ganged a bunch of them together into a nice patch bank of matrixed LFOs you can use for hours of modulating fun.
That's it for now - as I I said at the very beginning, I have a very specific idea in mind for the use of this gen~-based version of low-frequency oscillator with my Eurorack analog synth setup. And in my next tutorial, I'm going to do just that. Please stay tuned, and in the meantime – happy patching!