Score

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.

Oscilla Spatialisation
The IEM CUBE demo: 24 speakers (12 + 8 + 4) seen from above, seven sources

Try it

  1. 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.
  2. Open the project's sc/oscilla-spatial.scd in SuperCollider and run the whole file, or open pd/oscilla-spatial.pd in Pd and turn DSP on. Each source plays a test sound (noise pulses, a band each).
  3. 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 elev dist (format:aed, all floats)

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.

A spatial controller drawn in Inkscape, driving the IEM CUBE

Not done


Demos

demo-spatial-quad · demo-spatial-dome-8ch · demo-spatial-dome-24ch

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