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
- Build yourself some dev tools to improve your quality of life.
- 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. - 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.
- Save backups periodically and/or use version control.
- 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.
- I started with a schematic that I periodically updated as I went.
- One of my goals for this project was to build everything as loosely-coupled as I could.
- 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
- norns
- Define note length per channel.
- Layer / re-trigger notes.
- Expand/contract to different grid sizes.
- Configure midi note out with mouse.
- Shift patterns up and down to other rows.
- Lookup performance improvements.
Changelog
-
v2.0.1
- Fix issue with note off being hardcoded to 5 seconds.
- Fix issue with midi notes being transposed an octave down.
- Change live.text names of UI elements.
- Change default midi notes from C4 to C1-F#2.
- Clear all LEDs on arc and grid on close.
- Configure demo
.als
set to default to 808 drums instead of Operator.
-
v2.0.0
- Complete rewrite.