nanosynth is a Python package that embeds SuperCollider's libscsynth and supernova synthesis engines in-process using nanobind. It makes it possible to define SynthDefs in Python, compile them to SuperCollider's SCgf binary format, boot the embedded audio engine, and control it via OSC -- all without leaving Python.
-
Self-contained with embedded synthesis engines -- both libscsynth and supernova run in-process as Python extensions (vendored and built from source), no separate process required. supernova is SuperCollider's parallel DSP engine -- it distributes independent synth nodes across CPU cores via
ParGroups, while scsynth runs everything on a single audio thread -
High-level
Serverclass -- boot/quit lifecycle, node ID allocation, SynthDef dispatch, buffer management, OSC reply handling, and convenience methods (synth,group,par_group,free,set). Context manager support andmanaged_synth()/managed_group()/managed_par_group()/managed_buffer()for automatic resource cleanup. Same API for both engines -- just passprotocol=EmbeddedSupernovaProtocol()to use supernova -
Pythonic SynthDef builder -- define UGen graphs using a context manager and operator overloading, compiled to SuperCollider's
SCgfbinary format -
340+ UGens -- oscillators, filters, delays, noise, chaos, granular, demand, dynamics, panning, physical modeling, reverb, phase vocoder, machine listening, stochastic synthesis, and more
-
Rich operator algebra -- 43 binary and 34 unary operators on all UGen signals, including arithmetic, comparison, bitwise, power, trig, pitch conversion (
midicps/cpsmidi), clipping (clip2/fold2/wrap2), and more. Compile-time constant folding and algebraic optimizations -
Non-real-time (NRT) rendering --
Scoreclass for offline audio rendering to WAV/AIFF files without audio hardware. Timestamped OSC commands are serialized and rendered by the embedded engine -
Bus allocation --
Busproxy class withServer.audio_bus(),Server.control_bus(),managed_audio_bus(),managed_control_bus(). Eliminates hardcoded magic bus numbers in effect chains.int()compatible for passing as synth parameters -
Pattern sequencing --
Pbind,Pseq,Prand,Pwhite,Pseries,Pgeom,Pchoose,Pn,Pconst,Rest,Clock, andPlayerfor musical event scheduling. Patterns are reusable iterables;Pbindproduces event streams that drive synth creation with automatic gate release.Clockprovides tempo-driven playback with drift-free scheduling -
MIDI input --
MidiInclass for receiving MIDI from hardware controllers (via embedded RtMidi: CoreMIDI on macOS, ALSA on Linux, WinMM on Windows). Parsed message types (NoteOn,NoteOff,ControlChange,PitchBend) with handler registration. High-level helpers:midi_note_map()for polyphonic note-to-synth mapping,midi_cc_map()for CC-to-parameter control -
NodeProxy / Ndef -- live coding with hot-swappable synth definitions.
NodeProxyowns a private audio bus, a source synth (with ASR envelope for crossfade), and a monitor synth. Swap the source seamlessly while audio plays.Ndefis a global named proxy registry for concise live-coding workflows -
Server recording --
Server.record(path)captures real-time audio output to WAV/AIFF via DiskOut.stop_recording()finalizes the file. Configurable channel count, bus, and format -
Buffer management --
alloc_buffer,read_buffer,write_buffer,free_buffer,zero_buffer,close_buffer, and context managers for automatic cleanup -
Reply handling -- bidirectional OSC communication with the engine: persistent handlers (
on/off), blocking one-shot waits (wait_for_reply), and send-and-wait (send_msg_sync) -
SynthDef graph introspection --
SynthDef.graph()returns a structured DAG ofUGenNode/UGenInputNamedTuples for programmatic traversal.SynthDef.to_dot()exports to Graphviz DOT format -
Envelope system --
Envelopeclass with factory methods (adsr,asr,linen,percussive,triangle) and theEnvGenUGen -
OSC codec -- pure-Python
OscMessage/OscBundleencode/decode with optional C++ acceleration via nanobind -
@synthdefdecorator -- shorthand for defining SynthDefs as plain functions with parameter rate/lag annotations -
Full type safety -- passes
mypy --strict, complete type annotations throughout
-
Python 3.10+
-
uv (package manager)
-
For embedded engines: SuperCollider 3.14.1 (both scsynth and supernova), libsndfile, and PortAudio are vendored and built from source automatically. Audio backend: CoreAudio on macOS, PortAudio (ALSA) on Linux, PortAudio (WASAPI) on Windows -- no system-level audio dependencies beyond the compiler toolchain.
pip install nanosynthOr build from source:
# Editable install with embedded scsynth + supernova
uv pip install -e .
# Build wheel (incremental -- reuses cmake build cache in build/)
make build
# Install without supernova (scsynth only)
uv pip install -e . -C cmake.define.NANOSYNTH_EMBED_SUPERNOVA=OFF
# Install without any audio engine (OSC codec + SynthDef compiler only)
uv pip install -e . -C cmake.define.NANOSYNTH_EMBED_SCSYNTH=OFF -C cmake.define.NANOSYNTH_EMBED_SUPERNOVA=OFFmake demos # scsynth demos
make demos-supernova # supernova demosThe Server class manages the embedded engine lifecycle. Define a SynthDef, boot the server, and play:
import time
from nanosynth import Options, Server
from nanosynth.envelopes import EnvGen, Envelope
from nanosynth.synthdef import DoneAction, SynthDefBuilder
from nanosynth.ugens import Out, Pan2, SinOsc
# Define a SynthDef
with SynthDefBuilder(frequency=440.0, amplitude=0.3) as builder:
sig = SinOsc.ar(frequency=builder["frequency"])
sig = sig * builder["amplitude"]
env = EnvGen.kr(
envelope=Envelope.linen(attack_time=0.1, sustain_time=1.8, release_time=0.1),
done_action=DoneAction.FREE_SYNTH,
)
sig = sig * env
Out.ar(bus=0, source=Pan2.ar(source=sig))
synthdef = builder.build(name="sine")
# Boot the server, send the SynthDef, create a synth
with Server(Options(verbosity=0)) as server:
synthdef.send(server)
time.sleep(0.1)
node = server.synth("sine", frequency=440.0, amplitude=0.3)
print(f"Playing 440 Hz sine (node {node}) for 2 seconds...")
time.sleep(2.0)
server.free(node)
# Engine shuts down automatically on context exitOr use SynthDef.play() to send and create a synth in one call:
with Server() as server:
node = synthdef.play(server, frequency=880.0, amplitude=0.2)
time.sleep(2.0)managed_synth() and managed_group() create nodes that are automatically freed on context exit, even if an exception occurs:
import time
from nanosynth import Server
with Server() as server:
synthdef.send(server)
time.sleep(0.1)
with server.managed_synth("sine", frequency=440.0, amplitude=0.3) as node:
print(f"Playing node {node}...")
time.sleep(2.0)
# node freed automatically here
# Group multiple voices and free them together
with server.managed_group(target=1) as group:
server.synth("sine", target=group, frequency=261.63, amplitude=0.2)
server.synth("sine", target=group, frequency=329.63, amplitude=0.2)
server.synth("sine", target=group, frequency=392.00, amplitude=0.2)
time.sleep(2.0)
# entire group freed hereUse AddAction to control node execution order and audio_bus() to allocate private buses for effect routing -- no more hardcoded magic bus numbers:
import time
from nanosynth import AddAction, Options, Server
with Server(Options(verbosity=0)) as server:
src_def.send(server)
delay_def.send(server)
time.sleep(0.1)
# Allocate a private bus for routing source -> effect
with server.managed_audio_bus(2) as fx_bus:
# Source group executes first, effect group after
src_group = server.group(target=1, action=AddAction.ADD_TO_HEAD)
fx_group = server.group(target=int(src_group), action=AddAction.ADD_AFTER)
# Effect reads from the allocated bus, writes to hardware output
server.synth("comb_delay", target=int(fx_group),
in_bus=float(int(fx_bus)), delay_time=0.375, mix=0.4)
# Source writes to the allocated bus
server.synth("perc_src", target=int(src_group),
out_bus=float(int(fx_bus)), frequency=440.0)
time.sleep(2.0)
# bus freed automaticallyUse supernova instead of scsynth to distribute independent synth nodes across CPU cores. The API is identical -- just pass a different protocol:
import time
from nanosynth import AddAction, EmbeddedSupernovaProtocol, Options, Server
from nanosynth.envelopes import EnvGen, Envelope
from nanosynth.synthdef import DoneAction, SynthDefBuilder
from nanosynth.ugens import Out, Pan2, SinOsc
with SynthDefBuilder(frequency=440.0, amplitude=0.3) as builder:
sig = SinOsc.ar(frequency=builder["frequency"]) * builder["amplitude"]
env = EnvGen.kr(
envelope=Envelope.linen(attack_time=0.5, sustain_time=2.0, release_time=0.5),
done_action=DoneAction.FREE_SYNTH,
)
Out.ar(bus=0, source=Pan2.ar(source=sig * env))
voice = builder.build(name="voice")
# Boot supernova instead of scsynth
with Server(
Options(verbosity=0, load_synthdefs=False),
protocol=EmbeddedSupernovaProtocol(),
) as server:
voice.send(server)
time.sleep(0.1)
# ParGroup: children execute in parallel across CPU cores
par = server.par_group(target=1, action=AddAction.ADD_TO_HEAD)
for freq in [261.63, 329.63, 392.00, 523.25]:
server.synth("voice", target=par, frequency=freq, amplitude=0.15)
time.sleep(3.0)With scsynth, all voices execute sequentially on one audio thread. With supernova, voices inside a ParGroup are distributed across cores -- providing a measurable speedup for dense polyphony and independent effect chains.
Capture real-time audio output to a file:
import time
from nanosynth import Server
with Server() as server:
synthdef.send(server)
time.sleep(0.1)
# Start recording to WAV
server.record("output.wav", header_format="wav", sample_format="int16")
# Play some audio
node = server.synth("sine", frequency=440.0, amplitude=0.3)
time.sleep(2.0)
server.free(node)
# Stop recording -- finalizes the file
server.stop_recording()Recording options include num_channels (defaults to output bus count), bus (which bus to record from), header_format ("wav" or "aiff"), and sample_format ("int16", "int24", "float").
Render audio to a file without real-time audio hardware -- useful for batch processing, testing, and CI pipelines:
from nanosynth import Score, SynthDefBuilder, DoneAction
from nanosynth.envelopes import EnvGen, Envelope
from nanosynth.ugens import Out, Pan2, SinOsc
# Define a SynthDef
with SynthDefBuilder(freq=440.0, amp=0.3) as builder:
sig = SinOsc.ar(frequency=builder["freq"]) * builder["amp"]
env = EnvGen.kr(
envelope=Envelope.percussive(attack_time=0.01, release_time=0.5),
done_action=DoneAction.FREE_SYNTH,
)
Out.ar(bus=0, source=Pan2.ar(source=sig * env))
sd = builder.build(name="sine")
# Build a Score -- a sequence of timestamped OSC commands
score = Score()
score.add_synthdef(0.0, sd)
score.add_synth(0.0, "sine", freq=440.0, amp=0.3)
score.add_synth(0.5, "sine", freq=554.37, amp=0.2)
score.add_synth(1.0, "sine", freq=659.26, amp=0.2)
# Render to a WAV file (no audio hardware needed)
score.render("output.wav", sample_rate=44100, header_format="WAV", sample_format="int16")Replace manual time.sleep() loops with musical patterns. Pbind binds keys to patterns or scalars to produce event streams; Clock drives playback at a given tempo:
import time
from nanosynth import Options, Server
from nanosynth.patterns import Clock, Pbind, Prand, Pseq, Pwhite, Rest
with Server(Options(verbosity=0)) as server:
# (assume a "default" SynthDef with freq, amp, gate params is loaded)
clock = Clock(bpm=140)
# Ascending melody
melody = Pbind(
instrument="default",
freq=Pseq([261.63, 293.66, 329.63, 392.00, 440.00]),
dur=Pseq([0.5, 0.5, 0.5, 0.5, 1.0]),
amp=0.2,
)
melody.play(clock, server)
time.sleep(3.0)
# Randomized melody with rests and dynamic amplitude
rand_melody = Pbind(
instrument="default",
freq=Prand([261.63, 329.63, 392.00, 440.00], repeats=8),
dur=Pseq([0.25, 0.25, Rest(0.5), 0.5], repeats=2),
amp=Pwhite(0.1, 0.25, repeats=8),
)
player = rand_melody.play(clock, server)
time.sleep(4.0)
player.stop()
clock.stop()Available patterns: Pseq (sequential), Prand (random choice), Pwhite (uniform random float), Pseries (arithmetic series), Pgeom (geometric series), Pchoose (weighted random), Pn (repeat N times), Pconst (yield until sum reaches total). Patterns support chaining with | and preview with .take(n).
Connect hardware MIDI controllers. Requires the _midi C extension (built by default with NANOSYNTH_EMBED_MIDI=ON):
import time
from nanosynth import Options, Server
from nanosynth.midi import MidiIn, midi_note_map, midi_cc_map
# List available MIDI ports
print(MidiIn.list_ports())
with Server(Options(verbosity=0)) as server:
# (assume a gated SynthDef "synth" is loaded)
with MidiIn(port=0) as midi:
# Polyphonic note mapping: note-on creates synth, note-off sends gate=0
cleanup_notes = midi_note_map(midi, server, "synth")
# Map CC1 (mod wheel) to a parameter on an existing synth
# cleanup_cc = midi_cc_map(midi, server, some_synth,
# cc_map={1: "cutoff"}, range_min=200.0, range_max=8000.0)
# Or register handlers directly
midi.on_note_on(lambda msg: print(f"Note {msg.note} vel {msg.velocity}"))
midi.on_cc(lambda msg: print(f"CC {msg.control} = {msg.value}"))
input("Press Enter to quit...")
cleanup_notes()Hot-swap synth definitions while audio plays. NodeProxy manages a private audio bus, source synth, and monitor synth -- swapping replaces only the source with a crossfade:
import time
from nanosynth import Options, Server
from nanosynth.proxy import Ndef, NodeProxy
from nanosynth.ugens import LFNoise1, LPF, Saw, SinOsc
with Server(Options(verbosity=0)) as server:
# NodeProxy: manual usage
proxy = NodeProxy(server)
proxy.source = lambda: SinOsc.ar(frequency=440) * 0.2
proxy.play()
time.sleep(2.0)
# Hot-swap to saw wave (crossfades automatically)
proxy.source = lambda: Saw.ar(frequency=330) * 0.15
time.sleep(2.0)
proxy.clear()
# Ndef: concise named proxy registry
Ndef(server, "pad", lambda: SinOsc.ar(frequency=220) * 0.2)
Ndef(server, "pad").play()
time.sleep(1.5)
# Hot-swap via Ndef
Ndef(server, "pad", lambda: Saw.ar(frequency=165) * 0.15)
time.sleep(1.5)
Ndef.clear_all(server)The following examples show SynthDef definitions for various synthesis techniques. Each can be played using the Server class as shown above.
For simpler definitions, use the decorator to skip the builder boilerplate. Parameter rates and lags are specified positionally:
from nanosynth import synthdef, DoneAction
from nanosynth.envelopes import EnvGen, Envelope
from nanosynth.ugens import Out, Pan2, SinOsc
@synthdef("kr", ("kr", 0.5)) # freq: control rate, amp: control rate with 0.5s lag
def my_sine(freq=440.0, amp=0.3):
sig = SinOsc.ar(frequency=freq)
env = EnvGen.kr(
envelope=Envelope.percussive(attack_time=0.01, release_time=1.0),
done_action=DoneAction.FREE_SYNTH,
)
Out.ar(bus=0, source=Pan2.ar(source=sig * amp * env))
scgf_bytes = my_sine.compile() # my_sine is a SynthDef instancefrom nanosynth import SynthDefBuilder, DoneAction
from nanosynth.envelopes import EnvGen, Envelope
from nanosynth.ugens import LFNoise1, LPF, Out, Pan2, RLPF, Saw, WhiteNoise, XLine
# Saw wave through a sweeping low-pass filter
with SynthDefBuilder(frequency=110.0, amplitude=0.4) as builder:
sig = Saw.ar(frequency=builder["frequency"])
cutoff = XLine.kr(start=8000.0, stop=200.0, duration=3.0,
done_action=DoneAction.FREE_SYNTH)
sig = LPF.ar(source=sig, frequency=cutoff)
sig = sig * builder["amplitude"]
Out.ar(bus=0, source=Pan2.ar(source=sig))
filtered_saw = builder.build(name="filtered_saw")
# White noise through a resonant LPF with LFO-modulated cutoff
with SynthDefBuilder(amplitude=0.15) as builder:
sig = WhiteNoise.ar()
lfo = LFNoise1.kr(frequency=4.0)
cutoff = lfo * 1900.0 + 2100.0 # map [-1,1] to [200, 4000]
sig = RLPF.ar(source=sig, frequency=cutoff, reciprocal_of_q=0.1)
env = EnvGen.kr(
envelope=Envelope.linen(attack_time=0.5, sustain_time=2.0, release_time=0.5),
done_action=DoneAction.FREE_SYNTH,
)
sig = sig * env * builder["amplitude"]
Out.ar(bus=0, source=Pan2.ar(source=sig))
resonant_noise = builder.build(name="resonant_noise")from nanosynth import SynthDefBuilder, DoneAction
from nanosynth.envelopes import EnvGen, Envelope
from nanosynth.ugens import Out, Pan2, SinOsc
with SynthDefBuilder(
carrier_freq=440.0, mod_ratio=2.0, mod_index=3.0,
amplitude=0.3, gate=1.0,
) as builder:
mod_freq = builder["carrier_freq"] * builder["mod_ratio"]
modulator = SinOsc.ar(frequency=mod_freq) * builder["mod_index"] * mod_freq
carrier = SinOsc.ar(frequency=builder["carrier_freq"] + modulator)
env = EnvGen.kr(
envelope=Envelope.adsr(
attack_time=0.01, decay_time=0.1, sustain=0.7, release_time=0.3,
),
gate=builder["gate"],
done_action=DoneAction.FREE_SYNTH,
)
sig = carrier * env * builder["amplitude"]
Out.ar(bus=0, source=Pan2.ar(source=sig))
fm_synth = builder.build(name="fm_synth")Sum harmonics with decreasing amplitude to build a rich tone from pure sine partials:
from nanosynth import SynthDefBuilder, DoneAction
from nanosynth.envelopes import EnvGen, Envelope
from nanosynth.ugens import Out, Pan2, SinOsc
with SynthDefBuilder(frequency=200.0, amplitude=0.3) as builder:
sig = SinOsc.ar(frequency=builder["frequency"]) * 1.0
sig = sig + SinOsc.ar(frequency=builder["frequency"] * 2.0) * 0.5
sig = sig + SinOsc.ar(frequency=builder["frequency"] * 3.0) * 0.33
sig = sig + SinOsc.ar(frequency=builder["frequency"] * 4.0) * 0.25
sig = sig + SinOsc.ar(frequency=builder["frequency"] * 5.0) * 0.2
sig = sig * 0.3 # normalize
env = EnvGen.kr(
envelope=Envelope.percussive(attack_time=0.01, release_time=2.0),
done_action=DoneAction.FREE_SYNTH,
)
sig = sig * env * builder["amplitude"]
Out.ar(bus=0, source=Pan2.ar(source=sig))
additive = builder.build(name="additive")Karplus-Strong style plucked string using the Pluck UGen:
from nanosynth import SynthDefBuilder, DoneAction
from nanosynth.envelopes import EnvGen, Envelope
from nanosynth.ugens import Dust, Out, Pan2, Pluck, WhiteNoise
with SynthDefBuilder(frequency=440.0, amplitude=0.5, decay=5.0) as builder:
trig = Dust.ar(density=1.0)
sig = Pluck.ar(
source=WhiteNoise.ar(),
trigger=trig,
maximum_delay_time=1.0 / 100.0,
delay_time=1.0 / builder["frequency"],
decay_time=builder["decay"],
coefficient=0.3,
)
sig = sig * builder["amplitude"]
Out.ar(bus=0, source=Pan2.ar(source=sig))
pluck = builder.build(name="plucked_string")Process a dry signal through comb delay and FreeVerb:
from nanosynth import SynthDefBuilder, DoneAction
from nanosynth.envelopes import EnvGen, Envelope
from nanosynth.ugens import CombC, FreeVerb, Out, Pan2, Saw, LPF
with SynthDefBuilder(frequency=220.0, amplitude=0.3) as builder:
# Dry signal: filtered saw
dry = Saw.ar(frequency=builder["frequency"])
dry = LPF.ar(source=dry, frequency=2000.0)
env = EnvGen.kr(
envelope=Envelope.percussive(attack_time=0.005, release_time=0.3),
done_action=DoneAction.FREE_SYNTH,
)
dry = dry * env * builder["amplitude"]
# Comb delay for metallic echo
sig = CombC.ar(
source=dry,
maximum_delay_time=0.2,
delay_time=0.15,
decay_time=2.0,
)
# Reverb
sig = FreeVerb.ar(source=dry + sig, mix=0.4, room_size=0.8, damping=0.3)
Out.ar(bus=0, source=Pan2.ar(source=sig))
delay_reverb = builder.build(name="delay_reverb")Use demand UGens to sequence pitches without host-side scheduling:
from nanosynth import SynthDefBuilder, DoneAction
from nanosynth.envelopes import EnvGen, Envelope
from nanosynth.ugens import Duty, Dseq, Out, Pan2, SinOsc
with SynthDefBuilder(amplitude=0.3) as builder:
# Dseq loops a sequence of MIDI-note frequencies at demand rate
freq_pattern = Dseq.dr(
repeats=4,
sequence=[261.63, 293.66, 329.63, 392.00, 440.00, 392.00, 329.63, 293.66],
)
# Duty reads from the demand pattern every 0.25 seconds
freq = Duty.kr(duration=0.25, level=freq_pattern)
sig = SinOsc.ar(frequency=freq) * builder["amplitude"]
env = EnvGen.kr(
envelope=Envelope.linen(attack_time=0.01, sustain_time=7.9, release_time=0.1),
done_action=DoneAction.FREE_SYNTH,
)
sig = sig * env
Out.ar(bus=0, source=Pan2.ar(source=sig))
sequencer = builder.build(name="sequencer")Multiply two signals together for classic ring modulation:
from nanosynth import SynthDefBuilder, DoneAction
from nanosynth.envelopes import EnvGen, Envelope
from nanosynth.ugens import LFTri, Out, Pan2, SinOsc
with SynthDefBuilder(
carrier_freq=440.0, mod_freq=60.0, amplitude=0.3,
) as builder:
carrier = SinOsc.ar(frequency=builder["carrier_freq"])
modulator = LFTri.ar(frequency=builder["mod_freq"])
sig = carrier * modulator # ring mod = simple multiplication
env = EnvGen.kr(
envelope=Envelope.linen(attack_time=0.05, sustain_time=2.0, release_time=0.5),
done_action=DoneAction.FREE_SYNTH,
)
sig = sig * env * builder["amplitude"]
Out.ar(bus=0, source=Pan2.ar(source=sig))
ring_mod = builder.build(name="ring_mod")Fatten a sound by panning two slightly detuned oscillators:
from nanosynth import SynthDefBuilder, DoneAction
from nanosynth.envelopes import EnvGen, Envelope
from nanosynth.ugens import LPF, Out, Saw
with SynthDefBuilder(frequency=110.0, detune=0.5, amplitude=0.4) as builder:
left = Saw.ar(frequency=builder["frequency"] - builder["detune"])
right = Saw.ar(frequency=builder["frequency"] + builder["detune"])
left = LPF.ar(source=left, frequency=3000.0)
right = LPF.ar(source=right, frequency=3000.0)
env = EnvGen.kr(
envelope=Envelope.linen(attack_time=0.1, sustain_time=2.0, release_time=0.5),
done_action=DoneAction.FREE_SYNTH,
)
left = left * env * builder["amplitude"]
right = right * env * builder["amplitude"]
Out.ar(bus=0, source=[left, right]) # direct stereo output
stereo_saw = builder.build(name="stereo_saw")Apply compression to a signal using Compander:
from nanosynth import SynthDefBuilder, DoneAction
from nanosynth.envelopes import EnvGen, Envelope
from nanosynth.ugens import Compander, Dust, Out, Pan2, Ringz
with SynthDefBuilder(amplitude=0.5) as builder:
# Sparse impulses through a resonant filter -- wide dynamic range
sig = Ringz.ar(
source=Dust.ar(density=3.0),
frequency=2000.0,
decay_time=0.2,
)
# Compress: bring quiet parts up, loud parts down
sig = Compander.ar(
source=sig,
control=sig,
threshold=0.3,
slope_below=2.0, # expand below threshold
slope_above=0.5, # compress above threshold
clamp_time=0.01,
relax_time=0.1,
)
env = EnvGen.kr(
envelope=Envelope.linen(attack_time=0.01, sustain_time=3.0, release_time=0.5),
done_action=DoneAction.FREE_SYNTH,
)
sig = sig * env * builder["amplitude"]
Out.ar(bus=0, source=Pan2.ar(source=sig))
compressed = builder.build(name="compressed")SynthDef graphs can be compiled to SuperCollider's SCgf binary format without booting the audio engine -- useful for generating SynthDefs for any SuperCollider server:
from nanosynth import SynthDefBuilder, compile_synthdefs
from nanosynth.ugens import Out, Pan2, SinOsc
with SynthDefBuilder(frequency=440.0) as builder:
Out.ar(bus=0, source=Pan2.ar(source=SinOsc.ar(frequency=builder["frequency"])))
synthdef = builder.build(name="sine")
scgf_bytes = synthdef.compile()
# Or compile multiple SynthDefs into a single SCgf blob
blob = compile_synthdefs(synthdef1, synthdef2, synthdef3)SynthDef.dump_ugens() prints a human-readable UGen graph (like SuperCollider's SynthDef.dumpUGens):
print(synthdef.dump_ugens())
# SynthDef: sine
# 0: Control.kr - frequency, amplitude
# 1: SinOsc.ar(frequency: Control[0], phase: 0.0)
# 2: BinaryOpUGen.ar(MULTIPLICATION, a: SinOsc[0], b: Control[1])
# ...Walk the UGen graph programmatically or export to Graphviz DOT format:
# Structured graph -- returns UGenNode/UGenInput NamedTuples
graph = sd.graph()
for node in graph.nodes:
print(f"{node.node_index}: {node.type_name}.{node.rate}")
for inp in node.inputs:
if inp.source is not None:
print(f" {inp.name} <- {inp.source.type_name}[{inp.output_index}]")
else:
print(f" {inp.name} = {inp.value}")
# Export to Graphviz DOT
dot = sd.to_dot(rankdir="LR")
print(dot) # pipe to `dot -Tpng -o graph.png`The OSC module works standalone for any OSC communication needs:
from nanosynth import OscMessage, OscBundle
# Encode
msg = OscMessage("/s_new", "sine", 1000, 0, 1, "frequency", 440.0)
datagram = msg.to_datagram()
# Decode
decoded = OscMessage.from_datagram(datagram)
assert decoded == msg
# Bundles
bundle = OscBundle(
timestamp=None, # immediately
contents=[
OscMessage("/s_new", "sine", 1000, 0, 1),
OscMessage("/n_set", 1000, "frequency", 880.0),
],
)
bundle_bytes = bundle.to_datagram()Organized by category:
| Category | UGens |
|---|---|
| Oscillators | SinOsc, Saw, Pulse, Blip, Klank, LFSaw, LFPulse, LFTri, LFCub, LFPar, VarSaw, SyncSaw, Impulse, FSinOsc, LFGauss, Vibrato, Osc, OscN, COsc, VOsc, VOsc3 |
| Filters | LPF, HPF, BPF, BRF, RLPF, RHPF, MoogFF, Lag, Lag2, Lag3, LagUD, Lag2UD, Lag3UD, Ramp, Decay, Decay2, Ringz, Formlet, Median, LeakDC, OnePole, OneZero, TwoPole, TwoZero, APF, FOS, SOS, MidEQ, Slew, Slope, Integrator, DetectSilence, Changed |
| BEQ Filters | BLowPass, BHiPass, BBandPass, BBandStop, BAllPass, BLowShelf, BHiShelf, BPeakEQ, BLowCut, BHiCut |
| Noise | WhiteNoise, PinkNoise, BrownNoise, GrayNoise, ClipNoise, Dust, Dust2, Crackle, LFNoise0, LFNoise1, LFNoise2, LFDNoise0, LFDNoise1, LFDNoise3, LFClipNoise, LFDClipNoise, Logistic |
| Stochastic | Gendy1, Gendy2, Gendy3 |
| Delays | DelayN, DelayL, DelayC, Delay1, Delay2, CombN, CombL, CombC, AllpassN, AllpassL, AllpassC, BufDelayN, BufDelayL, BufDelayC, BufCombN, BufCombL, BufCombC, BufAllpassN, BufAllpassL, BufAllpassC, DelTapRd, DelTapWr |
| Envelopes | EnvGen, Linen, Done, Free, FreeSelf, FreeSelfWhenDone, Pause, PauseSelf, PauseSelfWhenDone |
| Panning | Pan2, Pan4, PanAz, PanB, PanB2, BiPanB2, Balance2, Rotate2, DecodeB2, XFade2, Splay |
| Demand | Dseq, Dser, Dseries, Drand, Dxrand, Dshuf, Dwrand, Dwhite, Dbrown, Diwhite, Dibrown, Dgeom, Demand, Duty, DemandEnvGen, Dbufrd, Dbufwr, Dstutter, Dreset, Dswitch, Dswitch1, Dunique |
| Dynamics | Compander, CompanderD, Limiter, Normalizer, Amplitude |
| Chaos | LorenzL, HenonN/L/C, GbmanN/L, LatoocarfianN/L/C, LinCongN/L/C, CuspN/L, QuadN/L/C, StandardN/L, FBSineN/L/C |
| Granular | GrainBuf, GrainIn, PitchShift, Warp1 |
| Buffer I/O | PlayBuf, RecordBuf, BufRd, BufWr, ClearBuf, LocalBuf, MaxLocalBufs, ScopeOut, ScopeOut2 |
| Disk I/O | DiskIn, DiskOut, VDiskIn |
| Physical Modeling | Pluck, Ball, TBall, Spring |
| Reverb | FreeVerb |
| Convolution | Convolution, Convolution2, Convolution2L, Convolution3 |
| Phase Vocoder | FFT, IFFT, PV_Add, PV_BinScramble, PV_BinShift, PV_BinWipe, PV_BrickWall, PV_ConformalMap, PV_Conj, PV_Copy, PV_CopyPhase, PV_Diffuser, PV_Div, PV_HainsworthFoote, PV_JensenAndersen, PV_LocalMax, PV_MagAbove, PV_MagBelow, PV_MagClip, PV_MagDiv, PV_MagFreeze, PV_MagMul, PV_MagNoise, PV_MagShift, PV_MagSmear, PV_MagSquared, PV_Max, PV_Min, PV_Mul, PV_PhaseShift, PV_PhaseShift90, PV_PhaseShift270, PV_RandComb, PV_RandWipe, PV_RectComb, PV_RectComb2, RunningSum |
| Machine Listening | BeatTrack, BeatTrack2, KeyTrack, Loudness, MFCC, Onsets, Pitch, SpecCentroid, SpecFlatness, SpecPcile |
| Hilbert | FreqShift, Hilbert, HilbertFIR |
| I/O | In, Out, InFeedback, LocalIn, LocalOut, OffsetOut, ReplaceOut, XOut |
| Lines | Line, XLine, LinExp, LinLin, DC, K2A, A2K, AmpComp, AmpCompA, Silence |
| Triggers | Trig, Trig1, Latch, Gate, Schmidt, Sweep, Phasor, Peak, PeakFollower, RunningMax, RunningMin, SendTrig, Poll, SendReply, SendPeakRMS, ToggleFF, TDelay, ZeroCrossing, LeastChange, MostChange, Clip, Fold, Wrap, InRange |
| Mouse/Keyboard | KeyState, MouseButton, MouseX, MouseY |
| Info | SampleRate, SampleDur, BlockSize, ControlRate, ControlDur, SubsampleOffset, RadiansPerSample, NumRunningSynths, BufFrames, BufSamples, BufSampleRate, BufRateScale, BufChannels, BufDur, NumOutputBuses, NumInputBuses, NumAudioBuses, NumControlBuses, NumBuffers, NodeID |
| Random | Rand, IRand, ExpRand, LinRand, NRand, TRand, TIRand, TExpRand, CoinGate, TWindex, RandID, RandSeed, Hasher, MantissaMask |
| Utility | MulAdd, Sum3, Sum4, Mix |
| Safety | CheckBadValues, Sanitize |
from nanosynth import Envelope
Envelope.adsr(attack_time=0.01, decay_time=0.3, sustain=0.5, release_time=1.0)
Envelope.asr(attack_time=0.01, sustain=1.0, release_time=1.0)
Envelope.linen(attack_time=0.01, sustain_time=1.0, release_time=1.0)
Envelope.percussive(attack_time=0.01, release_time=1.0)
Envelope.triangle(duration=1.0, amplitude=1.0)
# Custom envelope
Envelope(amplitudes=[0, 1, 0.5, 0], durations=[0.1, 0.3, 0.6], curves=[-4])API reference docs are auto-generated from docstrings using mkdocs-material and mkdocstrings.
make docs # build static site to site/
make docs-serve # serve locally at http://127.0.0.1:8000 with live reload
make docs-deploy # deploy to GitHub PagesBrowse the docs at shakfu.github.io/nanosynth.
make dev # uv sync + editable install
make build # build wheel (incremental via build cache)
make sdist # build source distribution
make test # run tests
make lint # ruff check --fix
make format # ruff format
make typecheck # mypy --strict
make qa # all of the above
make demos # run scsynth demo scripts
make demos-supernova # run supernova demo scripts
make clean # remove transitory files (preserves build cache)
make reset # clean everything including build cacheThe GitHub Actions workflow (.github/workflows/build.yml) builds wheels for CPython 3.10--3.14 on macOS ARM64, Linux x86_64, and Windows x86_64 using cibuildwheel. A qa job runs lint, format check, typecheck, and tests on every push. A source distribution ('sdist') is built separately and all artifacts are aggregated into a single downloadable archive.
A separate release workflow (.github/workflows/release.yml) publishes to PyPI on tag push via trusted publisher, with manual dispatch for TestPyPI.
- SuperCollider -- the audio synthesis engine and programming language that nanosynth embeds.
- supriya -- the inspiration for nanosynth; its UGen system and SynthDef compiler were the basis for this project's graph compilation pipeline.
- sc3 - Another SuperCollider library for Python with less features than supriya.
- TidalCycles -- live coding pattern language for music, built on SuperCollider.
- Strudel -- JavaScript port of TidalCycles for browser-based live coding.
- Sonic Pi -- live coding music synth built on SuperCollider.
- RtMidi -- cross-platform MIDI I/O library, vendored for the
_midiextension. - nanobind -- the C++/Python binding library used to embed libscsynth, RtiMidi and the OSC codec.
MIT