Spatialisation — sound placed in space
A circle drawn in the score is the room. Objects moving inside it are
sound sources: on a path (o2p), by hand (controlXY), or both. Oscilla
works out where each source is — azimuth, elevation, distance — and
sends it over OSC, about 30 times a second. Something else pans the sound:
SuperCollider or Pd (both ready-made, in the demos), or any renderer that
takes positions over OSC.
SuperCollider / Pd ← OSC: where each source is ← Oscilla (the score) ↓ the speakers: a quad, a dome, the IEM CUBE …
Oscilla sends position only; no sound passes through it.
Try it
- Open demo-spatial-quad (4 speakers), demo-spatial-dome-8ch or demo-spatial-dome-24ch (the IEM CUBE). It opens on a page: the circle, the speakers and the sources, which start moving.
- Open the project's
sc/oscilla-spatial.scdin SuperCollider and run the whole file, or openpd/oscilla-spatial.pdin Pd and turn DSP on. Each source plays a test sound (noise pulses, a band each). - No speakers to hand? In the
.scd, set~layout = \stereo: a left–right fold-down for headphones.
Oscilla sends to UDP port 57120 by default (sclang's port; --osc-out
changes it). Pd and sclang cannot both listen on 57120 — run one of them,
or move one.
Writing it
A source on a path — o2p
o2p(path:orbit, uid:src1, dur:10, loop:0, spatial:plan, bounds:room, osc:1)
The object follows orbit; every frame its position on the circle
room is turned into azimuth / elevation / distance and sent to
/oscilla/spat/src1. With trig:touch the performer drags it along
the path instead (a rail: a fixed ring of the dome, say). With drag:1,
clicking it stops it and it can be dragged anywhere in the circle;
clicking again sends it back to the nearest point of its path.
Sources by hand — controlXY
controlXY(bounds:room, uid:mixer, touchpt:[src4, src5], spatial:plan, osc:1, label:true, launcher:false)
Each touchpoint is a source, named by its id: /oscilla/spat/src4,
/oscilla/spat/src5. Handles start where they are drawn and stay
inside the circle. label:true shows each one's azimuth / elevation /
distance.
The keys
| key | ||
|---|---|---|
spatial: |
plan or dome |
turns it on (below) |
bounds: |
an element id | the room: a <circle>, an <ellipse> (its smaller radius) or any shape (the circle in its box). o2p without it uses its path's box |
format: |
aed (default), ae, ad, xyz, aedg, raw |
what is sent (below) |
elev: |
degrees | plan mode only: the source's height (default 0, ear level). On a controlXY with a rotation handle, the handle sets it instead |
osc: |
1 |
send it (the bus channels below are published either way) |
oscAddr: |
an address | instead of /oscilla/spat/<name>. On a controlXY, the handle's id is added: oscAddr:"/room" → /room/src4 |
Plan or dome
spatial:plan — the listener in the middle, seen from above. The
middle is here, the edge is the circle of speakers: distance 0..1.
Height comes from elev:.
spatial:dome — a dome seen from above, for speakers at several
heights. The middle is straight up (90°), the edge is ear level
(0°); a ring halfway out is 45°. A source moving inward rises; one
crossing the middle goes overhead. Distance is always 1.
Front is the top of the circle in both.
What is sent
/oscilla/spat/
| azi | degrees, 0 = front, positive = left (counter-clockwise seen from above), −180..180; behind is ±180 |
| elev | degrees, 0 = ear level, 90 = straight up, −90 = below |
| dist | 0 = the middle .. 1 = the circle (plan); 1 (dome) |
This is the convention of VBAP (SuperCollider's and Pd's), the IEM
plugins, ATK and Ambisonics generally. A renderer that counts azimuth
the other way round (clockwise) needs -azi.
Every argument is a float, always — an azimuth of exactly 90 is sent as
90.0, not as an integer, so a receiver can match on fff.
format: |
arguments |
|---|---|
aed |
azi elev dist |
ae |
azi elev |
ad |
azi dist |
xyz |
x y z — x front, y left, z up; on the unit sphere when dist = 1 |
aedg |
azi elev dist gain (gain = 1 − dist/2: louder nearer the middle) |
raw |
the three 0..1 values of the bus (below) |
On the score: the bus
Each source also publishes three channels, 0..1, that anything in the score can follow:
| channel | 0 | 0.5 | 1 |
|---|---|---|---|
<uid>.azi |
front | behind | front again (counter-clockwise: 0.25 = left) |
<uid>.elev |
below | ear level | straight up |
<uid>.dist |
the middle | the circle |
For a controlXY they are <pad uid>.<handle id>.azi and so on.
scale(uid:ring, sx: follow(src2.dist, 0, 1, 0.9), sy: follow(src2.dist, 0, 1, 0.9)) synth(uid:pad, freq: follow(src1.elev, 200, 800))
The quad demo's dotted ring follows src2's distance; the CUBE demo's
follows src1's elevation. A ring like that drawn over the sources must
not take the pointer (style="pointer-events:none", and put it before
them), or it is in the way of every drag.
The receivers
SuperCollider — sc/oscilla-spatial.scd
Needs sc3-plugins (for VBAP). Every new /oscilla/spat/<name> gets a
source of its own, whatever the name. At the top of the file:
~layout |
\stereo, \quad, \octo, \dome8, \cube24 — or add your own to ~layouts: [azimuth, elevation] per speaker, in output order, azimuth positive to the left |
~out |
the first output |
~useInputs |
true: source n plays audio input n (in order of arrival) — an instrument, or another program's tracks (below) |
~sound |
the test sound; replace it with your own |
Position is smoothed as a direction, so a source crossing behind
(179° → −179°) goes the short way. Distance becomes spread: a source
in the middle sounds from every speaker, one at the edge from the
nearest. ~stopAll.() silences everything.
Pd — pd/oscilla-spatial.pd
Needs the vbap external (Pd: Help → Find externals → vbap; Debian /
Ubuntu: pd-vbap). Each demo's patch has that demo's speakers and its
source names (src1 …) — Pd routes by name, so a new name needs a new
block. The patches are generated: edit
scripts/gen-oscilla-spatial-pd.py and run it, rather than editing the
.pd. Replace a source's [noise~] part with your own sound or an
[adc~].
Ardour (or any DAW) as the sound
The DAW keeps the material; SuperCollider places it. Set ~useInputs = true and connect the DAW's track outputs to SuperCollider's inputs
(JACK / PipeWire: Ardour's track outs → SuperCollider:in_1, in_2 …):
the first source heard plays input 1, and so on. Ardour's own panners
are not driven by Oscilla.
Another renderer
Point oscAddr: at the address it expects, choose format:, and check
which way it counts azimuth.
Speakers
Directions as Oscilla and both receivers use them (azimuth positive = left). Channel numbers are output numbers.
Quad — 1 front-left 45°, 2 front-right −45°, 3 rear-right −135°, 4 rear-left 135°.
Dome, 8 — 1–4 at ear level as the quad, 5–8 the same directions at 45° up.
IEM CUBE, 24 — 12 at ear level, 8 at about 28°, 4 at about 57°, numbered clockwise from the front:
| azimuth / elevation | |
|---|---|
| 1–6 | −0.1/−0.5, −23.7/0, −48.0/0.2, −72.3/0.3, −102.7/0.3, −138.2/−0.2 |
| 7–12 | −179.7/−0.1, 138.1/−0.3, 100.6/0.2, 69.4/0, 44.6/0.1, 21.2/0.1 |
| 13–20 | −22.6/27.9, −67.5/28.0, −113.8/27.6, −157.5/28.4, 156.3/28.5, 112.9/28.2, 65.0/28.1, 22.4/27.4 |
| 21–24 | −46.2/56.3, −132.4/56.9, 132.7/56.7, 42.6/57.1 |
Worked out from the 2008 survey of the room (TU Graz, report
IGMS-2008-05, in the demo's resources/), seen from the middle of the
lower ring at its height. The speakers are 3.3 to 6.3 m away; VBAP does
not correct for that. Check with the room's current setup before a
concert.
Page mode
A spatial score is a surface to play, not a timeline: the demos open in
page mode (defaultViewMode: page, defaultPage: home in
preferences.json, the surface in pages/home.svg). Sources start when
the page opens; nothing scrolls away under the performer's hand. A
spatial surface can also sit in a scrolling score.
Not done
- Distance is spread, not loudness or delay. Nothing simulates a
source coming nearer (gain, air absorption, Doppler);
format:aedggives a renderer a gain to use. - One room per server. A source's running / paused state is kept by
uid on the server, for every project it serves: two projects open on
one server with a
src1each share it. - Several machines. Every client works out the positions and the server passes on one client's OSC; positions are not carried from machine to machine otherwise.
Demos
demo-spatial-quad · demo-spatial-dome-8ch · demo-spatial-dome-24ch
Tip: use ← → or ↑ ↓ to navigate the docs