gen~ for Beginners, Part 6: Thinking Inside the Codebox
Welcome to our next installment of our gen~ anxiety reduction regimen. Last time out, we talked about the codebox operator - what it does, how it works, how you can use it to create your own functions and - should you so desire - add some procedural coding to your gen~ patching life.
All the tutorials in this series: Part 1, Part 2, Part 3, Part 4, Part 5, Part 6, Part 7
Follow-up video tutorials: Working with Abstractions, Debugging and Signal-Rate Processing
We’ve suggested that the Code Tab in your patcher window is a great place to acquaint yourself with GenExpr - the language of the codebox. This tutorial is intended to help you leverage what you already know about Gen operators, operator outlets and attributes, and provide an introduction to how you can work with history, buffer, and data operators in the GenExpr language. It’s time for some thinking inside the box.
More about GenExpr - Outlets and Attributes
So far, we’ve talked about some of the basic features of GenExpr, and showed you some examples of how you can create your own simple patches.
Before we go on to consider the history, buffer, and data operators, I’d like to touch on two topics that we’ll encounter when doing buffer/data operator reads and writes:
I often use the counter operator to count through samples in a buffer. So far, we’ve been working with GenExpr and operators that output a single value. What happens when GenExpr specifies the output of an operator when there’s more than one outlet to the original operator?
When it comes to reading buffer data and other gen~ patching, we’ve often made use of operator attributes. How do we do that in gen~ patching inside of a codebox operator?
What About Operator Outlets?
By now, you’ve probably gotten used to using the patcher window’s Code Tab to see what the GenExpr version of the Gen operators you work with are, whether it’s to see what the default values associated with an operator are…
...or to see what the syntax of the operator is. And remember - if you can’t remember what a given inlet for an operator does, hover over the inlet with your mouse.
You will have noticed that the order in which parameters in GenExpr are listed corresponds to the inlets in the Gen operator of the same name, and are conveniently in left-to-right order. You’ve also noticed that you can assign a variable name to the outlet of one of those GenExpr-format operators, too. But how does GenExpr deal with operators that have more than one outlet?
To explore that, we’ll take a really simple example taken from an earlier gen~ for Beginners tutorial – an example of the counter operator. The following gen~ patcher that includes a counter operator with all of its inlets specified using constants. The counter counts to 44100 by ones, and is always on (that’s what the zero constant in the second inlet of the counter operator does). To take a look at the GenExpr code related to the counter operator, we can select it and take a look at the Code Tab, where the GenExpr code will be highlighted:
Multiple outlets from an operator are represented in GenExpr as a list of variables that appear to the left of the code associated with the operator (in this example, they’re counter_4, counter_5, and counter_6. As with the input values to the operator (the stuff inside of the counter operator’s parentheses), the outlets are also listed in the same order as the outlets, from left to right. Those are the variables you use inside of your codebox.
When you’re patching using operators, the GenExpr code you see in the Code Tab is automatically generated as you patch, and those automatically generated variable names are not very memorable. Since you know what the counter operator output is (the current count, the carry flag, and the carry count), you can label those variables yourself in a way that’s more intuitive. Here’s a codebox version of the same patch that names things a bit more clearly:
The example patch makes use of and outputs all of the output from the counter operator, but it may be that you only care about a single output (say, the count itself). When your GenExpr function returns multiple values but assigns to only one value, the unused return values will be ignored, and GenExpr compiler gets rid of most calculations that aren’t necessary automatically.
Using Operator Attributes
In addition to operator outputs, there’s another feature of the Gen operator that we’ve made note of from time to time: operator attributes. In previous tutorials in this series, we’ve seen attributes used for several things:
We’ve used attributes used to clip input and to assign minimum and maximum values.
When dealing with the peek operator to grab samples from a buffer, we’ve used attributes to specify that we wanted to interpolate the value we get, and also to set the behavior when addressing multichannel buffer objects.
There are a number of Gen patchers that use operators - in particular, the operators used to read from and write to buffer or data objects offer a number of options to you, which we’ll be exploring further in an upcoming tutorial. In GenExpr, function arguments correspond to operator inlets and function return values correspond to outlets. Attributes are set using a key/value style argument, and their listing follows that of the operator inlets.
As an example, let’s take the simple counter operator-based gen~ patcher we just looked at and add just a little bit more logic to the patch to do buffer playback - nothing fancy, we’ll just read one sample after another for the length of our buffer operator, and then start over. We’ll use 2 attributes to the peek operator: the @channelmode attribute to handle trying to read from buffer operators with unknown channel counts (see gen~ for beginners tutorial 4 for more on this), and we’ll even use the @channels operator to explicitly set the number of channels we’ll be reading.
We can use the Code Tab to see how GenExpr represents those two attributes - just select the peek operator, open the Code Tab, and look at the highlighted text.
The @channelmode attribute is set by putting the name of the mode in quotations, and @channels attribute just uses a number.
I hope you’re getting the basic idea that you can use gen~ patcher window’s Code Tab to explore and broaden your understanding of GenExpr by looking at patches you already have.
Using what we now know, let’s create a version of our simple buffer playback using a codebox:
While we’re at it, let’s take what we now know and take the random segment playback patch from back in tutorial 4, and see if we can create a codebox version of it.
Here’s our original patch:
Note: This gen~ patcher includes a minor change to the original patch - I decided I was always going to be working with stereo files, so I used the same @channelmode and @channels attributes that our last example includes).
The counter operator and peek operator portions should be looking pretty familiar to you by now - the parts of this patch we’ll want to take a look at have to do with the logic we use to generate a random number to function as the segment we want to play, as well as the way we calculate the proper count for the segment length. Here’s what the codebox version of this patch looks like:
So - how did I know how to write the GenExpr code for the random segment stuff, anyway?
When it came to naming the variables I was going to use, I went with some renaming to give me variable names that told me what was going on (segments, segment_length, buffer_length, and so on. When it came to the counter operator’s output, I repeated what I’ve been doing throughout the section - I changed the variable names to stuff that made more sense - instead of
counter_6, counter_7, counter_8 = counter(int_3, eq_5, dim_4);
I changed the line to read
current_count, carry_flag, carry_count - counter(1, in1, segment_length);
I looked at the Code Tab in my original gen~ patcher and found the lines that corresponded to that part of the program:
I noticed the fact that several of those lines of code just set a variable in one line that was immediately referenced in the next line - noise_10 was just the value returned by the noise operator. Similarly, latch_11 was just the output of the latch operator, which made use of noise_10 and the value of the carry flag output of the counter operator (it’s called counter_8 here - you can see it referenced in the line right above the highlighted stuff). The latch_11 stuff is used for scaling (scale_12), and so on. So what I did was to fold all those assignments up -
I replaced noise_10 in the next line of code with noise().
latch_11 = latch(noise(), carry_flag);
So what’s now called latch_11 is now a variable I can rename random_start.
random_start = latch(noise(), carry_flag);
Neat trick, huh? In turn, At each step along the way, I could remove the previous line I’d folded in.
I used the random_start variable and folded it into next few lines of code to give me another variable I could easily understand - the number of the sample to start playing my segment.
segment_2_play = trunc( scale(random_start, -1., 1., 0, in2, 1) );
I’m sure that other people have their own techniques for getting used to working with GenExpr. This is how I got my feet wet. Now, it’s time to look at history, buffer, and data operators in the codebox.
Those Who Do Not Study History….
As we discussed way back in our very first tutorial, your gen~ patcher is working on one sample at a time, and data is always synchronous. That means that things like feedback really aren’t possible (i.e. you can’t read and write a single sample from a buffer operator at the same time).
The Gen world provides a special operator called history to let us create feedback loops and perform other useful operations. The history operator acts like a single sample delay - it stashes its last value and outputs it at the start of the next sample’s worth of calculations.
That’s an incredibly useful thing to be able to do, but how can we do that when using a codebox operator? To see how it’s done, let’s take a real-world example from the Gen examples folder – the Lorenz attractor example included with the gen~.chaos.maxpat patch. It’s a good example of the use of the history operator, because chaotic functions do their work by performing recursive calculations – the result of the current sample’s worth of calculations are saved and become what is operated upon during the next sample iteration. Let’s look inside the patch’s gen~ object:
You’ll notice several parts of the patch that will likely be familiar to you - at the very bottom, we’re multiplying results by a 0. or 1.0 signal to to “freeze” our calculations. You’ll also notice that each of the three separate calculations in the center of the patcher calculate a change in the previous value of the a, b, and c parameters. When the input signal from the in 1 operator is a value of 1, The a, b, and c calculations are incremented by a fixed amount, and then added to the output of the three history operators at the top of the patch, with the result being stored in the same history operators. What you’re seeing, of course, is a single-sample delay - the sum of the dx + dt, dy + dt, and dz + dt calculations are added with the previous sample’s worth of calculation, and then stored to be reused. That’s a classic example of the history operator in action.
So, how might we perform this kind of calculation inside of a codebox operator? Let’s see if the Code Tab in the patcher window can give us any insight into using the history operator:
There are the three History operators, right up at the top of the codebox, along with the Param operators. You’ll notice that the History operators are all initialized (to the value specified in parentheses), and that the listing of them precedes the calculation portions of the GenExpr version of our patch. That’s because we want to make use of the values of those variables globally.
Compare that original Lorenz attractor patcher with a codebox version of the very same patch:
Again - notice that the History operators are listed along with the parameters (the Param declarations), and that both the History and Param declarations precede any calculations.
That’s how the history operator can be used in a codebox operator to provide single sample delays which are recursively recalculated
By the way - you might also want to take a look at the GenExpr code that is used to update the histories. Let’s look at one of those lines of code:
x = newval ? x + dx * dt : x;
It’s an interesting challenge to parse, but you’ll probably recognize its component parts, with a little study: the ? operator is used to test the variable newval (which is the 0. Or 1.0 signal at inlet 1 that is used to pass or freeze the calculation). There are two possible outputs following the name of the operator, separated by a colon. The first one (x + dx * dt) is the value that will be assigned to the variable x (on the left of the line) if newval is any non-zero value (on), and the previous x value will be assigned if newval is set to 0. The result of that line is what is stored in the history operator, waiting for the next sample’s worth of calculation.
Of course, you could also use if… else procedural coding to achieve a similar purpose. Perhaps that’d be an interesting assignment to yourself to try.
Buffers and Data in the Codebox
There are three ways to store and access data beyond the single sample you’re currently doing calculations on inside your gen~ object:
We’ve just discussed one of them, the history operator, which functions as a single-sample delay. It’s useful for any situations where you need access to something that you dealt with during your last sample’s worth of calculations inside your gen~ patcher.
The buffer operator lets you work with multichannel MSP buffer~ data that you can store information to and fetch information from. The buffer operator always references an external named MSP buffer~ object - the buffer operator takes a first argument that specifies the name by which to refer to the buffer in other Gen objects in our gen~ patcher (e.g. you can tell a peek operator to access samples using the name of a buffer operator).
The name you give the buffer operator inside of your codebox object or gen~ patcher can be a different one than the name of the MSP external in your parent patch. If you want to do that, you can provide an optional second argument to the operator.
Once instantiated, the buffer operator has two outlets – one that “broadcasts” the length of the buffer with every sample’s worth of calculations, and a second outlet that broadcasts the number of channels.
Rather than referring to external data, data operator in gen~ stores an array of sample data internally to the gen patcher. Like the buffer operator, it takes a first argument that specifies the name by which to refer to the data using other gen operators. Like the buffer operator, the data operator also broadcasts its length and number of channels from its left and right outlets.
However, the buffer and data operators differ in some important ways
The data contained in a buffer operator is 32-bit data, while the contents of a data operator consists of 64-bit data.
The buffer operator only refers to data that is defined within an MSP buffer~ object, and can be modified and resized within normal Max patchers. The data operator meanwhile is local to gen~, and takes one argument to specify its length in samples, and a second optional argument to define the number of channels. Once specified, the length of the data operator cannot be changed.
Working with buffer and data operators in the Codebox
The rules for declaring and accessing buffer and data operators in a codebox operator are simple:
You’ll need to include information about any buffer or data operators you’re going to work with in your codebox object at the beginning of the codebox - the same place as you do with history operators. They merely need to precede the definition of variables or calculations. While you can also add buffer or data operators in your gen~ patcher and then refer to them in the codebox operator, including that information in your codebox object is a good habit to develop
You declare a buffer operator this way. If you are referencing an external MSP buffer~ object, you can declare it as follows:
Buffer mydata("banging");
You declare a data operator with the size (in samples) of the data and number of channels (optional) as follows:
Data my_data_object(44100, 2);
There are two ways to acquire and use information about your buffer or data operator.
The left outlet of the buffer and data operators “broadcast” the number of samples in the buffer or data operator, and the right outlet does the same with the number of channels for the operator.
You can also use GenExpr equivalent of the dim and the channels operators to get that same data. Here’s an example of what that looks like in GenExpr
Buffer mydata("banging");
length = dim(mydata);
Data my_data_object(44100, 2);
channelcount = channels(my_data_object);
That’s really all there is to it. Let’s look at a few examples.
In Tutorial 4, we looked at a “Fripp in the box” patcher that recorded incoming data into a data object for up to 16 seconds of recirculating looping. Here’s the gen~ operator-based version of the patcher (with a nifty little addition):
To jog your memory a little, here’s a rundown of what’s going on:
We’re using a data operator to serve as the read/write area for the looper, with a maximum time set to 16 seconds.. This patch includes a nice modification that our original patch didn't have - one that we can make use of that will allow us to use it with any sample rate. Take a look at the data operator. We can specify the length of our data operator using an expression as an argument (data loopiness 16*samplerate 2). Neat, eh?
The actual number of samples that we count through when looping isn’t limited to our sample rate * 16, however - the delayamt parameter lets us specify a delay time in milliseconds, which it converts to a sample count using the mstosamps operator. That upper limit is used as the number of samples we count using the counter operator.
We use a peek operator to read from our data object, and a poke operator to write data to our data object. Gen patchers always place the poke operators last.
There’s a little bit of extra housekeeping (mixing wet and dry signals and setting regeneration, too. Now, let’s look at a commented version of this patch in codebox form:
We’re declared the data operator and all of our Param operators right at the start before getting down to business.
This GenExpr code demonstrates several features we’ve talked about in this tutorial - operators with multiple outputs (the counter and the peek operators), and nested operators to keep our code compact. See if you can spot that last update to our data operator in the previous patch.
Our second example is a deep and subtle bit of codeboxing from the Gen examples folder from Graham Wakefield: a gen~ patcher that functions in a manner similar to the Max urn object. It combines the use of the data operator with some procedural code.
How is the magic done? Here’s what’s inside the codebox operator:
We’re using a data operator to hold the number of items in our urn. Since it’s a data operator, we won’t be able to change the number of samples our the operator, so 1024 is the maximum size of our urn (You can edit what’s in the codebox if you want a larger number, of course).
You’ll notice that we’re making good use of the history operator in the codebox - we declare it at the beginning because we want access to the clear, count, value, and index variables outside of the procedural code, and need to retain their values between consecutive runs of the per-sample gen~ patcher.
There are two bits of procedural coding here - it’s an example of a bit of gen~ patching we can only do by making use of the codebox operator.
The first is a simple if… statement what will calculate output only when we receive a pulse in the gen~ object’s left inlet (in1).
The second bit of procedural coding refills and resizes the urn. It makes use the || (or) notation in its first line to indicate that the refill/resize will occur if any one of these three tests is positive:
If the count is zero (count == 0).
If the current size changes (change(size)).
If a clear message has been sent to the gen~ object’s first inlet (clear).
Thanks for hanging in there - there's one more awesome trick of the codebox operator: you can create your own libraries of functions for reusability. We'll look at that in Part 7. Until then, happy patching!
by Gregory Taylor on May 16, 2018