Lookahead Engine¶
The lookahead engine buffers all incoming MIDI events for a configurable window (up to 1000 ms) before playing them, enabling retrospective legato detection. A note that arrives alone can be identified as a legato destination after the fact, once a subsequent note confirms a legato interval.
Why Lookahead Exists¶
Prospective legato detection — the classic approach — requires a note already be held when the next note arrives. For expressive playing this is adequate, but in DAW recording with complex passages, players often release a note fractionally before pressing the next. Lookahead solves this by buffering: the engine holds notes before playing them, giving subsequent NoteOff events time to reach back and establish legato links retrospectively.
Key Concepts¶
Operating Modes¶
Zero Latency — No buffering. Notes play immediately. Legato detection is prospective only. Smart Attack works; retrospective legato does not.
Standard (Standard Latency Comp) — 300 ms buffer window (TKY.LOOKAHEAD_SHORT_MODE := TRUE). Retrospective legato detection works for most playing speeds.
Lookahead — Full 1000 ms buffer (lkh.window_ms). Maximum retrospective detection accuracy at one second of MIDI latency. Intended for DAW recording with transport running.
TKY.USE_LOOKAHEAD evaluates to TRUE only when NI_TRANSPORT_RUNNING = TRUE and the engine is in LOOKAHEAD mode. This prevents latency from applying during live performance even if the mode is accidentally selected.
The Action Type Pool¶
Action is a pool-allocated type with fields: type, timestamp, sync_offset, use_manual_sync, target_owner_action, target_action, voice, legato_speed.
Constructor and Destructor are overridden so every Action.new() automatically enqueues itself into lkh.ActionBuffer, and every Action.delete() removes itself:
function Action.Constructor(ref) override
Action[ref].timestamp := ENGINE_UPTIME
lkh.ActionBuffer.push.fast(ref)
end function
function Action.Destructor(ref) override
declare i := lkh.find_action_in_buffer(ref)
lkh.ActionBuffer.remove(i)
end functionlkh.ActionBuffer is an ADT queue with capacity 10,000.
NoteOn Flow¶
- Record legato eligibility into
Voice[self].lkh.use_legato - Optionally run Easy Artic evaluation (see tact)
- Create ON Action — Constructor fires, timestamping and enqueuing it
- Calculate
pre_rollfrom articulation sync time - Busy-wait:
while t_ela_ms < lkh.window_ms - pre_rollusingivls.wait_ms.unsafe; re-evaluatepre_rolleach iteration (artic may change) - At play time: if
target_actionwas set (legato link established), delete it; injectlegato.vo_srcinto the voice - Play the voice
The busy-wait is the core of the lookahead. The NoteOn flow suspends inside a taskfunc, waiting for the window to expire. During this time, NoteOff events from subsequent notes arrive and can reach back to modify Action[on_action].target_action before the NoteOn resumes.
ivls.wait_ms.unsafe is used because the voice is not yet "in flight" in the normal sense — the slot is allocated but has not started playing. The unsafe variant bypasses the check that would abort the wait if self is released.
The pre_roll is re-evaluated on every iteration because the user might change articulation while a note is buffered, changing the required sync offset.
NoteOff Flow¶
- Search backward in
lkh.ActionBufferfor ON actions within a 16th-note window - Feed found voices into the poly legato parser → outputs destination/source pairs
- For each legato pair: set sync offsets, write
target_actionlink on the ON action - Auto-detect RUN artic: if notes are <= 100 ms apart, flag
forced_run
The backward search is what makes detection retrospective. By the time a NoteOff arrives, the ON action for the previous note is already in the buffer with its timestamp. The 16th-note window sets the maximum interval for legato eligibility.
Auto-Run Detection¶
If lkh.option.use_auto_runs = ON, the NoteOff path checks whether notes were close enough to qualify as a run passage (notes <= 100 ms apart). If so, Voice[self].lkh.forced_run := TRUE and downstream nodes can select the RUN articulation without a keyswitch.
Param Sync: Delayed UI Writes¶
When in lookahead mode, a UI parameter change arrives at the engine 1000 ms before it would be audible. Without param sync, the parameter would jump a full window ahead of its expected timing relative to playback.
The param sync system queues UI writes and holds them until ENGINE_UPTIME - queue_time >= lkh.window_ms. The sync loop runs in LCB and applies writes when they reach their correct playback position. This only activates when tokyo.ui_sync_enable = TRUE and transport is running in LOOKAHEAD mode.
Transport Reset¶
When transport transitions from running to stopped, the lookahead buffer is flushed. When transport starts, the buffer is cleared and the window timer resets. This prevents stale actions from a previous playback pass from contaminating the new one.
Voice Fields¶
vo_field lkh.use_legato = FALSE if (in_range(lkh.use_legato, 0, 1))
vo_field lkh.on_action = -1 if (lkh.on_action >= -1)
vo_field pedal_state = FALSE if (in_range(pedal_state, 0, 1))
vo_field lkh.forced_run = FALSE if (in_range(lkh.forced_run, 0, 1))lkh.on_action stores the Action reference for the ON event. The NoteOff path uses Voice[self].on_action to find and modify the correct action in the buffer when a legato link is established.
Connections to Other Parts of IVLS¶
The lookahead uses the type-system for the Action pool-allocated type, the data-structures queue for lkh.ActionBuffer, and keymaps per-key latency to create the scheduling window. tact Easy Artic evaluation runs inside the NoteOn wait window. The stl-legato split architecture (assignment vs. playback) enables the lookahead to inject legato.vo_src retrospectively without modifying playback nodes.
Patterns and Caveats¶
- The busy-wait in NoteOn is intentional — it holds the voice in a taskfunc while the window runs. This is what makes lookahead work, and it is why
ivls.wait_ms.unsafeis used rather than the safe variant. check_ref(Action, on_action)guards are essential throughout lookahead code. Actions can be deleted by the NoteOff path while the NoteOn path is still waiting.- The param sync system must be disabled during live performance (
NI_TRANSPORT_RUNNING = FALSE). Without this guard, playing live with the lookahead knob in LOOKAHEAD mode would make all UI changes arrive 1 second late. - The 16th-note window for retrospective detection scales with tempo. At fast tempos the window is shorter in absolute time, so detection range is tempo-dependent.
Related¶
- type-system — the Action pool-allocated type and Constructor/Destructor override pattern
- data-structures — the queue backing
lkh.ActionBuffer - tact — Easy Artic evaluation runs inside the NoteOn wait window
- keymaps — per-key latency sets the initial scheduling delay