Lookahead¶
When a player records a MIDI performance in a DAW, the instrument hears notes one at a time -- each note arrives at the exact moment it's played. But some musical decisions can only be made in retrospect. Is this note the start of a legato phrase, or a standalone articulation? Should the transition be a fast run or a slow portamento? You can't know until you see what comes next.
Lookahead solves this by buffering incoming MIDI events and processing them with a delay, allowing the system to examine future notes before committing to an articulation or legato decision. It turns real-time input into retrospective analysis.
Lookahead is a product subsystem used in orchestral instruments like Tokyo Scoring Strings. It is not part of the core IVLS SDK.
The Action Pool and Buffer¶
Lookahead represents each MIDI event as an Action -- a pool-allocated type with fields for the event type (ON or OFF), timestamp, associated voice, sync offset, and legato linkage:
define Action.MEMBERS := type, timestamp, sync_offset, ...
use_manual_sync, target_owner_action, target_action, ...
voice, legato_speed
declare Action.INIT[] := (lkh.action.OFF, 0, 0, FALSE, -1, -1, -1, -1)Actions are allocated from a type pool (type.CreateCustomPool(Action, 1000000)) and stored in an ActionBuffer -- a ring buffer queue that maintains chronological order:
adt.CreateQueue(ActionBuffer, lkh.MAX_SIZE)When a new Action is created, its Constructor override automatically pushes it into the buffer. When it's deleted, its Destructor removes it. This keeps the buffer synchronized with the Action pool at all times.
Buffered MIDI Processing¶
When a note arrives during lookahead mode, the system creates an ON Action and then busy-waits until the lookahead window is about to expire:
cb NoteOn:
declare on_action := Action.new()
Action[on_action].type := lkh.action.ON
Action[on_action].voice := self
{ Calculate pre-roll: how early this note needs to start }
{ for sample alignment }
Action[on_action].sync_offset := -1 * lkh.calculate_vo_preroll(self)
{ Wait until window_ms minus pre_roll before playing }
declare t_ela_ms := 0
declare pre_roll := lkh.calculate_vo_preroll(self)
while (t_ela_ms < lkh.window_ms - pre_roll ...)
ivls.wait_ms.unsafe(lkh.min_time_chunk)
t_ela_ms := t_ela_ms + lkh.min_time_chunk
pre_roll := lkh.calculate_vo_preroll(self)
end whileThe busy-wait loop recalculates the pre-roll at each tick, because the pre-roll can change as the NoteOff analysis updates the voice's articulation and sync requirements. The minimum time chunk is 10ms, keeping the loop responsive without excessive CPU usage.
NoteOff Backward Search¶
The real analysis happens when a NoteOff arrives. The system creates an OFF Action and then searches backward through the ActionBuffer to find nearby events that might form legato pairs:
cb NoteOff:
{ Look backward from this OFF action }
declare self_act_i := lkh.find_action_in_buffer(off_action)
for act_i := 0 to lkh.ActionBuffer.count - 1
declare chk_i := math.mod_loop((self_act_i - act_i), LKH.MAX_SIZE)
declare chk_action := lkh.ActionBuffer[chk_i]
declare chk_timing_difference := ...
Action[off_action].timestamp - Action[chk_action].timestamp
if chk_timing_difference < (DURATION_SIXTEENTH / 4) / 1000
{ This action is within the legato catch window }
{ Collect ON actions and OFF actions for poly legato parsing }
else
{ Past the window -- stop searching }
act_i := lkh.ActionBuffer.count
end if
end forThe backward search collects all actions within a timing window (a fraction of a sixteenth note) and feeds them into the poly-legato parser. The parser matches NoteOffs to NoteOns to detect legato transitions -- pairs where one note releases as another begins.
When a legato pair is found, the destination Action is linked to the source Action via target_action and target_owner_action fields. The system also selects the appropriate legato speed based on the time gap between the two notes, and determines whether a run legato (very fast transition) should be used.
Parameter Sync¶
During lookahead mode, UI parameter changes can't be applied immediately -- they need to be delayed to match the buffered MIDI timing. The ParamSync node intercepts UI writes and queues them:
node Lookahead.ParamSync:
cb Tokyo.PreValueWrite:
if tokyo.cb_arg.from_ui = TRUE
if CONDITION.LOOKAHEAD_PARAM_READY
tky.ui.LookaheadCtrlQueue.push(par)
tky.ui.LookaheadInputQueue.push(value)
tky.ui.LookaheadTimeQueue.push(ENGINE_UPTIME)
exit
end if
end if
cb LCB:
{ Apply queued parameter changes once their timestamp }
{ has aged past the lookahead window }
while tky.ui.LookaheadCtrlQueue.count > 0 and ...
(ENGINE_UPTIME - tky.ui.LookaheadTimeQueue.peek.fast() >= lkh.window_ms)
{ Pop and apply the parameter change }
end while
end nodeThree parallel queues store the parameter index, value, and timestamp. The listener callback drains the queues, applying changes only after they've aged past the lookahead window. When transport stops, all queued changes are applied immediately.
Three Modes¶
The instrument provides three latency modes, selectable by the user:
Zero Latency -- no lookahead buffering. Notes play immediately with fixed sample offsets. Legato is handled by the standard real-time mono/poly legato nodes. Best for live performance.
Standard (300ms) -- a short lookahead window. Enables basic retrospective legato detection and articulation adjustment. A good balance between latency and intelligence. Uses the same lookahead system but with tokyo.short_lookahead = TRUE, which reduces the window size.
Lookahead (1000ms) -- the full 1-second window. Enables complete retrospective analysis including Easy Artic (velocity/duration-based automatic articulation selection), automatic legato speed detection, and run detection for very fast passages. Requires DAW recording with a 1-second pre-roll.
The mode selection is handled by a diverter node that routes the voice through either the standard or lookahead engine flow:
node Tokyo.Divert.EngineStyle:
cb NotePass:
if TKY.USE_LOOKAHEAD
ivls.reflow_voice(self, tky.flows.lookahead_engine)
else
ivls.reflow_voice(self, tky.flows.standard_engine)
end if
end nodeTransport Handling¶
When the DAW transport stops, all pending actions are cleared and all active voices are released:
function lkh.reset_data()
ivls.release_voice(ivls.omni_voice)
while (lkh.ActionBuffer.count > 0)
Action.delete(lkh.ActionBuffer.peek.fast())
end while
end functionThis prevents stale actions from carrying over into the next playback pass.
Lookahead is one of the more complex production systems in IVLS. It combines the type system (Action pool), data structures (ring buffer queue), custom callbacks (for parameter sync), and the voice processing model (async waits, flow rewriting) into a cohesive system that transforms a real-time MIDI stream into something approaching an offline analysis.