In the previous article we started by setting up a few systems for synchronization and quantization in Max. In this article we will begin putting that work to use when we start recording and playing back audio using some basic DSP buffer operations. We will implement this playback and recording system in gen~ using max-like patching techniques and code concurrently. It will be nice to see how the two translate and can work together. Everything will be sample accurate and perfectly in time with your Live Session.
You can find the finished patch for this section HERE, but I encourage you to try and follow along and experiment with using [scope~] and [number~] to investigate the signals at various points in the gen~ patch. Before digging into the code we just need to setup a few things; a place to hold our audio and something to display it. Let’s take the synchronization scheme that we put together last time as a starting point for this patch. The first thing we will want to do is create a named [buffer~] object with 2 channels, and a give it enough length to accommodate any recording we might be doing. 10 seconds should be more than sufficient. You can also create two [waveform~] objects to display the contents of the buffer. This isn’t necessary but we can do some cool stuff with the display later if we choose.
Now that we have our patch set up let’s create new a [gen~] object, connect our [plugin] outlets to its inlets, and double click to open it up. We’re presented with a few operators that just serve as a simple signal addition. If we listened to the output this would just be an addition of the left and right parts of our input. We can delete that addition operator and start to set our gen~ patch up for recording. To do that we will need to reference our buffer within our external buffer and use two [poke] objects. The [poke] object records, or pokes, sample values into a buffer. It takes the sample value to write in the left inlet and the sample index in the middle inlet. From this point onward I will be referring to the inlets of gen~ objects as arguments. When we begin writing code this will make a little more sense, but in the gen~ environment inlets and arguments are synonymous. It will benefit us to start thinking of functions taking the more traditional arguments, as opposed to inlets.
We can get our sample value from the inlets to our gen~ patch, but how do we get the sample index? Let’s get familiar with the [counter] object in gen~. This object increases its count by a specified amount each sample until it reaches a maximum (if defined, otherwise it will keep counting) and then starts over. At its default value of 1, it will count to 44100 (our sampling rate, or samples per second) in one second. It takes the value to add each sample as its first argument, a ‘trigger’ value as its second argument, and a max value as its third argument. With a little math, this count can be translated into all types of useful functions.
It is a pretty simple procedure to continually record one bar of audio. The trouble is, we need to get our recording length and synchronization trigger from outside of the gen~ patch. This is where parameters come into play. You can define parameters inside of a gen~ patch with the [param] object. Parameters are sent as messages consisting of the parameter name and its value, from the max world. Let’s create a [param start] and [param length] object in our gen~ patch and connect it to the second and third inlets of our counter object. In the upper level max patch, whip up a few message formatting objects. Send the transport state as the trigger parameter and the length of a bar as the length parameter. All you need to do is [prepend] the value with the parameter name ([prepend start], for example, will send our trigger value into the gen~ patch). Now we need to tell our counter to add 1 per sample, convert our length in ms to length in samples, and convert a 0 to 1 change to a trigger (by checking that the change in sample value is greater than 1). We have just created a rolling buffer that records the last bar of incoming audio.
Now that we have our audio, let’s create a very simple playback engine to mute the incoming audio and play back any sixteenth note repeatedly when we engage it. This won’t be all that musical, but we’ll get to that later. Let’s create our playback objects, [poke]’s cousin, [peek]. [peek] operates in a similar way, in that we give it a sample index and it will play back the value at that index. Sounds like another job for our counter object. We’ll set up an identical trigger situation to reset our counter when we turn it on and to switch playback from incoming audio using a [switch] operator. Along with our trigger, we’ll include an offset so that we can decide what portion of the buffer to play back. We also need to do a little math to figure out how many samples each division is and to wrap the values to avoid out of range indexes. There is also the third outlet, which gives us playing position in ms. I encourage always having a utility outlet in gen~ patches so you can easily see exactly what is going on.
All that’s left is to make a few GUI elements to control the few things in our gen~ patch and then we can start glitching incoming audio. I’ve added some random offset generation to provide variety, and I’ve hooked up our [waveform~] objects to display the recorded audio and current play position. In the next article we will port this all to genexpr code and compare the similarities and differences.