npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

supersonic-scsynth

v0.91.0

Published

SuperCollider scsynth on clockwork, for the browser as an AudioWorklet and natively as a server

Readme

SuperSonic 0.91.0

SuperCollider's scsynth in the browser as an AudioWorklet.

Auto-generated from project docs — see GitHub for full documentation.

Table of Contents


Quick Start

You've installed SuperSonic - now let's make some sound.

We'll create a simple page with two buttons: one to boot the audio engine, and one to trigger a synth.

<button id="boot-btn">boot</button>
<button id="trig-btn">trigger</button>
import { SuperSonic } from "https://unpkg.com/supersonic-scsynth@latest/dist/supersonic.js";

const supersonic = new SuperSonic({
  baseURL: "https://unpkg.com/supersonic-scsynth@latest/dist/",
  coreBaseURL: "https://unpkg.com/supersonic-scsynth-core@latest/",
  synthdefBaseURL: "https://unpkg.com/supersonic-scsynth-synthdefs@latest/synthdefs/",
});

const bootBtn = document.getElementById("boot-btn");
const trigBtn = document.getElementById("trig-btn");

bootBtn.onclick = async () => {
  await supersonic.init();
  await supersonic.loadSynthDef("sonic-pi-prophet");
};

trigBtn.onclick = () => {
  supersonic.send("/s_new", "sonic-pi-prophet", -1, 0, 0, "note", 28, "release", 8, "cutoff", 70);
};

Let's break down what's happening here.

User Interaction

Web browsers have an autoplay policy that prevents websites from making sound without user consent. Audio can only start after a user interaction like a click, tap, or keypress.

This is why we use a boot button - calling init() from a button handler satisfies the browser's autoplay policy and allows audio to begin.

Creating a SuperSonic Instance

const supersonic = new SuperSonic({
  baseURL: "https://unpkg.com/supersonic-scsynth@latest/dist/",
  coreBaseURL: "https://unpkg.com/supersonic-scsynth-core@latest/",
  synthdefBaseURL: "https://unpkg.com/supersonic-scsynth-synthdefs@latest/synthdefs/",
});

This creates a new SuperSonic instance configured to load its files from a CDN. SuperSonic is split into packages, so it needs to know where each one is:

  • baseURL - the client package, supersonic-scsynth, which holds the workers
  • coreBaseURL - the core package, supersonic-scsynth-core, which holds the WebAssembly engine and its AudioWorklet. The client package ships neither, so without this init() fails.
  • synthdefBaseURL - where to find synthdef files when you call loadSynthDef()

The instance doesn't start the audio engine yet - it just sets up the configuration. You can pass additional options to configure the transport mode, debug output, and scsynth engine settings (see API Reference). For anything you deploy, pin a version rather than using @latest (see Installation).

Booting the Engine

await supersonic.init();

Calling init() boots the scsynth audio engine. Behind the scenes this:

  1. Creates an AudioContext and AudioWorklet
  2. Loads the WebAssembly module containing scsynth
  3. Starts scsynth running in a dedicated high-priority audio thread

This is an async operation - the await ensures we don't proceed until the engine is ready.

Loading a Synth Definition

await supersonic.loadSynthDef("sonic-pi-prophet");

Before you can play a synth, you need to send its design to scsynth. This design is called a synth definition (or "synthdef") and is a recipe that describes a synth's audio graph - what oscillators, filters, and effects it uses and how they're connected.

The supersonic-scsynth-synthdefs package holds ready-to-use synthdefs from Sonic Pi. Here we're loading sonic-pi-prophet, a warm polyphonic synth inspired by the Prophet-5.

Note: you can also use SuperCollider's Desktop app to design your own synthdefs and directly import them live at runtime into your SuperSonic session.

Triggering a Synth

supersonic.send("/s_new", "sonic-pi-prophet", -1, 0, 0, "note", 28, "release", 8, "cutoff", 70);

Now for the fun part - making sound! The send() method sends OSC (Open Sound Control) messages to scsynth. The /s_new command creates a new synth instance.

The arguments are:

| | | | |---------------|----------------------|----------------------------------------| | synthdef name | "sonic-pi-prophet" | Which synth to create | | node ID | -1 | Let scsynth assign an ID automatically | | add action | 0 | Add to the head of the target group | | target | 0 | The root group | | params... | "note", 28, ... | Name/value pairs for synth parameters |

