Skip to content

Round Robins


Play the same note twice on a real instrument and it sounds slightly different each time -- different bow contact, different breath pressure, different hammer angle. Play the same note twice from a single sample and you hear an identical waveform repeated, producing an unnatural "machine-gun" effect that immediately breaks the illusion.

Round robins solve this by cycling through multiple recorded variations of each note. Instead of always triggering the same sample, the system picks the next variation from a shuffled sequence, so repeated notes sound organic.

IVLS provides Stl.RoundRobins, a standard library node that handles the full round-robin cycle for you: shuffle tables, counter tracking, transport reset, and multi-dimensional indexing. You configure it by telling it which voice fields define your RR context, and it writes the result directly into each voice.

Placing the Node


Stl.RoundRobins is a NotePass node. It sits in your flow before playback, reads the current voice's context, looks up the next RR position from a pre-seeded shuffle table, and writes the result into Voice[self].rr. Downstream nodes (typically your PlayEvent logic) use that value to select the correct sample group.

define FLOWS.MyFlow.NOTEPASS := ..., Stl.RoundRobins, ...

The node does its work in cb NotePass -- it queries the RR position, writes it to the voice, and advances the counter. You never call these functions yourself; they run automatically as voices pass through the flow.

Registering Dimensions


A round-robin system needs to know what constitutes "the same context." For a simple instrument, every note on a given key is the same context. For a multi-articulation instrument, the context might be key plus articulation -- you want separate RR cycles for staccato C3 and sustain C3.

You express this with ivls.RoundRobins.register_field(), which registers a voice field as an RR dimension:

ivls.RoundRobins.register_field(Voice.field.note, 128)
ivls.RoundRobins.register_field(Voice.field.articulation, 4)

The first argument is the voice field index. The second is the bound -- the upper limit (exclusive) for values in that dimension. For note numbers, 128 covers the full MIDI range. For an articulation field with four values (0--3), the bound is 4.

Internally, the node multiplies these dimension widths together to build a composite index. The note and articulation example above produces up to 512 unique RR contexts (128 x 4), each with its own independent counter.

Register your fields once during initialization -- typically in a product setup node's cb Init.

The get_max_rr Hook


Different notes or articulations may have different numbers of recorded round-robin layers. A fortissimo layer might have six variations while a pianissimo layer has three. The node needs to know the maximum RR count for each voice so it can wrap correctly within the shuffle table.

You provide this through the hooks.get_max_rr() override:

function ivls.RoundRobins.hooks.get_max_rr(vo) -> rr_max override
    rr_max := my_rr_counts[Voice[vo].articulation]
end function

The function receives a voice reference and returns the number of round-robin layers available for that voice's context. If you return 0, the node treats RR as disabled for that context and always writes position 0.

This is the only hook you must override. Without it, the default implementation returns 0 and no cycling occurs.

Transport Reset


When the DAW transport starts, Stl.RoundRobins resets all counters to zero in its cb TransportStart. This ensures that playback from the same position in a project always produces the same RR sequence -- important for reproducible renders and bouncing.

You get this behavior for free; no configuration needed.

Reading the Result


After Stl.RoundRobins runs on a voice, the RR position is available in Voice[self].rr. Your downstream playback nodes use this value to select the correct sample group:

{ In your PlayEvent or post-pass logic }
declare group_idx := base_group + Voice[self].rr

The value is an integer starting from 0, cycling through the shuffled positions up to get_max_rr() - 1.

The Shuffle Table


You may have noticed that the system doesn't simply increment a counter. It uses a pre-seeded shuffle table (rr.main_seed) loaded at init time. This table contains pre-randomized sequences for each possible RR limit (up to 32 layers). The counter is an index into this table, not a direct RR position.

The result is that repeated notes cycle through variations in a pseudo-random order rather than sequentially. Sequential cycling (1, 2, 3, 1, 2, 3...) can create audible patterns -- especially with short RR counts -- while shuffled cycling breaks those patterns.


For most products, round robins require just three steps: place Stl.RoundRobins in your flow, register your voice field dimensions, and override get_max_rr. The node handles the rest -- shuffling, counting, resetting on transport, and writing the result to each voice.