Skip to content

Cycles & Time

Resonon measures musical time in cycles. A cycle is one repeating window — think of it as a bar of music. A pattern doesn’t carry a tempo or a list of timestamps; it describes what happens within one cycle, and the runtime repeats that cycle forever. Put four notes in a pattern and they’re quarter-notes; put eight and they’re eighths. This page is the mental model for how that window is sized, how the clock advances it, and why your live edits always land in time.

A cycle is divided into beats. The two tempo controls are the cycle’s beats and its speed:

use "std/instruments" { Sampler, Kit };
project_bpm(120); // 120 BPM, 4 beats per cycle (the default)
let drums = AudioTrack("drums");
drums.load_instrument(Sampler(Kit("CR-78")));
drums << [bd sd bd sd];
PLAY;

project_bpm(bpm) sets the tempo; an optional second argument sets the beats per cycle, which defaults to 4:

project_bpm(140); // 140 BPM, still 4 beats per cycle
project_bpm(120, 3); // 120 BPM, 3 beats per cycle (a waltz feel)

The relationship is just arithmetic: 1 cycle = beats_per_cycle beats. By default that’s 120 BPM at 4 beats per cycle, so one cycle lasts 2 seconds and the four hits above fall one per beat, half a second apart. Add a fifth element and the cycle still lasts 2 seconds — every event simply gets a thinner slice. The cycle is the fixed frame; the pattern decides how to fill it.

Patterns are timeless on their own — [bd sd bd sd] is the same description whether you play it fast or slow. What turns that description into sound at a tempo is the scheduler: it walks wall-clock time forward, and each time enough real time has elapsed for one cycle, it advances the cycle counter and queries every playing pattern for that cycle’s events.

cycle 0 cycle 1 cycle 2
┌──────────┐ ┌──────────┐ ┌──────────┐
│ bd sd … │ │ bd sd … │ │ bd sd … │ ← pattern queried per cycle
└──────────┘ └──────────┘ └──────────┘
0s 2s 4s 6s ← wall-clock at 120/4

So tempo isn’t baked into your patterns — it’s the rate at which the scheduler consumes them. Change project_bpm and the same patterns simply stream by faster or slower.

This is the property that makes live coding feel musical rather than chaotic. When you re-send a pattern to a track that’s already playing:

drums << [bd bd sd bd];

the swap does not happen the instant you press execute. It’s quantized — held until the start of the next cycle, then applied cleanly on the downbeat. Your edit lands in time no matter when you trigger it mid-cycle, so the groove never tears. This is automatic and applies to every pattern update; there’s no mid-cycle swap to opt into.

Sometimes you want code to run every cycle — to compute the next pattern from the cycle number, advance a progression, or print where you are. on_cycle registers a callback that fires once per cycle and receives the current cycle number:

use "std/instruments" { SamplerMelodic, Kit };
let lead = AudioTrack("lead");
lead.load_instrument(SamplerMelodic(Kit("keys")));
on_cycle(fn(cycle) {
let note = 60 + (cycle % 12); // climb an octave, one step per cycle
lead << Pattern(#[note]);
});
PLAY;

The callback runs on the interpreter thread with full access to the language — you can branch, index a chord table by cycle % 4, PRINT, send patterns, anything. Whatever it sends is itself quantized, landing on the upcoming cycle.

Two companions complete the picture:

off_cycle(); // remove all registered cycle callbacks
prime_cycles(); // fire the callbacks once now, to set the cycle-0 pattern

off_cycle() clears every callback. prime_cycles() fires them immediately so a pattern is ready before the clock delivers the first cycle — PLAY does this for you on Resonon’s internal clock, so you mainly need it when slaving to an external MIDI clock, where the first downbeat would otherwise arrive empty.

You now have the timing model: cycles are the frame, the scheduler advances them in wall-clock time, and edits quantize to the boundary so they stay in the groove.