Boolean network music examples
Give each step of a Boolean network a duration and its stream of states can be read as notes, sample names or triggers. These extension functions on BooleanNetwork do it:
scheduledSteps(...)for timed Boolean states.scheduledNotes(...)for directIterator<ScheduledNote>mapping.asNoteProcess(...)for piano-roll friendly note streams.scheduledDrivenSteps(...)andasDrivenNoteProcess(...)for external clocks and oscillators.respondAhead(...)for live clocked side effects such as sample triggering.
From attractor cycle to NoteProcess
Take a small attractor, map rising edges to pitches, and render the result as a piano roll. step.lanes(...) takes one of four trigger modes and the choice does most of the musical work: a node that stays on for six steps gives six notes under the default ACTIVE, and one under RISING_EDGE, the mode the fence below asks for. FALLING_EDGE and CHANGE are the other two. Note also that the first step of a run has no previous state to compare against, so every lane already on at step zero counts as a rising edge.
:::static
data class Candidate(val seed: Int, val net: BooleanNetwork, val attr: Attractor)
val chosen =
(0..80)
.mapNotNull { seed ->
val net = kauffmanNetwork(n = 5, k = 2, seed = seed.toLong())
val attr =
net.findAttractors().filter { it.period >= 6 }.maxByOrNull { it.period } ?: return@mapNotNull null
Candidate(seed, net, attr)
}
.first()
val pitches = [36, 43, 48, 55, 60]
val labels = ["C2", "G2", "C3", "G3", "C4"]
val colors = ["#63b3ff", "#ff9b54", "#6df0a7", "#f67bff", "#ffe46b"]
val process = chosen.net.asNoteProcess(
initialState = chosen.attr.cycle.first(),
stepDuration = 0.25,
steps = 24,
) { step ->
step.lanes(BooleanTriggerMode.RISING_EDGE).map { lane ->
note(
pitch = pitches[lane],
duration = step.duration * 0.84,
start = step.start,
velocity = 0.74 + lane * 0.04,
label = labels[lane],
color = colors[lane],
)
}
}
val preview = process.notesBetween(0.0, 2.5).take(8).joinToString("\n")
:::fold render the piano roll and the first notes
markdown(
"main",
"""
#### seed ${chosen.seed} · period ${chosen.attr.period} · initial ${chosen.attr.cycle.first().joinToString("") { if (it) "1" else "0" }}
${process.pianoRollSvg(widthPx = 860, heightPx = 210, pastSeconds = 0.0, futureSeconds = 6.0, title = "Boolean attractor -> NoteProcess")}
~~~
$preview
~~~
""",
)
:::endfold
The example picks its seed by sweeping seeds 0 to 80 for a kauffmanNetwork(n = 5, k = 2) with an attractor of period 6 or more and takes the first that qualifies. What comes back is an ordinary NoteProcess, so notesBetween and pianoRollSvg work on it exactly as they do on a hand-written one.

The same 24 steps as sound. Each note plays the Tidal arpy sample, pitched the way Tidal pitches a
sample, by playback speed, so C3 is the sample as recorded and C2 an octave down at half speed. The
period-6 attractor is a figure that comes round every 1.5 seconds, four times.
Previewing Dirt tokens before playback
The same timing layer can carry Dirt sample labels instead of note names. Swap the mapping and the Boolean rhythm arrives as a sample rack you can read over before anything is played.
:::static
val net = kauffmanNetwork(n = 5, k = 2, seed = 27L)
val attr = net.findAttractors().maxByOrNull { it.period }!!
val tokens = ["bd:3", "sn:1", "hh:4", "cp:0", "perc:5"]
val colors = ["#69b8ff", "#ff8e5a", "#c4ff5f", "#ff6cc4", "#ffe36e"]
val preview = net.asNoteProcess(
initialState = attr.cycle.first(),
stepDuration = 0.25,
steps = 32,
) { step ->
step.lanes(BooleanTriggerMode.RISING_EDGE).map { lane ->
note(
pitch = 42 + lane * 3,
duration = step.duration * 0.8,
start = step.start,
velocity = if (lane == 0) 1.0 else 0.82,
label = tokens[lane],
color = colors[lane],
)
}
}
:::fold render the Dirt-token piano roll
markdown(
"main",
"""
#### seed 27 · longest attractor period ${attr.period} · labels are direct Dirt tokens
${preview.pianoRollSvg(widthPx = 860, heightPx = 210, pastSeconds = 0.0, futureSeconds = 8.0, title = "Boolean attractor -> Dirt token preview")}
""",
)
:::endfold
The symbols in the preview are the exact ones a live Dirt player would receive, so a typo in a sample name shows up here rather than as silence later.

