Generating Arbitrary Ramps in gen~ Like [what~] Subdivisions (Generating Sound... Inspired)

Steve Meyer's icon

I am working my way through Generating Sound & Organizing Time and gave myself a little test-your-knowledge assignment after getting through chapter 3: make a gen~ patch that behaves like the [what~] object, but for ramps, not just signal spikes. The idea is to have a parent phasor signal and arbitrarily sub-divided ramps within the parent phasor.

arbitrary-beats-in-measure-gen-codebox.maxpat
Max Patch
arbitrary ramp subdivisions (codebox)

arbitrary-beats-in-measure-gen.maxpat
Max Patch
arbitrary ramp subdivisions

For example, to use simple numbers for the sake of the math, if I am thinking in 4/4 time and I want a [what~] object to produce a signal spike on beats 1, 2 and 4 (skipping beat 3), I would give it the list "0. 0.25 0.75". I would like the equivalent set of ramps defined as:

  1. short: ramp from 0-1 over 0-0.25 phase of parent

  2. long: ramp from 0-1 over 0.25-0.75

  3. short: ramp from 0-1 over 0.75-1.0

Taking inspiration from the book and thinking in signals, as it were, it seems like it should be as simple as managing a scaling factor (that changes over the course of the parent phasor) and wrapping it. I am trying to:

  1. create a secondary signal that multiplies the original phasor by a scaling amount,

  2. wrapping that signal between 0 and 1,

  3. paying attention to the point when the wrap occurs, and grabbing a new scaling amount for the next signal

Taking inspiration from the FluCoMa folks, I figured I could just try to manage the list of scaling amounts in a buffer or gen~ data object. So given the short/long/short sub-sections above, it should simply mean that the buffer has 3 samples of 4/2/4. Scaling the original parent/measure phasor by 4 and wrapping it means it should wrap after 1 beat, the second section after 2 beats when scaled by 2, the third after 1 more also scaled by 4.

It is mostly working, but I have some gnarly order of operations issue that I cannot work out. I am wondering if any of the gen~ experts might be able to take a look at my patchers and see if they can spot my bug. I am either seeing the secondary phasor produce all four beats or when it is a combination of 1 long and 2 short sections, it will is never in the correct order (short/long/short), it is always either long/short/short or short/short/long.

Two patches are included here, one using a codebox, the other not. Note that if you test them out, sometimes you have to click the toggle to multiple times before a long ramp shows up.

Much appreciated if anyone cares to take a look. Any/all feedback welcome.

Steve Meyer's icon

I figured out a solution. See the attached patcher. It does require the book's gen~ abstractions. Screenshot included for the final result I was trying to achieve, including sending the rhythmic ramps, as it were, through unit shapers from the book.

Rhythmic ramps with unit shaping.

arbitrary-ramp-divisions.maxpat
Max Patch

Still curious if anyone knows a way to do the signal processing without a [buffer~] acting as an array.

Graham Wakefield's icon

Another way to get the different divisions is to use a go.ramp.div abstraction (or go.ramp.mul) -- that's what it's designed for :-)

Max Patch
Copy patch and select New From Clipboard in Max.

However one thing is that your slope divisions have to match -- it works with "4 2 2 4" because the two middle beats are both "2", but if you did e.g. "4 1 2 3" you'll get a pretty funky pattern. Maybe a good thing in its own right, but maybe not what you wanted?

Something closer perhaps is running a counter that increments by one each time the divided ramp wraps, and using that value to look up the next division factor. That could either be synced to the master phasor to keep it regular, or it could be unsynced for more free polymeters.

Max Patch
Copy patch and select New From Clipboard in Max.



See also the ratchet and tresillo examples in the book, as well as the Euclidean ramps example in Chapter 5, for different approaches to this area.

Steve Meyer's icon

Graham, thank you so much for these examples!

I had tried out a [counter] version and could not get it working so I will definitely study these. Seeing the tresillo rhythm example is what got me thinking about trying to get this wired up ("what if that last beat scaled to full height?" 🤔).

