Node Inheritance¶
When you build a sampled instrument, some behaviors need to be almost the same across products -- but not quite. Every instrument plays Kontakt events, but each one chooses different groups, adjusts different parameters, and handles dismissal its own way. You don't want to copy-paste the entire PlayEvent node into every product and modify three lines.
Node inheritance lets you write a reusable template node with extension points, then create product-specific child nodes that fill in only the parts that differ. The template handles the common lifecycle; the child customizes it.
The from Keyword¶
A child node declares its parent with the from keyword:
node MyPlayEvent from Stl.PlayEvent.Template:
cb EventArgs:
{ Customize the event parameters here }
cb EventGroups:
{ Allow specific groups here }
end nodeThe child does not repeat the parent's code. The compiler resolves the inheritance: every callback in the parent is copied into the child, and the child's own callbacks are woven in at designated points.
Virtual Extension Points¶
A parent node marks the places where children can inject code using __VIRTUAL__(Name):
node Stl.PlayEvent.Template:
cb NoteOn:
declare event.note := Voice[self].note
declare event.vel := Voice[self].vel
__VIRTUAL__(EventArgs)
Voice[self].event := play_note(event.note, event.vel, ...)
__VIRTUAL__(EventGroups)
cb NoteOff:
__VIRTUAL__(EventDismiss)
note_off(Voice[self].event)
end nodeEach __VIRTUAL__ is a named slot. When a child node declares cb EventArgs:, its code replaces the __VIRTUAL__(EventArgs) marker in the parent's NoteOn. If the child doesn't implement a particular virtual, the compiler raises an error -- the parent expects it to be filled.
This is how the standard Stl.PlayEvent.Template works. It defines three extension points:
EventArgs-- modify the note, velocity, offset, or duration before the event firesEventGroups-- set which sample groups are allowed for the eventEventDismiss-- run cleanup before the event is stopped on NoteOff
Injecting Parent Code with __PARENT__¶
Sometimes a child wants to run the parent's code and add something before or after it. The __PARENT__ directive injects the parent's callback code at that position:
node MyBrowser from Stl.Browsers.Template(...):
cb Init:
__PARENT__
{ Additional setup after the parent's Init runs }
end nodeWithout __PARENT__, the parent's code for that callback is replaced entirely. With it, you control exactly where the parent's code runs relative to yours.
Forcing Parent Code with __ALWAYS__¶
A parent can mark a callback with __ALWAYS__ to ensure its code always runs in the child, even if the child doesn't explicitly call __PARENT__:
node Stl.Browsers.Template(#name#, #max_items#, ...):
cb Init:
__ALWAYS__
family #name#
declare currentFilter := -1
{ ... browser infrastructure ... }
end family
end nodeWhen the compiler sees __ALWAYS__, it automatically inserts a __PARENT__ at the top of the child's corresponding callback if the child hasn't placed one. This guarantees the browser's core data structures are always created, regardless of what the child adds.
The Stl.Browsers.Template uses __ALWAYS__ extensively -- its Init, Reload, and UICBS callbacks all require the parent infrastructure to be present.
Parameterized Templates¶
Templates can accept compile-time arguments, allowing the same template to create differently-configured instances:
node Stl.Browsers.Template(#name#, #max_items#, #num_tags#, #num_entries#):
cb Init:
__ALWAYS__
family #name#
declare sorted_items[#max_items#]
{ ... }
end family
end nodeA child passes its specific values:
node MySoundBrowser from Stl.Browsers.Template(sound_browser, 500, 8, 12):
{ ... }
end nodeThe compiler substitutes #name# with sound_browser, #max_items# with 500, and so on throughout the parent's code.
Real Examples¶
The IVLS standard library provides several template nodes. Here are the most common:
Stl.PlayEvent.Template -- the event lifecycle template described above. The standard Stl.PlayEvent inherits from it and adds the freeze_mod_on_play voice field. Product-specific event nodes inherit from it to customize group selection and event arguments.
Stl.Browsers.Template -- the filterable sound browser. Products inherit from it with their own item counts, tag configurations, and rendering hooks.
Stl.Scrollbar.Template -- the scrollbar component used by browsers and other scrollable UI elements.
A product PlayEvent node typically looks like this:
node Phr.PlayEvent from Stl.PlayEvent.Template:
cb EventArgs:
event.note := event.note + phr.get_play_range_offset.const()
cb EventGroups:
event.groups[phr.get_sound_id_grp_idx(Voice[self].phr.sound_id)] := TRUE
cb EventDismiss:
{ Nothing extra needed }
end nodeThe child only defines the three virtual callbacks. Everything else -- firing play_note, applying volume/pan/tune, registering with VMC, handling NoteOff -- comes from the parent template.
Constraints¶
Node inheritance has a few rules:
- Single level only. A child can inherit from a parent, but that child cannot itself be a parent. There is no multi-level chain.
- Base nodes are not added to the assembly. Only the child node goes into
IVLS_NODES. The parent template exists solely as a source for inheritance and is removed after compilation. - Functions and Macros callbacks merge. Unlike other callbacks where the child replaces or extends the parent,
FunctionsandMacroscallbacks are always concatenated -- the parent's functions are included first, then the child's.
Node inheritance is the primary way IVLS achieves code reuse across products. The standard library provides the common behavior; your product fills in the specifics.