The synth parameters control the sound. Here we set note to 28 (a low E), release to 8 seconds, and cutoff to 70 (filter brightness). Each synthdef has its own parameters - see the synthdef documentation for available options.

We used -1 for the node ID here to let scsynth pick one for us. That's fine for fire-and-forget sounds, but if you need to control a synth after creating it - change its parameters, stop it, query it - you need to know its ID. That's where nextNodeId() comes in:

const id = supersonic.nextNodeId();
supersonic.send("/s_new", "sonic-pi-prophet", id, 0, 0, "note", 28, "release", 8, "cutoff", 70);

// Later — change the cutoff while it's playing
supersonic.send("/n_set", id, "cutoff", 100);

nextNodeId() is thread-safe - if you have multiple Web Workers sending OSC (see the Workers Guide), each one can allocate IDs independently, guaranteed unique with no clashes.

Working Example

See example/simple.html for a complete working example you can run locally, or example/simple_metrics.html for the same example with a live metrics dashboard.

Adding a Metrics Dashboard

SuperSonic includes a web component that renders a full metrics dashboard from the schema:

<link rel="stylesheet" href="https://unpkg.com/supersonic-scsynth@latest/dist/metrics-dark.css" />
<script type="module" src="https://unpkg.com/supersonic-scsynth@latest/dist/metrics_component.js"></script>

<clockwork-metrics id="metrics"></clockwork-metrics>

Connect it after boot to start live updates:

bootBtn.onclick = async () => {
  await supersonic.init();
  await supersonic.loadSynthDef("sonic-pi-prophet");
  document.getElementById("metrics").connect(supersonic, { refreshRate: 10 });
};

See Metrics Component for theming, layout control, and customisation.


API Overview

Coordinates WASM, AudioWorklet, SharedArrayBuffer, and IO Workers to run scsynth with low latency inside a web page.

Core

| Member | Description | | ------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | init() | Initialise the engine. | | shutdown() | Shut down the engine. | | destroy() | Destroy the engine completely. | | recover() | Smart recovery — tries a quick resume first, falls back to full reload. | | suspend() | Suspend the AudioContext and stop the drift timer. | | resume() | Quick resume. | | reload() | Full reload — destroys and recreates the worklet and WASM, then restores all previously loaded synthdefs and audio buffers. | | reset() | Shutdown and immediately re-initialise. | | send() | Send an OSC message to the engine. | | sendOSC() | Send pre-encoded OSC bytes to the engine. | | sync() | A barrier: resolves once everything sent before it has reached the engine, in order. | | purge() | Flush all pending scheduled OSC: clears the engine's scheduler and the IN ring so nothing already in flight will fire. |

Asset Loading

| Member | Description | | ----------------------------------- | ------------------------------------------------------------------------- | | loadSynthDef() | Load a SynthDef into the engine. | | loadSynthDefs() | Load several SynthDefs in parallel, each as loadSynthDef would. | | loadSample() | Load an audio sample into a buffer slot. | | sampleInfo() | Get sample metadata (including content hash) without allocating a buffer. |

Events

| Member | Description | | --------------------------------------------- | ------------------------------------------------------------- | | on() | Subscribe to an event. | | off() | Unsubscribe from an event. | | once() | Subscribe to an event once. | | removeAllListeners() | Remove all listeners for an event, or all listeners entirely. |

Node Tree

| Member | Description | | ------------------------------- | ---------------------------------------------------------------------------------------- | | getTree() | Get the node tree in hierarchical format. | | getRawTree() | Get the node tree in flat format with linkage pointers. | | getSnapshot() | Get a diagnostic snapshot with metrics (and their descriptions) and JS heap memory info. |

Metrics

| Member | Description | | ----------------------------------------- | -------------------------------------------------------------- | | getMetrics() | Get current metrics as a named object. | | getMetricsArray() | Get metrics as a flat Uint32Array for zero-allocation reading. | | getMetricsSchema() | Get the metrics schema describing all available metrics. |

Properties

| Member | Description | | ------------------------------------- | --------------------------------------------------------------------- | | initialized | Whether the engine has completed initialisation. | | initializing | Whether init is currently in progress. | | audioContext | The underlying AudioContext. | | node | AudioWorkletNode wrapper for custom audio routing. | | loadedSynthDefs | Map of loaded SynthDef names to their binary data — live, not a copy. |

Advanced

