Cascades v2.0.0

Cascades

Paint with all 65,536 combinations of 16th note patterns across seven tracks.

Development Process, Inspiration, etc...

Background

The original vision for Cascades was actually much grander. 256 combinations is a concession to the limited resolution the traditional midi encoder. In my first implementation, I needed to sum two knobs (0-127 + 0-127) to even get up to 256. An inelegant yet pragmatic solution.

arc, being an endless encoder, removes the resolution constraint. While developing 16 step resolution for (the abandoned) Cascades v1.2, it quickly became apparent that all the data structures needed refactoring. Everything assumed the limited paradigms of eighth notes and only 256 patterns. After one morning of development I decided Cascades needed a complete rewrite from scratch if I was to proceed.

Sixteenth notes was a feature request I was very interested in delivering. grid 128 has 16 columns. With Cascade v1, the Rule of Product told me I’d be working with 2^8 (256) combinations. To afford 16th notes would require 2^16 (65,536) combinations.

I also wanted to incorporate track shifting, track length, density caps, and velocity functions - all while remaining intuitive and above all FAST.

I named this thing before I knew [cascade~] existed. Sorry.

Development Process

Always start developing the riskiest part of your software first. Nothing is worse than putting off that one thing until the very end. You’ll blow your deadlines, budget, and enjoyment 99% of the time, which is statistically equivalent to “always.”

The riskiest part of Cascades was the pattern storage. All the sequences are stored as simple binary lists and I wanted to keep that design. I contemplated using hex values to store the patterns, but couldn’t see a benefit to doing so. I used [umenu] for the 256 pattern version Cascade. Would Max’s humble [umenu] be able to store all 65,536 combinations?


Building the dataset.


Sublime Text lets you edit multiple rows at once.

I was pleasantly surprised that Max was able to handle it. Sure, the .maxpat was almost 3mb from the single object but still… Satisfied, I figured I could use additional [umenu]s for the density caps. The math for the density caps is quite beautiful, by the way:

Number of Sixteenth Notes Possible Combinations
1 16
2 240
3 1680
4 7280
5 21840
6 48048
7 80080
8 102960
9 102960
10 80080
11 48048
12 21840
13 7280
14 1680
15 240
16 16

The next riskiest piece was figuring out how to store the row states. I wanted to keep it simple and as flat as possible. A simple embedded [dict] felt like the best way to go. I designed a basic API to go with it:


API definition.

Designing your API first is always more efficient (and fun) than figuring it out as you go.

The “setters” would always work anywhere in the patch, but the “getters” would be a problem. I couldn’t just simply have one send to return the getters results, otherwise it would send the results to every component every time. This require some type of bus. I took a shot in the dark that 10 different bus outputs would be good enough. The plan was that each one would be “registered” by filling out the a comment. There is a stronger way to do this with named returns (i.e. something highly verbose like patternStorageGridControllerRow1Return) but I didn’t want to over-engineer it before I knew more. (Spoiler - this was over-engineered.)

Satisfied that the riskiest pieces were out of the way, I compiled a backlog and made a simple kanban board. High priority features are at the top of the list and move from left to right until they’re done. This is a great way to work as you can prioritize what is next and remove the need to think too much about the “meta-work” of the work. Development is an exercise in many tiny decisions. While working, these decisions add up to what is known as decision fatigue. The more decisions you can shield yourself from making, the more brain juice you’ll to do the stuff that really matters. Over the past week I’d often dream about new features, wake up, slam them into Trello, and go back to bed.

30 Hours of Development Later…

I got a rough alpha going and I wasn’t happy with it.

My clever “specify-your-return-send-in-the-request” hub and spoke API design worked well enough writing data, but was too buggy for reading. Strange race conditions kept popping up. I had a suspicion it was too resource intensive. I refeactored it to something that resembles a “pub/sub”. Whenever the main data store updates, it bangs itself out to a [s allRowsMaster]. Then, other components of the patch can process and read this however they want without influencing one another.

While going through the (what I thought to be) final QA checks, I realized that humble [umenu] was definitely not the right tool to use. I refactored it to use an embedded [coll]. In my defense, I didn’t know that [coll] or [dict] existed when I first chose [umenu].

The [coll] approach quickly unlocked the “density” feature. If you are feeling adventurous, crack open [p densityGates].

Despite how proud I am of this part of the patch, I have a suspicion there is an elegant way compute the 2^16 patterns at run time. Saving them all to a [coll] is all my peanut brain could muster. Alas.

But, perhaps most catastrophically, I had fallen into some poor tight coupling designs between the grid/arc view states and the data. The completely de-coupled nature of monome hardware is still new to me. I went about the arduous task of unwinding them…

In the end, I’m super happy with how it turned out. I look forward to hearing what the lines community creates with it.

