Skip to main content

Minimal embedded SuperCollider synthesis engine (libscsynth) wrapper

Project description

nanosynth

nanosynth is a Python package that embeds SuperCollider's libscsynth synthesis engine 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.

Features

  • Embedded synthesis engine -- libscsynth runs in-process as a Python extension (vendored and built from source), no separate scsynth process required

  • High-level Server class -- boot/quit lifecycle, node ID allocation, SynthDef dispatch, buffer management, OSC reply handling, and convenience methods (synth, group, free, set). Context manager support and managed_synth()/managed_group()/managed_buffer() for automatic resource cleanup

  • Pythonic SynthDef builder -- define UGen graphs using a context manager and operator overloading, compiled to SuperCollider's SCgf binary 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 -- Score class for offline audio rendering to WAV/AIFF files without audio hardware. Timestamped OSC commands are serialized and rendered by the embedded engine

  • Bus allocation -- Bus proxy class with Server.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, and Player for musical event scheduling. Patterns are reusable iterables; Pbind produces event streams that drive synth creation with automatic gate release. Clock provides tempo-driven playback with drift-free scheduling

  • MIDI input -- MidiIn class 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. NodeProxy owns a private audio bus, a source synth (with ASR envelope for crossfade), and a monitor synth. Swap the source seamlessly while audio plays. Ndef is 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 of UGenNode/UGenInput NamedTuples for programmatic traversal. SynthDef.to_dot() exports to Graphviz DOT format

  • Envelope system -- Envelope class with factory methods (adsr, asr, linen, percussive, triangle) and the EnvGen UGen

  • OSC codec -- pure-Python OscMessage/OscBundle encode/decode with optional C++ acceleration via nanobind

  • @synthdef decorator -- shorthand for defining SynthDefs as plain functions with parameter rate/lag annotations

  • Full type safety -- passes mypy --strict, complete type annotations throughout

Requirements

  • Python 3.10+

  • uv (package manager)

  • For embedded scsynth: SuperCollider 3.14.1, 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.

Installation

pip install nanosynth

Or build from source:

# Editable install with embedded libscsynth
uv pip install -e .

# Build wheel (incremental -- reuses cmake build cache in build/)
make build

# Install without the audio engine (OSC codec + SynthDef compiler only)
uv pip install -e . -C cmake.define.NANOSYNTH_EMBED_SCSYNTH=OFF

Quick Start

Run the Audio Demos

make demos

Define a SynthDef and Play It

The 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 exit

Or 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 Nodes (Automatic Cleanup)

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 here

Effect Chains with Bus Allocation

Use 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 automatically

Recording

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").

Offline (NRT) Rendering

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")

Pattern Sequencing

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).

MIDI Input

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()

NodeProxy / Ndef (Live Coding)

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)

Synthesis Techniques

The following examples show SynthDef definitions for various synthesis techniques. Each can be played using the Server class as shown above.

Using the @synthdef Decorator

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 instance

Subtractive Synthesis

from 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")

FM Synthesis

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")

Additive Synthesis

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")

Plucked String (Physical Modeling)

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")

Delay and Reverb Effects

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")

Demand-Rate Sequencing

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")

Ring Modulation

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")

Stereo Width with Detuning

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")

Dynamics Processing

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")

Advanced Features

SynthDef Compilation (No Engine Required)

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)

Debugging SynthDef Graphs

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])
#   ...

