Skip to content

Flows

A flow is an ordered array of node references that a voice traverses. Flows are the routing backbone of an IVLS instrument. Rather than encoding all playback logic inside a single monolithic callback, you compose a sequence of nodes into a flow and let voices walk through them stage by stage.

A flow entry can be either a node (runs the node's callback for the current voice) or a redirect to another flow (the voice is immediately moved to the first node of the target). This allows tree-shaped routing topologies built from simple linear arrays.

Why Flows Exist

Flows make the instrument's signal path explicit and inspectable. When you look at a cb Flows declaration, you can read off the exact processing order for every note event. Features can be added or removed by inserting or removing nodes — no existing code changes. Branching is expressed by Divert nodes that call ivls.reflow_voice, keeping decision logic separate from algorithm logic.

Key Concepts

Declaring Flows

Flows are declared in cb Flows, which runs inside FirstLoad — once, when the instrument first loads:

node MyProduct.Flows:
    cb Flows:
        define FLOWS += mf.play_flow, mf.sound_flow

        mf.play_flow  := ivls.new_flow()
        mf.sound_flow := ivls.new_flow()

        ivls.register_node(mf.play_flow, MyProduct.Modify.Something)
        ivls.register_node(mf.play_flow, Stl.Pedals)
        ivls.register_node(mf.play_flow, MyProduct.Divert.PlayModes)

        ivls.register_node(mf.sound_flow, MyProduct.Modify.Volume)
        ivls.register_node(mf.sound_flow, MyProduct.PlayEvent)

        // At the end of play_flow, continue into sound_flow automatically
        ivls.register_move(mf.play_flow, mf.sound_flow)
end node

define FLOWS += registers variable names so the SDK allocates integer indices at load time. ivls.new_flow() assigns those indices. Flows can be declared across multiple nodes using += — each contributing node appends to the shared define.

Limits

The SDK allows up to 625 flows (derived from 1000000 / (IVLS.NODES_PER_FLOW * IVLS.ARGS_PER_NODE)), 100 entries per flow, and 16 arguments per node registration. These are compile-time constants.

How Voices Move Through Flows

Inside a NoteOn node, ivls.pass() advances the voice to the next stage. In a NotePass node the voice advances automatically — do not call ivls.pass(). ivls.stop() halts processing without advancing. When a flow is exhausted, the voice either continues into a redirect target (if ivls.register_move was called) or finishes.

Runtime Routing with ivls.reflow_voice

ivls.reflow_voice is the primary branching tool. It sets the voice's flow and resets its stage to the beginning of the target:

cb NotePass:
    if plc.layers[thread].arp_enabled = FALSE
        ivls.reflow_voice(self, plc.playback.direct)
    else
        ivls.reflow_voice(self, plc.arp_flow)
    end if

The Divert Node Pattern

Divert nodes handle routing without embedding branching inside algorithm nodes. An algorithm node (Modify, Spawn, Play) does one thing. A Divert node reads state and calls ivls.reflow_voice:

node Tokyo.Divert.EngineStyle:
    cb NotePass:
        if tokyo.lookahead_enabled = TRUE
            ivls.reflow_voice(self, tky.flows.lookahead_engine)
        else
            ivls.reflow_voice(self, tky.flows.standard_engine)
        end if
end node

The algorithm nodes in each branch know nothing about routing. This separation keeps both sides simple and reusable.

Flow Naming Convention

Name flows to reflect the event category and stage in the pipeline:

tky.flows.start                  // entry point for all note-ons
tky.flows.standard_engine        // non-lookahead processing
tky.flows.play_sound             // RR + dynamics + event triggering
tky.flows.play_release           // release-noise path

plc.playback.base_path           // main entry for Polycore note-ons
plc.playback.sound_path          // layer sound triggering
plc.playback.mono_legato         // mono legato path

Use the full description rather than abbreviating — flow names appear in debug output and document the architecture.

Case Study: Polycore

base_path
  → Keymap.DefaultPressedState   (records key pressed state)
  → Spawn.Layers                  (fans out one voice per active layer)
  → Modrix.Sys.ReceiveInput       (modulation bookkeeping)
  → Gate.Ranges                   (stops voices outside playable range)
  → Modify.FixedPitch             (overrides note if fixed pitch active)
  → Stl.Pedals                    (sustain/sostenuto handling)
  → Divert.PlayModes              (routes based on arp/seq state)
       ├── direct   → HoldLength → Divert.Portamento → sound_path
       ├── arp_flow → Steparp.Arp → sound_path
       └── seq_flow → Steparp.Seq → sound_path

sound_path
  → Modify.LayerBlend
  → Modify.VelVolume
  → PlayEvent

Case Study: Tokyo Scoring Strings

start
  → RROverride.PopAndStore
  → TACT.Modify.EvalArtics
  → TACT.Modify.FallbackArtics
  → Divert.EngineStyle
       ├── standard_engine → Pedals → Divert.PlaybackParse
       │     ├── non_legato → SmartAttack → ADSR → TACT.Spawn → play_sound
       │     ├── monoleg → MonoLegato → Divert.LegatoResponses
       │     └── polyleg → PolyLegato → Divert.LegatoResponses
       └── lookahead_engine → Lookahead → Divert.LegatoResponses

play_sound
  → RRConsolidate → RoundRobins → SkipBadRRs
  → TACT.TriggerArticVolume → Dynamics → SetADSR
  → DisplayArtic → Divert.PlayEvent
       ├── normal → PlayEvent
       └── withrelease → Spawn.ReleaseNoise → PlayEvent

play_interval and play_without_adsr are variants of play_sound with different ADSR handling for legato interval voices.

Connections to Other Parts of IVLS

The keymaps system maps each MIDI key to a flow entry point — when a key is pressed, the keymap determines which flow the resulting voice enters. nodes are registered into flows via cb Flows. voices are the objects that traverse flows stage by stage.

Patterns and Caveats

  • cb Flows runs once, inside FirstLoad. Flow topology is fixed after that. Dynamic branching happens at runtime via ivls.reflow_voice, not by modifying flow arrays.
  • ivls.register_move connects the end of one flow to the beginning of another. Without it, a voice reaching the end of a flow simply finishes.
  • Flow entries can be either nodes or redirects. Redirects let you build shared sub-flows that multiple parent flows converge into (like sound_path in both Polycore examples above).
  • The 625-flow limit is generous for most instruments but counts quickly in products with per-layer per-mode path variants.
  • nodes — node declaration, registration, and the NotePass/NoteOn decision
  • voices — how voices are created, moved, and released
  • keymaps — how key presses are mapped to flow entry points