| Member | Description | | --------------------------------------------------- | ----------------------------------------------------------------------------------------- | | getInfo() | Get engine info: sample rate, memory layout, capabilities, and version. | | createOscChannel() | Create an OscChannel for direct worker-to-worklet communication. | | startCapture() | Start capturing what the engine sends to the audio device. | | stopCapture() | Stop capturing and return what was captured. | | getCaptureFrames() | Get number of audio frames captured so far — or, with no capture running, written in all. | | isCaptureEnabled() | Check if audio capture is currently enabled. | | getMaxCaptureDuration() | Get maximum capture duration in seconds: the length of the capture ring. | | setClockOffset() | Set clock offset for multi-system sync (e.g. against an NTP server). |

Advanced

| Member | Description | | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | bufferConstants | Buffer layout constants from the WASM build. | | clock | Session-timeline service: tempo, beat origin, transport, NTP "now." See ClockworkClock for the full API surface. | | gamepad | The gamepad manager, when the gamepad option is on and it came up; otherwise null (gamepadError says why). | | gamepadError | Why gamepad is null although it was asked for: the error its start-up threw. | | midi | The Web MIDI manager, when MIDI is enabled (the midi option, or enableMidi) and came up. | | midiError | Why midi is null although MIDI was asked for: the error its start-up threw. | | mode | Active transport mode ('sab' or 'postMessage'). | | ringBufferBase | Ring buffer base offset in SharedArrayBuffer. | | sharedBuffer | The SharedArrayBuffer (SAB mode) or null (postMessage mode). | | allocSample() | Allocate an empty buffer, as loadSample does for a file: resolves once the engine has it. | | enableMidi() | Bring Web MIDI up after init. | | getEngineState() | Returns the current engine lifecycle state. | | getLoadedBuffers() | Get info about all loaded audio buffers. | | getScope() | Copy the newest frames frames of a ScopeOut2 scope stream. | | getScopeActivations() | How many times a scope slot has gone live. | | getScopes() | List the scope slots in use. | | getSystemReport() | Get a comprehensive system performance report. | | isRunning() | Returns true if the engine has finished booting and is ready to send and receive messages. | | nextNodeId() | Get the next unique node ID. | | request() | Send a message and wait for its reply. | | getRawTreeSchema() | Get schema describing the raw flat node tree structure. | | getScopeSchema() | Scope geometry: slot count, per-slot ring frames, channels. | | getTreeSchema() | Get schema describing the hierarchical node tree structure. |

OscChannel

OscChannel — unified dispatch for sending OSC to the AudioWorklet.

Obtain a channel via SuperSonic.createOscChannel on the main thread, then transfer it to a Web Worker for direct communication with the AudioWorklet.

| Member | Description | | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | mode | Transport mode this channel is using. | | transferable | Serializable config for transferring this channel to a worker via postMessage. | | transferList | Array of transferable objects (MessagePorts) for the postMessage transfer list. | | close() | Close the channel. | | getAndResetMetrics() | Get and reset this channel's local counters (for periodic reporting). | | getMetrics() | Get current metrics. | | nextNodeId() | Get the next unique node ID. | | now() | The engine's clock, in NTP seconds: the time its audio thread has reached, readable on any thread the channel is on — a worker cannot see the AudioContext. | | send() | Send an OSC message: frames it onto the IN ring (SAB) or postMessages it to the worklet (PM). | | fromTransferable() | Reconstruct an OscChannel from data received via postMessage in a worker. |

Example

// Main thread: create and transfer to worker
const channel = sonic.createOscChannel();
myWorker.postMessage(
  { channel: channel.transferable },
  channel.transferList,
);

// Inside worker: reconstruct and send
import { OscChannel } from 'supersonic-scsynth/osc-channel';
const channel = await OscChannel.fromTransferable(event.data.channel);
channel.send(oscBytes);

Constructors

Constructor

new OscChannel(): OscChannel

Returns

OscChannel


Accessors

mode
Get Signature

get mode(): TransportMode

Transport mode this channel is using.

Returns

TransportMode

transferable
Get Signature

get transferable(): OscChannelTransferable

Serializable config for transferring this channel to a worker via postMessage. In postMessage mode each read hands out a fresh range of node IDs and a port for more, so read it once per transfer.

Example
worker.postMessage({ ch: channel.transferable }, channel.transferList);
Returns

OscChannelTransferable

transferList
Get Signature

get transferList(): Transferable[]

