gen~ for Beginners, Part 4: Working with buffers (and data)
Welcome back to the fourth installment of our gen~ for beginners anxiety reduction program, where we're going through some of the things about learning to work in the Gen environment which can vex the beginner. This time out, we're going to extend what we looked at last week, and consider other forms of buffer operator playback.
It's something of a common problem - you're excited about learning to work in Gen, you open the gen~ object's help file, click on the buffer-and-data tab in the helpfile, double-click on the gen~ object to see what's inside, and...
...you come face to face with a bewildering set of options.
Don't get me wrong - What's in that helpfile is a succinct and helpful distillation of an entire universe of wonderful possibilities. It's just that maybe it'd be easier to step though some basic examples and maybe have a little fun messing around with them in the process. As before, we're going to go with my friend Perry Cook’s advice on designing new musical instruments from his 2001 NIME keynote address:
Instant music, subtlety later
Note: The techniques for playback we'll be looking at here work for both the buffer and data operators in your gen~ patcher, although I'm going to be focusing on the buffer operator.
To recap, let's remind ourselves of the buffer operator access technique we looked at last week - using the counter operator to fetch samples from a buffer:
Before we take a run at the other options available to you for buffer playback, I want to show you a variation of our basic playback patch from last week that will, I hope, remind you of the possibilities of working with the humble counting technique - random buffer segment playback. And, it even includes a cool new trick!
The inside of the peek_random_segment_playback patch isn't really all that different from our original example - it has a buffer operator, it grabs samples from the buffer using the peek operator, and it uses a counter operator to specify which samples from the buffer to fetch.
The rest of the patcher's logic is all about doing two things:
- Chopping up the contents of the buffer into equal sections.
- Choosing one of those random segments for playback.
Since we're counting by samples, dividing the buffer into equal sections is just a matter of dividing the number of samples in the buffer by the number of segments.
You'll notice that this patch includes an operator you haven't seen before: the dim operator. You can probably puzzle out what the operator does if you notice that the dim operator takes an argument that's the same name as the buffer operator we're working with in our patch - a dim operator "broadcasts" the number of samples in the named buffer. This can help to keep your patches a little more spaghetti-patch-cord free.
The part of this gen~ patcher that selects the random section for playback uses a noise operator as a random number source. Each time our counter operator hits its limit, it fires off a pulse to the latch object we met in our last tutorial, which grabs and holds a value from the noise operator (since the output of the noise operator is a waveform, we'll need to use a scale operator to get it into the proper range and give us a number between 0 and the final segment (the segment number -1, of course). Since we need our output to be an integer value, we're using the Gen trunc operator, which converts floating-point to integer values by stripping off the part of the value to the right of the decimal point.
Once we have our segment number and know how many samples are in our segment, it's merely a matter of offsetting the count value we send to the peek operator by the number of samples that corresponds to the start of the segment we want to play, which involves nothing more than a little multiplication and addition. One of the things you'll probably encounter working with these patches is that the example files here are mono files. You might want to work with stereo buffer~ objects - to do so, you would, of course, add your own stereo buffer~ object to the parent Max patch or use a replace message. How could you switch between mono and stereo audio files? Here's an easy and elegant way to handle the difference - using a new attribute for the peek operator: @channelmode wrap” attribute - it will guarantee that if you select a playback channel that doesn’t exist, it will wrap around and pick a channel that does exist. So, in the case of requesting channels 0 and 1, it will play 0 and 0 if you're working with a mono file. Neat trick, huh?
There's a world of fun to be had with finding interesting strategies for segmenting a buffer and playing those segments back in new orders and new directions and new speeds (remember that up/down/up-down counting patch from Part 3? You might try substituting that in for the counter operator portion of this patch's logic for random segment playback with a random mode of playback....).
Now, let's look at some other strategies for buffer/data operator access.
No Peeking This Time
As much fun as the peek operator is, the Gen environment provides you with other interesting methods for working with buffer or data operators. There are four of them: the sample/nearest, lookup, and wave operators. Simply put, these other methods use different kinds of signal input to fetch information from a buffer operator - methods that you may recognize from your MSP patching life.
A sample of the things to come
The sample and nearest operators are the simplest non-counter operator-based methods: they take a phase value in the range of 0. - 1.0, and use that value as an index into the buffer operator.
That means that all of your existing skills at working with phasor~ and rate~ objects in the Max world will serve you well in buffer mangling (especially since the phasor and rate operators in the gen~ world work just like their MSP cousins). Here are three very simple examples that I hope will spark your interest and get you playing around.
What about working with stereo output? Working with multichannel buffers in a gen~ patcher is a simple matter. The MSP buffer~ object determines the number of channels of a buffer operator it refers to. You indicate the number of channels for a data operator by using an argument after the length in samples (for example “data mydata 44100 2"). After that, accessing the buffer or data operator for individual channels is simply a matter of indicating a channel offset (channels number from zero, by the way) via the right-most inlet to your buffer-based access operator.
Note: The sample files we're working onhere are mono, and you may want to load some stereo files of your own, and to have them play back in stereo. Forum reader Arthur Sauer has suggested an elegant idea that will let us work with mono or stereo files - it uses the right outlet of the buffer operator. While the left outlet of the buffer operator "broadcasts" the length of the buffer in samples, the right outlet of the operator sends out a count of the number of channels in the buffer. Adding an == 2 (is equal to 2 ) operator to that right outlet will test the number of channels the buffer operator is referencing in the parent patch. If the buffer is stereo, the test result will be a 1 (true). We've connected that output to the right inlet of the sample operator and will sample the buffer with a channel offset of 1, which will give us the right channel (the sample operator defaults to a channel offset of zero, so the left sample objects will always output mono or the left channel). Another neat trick, and our thanks to Arthur for this great idea.
Now, let's look at some interesting ways to sample the buffer operator. Changing the speed
Want to play back the contents of your buffer operator at a different speed? Change the rate of your phasor. The sample_speed_mods example patch uses the rate operator for this trick.
Changing the start point
Want to change the point from which you start playing back your buffer? Treat the start point as a phase offset by adding a value between 0. and 1.0 and using a wrap 0 1 operator to create a repeating index value. You can group these together as a form of adelay line, as you can see/hear in the sample_phase_mods example patch:
Altering the playback
Want some non-standard playback modes? Feed your sample operator index values using something other than a standard phasor or external signal as input. The sample_phasor_mods_example patch uses a triangle operator to modify our phasor to play forward and backward, and to do so at different rates:
I know what you're thinking: "Hey, if I'm using a floating-point value as my index, I'm going to have to worry about doing some interpolation on my sampling, won't I?" Well, never fear - the sample operator takes care of that for you automatically - it defaults to linear interpolation when it samples. If, for some reason, you don't want to do any interpolation and enjoy the artifacts, that's precisely what the nearest operator does - think of it as a sample operator that doesn't interpolate and you'll be ready to rumble.
In all of these examples, you also probably noticed that the rate and triangle operators and the offset amounts added before sampling were done at fixed rates (that's why there's no second inlet for the rate, + and triangle operators). Each and every one of those can be easily modified given what you already know to change those values on the fly for more fun and mayhem.
Waveshaping and other forms of mayhem
When you think of it, there's another situation in which we use a value as an index to a collection of samples when doing signal processing: waveshaping. In that case, we use an input signal to index a buffer that contains some transfer function, and use those results for playback. Since the input we use for indexing is a signal, the range we use for indexing is the standard signal range of -1.0 to 1.0 instead of the 0. - 1.0 outputs that the phasor~/phasor gives us.
That's just what the lookup operator does (and you'll be familiar with its use if you've ever used the lookup~ object in MSP, too). The lookup_drawing_example patch shows a gen~ implementation of classic waveshaping, and also demonstrates how you can draw your own waveforms and transfer them to a buffer operator using an iter, counter, and poke~ object (along with a trigger object to handle the housekeeping) to write a multislider object's output to the buffer~ object. When the buffer~ is loaded, we then associate the buffer~ object with the buffer operator in our gen~ patch and we're ready to do some waveshaping. Doing the lookup and output is pretty simple in Gen, as you can see.
The lookup_audiofile_mangling example shows another approach to using the lookup operator - one which parallels theclassic use of the MSP lookup~ object. The gen~ implementation is a good deal easier, since we don't have to worry about calculating the number of samples in an audio file when we read it into a buffer~ (check out the MSP lookup~ object's helpfile if you want to see what it looks like in standard MSP) - the left outlet of the buffer operator provides us with that information automatically.
All we do is use the low-frequency waveform of our choice to provide the lookup operator with the input values to map to the transfer function. We can always use the cycle~ object for exciting "turntable effects" with this patch, but we can also go for something a little more exotic.
Remember the latching_fun example patch from Part 3 of the tutorial that produced those wonderfully messed-up waveform outputs? It's back - right there up top in our Max patcher. The latching_fun patch is putting out its seriously kinked-up output that we're sending directly into the lookup operator-based gen~ patcher shown at the right. The results are pretty satisfying.
Not drowning, wave-ing
The final buffer access operator available to you is a hybrid of the techniques we’ve been looking at so far – the wave operator. While it’s similar to the sample/nearest and lookup operators in that it uses the output of a phasor~ object or a phasor operator and maps the 0. – 1.0 range to the contents of the buffer (or data) operator, the wave operator lets you specify start and end points for a section of the buffer operator to be played back. Like the peek operator, it does its counting in samples.
The wave_simple_example patch shows this operator in action. Take a few minutes to experiment with different settings for the phasor~, and try different values for start and end points (you can specify very short distances, as you might expect).
I’m sure that some interesting opportunities come to mind, now that you’ve been playing with buffer access in Gen for a while:
- You can use something other than a straight phasor~ to drive the wave operators indexing.
- There’s a world of patching options available to you for changing the start and end points on the fly (random numbers, phasor~/phasor and rate~/rate pairs, and so on).
One thing you’ve probably noticed is the interaction between the phasor~ and the start and end points – changing the start/endpoints changes the apparent pitch of the playback since the same phasor~ is now traversing a different number of samples/range in the buffer operator. How could you modify this patch so that the rate of playback is automatically adjusted regardless of what the start-to-endpoint sample count is?
Hint: Start with playing back the full range of a loaded buffer, and then modify the endpoint in your patch so that it’s precisely half of the length of the original. How does the pitch change? Can you think of a gen~ operator you might use to adjust the playback rate to keep it consistent?
The wave_pbadjust_example patch provides one possible solution, and includes a new object that, like the dim operator, can help keep your patches neater. Since the Max patch itself looks exactly the same as the wave_simple_example patch, we’ll look at the contents of the gen~ object only:
This solution uses a rate object, which internally changes the rate of the phasor~ object's rate in the parent patch. The value sent to the rate operator is calculated based on knowing what percentage of the buffer is being played whenever on or both of the start and end point sample values are changed.
The other interesting feature of this patch is the use of a channels operator. Like the dim operator, it takes the name of a buffer as an argument. But the channels operator outputs the number of channels in the named buffer, just as though we'd used a connection from the right outlet of the buffer object itself.
‘Til Next Time
So there you have it – a short tour through the ways you can access the contents of a buffer or data operator in your gen~ object. I hope that I’ve provided you with some examples that will be an opportunity for exploration and a little fun, and that your Gen anxiety is attenuated, too.
I’d suggest that perhaps this would be a good time once again to open the gen~ object's help file, click on the buffer-and-data tab in the helpfile, and double-click on the gen~ object to see what's inside. If I've done my job, things are a little clearer now. There are options available to you that I haven’t said much about, so feel free to explore them, too. For example, you have several different options available to you for interpolating between samples. Rather than trying to work out the rocket surgery of the math or worry about why they all exist, try them out and trust your ears.
As I did last time out, I'm going to leave you with an interesting and (I hope) useful bit of patching that points the way to some new areas of work. The loop_record_example patch takes as its starting point a very simple idea: Can I take what I know about accessing a buffer and then read from it and then write back to that same buffer?
You sure can - that's what the gen~ poke operator was born to do - buffer writes. See if you can work out what's going on in the patch, and - oh yeah - have fun with it.
Gregory P.S. In addition to the pint of his choice we all already owe Graham Wakefield for bringing the gen~ object into the world, I am particularly indebted to him for his patience and insight over the course of this month of tutorial writing. Thanks, man.
by Gregory Taylor on
Mar 20, 2018 10:01 PM