SuperCollider Grids

Git repository is hosted at GitHub.

Screenshot of SC grids GUI

A topographic drum sequencer for SuperCollider based on the original module by Mutable Instruments.

Notes on the port

A quick remark on how the original source code was ported. When looking through the source code of the original module we will find two interesting files: pattern_generator.cc and resources.cc.

The drum maps

In resources.cc we will find 25 maps which are used to generate the patterns so now it boils down to understand how those maps are used to generate patterns.

Generating drum patterns logic

This is handled in pattern_generator.cc. At the beginning the 25 drum maps are arranged into a 5 by 5 array and stored in the variable drum_map. Keep in mind that these values are in a rather arbitrary order which we need to take into account later.

ReadDrumMap

Now lets start by taking a look at the ReadDrumMap method.

uint8_t PatternGenerator::ReadDrumMap(
        uint8_t step,
        uint8_t instrument,
        uint8_t x,
        uint8_t y) {
    uint8_t i = x >> 6;
    uint8_t j = y >> 6;
    const prog_uint8_t* a_map = drum_map[i][j];
    const prog_uint8_t* b_map = drum_map[i + 1][j];
    const prog_uint8_t* c_map = drum_map[i][j + 1];
    const prog_uint8_t* d_map = drum_map[i + 1][j + 1];
    uint8_t offset = (instrument * kStepsPerPattern) + step;
    uint8_t a = pgm_read_byte(a_map + offset);
    uint8_t b = pgm_read_byte(b_map + offset);
    uint8_t c = pgm_read_byte(c_map + offset);
    uint8_t d = pgm_read_byte(d_map + offset);
    return U8Mix(U8Mix(a, b, x << 2), U8Mix(c, d, x << 2), y << 2);
}

Some remarks what happens here:

x>>6

is a left bit shift of 6 on x. What exactly means this? Lets say we have the number 183 which has the unsigned byte representation of

1 0 1 1  0 1 1 1

Doing 6 bit shifts to the right yields

0 0 0 0  0 0 1 0

which is the binary representation of 2. Thinking a bit about this yields that >>6 is the equivalent of doing an integer division by 64 which stays in range 0 to 3 for the values of an unsigned 8 byte integer which ranges from 0 to 255.

We do the same for the variable y and use these both coordinates of to look up 4 drum maps. Now it makes sense that we are limiting x and y between 0 and 3 as we also accessing the adjacent maps in drum_map which is a 5 by 5 array.

After this an offset is calculated

uint8_t offset = (instrument * kStepsPerPattern) + step;

which tells us how the maps itself are structured. As each maps contains rows of 96 integers we can assume that they are stored as 32 beats in a row and after these 32 beats the beats for the next instrument starts.

After this section the 4 integers are read from the map and we come to the line

U8Mix(U8Mix(a, b, x << 2), U8Mix(c, d, x << 2), y << 2);

which is a bit tricky to understand.

U8Mix

By searching the repo we can not find this function so it must be included somewhere else. By taking a look at the used dependencies (the linked git repositories) we can indeed find the source code for this function which turns out to be in assembly

static inline uint8_t U8Mix(uint8_t a, uint8_t b, uint8_t balance) {
    Word sum;
    asm(
        "mul %3, %2"      "\n\t"  // b * balance
        "movw %A0, r0"    "\n\t"  // to sum
        "com %2"          "\n\t"  // 255 - balance
        "mul %1, %2"      "\n\t"  // a * (255 - balance)
        "com %2"          "\n\t"  // reset balance to its previous value
        "add %A0, r0"     "\n\t"  // add to sum L
        "adc %B0, r1"     "\n\t"  // add to sum H
        "eor r1, r1"      "\n\t"  // reset r1 after multiplication
        : "&=r" (sum)
        : "a" (a), "a" (balance), "a" (b)
        );
    return sum.bytes[1];
}

After some assembly research and thanks to the comments it turns out it is the same as

\[\text{balance} * b + (255 - \text{balance}) * a >> 8\]

