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.