Skip to content
Back to skills

Supercollider Live Coding

ASecurity

Build a SuperCollider piece up while it plays - start a sound, change it in place, layer it, and stop it cleanly - using the dsh-supercollider tools (sc_exec, sc_nodes, sc_play, sc_stop-via-sc_exec) and the idioms that make a session survive (Ndef, ProxySpace, Pbind, ~variables, s.freeAll).

  • 10 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 20, 2026
ai-agentsgonodeexpress

Works with

  • cli

Security analysis

A100/100

Scanned October 20, 2026

npx -y skills add vecnode/vn-code --skill supercollider-live-coding --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Supercollider Live Coding?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Supercollider Live Coding
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/vecnode-supercollider-live-coding/badge)](https://www.skillsdirectory.com/skills/vecnode-supercollider-live-coding)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: supercollider-live-coding
description: Build a SuperCollider piece up while it plays - start a sound, change it in place, layer it, and stop it cleanly - using the dsh-supercollider tools (sc_exec, sc_nodes, sc_play, sc_stop-via-sc_exec) and the idioms that make a session survive (Ndef, ProxySpace, Pbind, ~variables, s.freeAll).
whenToUse: Use when asked to make, play, improvise, accompany or live-code music or sound, when a piece should keep changing after it starts, when the user says "make a drone", "play something", "add a beat", "make it darker", "change the pitch while it runs" - and whenever more than one sound must exist at the same time.
---

# Live coding SuperCollider

## A shelf to start from

This package ships sixteen playable instruments — a modal gong, an FM bell, a
plucked string, three drum voices, a pad, a formant voice, a glass choir, an organ,
a grain cloud, a feedback drone, a noise texture, a bitcrusher, a theremin. They are
the fastest way to a piece that already sounds like something:

```
sc_project action=examples            # the catalogue
sc_project action=examples file="1"    # loads 01-modal-gong into the live session
sc_exec   code="~gong.value(62)"       # strike it
sc_exec   code="~gong.value(98)"       # and again, higher
```

Every file defines one `SynthDef` plus a `~helper`, so it slots straight into the
build-it-up workflow below: trigger the helper, layer a second one, then change the
running sound with `sc_nodes` action=set. Start there and edit, rather than
inventing an instrument when the user asks for "a bell" or "something that sounds
like water".

The point of a live session is that **the sound keeps playing while you change
it**. A SuperCollider session driven by `sc_exec` keeps its interpreter alive
between calls, so everything you define stays defined: a `~variable`, an `Ndef`,
a `Pbind` that is running. You build a piece by *adding* to it, not by
re-creating it.

Two rules decide whether a session is workable or a mess:

1. **Give every long-lived sound a name.** `Ndef(\drone, {...})` is a thing you
   can change later. `{ ... }.play` is a thing you can only stop by finding its
   node id.
2. **You are responsible for stopping.** Nothing cleans up after you. A drone
   left running is a drone the user has to kill.

## The three ways to make a sound

| What | Code | When |
|---|---|---|
| One-shot synth | `{ SinOsc.ar(440, 0, 0.2) }.play` | A click, a blip, a test. It plays until it is freed, so give it an envelope or a `doneAction`. |
| A named node you will change | `Ndef(\drone, { LFTri.ar(55) * 0.1 }).play` | Anything that should keep going and be edited. This is the main tool. |
| A pattern that schedules notes | `Pbind(...).play` | Rhythmic material: notes, events, sequences. |

### Start a drone and keep it editable

```supercollider
Ndef(\drone, { |freq = 55, amp = 0.12| LFTri.ar(freq) * amp }).play;
```

Change it while it sounds — no gap, no restart:

```supercollider
Ndef(\drone).set(\freq, 41.2);
Ndef(\drone).set(\amp, 0.06);
```

Change the *definition* while it sounds (the sound is replaced, not stopped and
restarted):

```supercollider
Ndef(\drone, { |freq = 41.2, amp = 0.08| LFSaw.ar([freq, freq * 1.005]) * amp * 0.5 });
```

### Layer a second voice under it

```supercollider
Ndef(\air, { |amp = 0.03| HPF.ar(PinkNoise.ar(amp), 1200) * LFTri.kr(0.07).range(0.2, 1) }).play;
```

### Add rhythmic material with a pattern

```supercollider
Pdef(\pulse,
	Pbind(
		\instrument, \default,
		\freq, Pseq([220, 330, 440, 330], inf),
		\dur, 0.25,
		\amp, 0.08
	)
).play;
```

`Pdef` names the pattern the same way `Ndef` names a synth, which is what makes
it stoppable with `Pdef(\pulse).stop` and re-definable without touching anything
else.

## Changing things in place

`Ndef(...).set(...)` is the cheapest and safest change: nothing is rebuilt, so
nothing clicks. Use it for parameters you will touch often.

```supercollider
Ndef(\drone).set(\freq, 36.7, \amp, 0.05);
```

Fades belong *inside* the definition, because a `set` is instantaneous:

```supercollider
Ndef(\drone, { |freq = 55, amp = 0.1| LFTri.ar(freq) * amp * EnvGen.kr(Env.asr(0.05, 1, 0.4), gate: 1) }).play;
```

For anything you cannot express as `Ndef`, work on the graph directly with
`sc_nodes`: `tree` shows the node ids, `set` changes a control on one, `free`
stops it.

## Stopping, in the right order

```supercollider
Ndef(\air).stop;      // stop one named synth
Pdef(\pulse).stop;    // stop one named pattern
Ndef.clear;           // stop every Ndef
s.freeAll;            // free every node on the server - the panic button
```

- `Ndef(\x).stop` removes that one and leaves everything else alone. Prefer it.
- `s.freeAll` frees *every* synth on the server, including ones another piece
  started — so call it when the user asks to stop everything, not to tidy up.
- `.clear` on a `Pbind`-family object stops the scheduling, but a node it already
  created keeps playing until its envelope ends. `s.freeAll` is what makes a
  rhythm stop now.

Tell the user what is still running. "A drone on 55 Hz, and I stopped the pulse"
is a useful answer; "done" is not.

## Idioms that keep a session workable

**Use `~variables` for things you will re-use.**

```supercollider
(
~root = 55;
Ndef(\drone, { |freq = 55| LFTri.ar(freq) * 0.1 }).play;
~root = 41.2;
Ndef(\drone).set(\freq, ~root);
)
```

Inside a function body a `~variable` reads its value **when the function is
evaluated**, so it is a snapshot rather than a live link: changing `~root` later
does not move a sound that is already playing. Use `.set` for that.

```supercollider
(
~root = 41.2;
Ndef(\drone, { |freq = 55| LFTri.ar(freq) * 0.1 }).play;
Ndef(\drone).set(\freq, ~root * 1.5);
)
```

**Wrap long submissions in one block.** `sc_exec` takes a block, and a `var`
declaration is only legal at the top of one:

```supercollider
(
var freqs = [55, 82.4, 110];
freqs.do { |f, i|
	Ndef(("drone" ++ i).asSymbol, { |amp = 0.05| SinOsc.ar(f) * amp }).play;
};
)
```

**Give every one-shot an envelope with `doneAction: 2`**, so it frees its own node
and does not accumulate:

```supercollider
{ SinOsc.ar(880, 0, 0.2) * EnvGen.kr(Env.perc(0.005, 0.35), doneAction: 2) }.play;
```

**Cap your amplitudes.** `amp` in the region of 0.05–0.2 per voice. Ten voices
at 0.2 is clipping and possibly a startled user — this is their real speakers.

## Recovering from trouble

| Symptom | Cause | Fix |
|---|---|---|
| `sc_exec` answered "the interpreter was restarted" | The snippet never returned: an infinite loop, or `.wait` on the main thread (headless `sclang` deadlocks on it) | Nothing is left of the old session. Re-define what you need. Never `.wait` at the top level. |
| `WARNING: server 'localhost' not running` | The snippet called `.play` before a server was bound | Run `sc_play` (or `sc_server` action=boot) first, then re-run the snippet. |
| A drone will not stop | It was started with `.play` and you do not have its node id | `sc_nodes` action=tree to find it, then `free`; or `s.freeAll` |
| Sound stopped entirely | All nodes were freed, or the server quit | `sc_status` says whether the server still answers |

## A worked example

```supercollider
(
// one drone, one moving filter, one pulse
Ndef(\drone, { |freq = 55, amp = 0.1|
	LFTri.ar(freq) * amp
}).play;

Ndef(\drone).set(\freq, 41.2);

Ndef(\air, {
	HPF.ar(PinkNoise.ar(0.02), LFTri.kr(0.05).range(400, 2400))
}).play;
)
```

```supercollider
// later: take it somewhere else, then stop cleanly
Ndef(\drone).set(\amp, 0.05);
Ndef(\air).stop;
Ndef(\drone).stop;
```

## Check what you actually made

Live coding is the easiest place to fool yourself: a node is playing, the tool
said OK, and the sound is not what you think it is. Before you describe a sound to
the user — especially before you call it a bell, a gong, a pluck or a drum — run
`sc_capture` on it and read the numbers. It reports peak, RMS, clipping, tonality
and the strongest partials, so "it sounds like noise" becomes a specific,
actionable fact instead of an argument.

Two things it catches that nothing else does:

- **The node exists but is not what you asked for.** `/s_new` has no reply, so a
  refused node looks exactly like a working one. `sc_capture` proves sound came out;
  `sc_server action=diagnose` prints the server log that names the refusal.
- **Clipping.** A resonator bank sums its modes, so an instrument that is lovely at
  one velocity squares off at another and is heard as noise. If `peak` is above 0.9,
  lower the gain and measure again.

## What this environment does and does not give you

Some of these are properties of a headless `sclang` driven over pipes, and knowing
them saves an hour of chasing a bug that is not in your code:

- **The interpreter cannot receive server replies.** Anything that waits for
  scsynth to answer — `s.sync`, `Buffer:loadToFloatArray`, `Bus:get`, `/b_getn`
  from the language — will hang or come back empty. This is why measurement is a
  tool (`sc_capture`) that talks OSC itself rather than something you do in
  sclang.
- **`.wait` is illegal on the main thread.** It deadlocks, the call times out, and
  the interpreter is restarted, losing everything. `.wait` is only legal inside a
  `Routine` or a `Task`.
- **Language-side scheduling is not reliable here.** `SystemClock.sched` and a
  `Routine` started with `.play` may never run, because the interpreter only
  advances when it is given work. To sequence sounds over time, schedule them **on
  the server** with a timestamped bundle, which needs no interpreter at all:

  ```supercollider
  // three strikes at 0.2 s, 6.5 s and 14 s: the server plays them, the
  // interpreter is not involved
  Server.default.sendBundle(0.2,  ["/s_new", "gong", 2001, 0, 0, "amp", 1.0]);
  Server.default.sendBundle(6.5,  ["/s_new", "gong", 2002, 0, 0, "amp", 0.85]);
  Server.default.sendBundle(14.0, ["/s_new", "gong", 2003, 0, 0, "amp", 0.7]);
  ```

- **A multi-line submission is written to a file and `interpret`ed by path**, so
  comments and newlines reach the compiler as written. Short single-line snippets
  are normalised instead. Either way, you do not have to rewrite your code to suit
  the transport.
- **A timed-out call restarts the interpreter** and everything it defined is gone.
  Keep snippets quick; put long unrolling in the server, not in a loop.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.