which mixes two values a and b according to a parameter balance which maps 0 to full a and 1 to full b and everything in between.

The interesting bit that in mathematical terms we define the arithmetic mean between two values \(a\) and \(b\) by

\[\frac{(255-\text{balance})a + (\text{balance})b}{255}\]

So the difference is that the source code is not dividing by \(255\) but is instead using \(>>8\) which is an equivalent of an integer division by 256 - this is a bit off because we do not observe the value 256 as a 8 bit integer only goes to 255 but it is probably more performant to use this bit shifting trick in trade for accuracy.

We can implement both functions in Python to see how they compare

def balance_a(a, b, balance):
    return int(((255-balance)*a + (balance)*b)/255)

balance_a(100, 200, 100)
--> 139

def balance_b(a, b, balance):
    return (balance * b) + (255-balance)*a >> 8

balance_b(100, 200, 100)
--> 138

We see both implementations differ in the result by 1 but this is probably acceptable. For our re-implementation in SuperCollider we will use the proper mix of two variables as we do not need to optimize on a bit shifting level.

Mixing values with U8Mix

After this short excursion we can come back to understanding

U8Mix(U8Mix(a, b, x << 2), U8Mix(c, d, x << 2), y << 2);

First we need to understand what x << 2 is doing - lets again assume x is 183 so

1 0 1 1  0 1 1 1

with << 2 becomes

1 1 0 1  1 1 0 0

which is 220.

This is another neat trick and is basically

\[\left( \frac{183}{64} \mod 1 \right) \cdot 256\]

Having in mind that earlier we divided by 64 to get a position between 0 and 3 to select a map this gives us the remainder of the division by 64 and is used to mix the values of each map.

So doing x>>6 (basically \(\frac{x}{64}\)) gives us a value between 0 and 3 to select a map and dropping the 2 highest bits via x<<2 (basically \(\left( \frac{x}{64} \mod 1 \right) \cdot 256\)) gives us the remainder of the division and therefore the position between the maps.

We do this for both maps and therefore get a in between position of maps which yields enriching results.

EvaluateDrums

Now we can take a look at the context when ReadDrumMap is called. This is the method EvaluateDrums which is described here.

The interesting bits are the for loop

for (uint8_t i = 0; i < kNumParts; ++i) {
    uint8_t level = ReadDrumMap(step_, i, x, y);
    // ...
    if (level > threshold) {
        if (level > 192) {
            accent_bits |= instrument_mask;
        }
        state_ |= instrument_mask;
    }
}
}

which calculates the level of each instrument according to the map and if this level exceeds the threshold a bit will be raised in a bit mask which is the equivalent of hit in our step sequencer.

Extracting data using Python

It turns out the repository has the drum maps stored as Python lists so we can simply load these maps into a Python interpreter.

import numpy as np

# loading nodes from python file
# ...

# converting the nested python list to numpy array
nodes = np.array(nodes)

Now we need to remember how these values were mapped in the C++ source code.

The first re-arrangement of the values was while mapping nodes to a 5 by 5 drum map. We can simply imitate this in Python using numpy

indices = np.array([
    [10, 8, 0, 9, 11],
    [15, 7, 13, 12, 6],
    [18, 14, 4, 5, 3],
    [23, 16, 21, 1, 2],
    [24, 19, 17, 20, 22],
])

drum_map = nodes[indices]

drum_map.shape
-> (5, 5, 96)

Now we need to take care of the offset to separate the drum maps which was implemented by

uint8_t offset = (instrument * kStepsPerPattern) + step;

So we want to separate our 5x5x96 matrix into 3 5x5x32 matrix.

instrument_maps = {}
for i, instrument in enumerate("kick", "snare", "hihat"]):
    instrument_maps[instrument] = drum_map[:, :, (i*32):((i+1)*32)]

where the order of the instruments are taken from the bit map commentary.

By showing us each instrument map in Jupyter Lab we now have them in a clean format to be used in SuperCollider.