Array of transferable objects (MessagePorts) for the postMessage transfer list. Read it after transferable.

Example
worker.postMessage({ ch: channel.transferable }, channel.transferList);
Returns

Transferable[]


Methods

close()

close(): void

Close the channel. In postMessage mode this closes its port; in SAB mode it does nothing.

Returns

void

getAndResetMetrics()

getAndResetMetrics(): OscChannelMetrics

Get and reset this channel's local counters (for periodic reporting).

Returns

OscChannelMetrics

getMetrics()

getMetrics(): OscChannelMetrics

Get current metrics. In SAB mode these are the shared totals for every sender; in postMessage mode, this channel's own.

Returns

OscChannelMetrics

nextNodeId()

nextNodeId(): number

Get the next unique node ID.

Thread-safe — can be called concurrently from multiple workers and no two callers will ever receive the same ID. IDs start at 1000: 0 is the root group and 1–999 are left for the client to assign by hand.

In postMessage mode a worker's channel takes IDs in ranges from the main thread, asking for the next range before it needs it; it throws if a tight loop uses a range up before the next has arrived.

Returns

number

A unique node ID (>= 1000)

now()

now(): number

The engine's clock, in NTP seconds: the time its audio thread has reached, readable on any thread the channel is on — a worker cannot see the AudioContext. The clock bundles are stamped on (SuperSonic.clock's now()), taken from the audio thread itself once a block: it stands still while the audio does (suspended, interrupted), and after a reload it is the new engine's from its first block. 0 until the engine has rendered one.

SAB mode reads the sample clock the audio thread publishes into shared memory; postMessage mode hears it from the audio thread every few blocks and counts on from the last word by the wall clock, a tenth of a second at most.

Returns

number

NTP seconds, or 0

Example
// Inside a worker: schedule half a second ahead on the engine's own clock
channel.send(osc.encodeBundle(channel.now() + 0.5, [["/s_new", "beep", -1, 0, 0]]));
send()

send(oscData): boolean

