cue:osc --- Discrete OSC Event Cue
osc(...) sends a single OSC message when triggered.
It is event-based, not continuous. It turns drawn objects into discrete control events, typically consumed by environments like SuperCollider, Pd, or Max.
It works especially well with propagate(), because a simple drawing
can become many individual OSC triggers.
BASIC FORM
osc(addr:
Required:
Key Meaning
addr OSC address name (without leading /)
Example:
osc(addr:voice, pitch:y, env:size)
EVENT BEHAVIOUR
- Sends one OSC message per trigger
- No looping, no animation, no scheduler
- Values are sampled at the moment of triggering
- Cue completes immediately
osc() is a logical cue, not an animation cue.
VISUAL PARAMETER SOURCES (NORMALISED)
All visual sources are normalised to 0–1.
Source Meaning
x Object centre X position
y Object centre Y position
size Max(width, height) mapped to viewport
scale Current visual scale value
rotation 0--360 mapped to 0--1
opacity Computed opacity
fill Numeric colour hash
Example:
osc(addr:voice, pitch:y, amp:size)
Here the Y-axis produces a continuous control pitch, interpreted downstream.
ONLY WHAT IS ON SCREEN — onscreen:
The playhead is a line through the whole height of the score, so zoomed in, a
cue crossing it above or below the visible part still fires. onscreen:1 makes
a cue fire only if its element is on screen at that moment:
osc(addr:star, pitch:0.4, trig:playhead, onscreen:1)
It works on any playhead-fired cue, not only osc(). A cue held back counts as
fired for that pass; a re-entrant one (osc, midi, synth) fires again the next
time it crosses while visible.
The same can be switched on for a whole client: Preferences → UI → "Trigger
only on screen" (stored on this machine, not in the project). It covers the
cues that sound or animate and leaves score control — pause, zoom, nav, page,
stop, speed, stopwatch, metro, ui — to fire wherever its marker is. onscreen:0
exempts a cue from the preference. With it on, each performer's zoom and pan
decide what they play.
FIXED CONTROL PITCH — pitch:<number>
pitch:0.63 sends a fixed 0–1 value in the same form as pitch:y
(pitchType 4), so a receiver built for pitch:y needs no change. Use it when an
object's note should not depend on where it happens to be on screen — zoomed,
panned or rotated:
osc(addr:star, pitch:0.63, env:size, trig:playhead)
SEMANTIC PITCH VALUES
Beyond visual values, osc() supports typed pitch declarations.
Hz (absolute)
osc(addr:voice, pitch:hz(440))
MIDI note number
osc(addr:voice, pitch:midi(60))
Scale degree + octave
osc(addr:voice, pitch:deg(5,3))
Represents:
- scale degree = 5
- absolute octave = 3
The receiver chooses how to interpret the scale.
OPTIONAL ROOT OVERRIDE
You may specify a root in the cue:
osc(addr:voice, pitch:deg(2,4), root:48)
If omitted, the receiver uses its global default (~oscRoot in SC).
PITCH PRIORITY
When multiple forms are present, resolution is:
deg(d,o)(scale-based symbolic pitch)hz()/midi()(absolute pitch)- visual mapping (e.g.,
pitch:y)
Only one pitch representation is used per event.
NOTE EVENTS — TWO APPROACHES
gate: — geometry-driven (recommended for piano-roll scores)
Pair two elements: an on-rect and an off-rect positioned at the note
boundaries. Both use pitch:y so moving them in Inkscape changes pitch.
Scaling the containing group on X adjusts timing without affecting pitch
or note-off alignment.
gate:1 and gate:0 are appended as the last OSC arg. No setTimeout is used.
Generated automatically by scripts/midi-to-svg.js.
dur: — timer-based
When dur: is supplied, a single element sends note-on at trigger time
and note-off after dur beats via setTimeout. Useful for hand-authored
cues where element geometry doesn't represent note duration.
osc(addr:voice, pitch:midi(60), dur:1.5, trig:playhead)
Caveat: scaling the element in Inkscape after the fact does not
update the baked-in dur: value.
AMPLITUDE OVERRIDE (amp:)
amp: accepts a static 0–1 value that overrides the visual size
field in the OSC payload. Useful when notes are generated from MIDI
velocity rather than element geometry.
osc(addr:voice, pitch:midi(64), amp:0.75, dur:0.5, trig:playhead)
TRIGGERS
Key Meaning
trig:auto Send immediately
trig:playhead Fire when playhead intersects
trig:click Fire on user click
prestate:ghostClickable Arm interaction ahead of time
Example:
osc(addr:voice, pitch:deg(0,4), trig:playhead)
INTERACTIVE BUTTON MODE (click:1)
Adding click:1 to any osc() element turns it into a clickable button.
An invisible HTML overlay is placed over the SVG element; clicks trigger
the cue. The SVG element itself provides the visual appearance.
toggle:1 — binary toggle with explicit state
toggle:1 makes the button cycle between two states. A green outline
appears when the button is on. Crucially, unlike a plain trigger, a toggle
button sends the new state explicitly as 0 or 1 (not always 1).
osc(addr:"arp/v0/mute", click:1, toggle:1)
SuperCollider — SET state, do not toggle blindly:
// ✓ Correct — sets state from explicit value OSCdef(\mute, { |msg| ~muted = msg[1] > 0.5; }, '/oscilla/arp/v0/mute');
// ✗ Wrong — will desync if any message is lost OSCdef(\mute, { |msg| ~muted = ~muted.not; // never do this with toggle:1 }, '/oscilla/arp/v0/mute');
Because msg[1] is the authoritative new state, a single missed or
duplicated message will not cause permanent desync. The next click
always resynchronises both sides.
radio:groupName — exclusive selection
Radio buttons form a group where only one can be active at a time. Clicking a radio button activates it and deactivates all others in the same group. The active button gets a green outline.
<rect data-oscilla="osc(addr:"gong/a/lp", click:1, radio:filterA)" .../> <rect data-oscilla="osc(addr:"gong/a/bp", click:1, radio:filterA)" .../> <rect data-oscilla="osc(addr:"gong/a/hp", click:1, radio:filterA)" .../>
Each radio button fires its own OSC address when selected. SC sets the parameter to a fixed value per button (not toggle logic):
OSCdef(\lp, { |m| ~synth.set(\filterType, 0) }, '/oscilla/gong/a/lp'); OSCdef(\bp, { |m| ~synth.set(\filterType, 1) }, '/oscilla/gong/a/bp'); OSCdef(\hp, { |m| ~synth.set(\filterType, 2) }, '/oscilla/gong/a/hp');
labels:'OFF_TEXT,ON_TEXT' — two-state button text
When combined with toggle:1, swaps the text of the next sibling SVG
<text> element each time the button is clicked.
- First value — shown when toggle is off (e.g. voice playing → show "PAUSE")
- Second value — shown when toggle is on (e.g. voice paused → show "PLAY")
<rect data-oscilla="osc(addr:"arp/v0/mute", click:1, toggle:1,
labels:'PAUSE,PLAY')"
x="189" y="928" width="90" height="55" rx="6"
style="fill:#666666"/>
<text x="234" y="959" text-anchor="middle" fill="white">PLAY</text>
Initial state: buttons with labels: initialise in the on state
(toggle on, green outline, showing the on-label). This means the SVG
initial text should be the on-label (e.g. PLAY). The corresponding SC
state should also start in the "on" (e.g. paused) condition so both sides
agree without a first-click resync.
group:groupName — preset group registration
Registers the button with a named o2p fader group so its state is captured and restored when presets are saved and recalled.
osc(addr:"arp/v0/mute", click:1, toggle:1, group:arpFaders, labels:'PAUSE,PLAY')
The group name must match the group: parameter used on the fader
elements (o2p(path:..., group:arpFaders, ...)). On preset recall,
toggle buttons are restored by programmatically clicking them, which also
fires the OSC message to resync SC.
Combined example — full mute button
<!-- Voice 0 MUTE — starts paused (PLAY showing), click to run, click again to pause -->
<rect
data-oscilla="osc(addr:"arp/v0/mute", click:1, toggle:1, group:arpFaders, labels:'PAUSE,PLAY')"
x="189" y="928" width="90" height="55" rx="6"
style="fill:#666666;stroke:#444444;stroke-width:2"/>
<text x="234" y="959" font-size="17px" font-weight="700"
text-anchor="middle" fill="white" pointer-events="none">PLAY</text>
SuperCollider side:
// Start paused (matches button initial on-state)
~players[0].pause;
~muted[0] = true;
OSCdef(\arp_v0_mute, { |msg|
~muted[0] = msg[1] > 0.5;
if (~muted[0]) { ~players[0].pause } { ~players[0].resume };
}, '/oscilla/arp/v0/mute');
Parameter reference
| Parameter | Values | Description |
|---|---|---|
click:1 |
1 |
Make SVG element clickable via invisible overlay |
toggle:1 |
1 |
Binary toggle; sends explicit 0 or 1 to SC |
radio:name |
string | Exclusive radio group name |
labels:'A,B' |
'off_text,on_text' |
Two-state sibling text update |
group:name |
string | Register with o2p preset group |
VISUAL FEEDBACK — flash:
flash:<scale> makes the element swell to <scale> and brighten to full
opacity each time the message is sent, then settle back. It follows the OSC
itself, so it also shows when an animation (e.g. a rotating group) carries the
element into a stopped playhead. flashdur:<beats> sets the length
(default 0.35).
osc(addr:star, pitch:y, env:size, trig:playhead, flash:3) propagate(osc(addr:grain, pitch:y, env:size, trig:playhead, flash:2, flashdur:0.5))
UID (OPTIONAL)
osc(addr:voice, pitch:y, uid:v1)
UID can be used downstream to associate events with voices/players/etc.
OSC OUTPUT FORMAT (AUTHORITATIVE)
Ready-made receivers for Pd, Max and SuperCollider, plus the address table for every other OSC-sending object, are on OSC Output.
osc() sends positional arguments, not keyed pairs — the layout below,
unless args: gives the arguments itself.
YOUR OWN ARGUMENTS — args:[…]
osc(addr:parallaxis/p1/event, args:[5, 0.092], trig:ph1) osc(addr:hit, args:[2, y, size, rand(0,1), "low"])
The message is exactly that list, in that order: numbers as written, rand()
drawn each time it fires, a visual source (x, y, size, scale,
rotation, opacity, fill, width, height, area, aspect) sampled
as it fires, and any other word as a string. gate:/dur: still append the
gate, and pos:1 the frame position.
GENERAL PACKET FORMAT (without dur:)
/oscilla/
PACKET FORMAT WITH dur: (note events)
/oscilla/
Where gate is 1 for note-on and 0 for note-off. All other fields
are identical between the two messages.
Where:
Field Meaning
pitchType 0=none, 1=deg/oct, 2=hz, 3=midi, 4=y-control
pitchA meaning depends on pitchType
pitchB octave (deg mode only)
size 0--1 (overridden by amp: if supplied)
env 0--1
density 0--1 (area-derived)
root optional per-event root override
gate 1=note-on, 0=note-off (present when gate: or dur: set)
SuperCollider decoding for piano-roll scores (pitchType 4 = y-control):
~pitchMin = 24; ~pitchMax = 108; midiNote = (~pitchMin + msg[1] * (~pitchMax - ~pitchMin)).round; gate = msg[msg.size - 1]; // 1 = on, 0 = off
Examples
Control pitch from Y
/oscilla/voice 1 5 3 0.41 0.41 0.12
Absolute pitch
/oscilla/voice 2 440 0 0.18 0.22 0.10
Degree + octave + root override
/oscilla/voice 1 5 3 0.33 0.20 0.15 48
Receivers are expected to decode according to this positional layout.
USE WITH propagate()
propagate( osc( addr:voice, pitch:y, env:size, trig:playhead ) )
Degree constellation examples
propagate( osc( addr:pontalist, pitch:deg(irand(0,11), irand(0,2)), env:size ) )
propagate( osc( addr:pontalist, pitch:deg(${1}, ${2}), env:size, root:48 ), rnd([0,2,4,5,7,9,11]), rnd([0,1,2,3,4,5]) )
DESIGN NOTES
- stateless
- no DOM ID use
- no continuous OSC streaming
- no scale-logic baked into DSL
- musical interpretation lives downstream
- arguments are positional, compact, and receiver-focused
SUMMARY
osc()converts visual gestures into one-shot OSC control events, supporting symbolic, absolute, and control-driven pitch --- while leaving musical semantics to the instrument.
Demo
See demo-osc-send — OSC output from cues, control lanes and animations.
http://localhost:8001/?project=demo-osc-send
Where the object is — pos:
pos:1 adds two values to the end of the message: where the object sits
inside its osc frame, from -1 to 1 about the frame's centre, y up.
/oscilla/star/Lyr [pitchType, pitchA, pitchB, size, env, density, posX, posY]
This is not the screen position sampleVisual() reports, and the difference
is the point. An object triggered as it crosses the playhead is, by
definition, at the playhead when it fires — on screen every one of them
shares the same x, so screen position says nothing about which object
spoke. Measured in the frame, each keeps its own place in the drawing however
the view is scrolled, zoomed or turned.
Which frame you name decides whether the position turns with the drawing.
Name a frame outside a rotate() and the position rotates with it; name one
inside and it holds still. A star map turning overhead can send either the
star's fixed place in the sky or where it has been carried to, by marking one
group or the other — no change to the cue.
A receiver can read the pair as a direction: atan2(posY, posX) for azimuth
and hypot(posX, posY) for distance from the centre. Where a drawing is an
azimuthal projection — a polar star map, a plan of a room — that is already
a bearing and an elevation, so the same message serves a stereo desk and a
24-speaker dome without the score changing. See
Spatialisation.
root: is appended before posX, when both are present.
Tip: use ← → or ↑ ↓ to navigate the docs