External Audio — a sound file played by another program
audio(uid:tape, src:"tape.wav", engine:external)
Oscilla draws the file's waveform, but does not play it. Another program plays it: SuperCollider, Pd, Max, or anything else that speaks OSC. That program reports where it is, and the cursor on the waveform shows that position.
Try it: the demo demo-tape-sync. Start one of the engines below, then press play. The playhead and the engine's cursor should cross every click on the waveform together. A gap between them is the error, in plain sight.
Why an external engine
- Multichannel. The engine addresses every output of the audio interface: ambisonics, a speaker ring, the spatialisation already in your patch. A browser gives you stereo.
- Your interface, your latency. The engine runs on JACK, ASIO or Core Audio, at the sample rate and buffer size you set.
- Stability. The sound runs in its own real-time process. If a browser stalls, only the picture hiccups, never the sound.
- Processing. The file feeds the same patch as the rest of the electronics.
- One player, many screens. Every connected browser draws the cursor where the one engine is.
What the browser keeps: nothing to install, and it works on a performer's
phone alone. engine:external is a choice per cue.
Engines
The engines live in the demo project's folder, in demo-tape-sync/sc/ and
demo-tape-sync/pd/, beside its score. They come along when you duplicate the demo into your own
projects.
| Engine | File | Notes |
|---|---|---|
| SuperCollider | oscilla-tape.scd | Several tapes at once, 1–8 channels, every message including rate (varispeed, reverse). Evaluate the whole file. |
| Pd vanilla 0.51+ | oscilla-tape.pd | One tape, streamed from disk with readsf~ (sample-exact at any length). Plays at 1×: rate is ignored. Run Pd at the file's sample rate. |
| Max | (recipe below) | Not yet tested. Build it from the protocol. |
The Pd patch is generated by scripts/gen-oscilla-tape-pd.py: change the
script and run it, rather than editing the patch's numbered connections
by hand.
Both example engines report their position 20 times a second.
Parameters
| Parameter | Description |
|---|---|
engine:external |
Played by the external engine (ext is the same). |
latency: |
Milliseconds the cursor is drawn ahead of the reported position (negative: behind). See Latency. |
speed: |
Sent as rate; it can follow() a signal. |
amp: |
Sent with play, and as amp when it changes. |
loop: |
1 once, N times, 0 for ever: the engine does the looping. |
in: |
Start position in seconds. |
The cue otherwise behaves like any audio(): it starts when the playhead
enters it and stops when it leaves, and hush() and stop cues reach it.
Published signals: <uid>.sec (the engine's position in seconds) and
<uid>.t (0–1 through the file). Other cues can follow the sound's real
position, e.g. follow(tape.t).
The cursor is dashed while no report has arrived, or none for a quarter of a second (the engine stopped, crashed or isn't running). It then stays where it was rather than run on.
The protocol
Oscilla → engine (UDP, Oscilla's --osc-out port, default 57120):
| Address | Arguments |
|---|---|
/oscilla/tape/play |
uid, path, startSec, rate, loop, relPath, amp |
/oscilla/tape/stop |
uid, fadeSec |
/oscilla/tape/seek |
uid, sec |
/oscilla/tape/rate |
uid, rate |
/oscilla/tape/pause |
uid |
/oscilla/tape/resume |
uid |
/oscilla/tape/amp |
uid, amp |
pathis the file's absolute path on the server's machine (resolved inside the project'saudio/folder).relPathis the project-relative path, for an engine on another machine with its own copy.loop: 1 once, N times, 0 for ever.- What an engine must do: only
playandstop. It may ignore the rest.
Engine → Oscilla (UDP, Oscilla's --osc-in port, default 57121):
| Address | Arguments |
|---|---|
/<uid>/pos |
sec, rate |
/<uid>/end |
(none): it has finished, optional |
- What to report: the position in the file, in seconds, and the rate it is playing at (0 while paused).
- How often: about 20 times a second is right. The cursor runs on between reports, so sending more buys no accuracy (see OSC Input, "How often to send").
Several browsers on one score: every browser runs the same cue. The
server passes on the first of each event (play, stop …) for each
sound and drops the repeats that arrive within 300 ms, so the engine hears
each play once, whichever browser sends it.
Max (untested recipe)
The protocol needs only standard objects:
[udpreceive 57120]→[route /oscilla/tape/play /oscilla/tape/stop …].- play:
[unpack s s f f i s f]:- the path →
[prepend open]→[sfplay~ 2] - start →
[* 1000]→seek $1 - then
1to start - keep the uid for the reports
- the path →
- Report on a
[metro 50]: the position (your own count, or a position outlet) as/<uid>/pos sec rate, through[sprintf /%s/pos]and[udpsend 127.0.0.1 57121]. - End: on sfplay~'s end-of-file bang, send
/<uid>/end.
If you build and test one, please send it in: it belongs next to the others.
Latency
A report says what the engine is computing; it is heard after the audio output latency (the engine's block plus the interface buffer, a few ms). The cursor is seen after the display's own lag (a frame, 17 ms at 60 Hz, plus the screen's). Measure once per setup:
- Play the demo's click file and film the screen and the speaker with a phone at 240 fps.
- Compare the frame where the cursor crosses a click on the waveform with the click's onset in the video's sound.
- If the picture is late, set
latency:to the difference (positive); if early, negative.
Wi-Fi adds delay to a performer's tablet that no setting removes. For the tightest work, use a wired client or the laptop's own screen.
Locked to the score
A tape started by the playhead is kept where the playhead says it should be: its start point plus the playhead's distance into the element, in the score's time. It is this window's playhead that counts, the line you see, so the cursor sits on it even while the playhead is still easing into step with the server after a pause or a seek.
- The start-up delay (the
playmessage's trip, the engine starting the sound: a few tens of ms) is put right with one seek in the first second. A jump there is not heard. - A slow drift between the audio interface's clock and the computer's
is played out by nudging the rate, by at most 0.5 % (under 9 cents). An
engine that ignores
rate(the Pd example) is kept in place by seeks alone, when it is more than 50 ms out. - Pausing the score pauses the tape; resuming puts it back where the playhead is. Rewinding, or jumping, stops it. Entering the box again starts it from the top.
- Transport speed (
speed()cues, the speed control) is followed: the tape plays faster or slower with the score.
The cursor always shows what the engine reports, so whatever error is left is in plain sight.
Measured (one machine, SuperCollider, 60 frames a second): after the first two seconds the tape stays within 3.3 ms of the score at the median, 8.4 ms at the 95th percentile and 11 ms at worst, with no bias (−0.9 ms on average). One seek at the start; then about two rate messages a second.
Starting and seeking. The tape starts where the score is in it: entering the box plays it from the top; pressing play with the playhead already inside it, or seeking or jumping into it, plays it from that point. A seek within the box moves the tape with it; a seek out of the box stops it.
Not yet
- Streaming in SuperCollider: the example loads each file into memory.
DiskInwould stream very long or high-channel-count files.
See also
- Timecode Chase -- the other way round: a DAW or film leads, the score follows.
Tip: use ← → or ↑ ↓ to navigate the docs