Skip to content

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 voice
  • porta.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 function

Usage 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 while

If 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 for

The 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. PortaFound releases both init and source voices and calls ivls.pass().
  • Polycore.Play.PortaInstant — instant pitch jump. PortaFound snaps 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 exactly time ms 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

  • AllowInitPorta must check product-specific fingered portamento settings. If the product requires portamento only on legato transitions, AllowInitPorta should leave porta_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.
  • stl-legato — portamento depends on legato link detection via legato.found() and vo_src/vo_dest
  • vmc — the tune modbit uses the same VMC modbit infrastructure as fade modulation