Skip to content

OSC protocol

blzr exposes an OSC (Open Sound Control) interface that lets external applications control synth parameters in real time. The current transport uses window.postMessage, making it ideal for iframe embeds — no servers, no ports, zero setup.

Embed blzr in an iframe with a preset patch and start sending OSC messages:

<iframe
id="blzr"
src="https://blzr.app?embed=true&patch=voder-voice"
allow="autoplay"
style="width:800px; height:600px; border:none;"
></iframe>
<script>
const blzr = document.getElementById("blzr");
// Wait for blzr to be ready
window.addEventListener("message", (e) => {
if (e.data?.type === "osc/ready") {
console.log("blzr ready:", e.data.patch, e.data["audio-state"]);
// Resume audio (required — browsers block autoplay)
blzr.contentWindow.postMessage({ type: "osc/resume-audio" }, "*");
}
if (e.data?.type === "osc/audio-state-changed") {
console.log("audio state:", e.data["audio-state"]);
}
});
// Set oscillator frequency to 880 Hz
blzr.contentWindow.postMessage({
type: "osc",
address: "/blzr/node/fo-osc/param/frequency",
args: [880]
}, "*");
</script>

Adding ?embed=true to the blzr URL activates embed mode:

  • All UI chrome is hidden (welcome panel, side panels, toolbar)
  • A minimal modeline appears at the bottom with audio toggle and “Open in blzr” link
  • The postMessage listener starts automatically
  • An osc/ready message is sent to the parent window once initialization is complete
ParameterTypeDescription
embed"true"Enable embed mode
patchstringLoad a module preset by name (see available patches)

When patch is set, the module is loaded in expanded mode — child nodes keep their original IDs (e.g., fo-osc, vca-amp) instead of being wrapped in a module container. This gives you stable, predictable OSC addresses.

All OSC addresses follow this pattern:

/blzr/node/{node-id}/param/{param-key}
SegmentDescription
/blzrNamespace prefix (always present)
nodeLiteral keyword
{node-id}The node’s ID in the patch (e.g., fo-osc, vca-amp)
paramLiteral keyword
{param-key}Parameter name (e.g., frequency, gain, cutoff)
AddressEffect
/blzr/node/fo-osc/param/frequencySet oscillator frequency
/blzr/node/fo-osc/param/detuneSet oscillator detune (cents)
/blzr/node/fo-flt/param/frequencySet filter cutoff frequency
/blzr/node/fo-flt/param/QSet filter resonance
/blzr/node/vca-amp/param/gainSet amplifier gain
/blzr/node/vca-const/param/offsetSet constant source offset

Node IDs depend on the loaded patch. When using a module preset via ?patch=, the IDs come from the module spec. You can find them in the Module Library.

For example, the filtered-osc module contains nodes: fo-osc, fo-flt, fo-amp.

Set one parameter value immediately.

iframe.contentWindow.postMessage({
type: "osc",
address: "/blzr/node/fo-osc/param/frequency",
args: [440]
}, "*");

The args array accepts raw numbers or typed OSC values:

// Raw number
args: [440]
// Typed OSC argument (equivalent)
args: [{ type: "f", value: 440.0 }]

Only the first argument is used. Additional arguments are ignored.

Send multiple parameter changes atomically in a single message.

iframe.contentWindow.postMessage({
type: "osc",
bundle: true,
messages: [
{ address: "/blzr/node/fo-osc/param/frequency", args: [440] },
{ address: "/blzr/node/fo-flt/param/frequency", args: [2000] },
{ address: "/blzr/node/fo-amp/param/gain", args: [0.8] }
]
}, "*");

Schedule parameter changes along a timeline with sample-accurate automation. Timetags are in milliseconds relative to “now” (the current AudioContext time when the message is received).

iframe.contentWindow.postMessage({
type: "osc",
bundles: [
{
timetag: 0,
messages: [
{ address: "/blzr/node/fo-osc/param/frequency", args: [440] }
]
},
{
timetag: 500,
messages: [
{ address: "/blzr/node/fo-osc/param/frequency", args: [880] }
]
},
{
timetag: 1000,
messages: [
{ address: "/blzr/node/fo-osc/param/frequency", args: [440] }
]
}
]
}, "*");

This plays 440 Hz → ramps to 880 Hz over 500ms → ramps back to 440 Hz over 500ms.

  1. All bundles arrive in a single postMessage
  2. Messages are grouped by [node-id, param-key]
  3. Each group produces one automation sequence on the Web Audio parameter:
    • cancelScheduledValues at the first timetag
    • setValueAtTime for the first value
    • linearRampToValueAtTime for each subsequent value
  4. Timetags are converted: audioContext.currentTime + (timetag / 1000)

This means multiple parameters can be automated independently and simultaneously — each parameter gets its own automation lane.

The embed lifecycle follows a strict handshake:

Parent blzr iframe
| |
| ←── osc/ready ─────────────────| (patch loaded, listener active)
| |
| ─── osc/resume-audio ────────→ | (user gesture required)
| |
| ←── osc/audio-state-changed ── | (state: "running")
| |
| ─── osc messages ────────────→ | (control parameters)
| ─── osc bundles ─────────────→ |
| ─── osc timed bundles ──────→ |
| |
| ─── osc/suspend-audio ──────→ | (pause)
| ←── osc/audio-state-changed ── | (state: "suspended")
| |
| ─── osc/ping ────────────────→ |
| ←── osc/pong ─────────────────|