And I'm glad you included the [multislider]. I was so focused on thinking in terms of the lists of 0.-1. floats, but that is such an elegant way to populate the buffer. I made this version to do relative weights so the number of non-zero sliders is equal to the number of subdivision ramps (e.g., a list of 1 2 1 produces the slope multipliers 4 2 4).

Wetterberg's icon

>Still curious if anyone knows a way to do the signal processing without a [buffer~] acting as an array.

It's off-topic, since it wouldn't be gen, but for people who are interested, subdiv~ does a TON of heavy lifting for this kind of stuff. It even supports "pattern" messages, which allows you to do passages of different subdivisions, creating these rhythms really well.

I currently use two subdiv~ setups after each other to do nested tuplets (hi Philip!) and I'm really happy with the performance, even though I use SO little of this object.

One thing that's pretty magical about it is you can use those "pattern", and binary "prob" messages to program out REALLY complex stuff.

techno~ is dead.

Roman Thilenius's icon

the funny thing is that you can do all of that using phasor~, edge~ and math objects. the easier the building blocks get, the more difficult they are to use as it seems.

Steve Meyer's icon

Wetterberg and Roman, thanks for sharing these thoughts.

I have also been looking at the [subdiv~] object and its patterns functionality. This post was inspired by trying to test out and apply my knowledge from the book content, to "think in signals" as I think it may have been stated early in the book. But where my last experiment ended up definitely does not require gen. Could just pass patterned [subdiv~] output to the unit shapers.

That said, I have been using [node.script] to create subdivision float lists for [what~]. So I might take my original example of beats 1, 2, 4 (0. 0.25 0.75), but then nudge some beats off the grid by swing-like amounts so the resulting list could be 0. 0.2857748 0.75. The very small swing amounts are also what I had in mind when I was thinking about arbitrary subdivisions. I find the [what~] API's distillation to a list of 0-1 floats really elegant.

Roman, you are also hitting on two stumbling blocks that I have in Max. First, I have a background in document-oriented, i.e., non-mathematical, computer programming (that web development world of relational databases and record structure parsing). I had no idea how much I don't do math-y programming until I came to MSP and gen stuff. My technical background is a barrier because one thinks about data flow and state sooooo much differently with signals than with documents/records.

Second, I only came to Max about 5 years ago and since it is so deep, I feel like I have missed a lot of technique discussions since they are no longer contemporary. And for concepts that are new to me, I don't even know the language to use to describe a problem and ask questions. So I have a challenge in terms of conceptualizing and then describing these kinds things, so I wouldn't know to think about it, let alone how to search the forum archive or ask a question about it.

So I completely agree, the simple building blocks are hard to use because they don't reveal, in a sense, to how they could be applied until I gain some experience with a macro-level task.

So this is a long way of saying, even if it seems not on the topic of gen itself, I definitely think it is on topic to talk about these non-gen ways of doing the same thing. It helps me understand gen better to know other ways to do the same thing.

Wetterberg's icon

>o I might take my original example of beats 1, 2, 4 (0. 0.25 0.75), but then nudge some beats off the grid by swing-like amounts so the resulting list could be 0. 0.2857748 0.75. The very small swing amounts are also what I had in mind when I was thinking about arbitrary subdivisions. I find the [what~] API's distillation to a list of 0-1 floats really elegant.

This was my starting point as well - this is how I handled swing beforehand, like; take a list of 16th notes, and add a list of swing values to that.


...Now, with signals, I get the added affordance of having arbitrary tuplets swing correctly, even though they do not fit the 16th-note grid, and you can have ramps between values that make things gel better.

It's nowhere near perfect at this point, but signals really help, and I fully support deconstructing the max objects with a gen~ approach.

Steve Meyer's icon

Here's the chapter 5 version: no buffer. It takes inspiration from the Integers as patterns section.

  1. Convert a gate sequence of 1's and 0's to an integer

  2. Pass that in as a gen~ param for the data transfer

  3. Unpack the bits

  4. Use a very short for loop to calculate the distances between gates

I suspect it could be improved by someone with experience with the bit shifting techniques.

ramp-sequencer-no-buffer.maxpat
Max Patch
Arbitrary ramp sequencer with no buffer