5 minute read

TLDR

Added support for triplets and other non-standard beat divisions to GrvMkr. It was kinda painful.

GrvMkr 9 days in

TSMD (Too Short, More Detail)

Another three days have passed, and I’m about ready to put this project on pause. The past three days have been slightly painful, as I had to rework the domain layer of GrvMkr to make supporting triplets easier.

Merging Grid Cells

It started with a proof of concept (POC) to figure out how I could merge grid cells to accommodate irregular beat divisions. I couldn’t be bothered to implement grid cell selection, so I went with a right-click contextual menu UI. This was something I’d never implemented before, but thankfully, it was relatively straightforward.

Right-clicking on a cell gives the option to merge left or right. I added cellsOccupied in the UI layer to represent how many columns the cell spans in the grid. I used this to set the grid-column style directly on the grid cell: style="grid-column: span {ui.cellsOccupied}". This worked nicely on the front end.

The backend, however, was slightly trickier. I had made what I now know to be a bad design decision that complicated the process of merging cells.

I originally modeled the grid as an array of GridRows, which contained Notation, which contained Bars, which contained Beats, which contained BeatDivisions. The cleverererest among you will notice that this deeply nested data structure isn’t very smart. I didn’t.

When merging a cell, I had access to the right-clicked cell, so I needed to find the one to the left or right. Easy, right? Not really. With this deeply nested structure, it was incredibly cumbersome to loop over each bar, beat, and division to find the adjacent cell—especially since, in theory, a merged cell could span across bars. For example, in a two-bar phrase, I might want to merge the last beat division of bar 1 with the first division of bar 2.

I asked ChatGPT for help, and it provided a bunch of super long, confusing functions that half-worked. After about a day of not being satisfied with the outcome, I was in the car wondering whether I should just remodel the entire structure to make it flatter—keeping the grid as a flat array of cells and moving the beat, bar, and division logic to more of a view concern. I consulted ChatGPT again, asking which approach was better, and it strongly favored the flatter approach.

It was a sad task to undo the last day’s work, but when it was done, the benefits were clear. Getting the adjacent cell is now a simple case of looping either forwards (right) or backwards (left) through a one-dimensional array from the clicked cell, finding the first cell with a cellsOccupied value > 1. Easy.

Playing Audio Out of Time

After sorting out the structure of the merged cells, I gave them an array of hits instead of just one to enable triplets, etc. Now I needed to figure out how to play them.

The current strategy of playing audio is incredibly basic, and I’m honestly surprised it works so well. I’m simply calculating the number of milliseconds between each grid cell and using setInterval to schedule a function that plays any hits from the active grid column.

Since this approach was working so well, I decided to extend it to support multiple hits per cell.

I ignored any cell with cellsOccupied < 1, as these are merged cells that are not visible and should not be playable. Then, I used a simple approach of scheduling the merged cell hits using setTimeout in a loop. To calculate the delay, I used a straightforward percentage of the overall cell time.

Say we’re looking at a two-cell merge, operating at 60 BPM, where every beat is 1000ms. Each beat is divided into four divisions (cells), making each cell worth 250ms. Two of these cells make 500ms. This is mergedCellTime. To calculate the delay, we divide the index of the hit by the number of hits to get a percentage of the mergedCellTime the sample should play at. Multiplying this by mergedCellTime gives us the delay, ensuring equal spacing between the hits within the mergedCellTime.

let mergedCellTime = currentlyPlayingGrid.msPerBeatDivision * cell.cells_occupied;
                
cell.hits.forEach((hit, i) => {
    let delay = (i / cell.hits.length) * mergedCellTime;
    setTimeout(() => {
        this.instrumentManager.playHit(hit);
    }, delay);
});

Honestly, musically, I’m not entirely sure if this is correct. Looking at sample grid notation from my mum, the spacing isn’t super clear on where the first hit should play. However, after reproducing a samba groove I know well from my days in a samba band, it sounded… close enough. So I left it until I can get a more musical opinion.

Grid Cell Selection

Earlier, I mentioned I couldn’t be bothered to implement cell selection. Well, with the ability to merge up to 8 cells and an instrument having no limit on the number of hits, the number of options in the right-click context menu would be huge.

There’s probably a better way to allow users to input off-time hits, but I went with “show ‘em all the options,” as seen below.

GrvMkr Cell Tools

Samba Instruments

I also spent some time finding some decent samba drum samples and made them available by default alongside the kick, snare, and hi-hat samples. This saves users from having to root around the internet for them, as they’re surprisingly hard to come by.

Final Thoughts

Aside from some more thorough testing and bug fixing, there are two things I want to do before putting this project to rest for a while.

  1. Copy-Paste

In samba, instruments play the same thing a lot of the time. Breaks are often played in unison. Manually inputting the same thing for 7 or 8 rows is tedious and time-consuming. Ideally, you’d be able to copy a row to another or multi-select some cells and paste them. It’d be fun to implement some keyboard shortcuts too—something I’ve never done before.

  1. State Refactoring

I’m not proud of the codebase currently. It’s mostly alright, but app_state.svelte.ts is a crime against code. It’s a 700-line monstrosity that evolved from me not really caring about the architecture because this was “only a small project” and “would be done soon.”

I’d love to spend some time decoupling playback, grid management, instrument management, etc. I think I used the fact that I’m not a JS/TS guy as an excuse to just throw something together and get it working. Hey, it’s only a personal project, right? True—but it could be a valuable learning experience for state management patterns in other languages. Maybe I’ll finally look into clean architecture. 🙈

  1. Merged Cell Styling

Okay, there’s a third thing. The dark gray backgrounds for columns that fall on the beat look weird when the cell is merged. I spent some time trying to figure out how best to tackle this, but I wasn’t happy with any approach that ChatGPT suggested. I’ll leave it for now but may revisit it later.

Comments