Miscellaneous Musings & Tips

  1. Build yourself some dev tools to improve your quality of life.

  2. Save your [dict] contents to an external file so you can quickly copy and paste it back in to reset everything. You will destroy and corrupt your data structure over and over again while developing.
  3. If you’re doing something “clever” or weird (especially with hardcoded magic numbers), leave a comment. Your future self and/or future developers will thank you.
  4. Save backups periodically and/or use version control.
  5. I wrote this devlog as I went. It helped to organize my thoughts and put closure to things as I finished them. Also made posting it at the end much easier.
  6. I started with a schematic that I periodically updated as I went.
  7. One of my goals for this project was to build everything as loosely-coupled as I could.
  8. Learn to enjoy deleting your code. My initialization routines started with only the grid. I spent a lot of time on the boot up animation and making sure everything got zeroed out. By the time I worked on what I thought was going to simply be the arc initialization, I had removed the ability for Cascades to save your last session, rendering my previous work obsolete. A simple sweep of encoder zero did the same thing that my grid boot up animation did. If you remain attached to the code you built, you limit yourself and your software. Just delete it. This is a further argument for how a little bit of planning a head of time can save your hours down the road.

Requirements

  • arc
  • grid
  • M4L

Documentation

(Note all keys, rows, and encoders are zero-indexed in this document.)

grid Row 0 = Menu

  • Key 0: Shift enables shift modes on arc.
  • Key 1: Density + any key in a track = filter out all patterns other the selected step count. So press key 4 on track 1 to only browse patterns with five hits. When you filter a track, that track will become selected (as if by arc Encoder 0). Double press the Density key to clear all density filters on all tracks while preserving the patterns.
  • Key 2: Reset + any column 0 key in a track = reset the sequencer to step 1 on next beat. Double press to reset all.
  • Key 3: Mute + any key on any track = mute the entire track. When you unmute, the unmuted track will become selected (as if by arc Encoder 0).
  • Key 15: Nuke = Double press to nuke all your patterns, settings, velocities, midi notes (everything!!) and start from a clean slate.

grid Rows 1 thru 7 = Tracks

  • The default state is a step sequencer. Just toggle steps on and off. There are no long press, double press, range select features. Menu options Density, Reset, and Mute require input on the tracks keys.

arc Encoder 0 or arc Encoder 0 + grid Shift = Track Select

  • Select which track you wish to operate on. This encoder functions the same despite the state of the shift key. The animation inverts to remind you that you are in shift mode. Only one track can be selected at a time. Various operations (such as adding or removing an individual step) do not require tracks to be selected. Follow your intuition.

arc Encoder 1 = Pattern Select

  • On the selected track, browse through all 65,536 combinations of 16th notes in chunks of 15 or so.

arc Encoder 1 + grid Shift = Precision Pattern Select

  • On the selected track, surgically cycle through all 65,536 combinations of 16th notes cycle one at at time.

arc Encoder 2 = Shift Pattern

  • On the selected track, shift the pattern forwards and backwards.

arc Encoder 2 + grid Shift = Track Velocity

  • On the selected track, change the velocity of the entire track.

arc Encoder 3 = Track Length

  • On the selected track, set the track length.

arc Encoder 3 + grid Shift = Midi Note Out

  • On the selected track, change the midi note out. The range is C-1 (0) to G9 (127).

Backlog

  • Expand/contract to different grid sizes.
  • Configure mdi note out with mouse.
  • Shift patterns up and down to other rows.
  • Lookup performance improvements.

Download

18 Likes

I probably said it before, but wow! This is impressive, can’t wait to find out time to play around with it (weekend probably) . I’m already super thankful :blush::blush:

2 Likes

I hope you enjoy it and it inspires you! I wanna hear what you create. Let me know if you find any bugs or want to see any new features.

1 Like

The toil is over. Time to shred.

Assemblage IV
minimal, Reichian, formal

Distress Beacon from Centillion Station
maximal, Skynet, post-singularity

3 Likes

had a first chance to try it out, so much fun to play around with!
4 observations:

  1. I have a grid 256 and it took some selecting until I had both Arc4 and grid connected. Worked though
  2. Shift + Pattern causes an unbelievable amount of CPU and introduces unwanted noises. I have an AMD 3950 Ryzen, so there is plenty of power available
  3. selecting notes (only) via arc is nice, but being able to additionally select notes/rowes via directly clicking the row on the grid would be more intuitive imho
  4. being able to use the full grid 256 (all rows) would be the icing on the cake ;-). I believe stretta has some good examples of making a patch adept to the grid size, which might be an easy way to get it implemented (and also make the patch work for a broader audience with e.g. also smaller grids). Of course I don’t have the faintest clue about programming in the first place, so take that with a grain of salt :smiley:

Thank you so much for your time, patience, and detailed feedback.

  1. That is great to hear your grid 256 worked. That comes as a surprise to me.
  2. I know. My lookup algorithm is about as basic as it gets. I’m aiming to learn more Max so I can optimize this. I’ll publish my backlog up above.
  3. You can select directly on grid! See my videos above. Or were you thinking of something else?
  4. I’d love to be able to contract/expand dynamically. Compatability is love. @strettara can you point me to some of your work in this area, please?

when i first saw this thread i was pretty excited about this…
but then i realized it’s not for norns.
:stuck_out_tongue:
any chance of a port?
:slight_smile:

2 Likes

I just got grid and arc this month. Help me rationalize another purchase.

4 Likes

heh!
i’m not the most rational person to ask…see this post here:


:stuck_out_tongue:

“Well… surely if SPIKE has 3 I can splurge for one… surely…” SIGH

Friends help friends make the right decisions. Lemme finish my current grid/M4L project then I’ll stare at the Buy Now button again.

1 Like