And the tokens played. The attractor has two states, so kick and hat alternate, one per
quarter-second step, for eight seconds. There is one perc hit at the very start, from the lane
that was on at step zero and never again.
Braiding several Boolean rhythms together
This example braids three Boolean attractors at different temporal resolutions: a kick-like layer on eighths, a change-trigger layer on dotted eighths, and a high-density active-state shimmer on sixteenths. The three grids are 2, 3 and 1 sixteenths wide, so they all line up again every six sixteenths (0.375 s) and stay in step.
:::static
fun <T> Iterator<T>.collectAll(): List<T> {
val out = mutableListOf<T>()
while (hasNext()) out += next()
return out
}
data class Layer(
val seed: Long,
val stepDuration: Double,
val steps: Int,
val mode: BooleanTriggerMode,
val basePitch: Int,
val label: String,
val color: String,
val velocity: Double,
)
data class Chosen(val layer: Layer, val seed: Long, val net: BooleanNetwork, val attr: Attractor)
val layers = [
Layer(
seed = 27L,
stepDuration = 0.125,
steps = 64,
mode = BooleanTriggerMode.RISING_EDGE,
basePitch = 36,
label = "bd",
color = "#69b8ff",
velocity = 0.98,
),
Layer(
seed = 11L,
stepDuration = 0.1875,
steps = 48,
mode = BooleanTriggerMode.CHANGE,
basePitch = 50,
label = "sn",
color = "#ff8e5a",
velocity = 0.76,
),
Layer(
seed = 42L,
stepDuration = 0.0625,
steps = 128,
mode = BooleanTriggerMode.ACTIVE,
basePitch = 74,
label = "hh",
color = "#c4ff5f",
velocity = 0.42,
),
]
val chosen = layers.map { layer ->
// The first seed from the layer's own whose network has an attractor of period 4 or more:
// a fixed point never changes, so a CHANGE layer on one would fire once and stop.
(layer.seed until layer.seed + 80).firstNotNullOf { seed ->
val net = kauffmanNetwork(n = 5, k = 2, seed = seed)
net.findAttractors().filter { it.period >= 4 }.maxByOrNull { it.period }?.let { Chosen(layer, seed, net, it) }
}
}
val notes = buildList {
chosen.forEach { ch ->
addAll(
ch.net
.scheduledNotes(
initialState = ch.attr.cycle.first(),
stepDuration = ch.layer.stepDuration,
steps = ch.layer.steps,
) { step ->
val lanes = when (ch.layer.mode) {
BooleanTriggerMode.ACTIVE ->
step.state.indices
.filter { lane -> step.isActive(lane) && (lane + step.stepIndex.toInt()) % 2 == 0 }
.take(2)
else -> step.lanes(ch.layer.mode)
}
lanes.map { lane ->
note(
pitch = ch.layer.basePitch + lane * 2,
duration =
if (ch.layer.mode == BooleanTriggerMode.ACTIVE) step.duration * 0.42
else step.duration * 0.78,
start = step.start,
velocity = (ch.layer.velocity - lane * 0.04).coerceAtLeast(0.2),
label = "${ch.layer.label}:$lane",
color = ch.layer.color,
)
}
}
.collectAll(),
)
}
}
.sortedWith(compareBy<ScheduledNote> { it.start }.thenBy { it.pitch })
val process = noteProcess(defaultPastSeconds = 0.0, defaultFutureSeconds = 8.0) {
for (scheduled in notes) emit(scheduled)
}
val summary =
chosen.joinToString("\n") { ch ->
"${ch.layer.label} seed ${ch.seed} · period ${ch.attr.period} · ${ch.layer.mode} every ${ch.layer.stepDuration}s"
}
:::fold render the braided piano roll and the layer summary
markdown(
"main",
"""
#### Three attractors, three clocks, one shared note space
${process.pianoRollSvg(widthPx = 900, heightPx = 260, pastSeconds = 0.0, futureSeconds = 8.0, title = "Braided Boolean rhythm")}
~~~
$summary
~~~
""",
)
:::endfold

