Skip to content

TACT (The Articulation System)

TACT is the IVLS standard system for managing articulations in sampled instrument products. It defines a pipeline from CSV source data through Python-generated KSP constants and NKA binary data, through a node chain that evaluates, falls back, adjusts volume, and sets ADSR on every voice.

Why TACT Exists

Hard-coding articulation IDs as raw integers is fragile — any reordering in the CSV breaks everything. TACT separates the data definition (CSV) from the runtime logic (KSP constants). Named constants like tact.artic_id.SUS_NORMAL remain stable across CSV reorders. The NKA metadata system keeps per-articulation parameters (volume, ADSR, group indices) out of the code entirely.

Key Concepts

The CSV-to-KSP-to-NKA Pipeline

The entry point is tact-config-importer.py, which reads a CSV and outputs:

  • A KSP const artic_id block with one named constant per articulation
  • A !artic_names[] string array parallel to the const block
  • A _meta.nka file with 64 parameters per articulation (volume, ADSR, group indices, flags)
  • An artic_names.nka for UI display

Generated constants are marked { DO NOT EDIT }. Human-authored code references names (tact.artic_id.SUS_NORMAL), not raw indices. Adding or reordering articulations in the CSV does not break code as long as names remain stable.

Groups are resolved at init time using find_group(), not CSV-supplied indices:

declare grp_stac := find_group("STAC")   // correct: decoupled from group order
declare grp_stac := 14                   // wrong: breaks on any group reorder

The Evaluation Node Chain

Every NoteOn flows through this sequence:

TACT.Modify.EvalArtics
  → TACT.Modify.FallbackArtics
    → ... (routing to playback nodes) ...
      → TACT.Modify.TriggerArticVolume
        → TACT.Sys.Modify.Dynamics
          → TACT.Sys.SetADSR

EvalArtics reads keyswitch state, CC assignments, and product rules to determine Voice[self].tact.main_artic — the primary articulation selection.

FallbackArtics checks whether the selected articulation is available in the current product tier. This is where run-to-legato fallback lives: if tact.artic_id.RUN is selected but time_since_prev_note > 120 ms, the artic is downgraded to LEGATO_BOW or LEGATO_SLUR at speed 3.

TriggerArticVolume applies a per-artic volume offset from the _meta.nka to compensate for sample-level differences between articulations.

Dynamics evaluates dynamic layer selection (PP/MP/MF/F/FF) from velocity or a dedicated CC.

SetADSR applies the articulation's ADSR values. This is always the final TACT node, running after all other modifications.

Smart Attack Overlay Pattern

Smart Attack blends a short articulation overlay with the beginning of a sustained note to produce a realistic bow attack transient. It triggers on SUS_NORMAL articulation when enabled.

Velocity thresholds determine which overlay: - TSS2: vel >= 80 → STACMO, vel >= 50 → STAC - Solo: vel >= 120 → SFORZ_E, vel >= 50 → STAC

Mechanism:

declare overlay_vo := tact.make_overlay_voice()
Voice[overlay_vo].tky.voice_type := tky.events.voice_type.SMART_ATTACK_OVERLAY

// Apply volume offset via modbit, play as oneshot
Modbit.set(mb, smart_attack_volume_offset)
ivls.play.oneshot(overlay_vo, duration)

// Wait, then soft-start the sustain
ivls.wait_ms(short_preroll)
Voice[self].tky.attack_override := TRUE

The overlay voice is tagged with SMART_ATTACK_OVERLAY so legato and other downstream nodes can skip it. The sustain voice gets attack_override to soft-start — the transient comes from the overlay, not the sustain sample's own attack.

The overlay must remain as a playable group even in lower product tiers (ESSENTIALS, FREE) because Smart Attack references the STAC group even when the user cannot explicitly select that articulation.

Adaptive Release

Adaptive release ensures release samples seamlessly match the sustain sample's amplitude at the moment of key release, regardless of how long the note was held. This is covered in its own module: adaptive-release.

Product Tiering

IVLS products can ship in multiple tiers from a single NKI by reading a tier flag from the engine:

product_flag := get_engine_par(ENGINE_PAR_START_CRITERIA_CYCLE_CLASS, ...)
// 0 = FULL, 1 = ESSENTIALS, 2 = FREE

Lower tiers are subsets of the FULL product. The engine conditionally routes around unavailable artics at runtime rather than shipping different samples per tier.

Connections to Other Parts of IVLS

TACT feeds into stl-round-robins (the RR system reads TACT articulation metadata to determine the correct RR count), lookahead (Easy Artic evaluation can run inside the lookahead wait window), and adaptive-release (the per-artic flag in _meta.nka controls whether normal or adaptive release is used).

Patterns and Caveats

  • Never hard-code articulation group indices in code. Always use find_group() at init time.
  • SetADSR must be the final TACT node in the chain. Earlier nodes may change the articulation; SetADSR must see the final selection.
  • The { DO NOT EDIT } sections in generated KSP files are always regenerated from CSV. Do not add manual code there.
  • Fallback logic in FallbackArtics is performance-critical — it runs on every note. Keep it simple.
  • lookahead — Easy Artic evaluation runs inside the lookahead window
  • adaptive-release — the adaptive release subsystem, documented separately
  • stl-round-robins — RR count comes from TACT articulation metadata