Sent once after initialization is complete, patch is loaded, and the listener is active.

{
type: "osc/ready",
patch: "voder-voice", // patch name from URL param, or null
"audio-state": "suspended" // initial AudioContext state
}

Always wait for this message before sending any OSC data.

Sent whenever the AudioContext state changes.

{
type: "osc/audio-state-changed",
"audio-state": "running" // "running" | "suspended" | "closed"
}

Reply to a ping. Use for health checks.

{ type: "osc/pong" }

Resume the AudioContext. Required after page load due to browser autoplay policies — the browser will not start audio without a user gesture. Your host page must call this in response to a user interaction (click, keypress, etc.).

iframe.contentWindow.postMessage({ type: "osc/resume-audio" }, "*");

Suspend the AudioContext (pause all audio processing).

iframe.contentWindow.postMessage({ type: "osc/suspend-audio" }, "*");

Check if the embed is alive. Receives osc/pong in response.

iframe.contentWindow.postMessage({ type: "osc/ping" }, "*");

These module presets can be loaded with ?patch=:

Patch nameDescriptionKey node IDs
vcaVoltage-controlled amplifiervca-amp, vca-const
filtered-oscOscillator → filter → ampfo-osc, fo-flt, fo-amp
lfo-modLFO modulation sourcelfo-osc, lfo-amp, lfo-const
fm-voiceFM synthesis voicefm-carrier, fm-modulator, fm-mod-amp, fm-mod-depth
midi-voiceMIDI-controlled voicemv-osc, mv-flt, mv-amp, mv-adsr
voder-excitationVODER excitation sourceve-buzz-osc, ve-noise, ve-mix
voder-filterbankVODER filter bankvf-band-0 through vf-band-9, vf-merge
voder-masterVODER master controlvm-amp, vm-tilt-flt
voder-voiceComplete VODER voiceAll voder nodes combined

Each module’s full node list and parameter specs are documented in the Module Library.

A minimal host page that loads a filtered oscillator and sweeps the cutoff frequency:

<!DOCTYPE html>
<html>
<head><title>blzr OSC demo</title></head>
<body>
<button id="start">Start</button>
<input id="cutoff" type="range" min="20" max="20000" value="2000" step="1">
<label for="cutoff">Filter cutoff</label>
<iframe
id="blzr"
src="https://blzr.app?embed=true&patch=filtered-osc"
allow="autoplay"
style="width:100%; height:400px; border:1px solid #333;"
></iframe>
<script>
const blzr = document.getElementById("blzr");
const startBtn = document.getElementById("start");
const cutoffSlider = document.getElementById("cutoff");
let ready = false;
window.addEventListener("message", (e) => {
if (e.data?.type === "osc/ready") {
ready = true;
startBtn.disabled = false;
}
});
startBtn.addEventListener("click", () => {
blzr.contentWindow.postMessage({ type: "osc/resume-audio" }, "*");
});
cutoffSlider.addEventListener("input", (e) => {
if (!ready) return;
blzr.contentWindow.postMessage({
type: "osc",
address: "/blzr/node/fo-flt/param/frequency",
args: [Number(e.target.value)]
}, "*");
});
</script>
</body>
</html>

Schedule a simple melody using timed bundles:

function playMelody(iframe) {
const notes = [
{ freq: 261.63, time: 0 }, // C4
{ freq: 329.63, time: 250 }, // E4
{ freq: 392.00, time: 500 }, // G4
{ freq: 523.25, time: 750 }, // C5
{ freq: 392.00, time: 1000 }, // G4
{ freq: 329.63, time: 1250 }, // E4
{ freq: 261.63, time: 1500 }, // C4
];
iframe.contentWindow.postMessage({
type: "osc",
bundles: notes.map(n => ({
timetag: n.time,
messages: [{
address: "/blzr/node/fo-osc/param/frequency",
args: [n.freq]
}]
}))
}, "*");
}

The entire sequence is scheduled ahead of time on the AudioContext timeline, so playback is glitch-free regardless of main thread activity.

The OSC protocol opens blzr to control from any web-based tool:

  • Live coding — Send parameter changes from Strudel, Hydra, or custom REPL environments
  • Sequencers — Drive blzr patches from web-based step sequencers or DAWs
  • Data sonification — Map data streams to synth parameters
  • Interactive installations — Control audio from sensor data, webcam tracking, or touch interfaces
  • Education — Build teaching tools that demonstrate synthesis concepts

Since the transport is postMessage, any JavaScript context that can reference the iframe can control blzr — including browser extensions, Web Workers (via proxy), and other iframes on the same page.

  • Messages with invalid addresses (wrong prefix, missing segments) are silently ignored
  • Messages with unrecognized type values are silently ignored
  • Non-object postMessage data is silently ignored
  • If the target node ID does not exist in the current patch, the parameter set is a no-op
  • Sending OSC messages before osc/ready has no effect — the listener is not yet active
  • Transport: Only postMessage is supported (no WebSocket or UDP yet)
  • Direction: Parameter control is parent → iframe only. There is no way to read parameter values back via OSC
  • Enum parameters: Setting enum params (like oscillator type) via OSC works but is not schedulable with timed bundles — only AudioParam-backed numeric parameters support linearRampToValueAtTime
  • Same-origin: postMessage with "*" target origin works cross-origin, but consider restricting to a specific origin in production for security
  • Autoplay: Browsers require a user gesture before audio can start. Your host page must handle the osc/resume-audio call in response to user interaction