Played through Dirt-Samples, the kick and snare carry the rhythm for the whole eight seconds. The
shimmer is a wash rather than a ticking hat, because in Dirt-Samples hh:1 to hh:4 are a crash and
three long hits.
Driving the network with an oscillatory input
The driven scheduling helpers let something outside the network write into its state before each step: an oscillator forces one node, and the rest of the network updates from that forced value as if it had produced it.
:::static
fun <T> Iterator<T>.collectAll(): List<T> {
val out = mutableListOf<T>()
while (hasNext()) out += next()
return out
}
val net = BooleanNetwork(
[
BoolNode("clk", "external", emptyList()) { s -> s[0] },
BoolNode("kick", "clk XOR clap", [0, 3]) { s -> s[0] xor s[3] },
BoolNode("snare", "kick OR hat", [1, 4]) { s -> s[1] || s[4] },
BoolNode("clap", "snare AND NOT clk", [2, 0]) { s -> s[2] && !s[0] },
BoolNode("hat", "kick XOR snare", [1, 2]) { s -> s[1] xor s[2] },
],
)
val driver = { stepIndex: Long, _: Double, _: Double, state: BooleanArray ->
state[0] = sin(2.0 * PI * stepIndex.toDouble() / 12.0) > 0.0
}
val steps =
net.scheduledDrivenSteps(
initialState = BooleanArray(net.size),
stepDuration = 0.125,
steps = 64,
driver = driver,
)
.collectAll()
val labels = ["clk", "bd", "sn", "cp", "hh"]
val pitches = [30, 36, 50, 57, 74]
val colors = ["#5fa8ff", "#69b8ff", "#ff8e5a", "#ff6cc4", "#c4ff5f"]
val process = net.asDrivenNoteProcess(
initialState = BooleanArray(net.size),
stepDuration = 0.125,
steps = 64,
driver = driver,
) { step ->
step.lanes(BooleanTriggerMode.RISING_EDGE).map { lane ->
note(
pitch = pitches[lane],
duration = if (lane == 4) step.duration * 0.45 else step.duration * 0.9,
start = step.start,
velocity = if (lane == 0) 0.4 else 0.78 + lane * 0.04,
label = labels[lane],
color = colors[lane],
)
}
}
lines(
drawRhythmGrid(steps.map { it.state }, cellW = 18.0, cellH = 16.0, stepGap = 3.0, laneGap = 4.0, accentEvery = 8),
channel = "driven-grid",
w = 1080.0,
h = 170.0,
fitToSize = true,
)
:::fold render the driven piano roll
markdown(
"main",
"""
#### lane 0 is a sine-derived square wave; lanes 1-4 are the network's response
${process.pianoRollSvg(widthPx = 900, heightPx = 220, pastSeconds = 0.0, futureSeconds = 8.0, title = "Oscillator-driven Boolean network")}
""",
)
:::endfold
The upper grid is the raw Boolean state and the piano roll underneath is the same run read as events, so the forced node shows up twice: as a regular stripe in the grid, and as whatever the rest of the network made of it in the roll.

Heard, the forced lane is the quiet click, and the network's
answer comes in bursts of kick, snare, clap and hat that repeat every 1.5 seconds, twelve steps,
the oscillator's period. Each cycle has a gap of about half a second in which nothing fires.
Driving a live Dirt player
respondAhead(...) clocks the network in real time and calls back with BooleanNetworkStep values, enough to drive a DirtSamplePlayer directly. It does not start anything itself: it builds a BooleanNetworkResponseSpec, and a registered return-value handler launches that spec when it turns up as the box's last expression. So the responder runs because the example ends with it, and the job it starts belongs to the box. Stop the box and the sound stops.
val clock = musicClock().reset(started = true)
val net = kauffmanNetwork(n = 5, k = 2, seed = 27L)
val attr = net.findAttractors().maxByOrNull { it.period }!!
val tokens = ["bd:3", "sn:1", "hh:4", "cp:0", "perc:5"]
val lib = dirt("/absolute/path/to/Dirt-Samples", preload = false)
val kit = dirtPlayer(library = lib, voices = 24, gain = 0.22.p)
audio { kit } +
net.respondAhead(
initialState = attr.cycle.first(),
ahead = 0.05,
stepDuration = 0.25,
clock = clock,
) { step ->
step.lanes(BooleanTriggerMode.RISING_EDGE).forEach { lane ->
kit.trigger(
token = tokens[lane],
gain = if (lane == 0) 1.15f else 0.82f,
pan = (lane - 2) * 0.18f,
)
}
}
.onBeat(every = 1.0, clock = clock)
For the underlying state-space pictures, continue with Boolean networks examples. For piano-roll and synth display tooling, see Visualizers & Controls.
The public declarations used on this page are listed, with their KDoc, in the codebase reference: booleannetworks.kt. Start from what's already in the box.