speed()

Controls the playback scroll speed of the score.

The speed() cue sets the playback speed multiplier. Speed values are relative to the base speed set in project preferences. Speed changes may be applied instantly or gradually over time.


Syntax

Positional marker form (position watcher — crossing-based, direction-aware):

speed(<multiplier>)
speed(<multiplier>, uid:<id>, loop:<N>)

Keyed trigger form (fires like any other cue; supports ramping):

speed(
  value:<multiplier>,
  add:<offset>,
  dur:<seconds>,
  ease:<easing>,
  uid:<id>,
  revtrig:1
)

The leading bare number is what makes a marker: speed(2) and speed(-1, loop:2) are markers; speed(value:2, dur:3) is a trigger cue.


Parameters


Base Speed Multiplier (Preferences)

The base speed multiplier is set in your project's preferences.json:

{
  "defaultPlaybackSpeed": 0.3
}

All speed() cues multiply against this base value:

Preference Cue Actual Speed
0.3 speed(1) 0.3 (1 × 0.3)
0.3 speed(2) 0.6 (2 × 0.3)
0.3 speed(3) 0.9 (3 × 0.3)
1.0 speed(0.5) 0.5 (0.5 × 1.0)

This allows you to:


Two Types of Speed Cues

1. Position-Based Speed Markers

Simple speed markers embedded in the SVG that apply instantly when the playhead crosses them:

speed(1)
speed(2)
speed(0.5)

The marker DSL can live in the element's id (authored directly in the SVG) or in its data-oscilla attribute (where the DSL editor saves it). Markers:

2. Triggered Speed Cues

Keyed syntax with ramping support:

speed(value:2, dur:3)

These support gradual transitions and are triggered like other cues. Adding dur: or add: is what makes a speed cue trigger-based.

Editing in the DSL editor

The DSL editor exposes all speed options (value, uid, loop, dur, add, revtrig) and writes the correct form automatically: with dur or add set it saves the keyed trigger form, otherwise the positional marker form. Saving rebuilds the watcher map immediately (no reload needed) and resets loop:N counters, so an exhausted bounded loop re-arms.


Position Awareness

When you rewind or seek to a position before a speed marker, the system automatically applies the correct speed for that position:

Position:    0 -------- 5000 -------- 10000 -------- 15000
Markers:     speed(1)   speed(3)      speed(1)
             
Playhead at 7000 → speed = 3 × base
Rewind to 3000   → speed = 1 × base (automatically updated)

This ensures consistent playback regardless of navigation direction.


Reverse Playback (negative speed)

A negative speed value scrolls the score backwards:

speed(-1)              // reverse at normal rate
speed(-0.5)            // reverse at half rate
speed(value:-1, dur:4) // ramp into reverse over 4 seconds

The playhead moves back through the score and clamps at the start. While moving in reverse, speed markers are applied by crossing, in the direction of travel: the last speed() cue the playhead crossed determines the current speed. This makes region loops easy to author:

Position:   ...... A ............ B ......
Markers:        speed(1)      speed(-1)

Forward playback crosses A (speed 1) then B (reverse).
Moving backwards it re-crosses A (forward again) → the
playhead loops the region between A and B indefinitely.

When a score repeats the same marker value (two speed(1) elements, say), give each a uid: so every cue has a distinct identity:

speed(1, uid:loopA)
speed(-1, uid:loopB)

Bounded Loops — loop:N

A speed marker with loop:N applies at most N times, then goes inert — the playhead passes straight through. On a reversing marker, loop:N means N reversals, giving the loop a natural exit:

speed(1, uid:loopA) ......... speed(-1, loop:2, uid:loopB)

The region between A and B plays forward three times and backward twice, then playback continues past B. A full rewind to the start resets the counters so the loop replays.

speed() loops are palindromes — the score physically plays backwards through the region. For classic jump-based repeats (instant cut back to a marker, repeat-barline style) use nav() instead:

nav(scroll@A, repeats:3, uid:rep1)

Cue Triggering in Reverse

By default, cues only fire on forward crossings — reversing over a score does not re-fire its cues. Audio-family region cues (audio, audioPool, audioImpulse, synth) are the exception: they are region-based, so they still sound while the playhead is inside their region from either direction.

A cue can opt in to reverse firing with revtrig. The value picks which edge fires on the reverse crossing:

fade(target:out, dur:2, revtrig:1)
color(vals:[#f00, #0f0], dur:0.5, revtrig:2)

Each reverse crossing re-arms the forward trigger — so a playhead ping-ponging over the cue keeps firing it. Supported on animation cues (rotate, scale, fade, color), text, metro, video, ui, and ext.


Behaviour


Examples

Simple Position Markers (in SVG)

speed(1)

Base playback speed.

speed(0.5)

Half of base speed.

speed(2)

Double base speed.

Triggered Cues with Ramping

speed(value:1.25, dur:3)

Gradually change to 1.25× base speed over 3 seconds.

speed(add:-0.5, dur:2)

Gradually reduce speed multiplier by 0.5 over 2 seconds.

speed(value:2, dur:6, revtrig:1)

Ramp to 2× base speed; also fires if crossed during reverse playback.


Project Setup

preferences.json

{
  "defaultPlaybackSpeed": 0.3,
  "duration_minutes": 10,
  "darkMode": false
}

SVG Score

Place speed markers as elements with appropriate IDs:

<circle id="speed(1)" cx="100" cy="50" r="5" fill="blue"/>
<circle id="speed(3)" cx="5000" cy="50" r="5" fill="red"/>
<circle id="speed(1)" cx="10000" cy="50" r="5" fill="blue"/>

The visual element (circle, rect, path, etc.) marks the position; only the id matters for the cue system.


Server Synchronization

Speed changes are broadcast to all connected clients:

{
  type: "set_speed_multiplier",
  multiplier: 0.9,        // actual speed (cue × base)
  cueSpeed: 3,            // raw cue value
  baseMultiplier: 0.3,    // from preferences
  source: "position_watch"
}

The server maintains the authoritative speed state and syncs it at ~4Hz.


Debugging

Check speed state in browser console:

console.log("baseSpeedMultiplier:", window.baseSpeedMultiplier);
console.log("speedMultiplier:", window.speedMultiplier);
console.log("speedCueMap:", window.speedCueMap);  // position-based markers

Watch speed changes in real-time:

[speedWatch] pos=5000, cue=3, base=0.3, actual=0.90, current=0.30
[speedWatch] ⚡ SPEED CHANGE: 0.30 → 0.90 (cue=3 × base=0.3)

Notes

Demo

See demo-speed — speed changes as values and as ramps, and how they interact with the playhead.

http://localhost:8001/?project=demo-speed

Jumping into a speed region

The speed that applies after a jump — a rehearsal mark, the seek bar, the overview strip, a nav() — is that of the last speed cue at or before where the playhead lands, whether or not it was crossed on the way. A ramp (speed(N, dur:…)) counts at its target, since that is what the region after it runs at: jump from a section at speed 3 straight into one that follows a speed(1, dur:3) and playback continues at 1.

Tip: use ← → or ↑ ↓ to navigate the docs