SynthDef Graph Introspection

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`

OSC Codec

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()

Available UGens

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

Envelope Types

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])

Documentation

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 Pages

Browse the docs at shakfu.github.io/nanosynth.

Development

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 clean       # remove transitory files (preserves build cache)
make reset       # clean everything including build cache

CI

The 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. An 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.

Attributions

  • 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.
  • 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 _midi extension.
  • nanobind -- the C++/Python binding library used to embed libscsynth and the OSC codec.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nanosynth-0.1.4.tar.gz (7.0 MB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

nanosynth-0.1.4-cp314-cp314-win_amd64.whl (1.4 MB view details)

Uploaded CPython 3.14Windows x86-64

nanosynth-0.1.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (2.2 MB view details)

Uploaded CPython 3.14manylinux: glibc 2.27+ x86-64manylinux: glibc 2.28+ x86-64

nanosynth-0.1.4-cp314-cp314-macosx_11_0_arm64.whl (1.2 MB view details)

Uploaded CPython 3.14macOS 11.0+ ARM64

nanosynth-0.1.4-cp313-cp313-win_amd64.whl (1.3 MB view details)

Uploaded CPython 3.13Windows x86-64

nanosynth-0.1.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (2.2 MB view details)

Uploaded CPython 3.13manylinux: glibc 2.27+ x86-64manylinux: glibc 2.28+ x86-64

nanosynth-0.1.4-cp313-cp313-macosx_11_0_arm64.whl (1.2 MB view details)

Uploaded CPython 3.13macOS 11.0+ ARM64

nanosynth-0.1.4-cp312-cp312-win_amd64.whl (1.3 MB view details)

Uploaded CPython 3.12Windows x86-64

nanosynth-0.1.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (2.2 MB view details)

Uploaded CPython 3.12manylinux: glibc 2.27+ x86-64manylinux: glibc 2.28+ x86-64

nanosynth-0.1.4-cp312-cp312-macosx_11_0_arm64.whl (1.2 MB view details)

Uploaded CPython 3.12macOS 11.0+ ARM64

nanosynth-0.1.4-cp311-cp311-win_amd64.whl (1.3 MB view details)

Uploaded CPython 3.11Windows x86-64

nanosynth-0.1.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (2.2 MB view details)

Uploaded CPython 3.11manylinux: glibc 2.27+ x86-64manylinux: glibc 2.28+ x86-64

nanosynth-0.1.4-cp311-cp311-macosx_11_0_arm64.whl (1.2 MB view details)

Uploaded CPython 3.11macOS 11.0+ ARM64

nanosynth-0.1.4-cp310-cp310-win_amd64.whl (1.3 MB view details)

Uploaded CPython 3.10Windows x86-64

nanosynth-0.1.4-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (2.2 MB view details)

Uploaded CPython 3.10manylinux: glibc 2.27+ x86-64manylinux: glibc 2.28+ x86-64

nanosynth-0.1.4-cp310-cp310-macosx_11_0_arm64.whl (1.2 MB view details)

Uploaded CPython 3.10macOS 11.0+ ARM64

File details

Details for the file nanosynth-0.1.4.tar.gz.

File metadata

  • Download URL: nanosynth-0.1.4.tar.gz
  • Upload date:
  • Size: 7.0 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.2

File hashes

Hashes for nanosynth-0.1.4.tar.gz
Algorithm Hash digest
SHA256 5ae37139651b8ac4dd36533d88fae6bc007bd9b95976415607b79b8f505e6704
MD5 797b02307c3adc1cdae7981f7498571d
BLAKE2b-256 d45915868c0eab0cd5c40bb6d8be19df885470b94d97b6e35c4aae22342bf243

See more details on using hashes here.

File details

Details for the file nanosynth-0.1.4-cp314-cp314-win_amd64.whl.

File metadata

  • Download URL: nanosynth-0.1.4-cp314-cp314-win_amd64.whl
  • Upload date:
  • Size: 1.4 MB
  • Tags: CPython 3.14, Windows x86-64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.2

File hashes

Hashes for nanosynth-0.1.4-cp314-cp314-win_amd64.whl
Algorithm Hash digest
SHA256 ce1e83badb0b022fa71965a79d59b8b441015e8dfa75c7c2f8d9f9559965d9d1
MD5 08676b2fc759546480cdf5ab93dce07e
BLAKE2b-256 26a1007e6d5422e2c73263d43c3c7779c1944c6a223ccb6e7f94f87a1d6a4673

See more details on using hashes here.

File details

Details for the file nanosynth-0.1.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for nanosynth-0.1.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 9cab3d3830202d9b50bf01d3071c7a5a996e812071a2f1979c45a5ad6878b915
MD5 dbc00db6f59c8e8e141569f1187c094b
BLAKE2b-256 05c17dc2927cd043d084b72f3803e940d95421c2ab9be62abb9b7b60d60db5d1

See more details on using hashes here.

File details

Details for the file nanosynth-0.1.4-cp314-cp314-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for nanosynth-0.1.4-cp314-cp314-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 ff5303ac7604450bf280f67dee39bb7e46c5a4d9e8c2534e26282f7b4174d6f7
MD5 a1a7fb790c84f985fe6439f04c410c33
BLAKE2b-256 880bd497ffad44d9d436b3013348befcd51ac191b7219e595666c2646835aa17

See more details on using hashes here.

File details

Details for the file nanosynth-0.1.4-cp313-cp313-win_amd64.whl.

File metadata

  • Download URL: nanosynth-0.1.4-cp313-cp313-win_amd64.whl
  • Upload date:
  • Size: 1.3 MB
  • Tags: CPython 3.13, Windows x86-64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.2

File hashes

Hashes for nanosynth-0.1.4-cp313-cp313-win_amd64.whl
Algorithm Hash digest
SHA256 5159db6a602b5b3e468ce85a7094fdc4508e56616cf78d3881230f573a4ee2bc
MD5 e7fdf999f301fe2b7c74f885bad93471
BLAKE2b-256 4ccff7cc6f5c76124aa3189cf17db8c4d964bf551309bbcba0e1cb4a1dd518ab

See more details on using hashes here.

File details

Details for the file nanosynth-0.1.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for nanosynth-0.1.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 1b2d2cb7ad80ba13f5d2e413d7148d278aee27f9f347e560f73764be8ee28ebb
MD5 0a2202c6fd9ba5055e2a53a1224fbb4e
BLAKE2b-256 83645026b50d51b7aa5ba65e0e6fcf34cfa27d4ccdf80ed27484f42e1d71d341

See more details on using hashes here.

File details

Details for the file nanosynth-0.1.4-cp313-cp313-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for nanosynth-0.1.4-cp313-cp313-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 61a01564849c61164307f701bd6da7eb1ed2608afcd88be3a46022ff4d0a4bc7
MD5 147cd6af1d69fad421060e8b95f6ff18
BLAKE2b-256 de33e5b106e5098229bd0a377778070e115c81bedb618b052842ef4e31d75c77

See more details on using hashes here.

File details

Details for the file nanosynth-0.1.4-cp312-cp312-win_amd64.whl.

File metadata

  • Download URL: nanosynth-0.1.4-cp312-cp312-win_amd64.whl
  • Upload date:
  • Size: 1.3 MB
  • Tags: CPython 3.12, Windows x86-64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.2

File hashes

Hashes for nanosynth-0.1.4-cp312-cp312-win_amd64.whl
Algorithm Hash digest
SHA256 b98dd2fbc9f934abe3a1f08547a3c649fdaebcd53ac7fe143dc7e9da13a14777
MD5 8bb6ca1442a8ff492c6caa72d0a5d811
BLAKE2b-256 2f07d33d2a5fd9df8505fae4d71f72f630a2034450491b62d5732bbf9f4b3c98

See more details on using hashes here.

File details

Details for the file nanosynth-0.1.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for nanosynth-0.1.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 82968a9a1ca193d78289a41182546518ec5976435651520d392df4f8091b2cf6
MD5 853301baa8022668f9aead7f34916147
BLAKE2b-256 a16c016ea2b0083940155bf50544fdc41d35e3d8b681c0c435158fb9e44eb14d

See more details on using hashes here.

File details

Details for the file nanosynth-0.1.4-cp312-cp312-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for nanosynth-0.1.4-cp312-cp312-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 29f7e0c66ff184b032a092f3ef9c7a8ace675063d9d953844b4ea0fc2f940cd9
MD5 6329d4c47c2cb3817e32f9fd32535183
BLAKE2b-256 b73b0e150446388a4bda75dbfc0c5d52e573d206099816d564428b4693d9726b

See more details on using hashes here.

File details

Details for the file nanosynth-0.1.4-cp311-cp311-win_amd64.whl.

File metadata

  • Download URL: nanosynth-0.1.4-cp311-cp311-win_amd64.whl
  • Upload date:
  • Size: 1.3 MB
  • Tags: CPython 3.11, Windows x86-64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.2

File hashes

Hashes for nanosynth-0.1.4-cp311-cp311-win_amd64.whl
Algorithm Hash digest
SHA256 f786c9578ba838728912d340c98507a9606e039dea29d4d5b8c5d028a672c334
MD5 4b4481272b7dad0d4a8f29a1f7dd335c
BLAKE2b-256 951faaa3423b8a2d6727487a92ff386ba143d695f2101f914920433a0ba76f61

See more details on using hashes here.

File details

Details for the file nanosynth-0.1.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for nanosynth-0.1.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 d72f72d59fa06eed3f0ea8a0029516be7cf5cfc7d65c7329163a20047c1618ce
MD5 49ca62288344de75c7301a0e20a7ba62
BLAKE2b-256 027223bc500032f3cc3587c05615e2d3eba5a2b892e96ab47ad2ee9cd4122905

See more details on using hashes here.

File details

Details for the file nanosynth-0.1.4-cp311-cp311-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for nanosynth-0.1.4-cp311-cp311-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 8c88c6065e2f46574593e949f541e759db7f8e1a36a17fd296bc1adeddfb36dd
MD5 41e2666d9d85810caad02437cb4a30a6
BLAKE2b-256 6490de700a037a9fb471299ad213a7508c31bc04ec6275c3bb46c428a4cca146

See more details on using hashes here.

File details

Details for the file nanosynth-0.1.4-cp310-cp310-win_amd64.whl.

File metadata

  • Download URL: nanosynth-0.1.4-cp310-cp310-win_amd64.whl
  • Upload date:
  • Size: 1.3 MB
  • Tags: CPython 3.10, Windows x86-64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.2

File hashes

Hashes for nanosynth-0.1.4-cp310-cp310-win_amd64.whl
Algorithm Hash digest
SHA256 853c0b3627408cea322da0bae8b4c17d38b070d3a8f18fc604b88e30d98fdd38
MD5 822adbd8da9558e23683bf590be8a809
BLAKE2b-256 c58bfc2aede2a6fff6920bf0a50e7a2bf95b12dd227d236500c60f8b4608a7bf

See more details on using hashes here.

File details

Details for the file nanosynth-0.1.4-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for nanosynth-0.1.4-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 9e571dc9f0ad9b8816aaaf9a646656e130d84b7894d842caa3d7b92df03a3e29
MD5 7ef0169dfe622c002ab614609f1f29dc
BLAKE2b-256 e8a4aa8655f4311ecf9a994086956dafd5fac264ba90ca227a26421b88840e91

See more details on using hashes here.

File details

Details for the file nanosynth-0.1.4-cp310-cp310-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for nanosynth-0.1.4-cp310-cp310-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 de596c665be32fbe36df07da7771c95a2ddb05f02d83e642a70a7b6d5867e4be
MD5 53036d2d01b8a094eea30696bf69eaaa
BLAKE2b-256 467fec98732c0f1db406e6d071d33433e8d23eb3d233cc9e7dc774ba718280b7

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page