Stl.Portamento.Template¶
Stl.Portamento.Template provides the structural skeleton for pitch glide nodes. It handles legato detection, init-voice setup, and source-voice cleanup, while delegating the glide calculation to two virtual callbacks: AllowInitPorta and PortaFound.
Why This Exists¶
Portamento involves several interlocking concerns: detecting whether this is a first note or a legato transition, managing the tuning modbit that drives the pitch, handling glide-ownership handoffs when multiple notes compete, and capturing the cancelled pitch when a glide is interrupted. Stl.Portamento.Template handles all of this boilerplate, leaving products to implement only the actual glide animation.
Key Concepts¶
Prerequisites¶
Portamento is built on top of the stl-legato system. legato.found(self), legato.get_src_vo(), and legato.get_dest_vo() must be available — Stl.LegatoBase and typically Stl.MonoLegato or Stl.PolyLegato must be in the composition chain.
Per-Voice Fields¶
Stl.Portamento.Core declares:
| Field | Default | Purpose |
|---|---|---|
porta.init_note |
0 |
The pitch from which the glide starts |
porta.init_vo |
-1 |
The initial voice that owns the tune modbit |
porta.modbit |
-1 |
The tune modbit being driven by the glide loop |
porta.run_owner |
-1 |
The voice currently running the glide loop |
Two thread-level arrays track inter-note state:
porta.last_pitch[thread]— records the note played on the previous voiceporta.cancelled_tune[thread]— captures the modbit value when a glide is interrupted
cancelled_tune enables the next glide to continue from the interrupted pitch rather than snapping back to the note's nominal pitch.
Virtual Callbacks¶
| Callback | When it fires | Purpose |
|---|---|---|
AllowInitPorta |
First note (no legato source) | Set porta_on_init := TRUE to trigger a glide even on the initial note |
PortaFound |
When a legato link exists or porta_on_init is TRUE |
Implement the actual glide animation |
PortaFound is not called for plain non-legato, non-init notes. Products only implement glide logic inside it.
Template NoteOn Flow¶
The non-legato path creates a new formal voice via ivls.new_formal_voice(self) and plays it. The legato path transfers modbit state from source to destination and does not play a new voice — it modifies the existing destination voice's tune modbit in-place.
Ownership: seize_tuning_for and still_owned_by¶
Because the glide loop runs asynchronously with ivls.wait_ms(1), multiple competing glide voices can be running simultaneously. Stl.Portamento.Core provides two SDK-level functions to manage ownership and prevent stale glides from colliding:
function portamento.seize_tuning_for(init_vo, vo) -> owner
Voice[init_vo].porta.run_owner := vo
end function
function portamento.still_owned_by(init_vo, vo) -> bool
bool := math.equals(Voice[init_vo].porta.run_owner, vo)
end functionUsage in a glide loop:
// Claim ownership of the glide for this voice
portamento.seize_tuning_for(init_vo, self)
// Inside the loop, check if still owned
while path_on and portamento.still_owned_by(init_vo, self)
Modbit.set(mb, current_tune + stride * t_elapsed)
ivls.wait_ms(1)
end whileIf a newer voice seizes ownership, the old loop exits. The newer glide starts from wherever the modbit currently is.
Glide via Modbit Loop¶
cb PortaFound:
portamento.seize_tuning_for(init_vo, self)
declare target_tune_offset := (new_note - init_note) * 100000
declare current_tune := Modbit[mb].value
declare distance := target_tune_offset - current_tune
declare stride := distance / time
for t_ela := 0 to time
if path_on and portamento.still_owned_by(init_vo, self) = TRUE
Modbit.set(mb, current_tune + stride * t_ela)
ivls.wait_ms(1)
else
t_ela := time // exit
end if
end forThe modbit targets ivls.mod_par.TUNE with type ivls.mod_type.ADD. Pitch is expressed in millicents (semitones × 100,000).
NoteOff: Capturing Interrupted Pitch¶
On NoteOff, the cancelled pitch offset is captured in porta.cancelled_tune[thread]. The next glide from the same thread reads this value in the porta_on_init correction block to begin from the interrupted pitch rather than the note's nominal tuning.
Polycore Implementations¶
Polycore ships three variants that all inherit from Stl.Portamento.Template:
Polycore.Play.MonoSwitch— no glide, plain voice switch.PortaFoundreleases both init and source voices and callsivls.pass().Polycore.Play.PortaInstant— instant pitch jump.PortaFoundsnaps the modbit to target in one step.Polycore.Play.PortaTimed— full timed glide with two sub-modes:- Constant time (
porta_time_constant = TRUE): glide completes in exactlytimems regardless of interval distance - Proportional speed (
porta_time_constant = FALSE): fixed semitones-per-ms; wider intervals take longer
Connections to Other Parts of IVLS¶
Stl.Portamento.Template depends on stl-legato for the legato link fields. The tune modbit it uses is the same vmc modbit system used for fade modulation, but targeting TUNE ADD instead of VOLUME MULT. The ownership API (portamento.seize_tuning_for, portamento.still_owned_by) is defined in Stl.Portamento.Core and provides a reusable mechanism for controlling competing async loops.
Patterns and Caveats¶
AllowInitPortamust check product-specific fingered portamento settings. If the product requires portamento only on legato transitions,AllowInitPortashould leaveporta_on_init := FALSE.- The modbit is attached to the destination voice (
Modbit.add_to_voice(offset, dest)). Releasing the destination voice will automatically clean up the modbit. - The glide loop uses
ivls.wait_ms(1)— one iteration per millisecond. For fast passages this produces many iterations; the ownership check ensures only the current voice's loop advances the modbit.
Related¶
- stl-legato — portamento depends on legato link detection via
legato.found()andvo_src/vo_dest - vmc — the tune modbit uses the same VMC modbit infrastructure as fade modulation