Send an OSC message: frames it onto the IN ring (SAB) or postMessages it to the worklet (PM). Classification and scheduling happen on the audio thread (the engine's OscIngress + BundleScheduler) — the producer never classifies.

Parameters

| Parameter | Type | Description | | --------- | ------------ | ----------------- | | oscData | Uint8Array | Encoded OSC bytes |

Returns

boolean

true if sent; false if the IN ring had no room (SAB, counted as ringBufferDirectWriteFails) or the channel is closed (PM)

fromTransferable()

static fromTransferable(data): Promise<OscChannel>

Reconstruct an OscChannel from data received via postMessage in a worker. Asynchronous: in SAB mode the worker opens its own instance of the engine module over the shared memory.

Parameters

| Parameter | Type | Description | | --------- | ----------------------------------------------------- | --------------------------------------------------- | | data | OscChannelTransferable | The transferable config from channel.transferable |

Returns

Promise<OscChannel>

Example
// In a Web Worker:
self.onmessage = async (e) => {
  const channel = await OscChannel.fromTransferable(e.data.ch);
  channel.send(oscBytes);
};

Variables


Patterns

Scheduling with OSC Bundles

Full-Precision NTP Timestamps

When scheduling bundles, a plain JavaScript number loses sub-microsecond precision because IEEE 754 float64 only has 52 mantissa bits — not enough for NTP's full 64-bit range. Use a [seconds, fraction] uint32 pair for lossless timestamps:

// Float — easy but loses precision at large NTP values
const ntpTime = SuperSonic.osc.ntpNow();
SuperSonic.osc.encodeBundle(ntpTime + 0.5, [
  ["/s_new", "sonic-pi-beep", 1001, 0, 0],
]);

// Uint32 pair — full 64-bit precision, no float loss
SuperSonic.osc.encodeBundle([3913056000, 2147483648], [
  ["/s_new", "sonic-pi-beep", 1001, 0, 0],
  ["/n_set", 1001, "amp", 0.5],
]);

TimeTag formats accepted by encodeBundle():

| Value | Meaning | |---|---| | 1, null, undefined | Execute immediately | | [seconds, fraction] | NTP uint32 pair — preserves full 64-bit precision | | Number (e.g. 3913056000.5) | NTP float — seconds since 1900 (fractional part encoded) |

Sending OSC from Workers

Avoiding the Main Thread

When you send OSC from the main thread, it has to compete with everything else happening there - DOM updates, event handlers, animations, your application logic. For simple cases this is fine, but if you're running a sequencer, processing MIDI input, or doing other timing-sensitive work, you can end up with jitter or UI stuttering.

Web Workers let you move work off the main thread. The challenge is getting OSC from a worker to the AudioWorklet efficiently. You could send messages back to the main thread and have it forward them - but that adds latency and defeats the purpose of using a worker in the first place.

SuperSonic solves this with OscChannel - a transferable object that gives workers a direct line to the AudioWorklet.

OscChannel

Normally you'd just call supersonic.send() - but workers are separate threads with no access to your main thread's objects. The supersonic instance doesn't exist in the worker's world.

You could have the worker send messages back to the main thread via postMessage, and have the main thread forward them to scsynth. But that puts the main thread back in the middle of every message - defeating the purpose of using a worker.

The solution is to create a direct channel from the worker to scsynth running in the AudioWorklet. SuperSonic gives you an OscChannel for exactly this.

You create an OscChannel with createOscChannel (after init()) and then you need to transfer it to the worker.

Note: Transferring is critical because the core internal comms mechanisms cannot be copied to the worker with a standard postMessage, they must be explicitly transferred. This enables the worker to have full and unique ownership of the newly created OscChannel.

Here's how you create an OscChannel and transfer it to a worker:

// Main thread - create and transfer
const channel = supersonic.createOscChannel();
worker.postMessage(
  { type: "init", channel: channel.transferable },
  channel.transferList
);

The second argument to postMessage is optional - when provided, it lists which objects to transfer rather than copy.

In your worker, handle this as an init message and reconstruct the channel. OscChannel.fromTransferable() returns a promise, so await it:

// Worker thread
import { OscChannel } from "supersonic-scsynth";

let channel = null;

self.onmessage = async (event) => {
  if (event.data.type === "init") {
    channel = await OscChannel.fromTransferable(event.data.channel);

    // Now you can send OSC directly to the AudioWorklet
    channel.send(oscBytes);
  }
};

Using a type field lets you handle different message types cleanly - for example init, start, stop (see the sequencer example below).

Multiple Workers

Each call to createOscChannel() returns a new channel with a unique source ID. This means you can have multiple workers all sending OSC independently - they don't need to coordinate with each other, and their messages can be traced back to their source in metrics and logs.

const sequencerChannel = supersonic.createOscChannel(); // sourceId: 1
const lfoChannel = supersonic.createOscChannel(); // sourceId: 2
const midiChannel = supersonic.createOscChannel(); // sourceId: 3

sequencerWorker.postMessage({ channel: sequencerChannel.transferable }, sequencerChannel.transferList);
lfoWorker.postMessage({ channel: lfoChannel.transferable }, lfoChannel.transferList);
midiWorker.postMessage({ channel: midiChannel.transferable }, midiChannel.transferList);

In SAB mode there is a limit. Each channel a worker reconstructs takes one of the engine's eight client slots, and the main thread, the reply worker and the traffic-logging worker hold three of them, which leaves five. fromTransferable() rejects when none is free.

SAB vs PM Mode

OscChannel works identically in both modes - it detects the active mode and uses the appropriate communication method automatically. Your worker code doesn't need to know or care which mode is active.

See Communication Modes for details on choosing between SAB and postMessage modes.

Message Routing

OscChannel.send() does no routing of its own. It hands the bytes to the engine - into the ring buffer in SAB mode, down a MessagePort in postMessage mode - and the engine's audio thread does the rest. Messages run as they arrive; a bundle with a future timetag waits in the engine's scheduler until its time.

send() returns false when the message could not be sent. In SAB mode that means the ring buffer was full: the message is dropped and counted in the ringBufferDirectWriteFails metric.

OscChannel API

Methods

| Method | Description | | ---------------------- | ------------------------------------------ | | send(oscBytes) | Send OSC bytes to the engine; false if they could not be sent | | nextNodeId() | A unique node ID, from the same allocator as supersonic.nextNodeId() | | now() | The engine's clock in NTP seconds (0 until it has rendered a block) | | getMetrics() | { messagesSent, bytesSent }: in SAB mode the totals for every sender, in postMessage mode this channel's | | close() | Release resources |

Properties

| Property | Description | | --------------- | ------------------------------------------------------------- | | mode | 'sab' or 'postMessage' | | transferable | Data for postMessage transfer | | transferList | Transferable objects array |

Static Methods

| Method | Description | | ----------------------------------- | ----------------------------- | | OscChannel.fromTransferable(data) | Reconstruct channel in worker (returns a promise) |

Example: Sequencer Worker

Here's a worker that runs a simple step sequencer:

// sequencer-worker.js
import { OscChannel, osc } from "supersonic-scsynth";

let channel = null;
let running = false;
let step = 0;
let bpm = 120;

const pattern = [60, 62, 64, 65, 67, 65, 64, 62]; // Notes to play

self.onmessage = async (event) => {
  const { type, data } = event.data;

  if (type === "init") {
    channel = await OscChannel.fromTransferable(data.channel);
    self.postMessage({ type: "ready" });
  } else if (type === "start") {
    running = true;
    step = 0;
    tick();
  } else if (type === "stop") {
    running = false;
  } else if (type === "bpm") {
    bpm = data.bpm;
  }
};

function tick() {
  if (!running || !channel) return;

  const note = pattern[step % pattern.length];
  const msg = osc.encodeMessage("/s_new", [
    "sonic-pi-beep", -1, 0, 0,
    "note", note,
    "amp", 0.5
  ]);
  channel.send(msg);

  step++;
  const msPerBeat = 60000 / bpm;
  setTimeout(tick, msPerBeat / 4); // 16th notes
}

The main thread loads the synthdef, hands the worker its channel, and starts it once the worker says the channel is ready. Starting it straight away would race the await in the worker:

await supersonic.loadSynthDef("sonic-pi-beep");

const worker = new Worker("sequencer-worker.js", { type: "module" });
worker.onmessage = (event) => {
  if (event.data.type === "ready") worker.postMessage({ type: "start" });
};

const channel = supersonic.createOscChannel();
worker.postMessage({ type: "init", data: { channel: channel.transferable } }, channel.transferList);

Lifecycle & Recovery

Setup vs Ready

The setup event fires after init/recover completes, before ready. Async handlers are awaited. Use it for any persistent audio infrastructure that needs to exist on both initial boot and after recovery:

  • Groups — node tree organization
  • FX chains — reverb, filters, compressors
  • Bus routing — synths that read/write to audio buses
  • Persistent synths — always-on nodes like analyzers or mixers
supersonic.on("setup", async () => {
  // Create group structure
  supersonic.send("/g_new", 100, 0, 0); // synths group
  supersonic.send("/g_new", 101, 1, 0); // fx group (after synths)

  // Create FX chain
  supersonic.send("/s_new", "sonic-pi-fx_reverb", 2000, 0, 101,
    "in_bus", 20, "out_bus", 0, "mix", 0.3);

  await supersonic.sync();
});

Why setup instead of ready? When recover() falls through to a full reload(), WASM memory is destroyed and recreated, so all nodes are lost. The setup event lets you rebuild consistently on both initial boot and after recovery, regardless of transport mode. See Communication Modes for more on SAB vs postMessage.

Tab Visibility & Recovery

Use visibilitychange to recover when the user switches back to your tab:

document.addEventListener("visibilitychange", async () => {
  if (!document.hidden) {
    await supersonic.recover();
  }
});

Resume vs Reload

resume() is fast but only works if the worklet is still alive — when the AudioContext is not running, it calls purge() to flush stale scheduled messages, resumes the AudioContext, and resyncs timing. If the context is already running it changes nothing and only reports whether the worklet is alive. reload() is a full restart. When you don't know which is needed, use recover() — or handle it manually:

if (await supersonic.resume()) {
  console.log("Quick resume worked, nodes preserved");
} else {
  console.log("Worklet was killed, need full reload");
  await supersonic.reload();
}

Audio Routing

Connecting Microphone Input

const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const micSource = audioContext.createMediaStreamSource(stream);

// Connect mic to SuperSonic input
micSource.connect(supersonic.node.input);

// Audio flows through scsynth's input buses (bus 2+ by default)
// Use In.ar(2) in a synthdef to read the mic signal

Connecting to an Analyser

const analyser = supersonic.audioContext.createAnalyser();

// Connect SuperSonic output to analyser (in addition to speakers)
supersonic.node.connect(analyser);

// Read frequency data
const data = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(data);

Samples

Loading from Different Sources

// By name (uses sampleBaseURL)
await supersonic.loadSample(0, "loop_amen.flac");

// By full path/URL
await supersonic.loadSample(0, "./custom/my-sample.wav");

// From user-selected file
const file = document.querySelector('input[type="file"]').files[0];
await supersonic.loadSample(0, file);

// From ArrayBuffer (e.g., fetched manually)
const response = await fetch("./audio/sample.wav");
const arrayBuffer = await response.arrayBuffer();
await supersonic.loadSample(0, arrayBuffer);

// Load partial sample (frames 1000-2000)
await supersonic.loadSample(0, "long-sample.flac", 1000, 1000);

Deduplication with sampleInfo()

Use sampleInfo() to get a SHA-256 content hash without allocating a buffer, then check before loading:

const info = await supersonic.sampleInfo("kick.wav");
console.log(info.duration, info.numChannels, info.sampleRate);

// Check for duplicates
const loaded = supersonic.getLoadedBuffers();
if (loaded.some(b => b.hash === info.hash)) {
  console.log("Already loaded, skipping");
} else {
  await supersonic.loadSample(nextBufnum, "kick.wav");
}


Constructor Options

| Property | Type | Description | Required | | ------------------------------------------------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | | activityEvent? | ActivityLineConfig | Length limits for the text the activity events carry. | | | audioContext? | AudioContext | Provide your own AudioContext instead of letting SuperSonic create one. It stays yours: kept across reload(), left open by shutdown(). | | | audioContextOptions? | AudioContextOptions | Options passed to new AudioContext(), over the defaults { latencyHint: 'interactive', sampleRate: 48000 }. Ignored if audioContext is provided. | | | autoConnect? | boolean | Auto-connect the AudioWorkletNode to the AudioContext destination. Default: true. | | | baseURL? | string | Convenience shorthand when all assets (WASM, worklet, workers, synthdefs, samples) are co-located. | Yes* | | bufferGrowIncrement? | number | Bytes the sample buffer pool grows by each time it grows. Default: 32 MB. | | | bufferPoolSize? | number | Initial size of the sample buffer pool in bytes. Default: 4 MB. | | | coreBaseURL? | string | Base URL for the engine: the WASM and the AudioWorklet (the supersonic-scsynth-core package). Defaults to baseURL. | | | debug? | boolean | Enable all debug console logging. Default: false. | | | debugEngine? | boolean | Log the engine's debug output to the console. Default: false. | | | debugOscIn? | boolean | Log incoming OSC messages to console. Default: false. | | | debugOscOut? | boolean | Log outgoing OSC messages to console. Default: false. | | | fetchMaxRetries? | number | Max fetch retries when loading assets. Default: 3. | | | fetchRetryDelay? | number | Base delay between retries in ms (exponential backoff). Default: 1000. | | | gamepad? | boolean | Record<string, unknown> | The Gamepad API: true, or an object of options for the gamepad manager. Default: false. | | | maxBufferMemory? | number | Most the sample buffer pool may grow to, in bytes. The pool grows on demand up to this limit. Default: 768 MB. | | | midi? | boolean | Record<string, unknown> | Web MIDI. Off by default: some browsers ask the user's permission, and a page that never uses MIDI should not ask. true, or an object of options for the MIDI manager. Brought up during init(); to ask later, from the gesture that wants it, use SuperSonic.enableMidi. If it cannot come up the engine boots without it, emits 'error', and SuperSonic.midiError says why. Default: false. | | | mode? | TransportMode | Transport mode. - 'postMessage' — works everywhere, no special headers needed (the default off an isolated page) - 'sab' — lowest latency, requires the Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers (the default on a cross-origin isolated page) See docs/MODES.md for a full comparison of communication modes. | | | pageLifecycle? | object | What the page going away, or out of sight, does to the engine. pagehide: 'shutdown' (default — an engine left running after its page is audio with no page to stop it) or 'none'. hidden: 'keep' (default — a tab behind another plays on) or 'suspend' (and resume when shown). After a 'shutdown', a page restored from the back/forward cache calls init() again. | | | pageLifecycle.hidden? | "keep" | "suspend" | - | | | pageLifecycle.pagehide? | "shutdown" | "none" | - | | | sampleBaseURL? | string | Base URL for audio sample files (used by SuperSonic.loadSample). Defaults to baseURL + 'samples/'. | | | scsynthOptions? | ScsynthOptions | The engine's options (see ScsynthOptions). Validated by the constructor, which throws on a bad one. | | | snapshotIntervalMs? | number | How often to snapshot metrics and the node tree in postMessage mode (ms). Default: 150. | | | synthdefBaseURL? | string | Base URL for synthdef files (used by SuperSonic.loadSynthDef). Defaults to baseURL + 'synthdefs/'. | | | wasmBaseURL? | string | Base URL for WASM files. Defaults to coreBaseURL + 'wasm/'. | | | wasmUrl? | string | Full URL to the WASM binary. Overrides wasmBaseURL. | | | workerBaseURL? | string | Base URL for the worker scripts. Defaults to baseURL + 'workers/'. | | | workletUrl? | string | Full URL to the AudioWorklet script. Overrides coreBaseURL. | |

Required unless workerBaseURL and one of coreBaseURL / wasmBaseURL are provided.


Server Options

| Property | Type | Description | Default | Range | | ----------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------------- | | bufLength? | 128 | Audio buffer length — must be 128 (WebAudio API constraint). | 128 | 128 (fixed) | | loadGraphDefs? | 0 | 1 | Load synth definitions from the synthdef directory at boot: 0 or 1. Default: 0. | 0 | 0–1 | | maxGraphDefs? | number | Max synth definitions. Default: 1024. | 1024 | 1–4194304 | | maxNodes? | number | Max synthesis nodes — synths + groups. Default: 1024. | 1024 | 1–4194304 | | maxWireBufs? | number | Max wire buffers for internal UGen routing. Default: 64. | 64 | 1–4194304 | | memoryLocking? | boolean | Memory locking — not applicable in browser. Default: false. | false | — | | numAudioBusChannels? | number | Audio bus channels for routing between synths. Default: 1024. | 1024 | 1–4194304 | | numBuffers? | number | Max audio buffers (1–65535). Default: 1024. | 1024 | 1–65535 | | numControlBusChannels? | number | Control bus channels for control-rate data. Default: 16384. | 16384 | 1–16777216 | | numInputBusChannels? | number | Input channels read from the device (0 disables input). Default: 2. | 2 | 0+ | | numOutputBusChannels? | number | Output channels opened on the audio graph (1–128). Default: 2. | 2 | 1–128 | | numRGens? | number | Random number generators. Default: 64. | 64 | 1–65536 | | preferredSampleRate? | number | Preferred sample rate: 0, or 8000–384000. The rate the engine runs at is the AudioContext's: set it with audioContextOptions.sampleRate (default 48000), or by passing your own audioContext. | 0 | 0, 8000–384000 | | realTime? | boolean | scsynth's real-time flag. Always false here: the AudioWorklet drives the engine. | false | — | | realTimeMemorySize? | number | Real-time memory pool in KB for synthesis allocations. Default: 8192 (8MB). | 8192 | 1–4194304 | | verbosity? | number | How much the engine prints, 0–4; 0 is quiet. Default: 0. | 0 | 0–4 |


scsynth Command Quick Reference

On the web, send commands using send() which auto-detects types:

supersonic.send("/s_new", "sonic-pi-beep", 1000, 0, 0, "note", 60);

Or directly send OSC bytes that you have already pre-encoded via sendOSC():

supersonic.sendOSC(oscBytes);

The examples in this reference use send(). Natively, any OSC client can send the same messages to the server's port (UDP 57110 by default), and an Erlang or Elixir process can pass the encoded bytes to the NIF's send_osc/1.

| Command | Description | | -------------------------------------------- | -------------------------------------------------- | | Top-Level | | | /notify | Register for node event notifications | | /status | Query server status (UGens, synths, CPU) | | /sync | Wait for async commands to complete | | /version | Query server version info | | /rtMemoryStatus | Query realtime memory usage | | Synth Definitions | | | /d_recv | Load a synthdef from binary data | | /d_free | Free loaded synthdefs by name | | Nodes | | | /n_free | Delete nodes | | /n_run | Turn nodes on or off | | /n_set | Set node control values | | /n_setn | Set sequential control values | | /n_fill | Fill controls with a single value | | /n_map | Map controls to control buses | | /n_mapn | Map sequential controls to control buses | | /n_mapa | Map controls to audio buses | | /n_mapan | Map sequential controls to audio buses | | /n_before | Move node before another | | /n_after | Move node after another | | /n_query | Query node info | | /n_trace | Debug trace node execution | | /n_order | Reorder nodes within a group | | Synths | | | /s_new | Create a new synth | | /s_get | Get synth control values | | /s_getn | Get sequential synth control values | | [/s_noid]