Changelog¶
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]¶
Added¶
-
Native Rust port (synthdef): a companion project reimplements the SynthDef compiler in Rust, driven by this package's language-neutral UGen spec (
spec/nanosynth-ugens.json) and validated byte-for-byte against nanosynth's compiler via shared golden fixtures, so the two cannot silently diverge. On top of the compiler it adds a nanosynth-styleServerand SuperCollider-style pattern sequencing over the pure-Rustplyphonengine (native andwasm32), targeting environments a Python runtime cannot reach -- other languages via a C ABI, the browser via WASM, or a DAW plugin. Consuming the shared spec is the reason it was decoupled from the release version (below) -
Performance benchmark suite (
benchmarks/):pytest-benchmarkbenchmarks for the compile hot path (SynthDefBuilder.build()andSynthDef.compile()on a representative subtractive-synth reference graph) and OSC message/bundle encode/decode. Kept out oftests/so the coverage-gatedmake testrun does not time them.make benchruns them,make bench-baselineregenerates the committedbenchmarks/baseline.json, andmake bench-checkfails if any benchmark regresses more than 25% (median) versus the baseline on the same machine. A manual (workflow_dispatch-only)benchmarksCI job gates gross regressions by benchmarking HEAD and the previous commit on one runner (same-machine, apples-to-apples) at a looser 50% threshold that tolerates shared-runner noise
Changed¶
-
UGen spec now records output arity: each entry in
spec/nanosynth-ugens.jsongains anoutputsfield --{"kind": "fixed", "count": N}for most UGens (0 forOut, 2 forPan2, ...) or{"kind": "variable", "default": N}for multichannel UGens. The@ugendecorator stores the declaration on the class (_declared_channel_count/_declared_is_multichannel) for introspection without affecting the runtime channel count. Needed by non-Python frontends to emit multi-output UGens. -
Decoupled the UGen spec from the release version:
spec/nanosynth-ugens.jsonno longer carries agenerator_versionfield, so a version bump no longer trips thetests/test_ugen_spec.pydrift guard. The spec now regenerates only when the UGen metadata or enum tables actually change
[0.2.1]¶
Added¶
-
nanosynth compileCLI command: compiles Python-defined SynthDefs to.scsyndefbinaries for use with a standalonescsynth/supernova,/d_load, or deploy-time precompilation. Loads a.pyfile, discovers every module-levelSynthDef(both@synthdef-decorated functions andSynthDefBuilder.build()results), and writes one<name>.scsyndefper def (-o DIR) or a single bundled SCgf file holding all defs (-b FILE).-n/--nameselects specific defs (repeatable);--anonymousemits MD5-hash names. Reports actionable errors (missing/non-.pyinput, import failure, no SynthDefs found, unknown name, missing output dir, duplicate names that would overwrite) on stderr with a non-zero exit -
Property-based tests (
test_properties.py, hypothesis): randomized invariants over the graph frontend -- algebraic identity laws (sig + 0/0 + sig/sig - 0/sig * 1/1 * sig/sig / 1/sig ** 1return the original signal;sig * 0andsig ** 0fold to the constants 0 and 1), constant folding equivalence with Python float arithmetic, multichannel expansion arity (a width-n input expands to n channels; combining widths m and n yields max(m, n)), and compilation determinism (byte-identical SCgf and stable topological order across rebuilds, independent of the declared name). Recipes generate graphs up to 14 UGens deep -
CLI documentation (
docs/cli.md): documents both theinfoandcompilesubcommands, how SynthDefs are discovered, the option matrix, per-def vs bundled output, and exit-status behavior -
Deployment guide (
docs/deployment.md): the precompiled-artifact workflow for non-Python consumers -- compile SynthDefs to.scsyndefoffline withnanosynth compile, then load them at runtime via/d_load//d_loadDir//d_recvfrom nanosynth,sclang, or any OSC client. Clarifies that the runtime identifier is the SynthDef name embedded in the binary, not the filename -
Language-neutral UGen spec (
spec/nanosynth-ugens.json,scripts/generate_ugen_spec.py): a versioned, machine-readable table of all 341 UGens (names, calculation rates, parameter defaults, andunexpanded/pure/output/width-first flags), the operator/rate/done-action enum tables, and anoperator_ugensblock namingBinaryOpUGen/UnaryOpUGenand the enum table theirspecial_indexselects, so a non-Python implementation of the SynthDef frontend shares the drift-prone metadata rather than transcribing it by hand.tests/test_ugen_spec.pyfails if the committed spec drifts from the code (regenerate withpython scripts/generate_ugen_spec.py); included in sdists -
SCgf format specification (
docs/scgf-format.md): documents the SCgf version-2 binary byte layout (header, constant pool, parameter tables, UGen records, input specs) and the UGen spec schema, for writing loaders, validators, or independent compilers in other languages -
Source-tree CI test matrix: a
test-sourcejob runspytestagainst the source tree (not built wheels) across CPython 3.10--3.14 on Linux, building the C extension from source viauv sync. Closes the gap wheremake testwas previously only exercised against cibuildwheel-built wheels
[0.2.0]¶
Added¶
-
Node lifecycle notifications (
server.py):Server.enable_notifications()registers for the engine's node events (/notify); once on,/n_go//n_end//n_off//n_on//n_moveare parsed intoNodeEventobjects.on_node(callback)receives every event;wait_for_node_free(node_id, timeout)blocks until a node ends (including a self-freeingDoneActionsynth, which the client otherwise has no way to observe); andSynth.wait_free()/Synth.on_free(callback)are per-node conveniences (the latter is one-shot and self-unregisters).enable_notifications()waits for the/done /notifyconfirmation so registration is in effect before any node is created -- otherwise the first node's/n_gocan be missed.NodeEventis exported from the top-level package -
Server introspection & control (
server.py):Server.status()returns aServerStatus(CPU load, sample rate, and ugen/synth/group/synthdef counts from/status.reply);version()returns aServerVersion(/version.reply);query_tree(group, controls=...)returns the live node graph as a nestedNodeInfotree (groups, synths, and their current control values, parsed from/g_queryTree.reply);dump_tree()prints it to the engine log; andreset()is the "panic" equivalent -- frees all nodes (/g_freeAll), clears the scheduler (/clearSched), recreates the default group, and resets the node-id allocator (leaving loaded SynthDefs, buffers, and buses intact).ServerStatus/ServerVersion/NodeInfoare exported from the top-level package -
Direct numpy buffer data exchange (
server.py,_scsynth.cpp):Server.get_buffer_data(buffer_id)andset_buffer_data(buffer_id, array)copy samples directly between a numpy array and a buffer's in-process float storage viamemcpy-- no OSC round-trip and none of the/b_getn//b_setndatagram-size limits, which is the core advantage of the embedded engine.getreturns a(frames, channels)float32 array (owning a copy, so it stays valid across buffer reallocation);setaccepts 1-D (mono) or 2-D input, coerced to contiguous float32 and shape-checked against the buffer.buffer_info(buffer_id)returns(frames, channels, sample_rate), andalloc_buffer_from_array(array)allocates a correctly-sized buffer and fills it in one call (handy for loading wavetables/samples). scsynth-only (raisesEngineErrorfor supernova); numpy is an optional dependency (pip install nanosynth[numpy]), imported lazily with a clear error if missing. The C++ accessors (world_buffer_get/set/info) read the live buffer, so concurrent synth access during a transfer may tear/glitch (documented, never crashes) -
Server.sync()round-trip barrier: sends/syncwith a unique id and blocks until the matching/syncedreply, the canonical scsynth flush primitive. ReturnsTrueonce synced orFalseon timeout. Concurrentsync()calls are matched by id -
Reply matchers:
Server.wait_for_reply()andsend_msg_sync()accept an optionalmatchpredicate so a waiter accepts only a reply satisfying it (e.g. a/donewhose first argument is the originating command). Non-matching replies leave the waiter registered instead of resolving it -
Reclaiming id allocators (
server.py): node ids, buffers, audio buses, and control buses are now managed by allocators that reclaim freed ids. Buffers/buses use a coalescing free-list block allocator (_BlockAllocator) that hands out the lowest available contiguous block, reuses freed blocks, supports explicit-id reservation, and raisesEngineErroron exhaustion. Node ids use a wrapping allocator (_NodeIdAllocator) bounded within scsynth's id space (not reclaimed on free, since self-freeingDoneActionsynths are not reported to the client) -
Cross-engine process guard (
_scsynth.cpp,_supernova.cpp): scsynth and supernova each statically embed the full SuperCollider core and share process-global state (the dlopen'd UGen plugin registry, FFT init), so creating one kind after the other has run in the same process segfaults -- in either order, even after a clean quit, and even via NRT (which also creates a scsynth World). A guard in each extension's engine-creation entry point (coordinated through an environment variable, since the two modules share no symbols) now raisesServerCannotBootwith a clear message instead of letting the process crash. Same-kind reuse (scsynth reboot, repeated NRT renders) is unaffected -
Test hardening: golden SCgf byte fixtures (
tests/fixtures/scgf/, regenerable viapython tests/test_golden_scgf.py) that freeze proven-correct compiler output and catch silent format regressions; unit tests for the allocators (reclaim, coalescing, capacity, reservation, thread-safety),sync(), and reply matchers; forced cross-path OSC parity tests (test_osc_parity.py) asserting byte-identical encode, identical decode, and identical exception types across the native and pure-Python codecs; and opt-in realtime smoke tests (test_realtime_smoke.py, gated byNANOSYNTH_TEST_REALTIME=scsynth|supernova) that boot a real engine and exercisesync(), buffer reclaim, and clean quit. A--cov-fail-under-style coverage floor (90%) is enforced via[tool.coverage.report]
Fixed¶
-
OSC codec C++/Python parity (
_osc.cpp,osc.py,exceptions.py): the two codecs violated their "must stay compatible" contract. They now agree on (a) exception type -- both raiseOscErroron malformed input (the C++ path via a nanobind exception translator scoped to a dedicatedOscDecodeErrorC++ type, so it does not affectstd::runtime_errors from the_scsynth/_supernovaextensions -- nanobind translators are process-global; the Python path by wrappingstruct.error/IndexError/ValueError), andOscErrornow also subclassesValueErrorsoexcept ValueErrorkeeps working; (b) string encoding -- both use UTF-8 (Python previously used ASCII and raised on non-ASCII); and (c) unterminated strings -- the C++ decoder now rejects a string with no null terminator instead of silently accepting it, matching Python -
Reply waiter leak on timeout (
server.py): a timed-outwait_for_reply/send_msg_syncwaiter was never removed from_pending_replies, so it lingered and could be spuriously resolved by a later unrelated reply. Timed-out waiters are now unregistered -
atexit cleanup leak (
scsynth.py,supernova.py):atexit.register(self.quit)ran in__init__and was never unregistered, so every protocol object (booted or not) leaked an atexit callback holding a strong reference. Registration now happens onboot()and is removed onquit(), so never-booted objects accumulate nothing -
OSC bundle decoder out-of-bounds read (
_osc.cpp): the native bundle decoders (decode_bundle_from_raw,decode_bundle_bytes) read each element's length as a signedint32_tand bounds-checked it withoffset + (size_t)length > len. A malformed datagram with a negative length cast tosize_tcould wrap that addition and pass the check, then hand a negative/huge size tonb::bytes, causing an out-of-bounds read or oversized allocation. Lengths are now read asuint32_tand validated with the overflow-safeelement_len > len - offset(offset <= lenis guaranteed at that point) -
Engine boot/quit thread coordination (
scsynth.py,supernova.py):_shutdown()previously did a blindthread.join(timeout=5)followed by an unconditional forcedworld_cleanup/supernova_cleanup, which could tear the engine down concurrently with a wait/run thread still insideWorld_WaitForQuit/supernova_run(a double-free / use-after-free race on the engine handle). It now gates on the existingexit_future-- resolved by the wait/run thread only after the engine has been torn down internally -- while that thread is alive, with forced cleanup retained only as a logged last resort on a 5s timeout -
Boot is now exception-safe (
scsynth.py,supernova.py): if the on-boot callback or the wait/run-thread start raised, the engine handle was created and the process-global_active_world/_activeflag was left set, permanently wedging every subsequent boot withServerCannotBoot. The go-online sequence is now wrapped; on failure a new_abort_partial_boot()tears down the engine and clears the global flag. The thread is started andboot_futureresolved last, so rollback never races a running loop -
Pattern clock timing drift (
patterns.py):Player._tickadvanced the next event deadline from the wall-clock wake time (now + dur * beat_dur), baking every late wake-up into the following event and accumulating drift over a session. It now advances from the previous scheduled target (self._next_time + dur * beat_dur); if the clock falls behind, successive events fire back-to-back until it catches up, with no accumulated error -
Release and docs CI workflows (
.github/workflows/):release.ymlhad itspush: tagsandworkflow_dispatchinputs commented out while the publish steps gated on the resulting always-undefinedinputs.targetand a never-truegithub.event_name == 'push', so no PyPI publish could ever run. Triggers are restored and the PyPI gate now keys onstartsWith(github.ref, 'refs/tags/v').docs.ymlranuv sync --group docsagainst a non-existent dependency group (mkdocs deps live indev); it now runsuv syncand itspush: branches: [main]trigger is restored
Changed¶
-
Synchronous SynthDef load matches the sub-command (
server.py):send_synthdef()now waits for a/donewhose first argument is/d_recv, so a/donefrom an unrelated async command cannot resolve the wait early (still falls back to fire-and-forget on timeout for mock servers) -
Recording uses
sync()instead oftime.sleep(server.py):record()andstop_recording()replaced their fixedtime.sleep(0.1)pauses withself.sync()barriers, so the disk buffer is provably open beforeDiskOutstarts and final samples are flushed before the file is closed -
Single-sourced package version: the version was hard-coded in both
pyproject.tomlandsrc/nanosynth/__init__.pyand could drift.pyproject.tomlnow declaresdynamic = ["version"]and reads__version__fromsrc/nanosynth/__init__.pyvia scikit-build-core's regex metadata provider, making__init__.pythe single source of truth
[0.1.6]¶
Added¶
-
nanosynth infoCLI command: shows version, Python version/platform/architecture, audio backend (CoreAudio/PortAudio), UGen plugin path and count, and whether the scsynth and supernova C extensions loaded successfully (with error reason if not).--list/-lflag lists all available UGen classes alphabetically (341 UGens). Entry point registered via[project.scripts]inpyproject.toml. Usesargparsesubparsers for future extensibility -
MIDI usage guide (
docs/midi.md): covers port opening (by index, name substring, or virtual port), MIDI message types (NoteOn,NoteOff,ControlChange,PitchBend), handler registration and removal,midi_note_map()for note-to-synth mapping with frequency/velocity conversion,midi_cc_map()for CC-to-parameter mapping with linear scaling, and thread safety considerations for callbacks running on RtMidi's polling thread -
Threading model documentation (
docs/threading.md): documents all background threads (engine daemon, reply callback, Clock, Timer, MIDI), the engine lifecycle state machine (OFFLINE -> BOOTING -> ONLINE -> QUITTING), OSC reply dispatch with locking, thread-localSynthDefBuilderstacks with UUID scope checking, Clock/Player scheduling with adaptive sleep, and a reference table of thread-safe vs thread-unsafe operations -
Integration tests (
test_integration.py): 19 tests verifying the full compilation-to-audio pipeline (SynthDefBuilder -> SynthDef -> SCgf binary -> engine load -> audio synthesis -> WAV output) via NRT rendering. Covers sine wave synthesis (non-silence, amplitude scaling, frequency differentiation), parameter control, diverse UGens (WhiteNoise, Saw, LPF, RLPF, Pan2), envelopes (percussive decay verification), Mix, stereo panning,@synthdefdecorator, complex graphs (subtractive, additive, multi-SynthDef scores), and compilation roundtrip (determinism, anonymous names, optimization equivalence). No audio hardware required -
Basic UGen tests (
test_basic_ugens.py): 37 tests coveringMulAdd,Sum3,Sum4, andMix-- all algebraic simplification paths, rate computation and validation, zero-elision, input swapping for rate validity, multichannel expansion, recursive Mix grouping (Sum3/Sum4 tree), and SCgf compilation. Coverage ofbasic.pyraised from 26% to 86% -
Adversarial compiler tests (
test_adversarial.py): 32 tests for edge cases -- deep UGen chains (100-deep filter cascades, 200-deep arithmetic chains), large graphs (500 parallel UGens, 100 parameters, 200+ constants), name encoding boundaries (empty, 1-char, 255-char, 256-char overflow, non-ASCII rejection), scope isolation (nested/sequential/cross-scope errors), degenerate graphs (constant-only, parameter-only, all-dead-code), topological sort edge cases (diamond dependencies, wide fan-out/fan-in, disconnected subgraphs), compilation determinism, and custom UGen type name encoding -
OSC edge case tests (
test_osc_edge_cases.py): 48+ tests (parametrized across native C++ and pure-Python backends) covering NTP timestamp edge cases (zero, fractional, large, immediate, non-realtime), deeply nested bundles (3-level, 5-level, mixed message/bundle), special characters in addresses (underscores, digits, dots, long, minimal, OSC wildcards), equality edge cases,format_datagram/str/repr,to_list/to_osc,find_free_port, unsupported type encoding, and empty/no-arg messages -
Concurrency stress tests (
test_concurrency.py): 10 tests covering SynthDefBuilder thread isolation (50 concurrent builds with unique frequencies, barrier-synchronized stack checks, nested builder isolation, cross-thread UGen rejection, 30 concurrent complex graphs with Mix/LPF/params, deterministic output under concurrency) and Server reply dispatch (20 concurrent waiters resolved via raw datagram dispatch, concurrent handler registration/unregistration, 100 concurrent dispatches, waiter timeout) -
Low-coverage UGen module tests (
test_ugen_coverage.py): 30 tests covering LocalIn (single/multi-channel, default cycling, scalar defaults, feedback loops with LocalOut, control rate), BiPanB2 (audio/control rate, 3-channel output), DecodeB2 (4/8 channel counts), Splay (single/multiple sources, normalize/no-normalize, spread/center, control rate), Klank (basic, explicit amplitudes, decay times, default fill, empty frequencies rejection, frequency scale/offset/decay scale), LinLin (ar/kr mapping, identity), and Silence (mono/stereo/8-channel) -
Typed exception hierarchy (
exceptions.py): All exceptions defined in a single module importable viafrom nanosynth.exceptions import ....NanosynthErrorbase class withOscError(OSC encode/decode),EngineError(engine lifecycle),ServerCannotBoot(boot failures, subclass ofEngineError),MidiError(MIDI port/callback), andSynthDefError(graph construction) subclasses. All six exception classes exported from the top-level package -
Supernova demos (
demos/supernova/): 5 new demos -- demand-rate sequencing with parallel voices (07_demand_sequencer.py), pattern sequencing via Pbind/Clock (08_patterns.py), FFT spectral processing (09_spectral.py), managed ParGroup context manager with chord progression (10_managed_pargroup.py), and NodeProxy/Ndef live coding (11_nodeproxy.py)
Fixed¶
-
Synchronous SynthDef loading (
server.py):send_synthdef()now waits for the engine's/done /d_recvreply before returning (0.1s timeout), ensuring the SynthDef is ready for immediate use. Previously the fire-and-forget/d_recvcaused race conditions on supernova where/s_newcould arrive before the SynthDef was loaded (e.g. NodeProxy source swaps failing with "Cannot create synth"). Falls back gracefully to fire-and-forget on timeout, so mock servers in tests are unaffected -
NRT render crash with persistent delay synths (
score.py):Score.render()would segfault during World cleanup when persistent synths (noDoneAction.FREE_SYNTHor explicit/n_free) containing delay UGens (DelayN, CombC, etc.) reading from private buses viaIn.arwere still running at end-of-score. The engine freed delay line buffers while they were still referenced. Fixed by sending/g_freeAll 0(free all nodes in the default group) before the end-of-score marker, ensuring clean synth teardown before World cleanup
Changed¶
-
Consolidated exceptions into
nanosynth.exceptions:ServerCannotBootmoved fromscsynth.pyandSynthDefErrormoved fromsynthdef.pyinto the newexceptions.pymodule. Both are re-exported from their original modules for internal use but the canonical import path is nownanosynth.exceptions -
Narrowed broad
except Exceptioncatches:server.py:_dispatch_replyOSC decode catch narrowed to specific decode-failure types (ValueError,IndexError,struct.error,OscError,RuntimeError).patterns.py:_release_synthnarrowed to(EngineError, OSError). Handler callback catch retained asexcept Exceptionwith explicit annotation for intentional user-callback isolation -
Replaced generic
RuntimeErrorwith typed exceptions:scsynth.pyandsupernova.pysend_packetraiseEngineErrorinstead ofRuntimeErrorwhen the server is not running.server.pyraisesEngineErrorfor bus/recording state errors.proxy.pyraisesEngineErrorfor missing source synth.midi.pyraisesMidiErrorfor port-not-found.osc.pyraisesOscErrorfor unparseable type tags
[0.1.5]¶
Added¶
-
Embedded supernova:
EmbeddedSupernovaProtocolruns SuperCollider's parallel DSP engine (supernova) in-process via nanobind (_supernova.cpp), as a drop-in replacement forEmbeddedProcessProtocol. SameServerAPI -- just passprotocol=EmbeddedSupernovaProtocol(). Supernova schedules independent nodes across CPU cores when placed inParGroups, providing parallel DSP execution that scsynth cannot. C++ wrapper (_supernova.cpp, ~400 lines) wraps supernova's C++ classes directly (no C API exists), synthesizingserver_argumentsfrom Python kwargs, implementing a custom reply endpoint for in-process OSC responses, and managing the event loop on a daemon thread. Supernova plugins (24_supernovavariants) are compiled withSUPERNOVAdefined and bundled alongside scsynth plugins. Build gated byNANOSYNTH_EMBED_SUPERNOVA=ONCMake option -
ParGroup support:
ParGroupproxy class (subclass ofGroup) andServer.par_group()/Server.managed_par_group()for creating parallel groups via/p_new. ParGroups evaluate their child nodes in parallel across CPU cores (supernova only; scsynth treats them as regular groups) -
SynthDef disk I/O:
SynthDef.save(path)writes compiled SCgf bytes to a.scsyndeffile (with optionaluse_anonymous_nameflag).Server.load_synthdef(path)loads a.scsyndeffile into the engine via/d_load, resolving to an absolute path for the engine -
Demo scripts reorganized into
demos/scsynth/(21 demos) anddemos/supernova/(6 demos). Supernova demos:01_sine.py(basic sine wave),02_parallel_voices.py(24 voices in ParGroup),03_fx_chain.py(delay + reverb effect chain),04_parallel_fx.py(three independent effect chains on private buses in a ParGroup),05_nested_pargroups.py(nested ParGroups with left/right voice banks and sequential fx group),06_dense_polyphony.py(64 simultaneous voices stress test)
[0.1.4]¶
Added¶
-
Patterns / sequencing:
Pattern[T]abstract base class with__iter__,take(n), and|(chain) operator. Eight value patterns:Pseq(sequential with nested pattern flattening),Prand(random selection),Pwhite(uniform random float),Pseries(arithmetic series),Pgeom(geometric series),Pchoose(weighted random),Pn(repeat a pattern N times),Pconst(yield until sum reaches total).Pbindbinds keys to patterns/scalars to produceEventdicts, stops on shortest pattern, merges with configurable defaults, auto-derivessustainfromdurandfreqfrommidinote.Clock(background daemon thread withtime.monotonic()scheduling, settablebpm) andPlayer(pulls events, creates synths, schedules gate release viathreading.Timer).Restsentinel class for silent beats -
MIDI input: C++ nanobind extension (
_midi.cpp) wrapping RtMidiIn from vendored rtmidi 6.0.0 (CoreMIDI on macOS, ALSA on Linux, WinMM on Windows; JACK disabled). Python layer (midi.py): frozen dataclass message types (NoteOn,NoteOff,ControlChange,PitchBend),MidiInclass with handler registration (on_note_on/off_note_on,on_cc/off_cc, etc.), pure-Python_parse()for raw MIDI bytes (velocity-0 note-on treated as note-off), context manager support, port opening by index/name/virtual. High-level helpers:midi_note_map()(note-on creates synth with freq/amp, note-off sends gate=0) andmidi_cc_map()(CC values scaled to parameter range). Build integration:NANOSYNTH_EMBED_MIDICMake option, rtmidi built as static library viaadd_subdirectory -
NodeProxy / Ndef:
NodeProxyowns a private audio bus, a source synth (with ASR envelope for clean crossfade on swap), and a monitor synth (reads from private bus, writes to hardware output). Source can be a callable (auto-wrapped inSynthDefBuilderwith envelope) or aSynthDef. Hot-swap sends gate=0 to old source (10ms release) and creates new source (10ms attack) for overlap crossfade.play()/stop()control monitoring,clear()frees all resources,set()updates source synth params.Ndefis a global named proxy registry:Ndef(server, "name", source)creates or retrieves aNodeProxy,Ndef.clear_all(server)frees all proxies for a server. Registry keyed by(id(server), name)to support multiple servers -
Bus allocation:
Busproxy class withServer.audio_bus(),Server.control_bus(),free_bus(),managed_audio_bus(),managed_control_bus(). Audio buses allocate from the private range (after hardware I/O); control buses from 0.Bussupportsint()conversion,set()for control buses, equality/hashing, andfree().Server.optionsproperty exposes theOptionsconfiguration -
Server recording:
Server.record(path)captures real-time audio output to WAV/AIFF via DiskOut.stop_recording()finalizes the file.is_recordingproperty for state inspection. Configurable channel count, bus, header/sample format. Recorder SynthDef built and cached on first use per channel count.write_buffer()gains aleave_openparameter for disk-streaming buffers -
NRT (non-real-time) rendering:
Scoreclass for offline audio rendering without real-time audio hardware.Score.add(),add_synthdef(),add_synth()build a timestamped sequence of OSC commands;to_binary()serializes to SC's binary command file format;render()invokes the embedded scsynth NRT engine to produce WAV/AIFF files. C++ bindingworld_nrt_render()wrapsWorld_NonRealTimeSynthesiswith configurable sample rate, format, channels, and engine options -
SynthDef graph introspection:
SynthDef.graph()returns aSynthDefGraphNamedTuple containingUGenNodeandUGenInputstructures for programmatic DAG walking.SynthDef.to_dot()exports the graph as a Graphviz DOT string for visualization. Handles BinaryOpUGen/UnaryOpUGen operator names, Control parameter names, multi-output UGens, and constant inputs -
Demo script
19_nrt_render.py: offline rendering of a C major arpeggio to WAV and AIFF files -
Demo script
20_patterns.py: pattern-based sequencing with Pseq, Prand, Pwhite, Pbind, Clock, and Rest -
Demo script
21_nodeproxy.py: NodeProxy hot-swapping (sine/saw/noise) and Ndef named proxy registry -
Concepts documentation page (
docs/concepts.md): explains 8 non-obvious internal concepts -- calculation rates, multichannel expansion, theunexpandedflag, parameter rates, the builder scope, graph optimization (constant folding and dead code elimination), width-first ordering, and theDefaultsentinel. Each section includes code examples and practical implications
Changed¶
-
Demos 01--11 converted from low-level
_scsynthAPI to high-levelServerAPI (server.synth(),server.group(),server.free(),node.set(), context manager lifecycle) -
README restructured: Quick Start now leads with the
Server-based workflow (define, boot, play), followed by managed nodes, effect chains withAddAction, and NRT rendering. Synthesis technique examples moved to a dedicated section. SynthDef compilation without engine, graph introspection, and OSC codec moved to Advanced Features
Fixed¶
- Windows NRT render crash (
score.py):Score.render()usedtempfile.NamedTemporaryFile(delete=True)for the binary command file, which on Windows holds an exclusive file lock preventing the C++ engine from opening it -- causing an access violation (0xC0000005) inWorld_New. Replaced withtempfile.mkstemp()so the file is closed beforeworld_nrt_renderreads it, with manual cleanup in afinallyblock
[0.1.3]¶
Added¶
-
Synth/Groupproxy objects:Server.synth()andServer.group()now return lightweightSynthandGroupproxy objects instead of raw ints. Proxies support.set(**params),.free(), context manager usage (with server.synth(...) as node:), and are fully int-compatible via__int__(),__index__(),__eq__(), and__hash__().managed_synth()andmanaged_group()also yield proxies. Existing code comparing against ints continues to work unchanged -
control()function: convenience constructor for SynthDef parameters with rate and lag metadata --control(440.0, rate="ar")is equivalent toParameter(value=440.0, rate=ParameterRate.AUDIO). Accepts string rate tokens ("ar","kr","ir","tr") orParameterRateenum values -
Tuple syntax for
SynthDefBuilder: parameters can be specified as tuples --SynthDefBuilder(freq=("ar", 440.0))for(rate, value)orSynthDefBuilder(amp=("kr", 0.5, 0.1))for(rate, value, lag). Works alongsidefloat,Parameter, andcontrol()styles -
Trimmed
__all__exports:from nanosynth import *now exports ~60 names (core API + 29 common UGens) instead of 340+. The full UGen set remains available viafrom nanosynth.ugens import *or qualified imports -
Extended operators:
BinaryOperatorexpanded from 7 to 43 entries,UnaryOperatorfrom 2 to 34, covering SC's full practical operator set (power, integer division, comparisons, bitwise ops, trig, pitch conversion, clipping, ring modulation, etc.) -
Operator methods on UGenOperable: 16 new dunder methods (
__pow__,__floordiv__,__le__,__ge__,__and__,__or__,__xor__,__lshift__,__rshift__and their reverse variants),equal()/not_equal()explicit comparison methods, 25 named binary methods (min_,max_,clip2,fold2,wrap2,ring1--ring4,atan2,hypot, etc.), and 32 named unary methods (midicps,cpsmidi,dbamp,ampdb,tanh_,softclip,distort,squared,sqrt_,exp_,log_,sin_,cos_, etc.) -
Constant folding for new operators:
POWER,INTEGER_DIVISION,MINIMUM,MAXIMUM, comparison ops, and all math-stdlib unary ops foldfloat op floatat compile time -
POWER optimizations in
BinaryOpUGen._new_single:x ** 0folds to1,x ** 1folds tox -
Buffer management on
Server:alloc_buffer(),read_buffer(),write_buffer(),free_buffer(),zero_buffer(),close_buffer(),next_buffer_id(), plusmanaged_buffer()andmanaged_read_buffer()context managers. Buffer IDs are auto-allocated (monotonically from 0) or explicitly specified; allocated buffers tracked in_allocated_buffersset -
Reply handling: C++ reply callback (
set_reply_funcin_scsynth.cpp) routes OSC responses from the engine back to Python;EmbeddedProcessProtocol.set_reply_callback()wires it at boot;Servergains_dispatch_reply()router,on()/off()for persistent handlers,wait_for_reply()for blocking one-shot waits, andsend_msg_sync()for send-and-wait patterns -- all thread-safe -
Demo script
18_operators_buffers.py: extended operators (midicps,tanh_,clip2,dbamp,softclip,distort), managed buffer allocation, and synchronous reply handling (send_msg_sync) -
Documentation site (mkdocs-material + mkdocstrings): auto-generated API reference from docstrings, organized by core modules and 28 UGen categories, with Getting Started guide and changelog. Served locally via
make docs-serve, deployed to GitHub Pages viamake docs-deploy. Newdocsdependency group inpyproject.toml, GitHub Actions workflow (.github/workflows/docs.yml) for auto-deploy on push to main -
Comprehensive docstrings across all core modules:
enums.py: all 6 enum classes (CalculationRate,ParameterRate,BinaryOperator,UnaryOperator,DoneAction,EnvelopeShape) with member descriptions andfrom_expr()methodssynthdef.py:SynthDef,SynthDefBuilder,UGen,UGenOperable,UnaryOpUGen,BinaryOpUGen,Parameter,Control,SynthDefError,UGenSerializable; 25 named binary methods with formulas (ring1--ring4,clip2,fold2,wrap2,difsqr,sumsqr, etc.); 8 pitch/amplitude conversion methods with examples (midicps,cpsmidi,dbamp,ampdb); waveshaping methods (distort,softclip)envelopes.py:Envelopeclass with full Args section, all 5 factory methods (adsr,asr,linen,percussive,triangle),EnvGenwith parameter descriptionsosc.py:OscMessage,OscBundlewith public method docstrings (to_datagram,from_datagram,to_list),find_free_port()scsynth.py:Optionswith commonly adjusted fields,BootStatus,ServerCannotBoot,EmbeddedProcessProtocol.boot()and.quit()-
compiler.py:compile_synthdefs()with Args/Returns documenting the SCgf binary format -
AddActionenum (enums.py):ADD_TO_HEAD,ADD_TO_TAIL,ADD_BEFORE,ADD_AFTER,REPLACE-- replaces opaque raw ints inServer.synth(),Server.group(), and their managed variants. Raw int values still accepted for backwards compatibility -
ServerProtocolstructural type (synthdef.py):SynthDef.send()andSynthDef.play()now accept any object satisfyingServerProtocolinstead ofAny, restoring type safety without circular imports
Fixed¶
-
Gendy1/2/3 parameter wire order (
gendyn.py): all three Gendy UGens had incorrect parameter ordering and counts vs the C++ plugin (GendynUGens.cpp). Gendy1/2 had a singlefrequencywhere SC expectsmin_frequency/max_frequency, and had four distribution parameters (amplitude_parameter_one/two,duration_parameter_one/two) where SC expects two (amplitude_parameter,duration_parameter). Wire positions 2--10 were all wrong, causing silence or garbage output. Gendy3's parameter order was also incorrect (it has a different layout from Gendy1/2 in the C++ -- singlefrequency, no min/max). All three now match theirZIN0()indices exactly -
Klank audio rate (
ffsinosc.py):Klank.ar()passedcalculation_rate=Noneto_new_expanded, which resolved toCalculationRate.SCALAR-- the filter bank was computed once at init instead of processing audio samples per block. Changed toCalculationRate.AUDIO -
ParameterRate.from_expr("tr"): the"tr"token for trigger rate was missing from the string-to-enum mapping, causingKeyErrorwhen usingcontrol(rate="tr"). Added alongside"ar","kr","ir" -
help(nanosynth)crash: dynamically generated rate methods (.ar,.kr,.ir) created viaexecin_create_fnhad__module__ = None, causingpydocto raiseTypeError: unsupported operand type(s) for +: 'NoneType' and 'str'when rendering help text. Now sets__module__from the owning class before applying decorators. -
__bool__trap onUGenOperable:UGenOperable.__bool__now raisesTypeErrorinstead of silently returningTrue. Catches the common footgun whereif sig > 0:always takes the truthy branch because comparison operators returnUGenOperableobjects, not booleans -
Server.quit()decoupled from protocol internals:Server.quit()now delegates toEmbeddedProcessProtocol.quit()instead of reaching into the private_shutdown()method, properly setting theQUITTINGstate for clean shutdown callbacks -
Thread-local builder guard centralized: replaced three inconsistent
hasattr(_local, "_active_builders")guard patterns inSynthDefBuilder.__init__,__enter__, andUGen.__init__with a single_get_active_builders()function -
Topological sort descendant ordering:
_initiate_topological_sorthadkey=lambda x: ugens.index(ugen)which captured the loop variable, making the sort a no-op. Fixed tokey=lambda x: ugens.index(x)to sort descendants by their position in the UGen list
[0.1.2]¶
Added¶
- ~80 new UGen classes (290 -> 346 total), achieving full parity with supriya's UGen surface:
- Phase vocoder (
pv.py):FFT,IFFT,PV_ChainUGen, and 34PV_*analysis/resynthesis UGens, plusRunningSum - Machine listening (
ml.py):BeatTrack,BeatTrack2,KeyTrack,Loudness,MFCC,Onsets,Pitch,SpecCentroid,SpecFlatness,SpecPcile - Stochastic synthesis (
gendyn.py):Gendy1,Gendy2,Gendy3 - Hilbert transforms (
hilbert.py):FreqShift,Hilbert,HilbertFIR - Mouse/keyboard (
mac.py):KeyState,MouseButton,MouseX,MouseY - Disk I/O (
diskio.py):DiskIn,DiskOut,VDiskIn - Utility (
basic.py):MulAdd,Sum3,Sum4,Mix(signal mixer with Sum3/Sum4 tree optimization) -
Additions to existing modules:
LocalBuf,ScopeOut2(bufio);Demand,Dwrand(demand);Poll,SendReply,SendPeakRMS(triggers);LocalIn(inout);Klank(ffsinosc);LinLin,Silence(lines);Changed(filters);CompanderD(dynamics);Splay(panning) -
Defaultsentinel class insynthdef.pyfor parameters whose defaults are computed from other parameters at construction time (used byFFT,Gendy1-3,ScopeOut2) -
PseudoUGenbase class for virtual UGens that compose other UGens (Mix,Changed,CompanderD,LinLin,Silence,Splay) -
_postprocess_kwargshook onUGen.__init__for transforming parameters at construction time (dynamic channel counts, Default resolution, rate forcing) -
GREATER_THANandLESS_THANbinary operators with corresponding__gt__/__lt__onUGenOperable -
Demo scripts:
14_spectral.py(FFT/PV spectral processing),15_gendy.py(stochastic synthesis),16_klank_splay.py(resonant filter banks),17_freqshift.py(Bode frequency shifting)
Fixed¶
LocalBufcrash:SynthDefBuilder.build()now runs a_cleanup_local_bufspass that automatically inserts aMaxLocalBufsUGen whenLocalBufinstances are present in the graph (e.g. fromFFT's auto-allocated buffer). Without this, scsynth would crash withLocalBuf tried to allocate too many local buffers
[0.1.1]¶
Added¶
-
Serverclass (nanosynth.server) -- high-level wrapper around the embedded scsynth engine with boot/quit lifecycle, node ID allocation, SynthDef dispatch, and convenience methods (synth,group,free,set). Supports context manager usage -
Server.managed_synth()andServer.managed_group()context managers -- create a synth or group and automatically free it on context exit (including on exceptions); guard against freeing if the server has already stopped -
SynthDef.send(server)andSynthDef.play(server, **params)convenience methods for sending SynthDefs and creating synths in one call -
SynthDef.dump_ugens()pretty-printer -- returns a human-readable UGen graph representation (modeled on SuperCollider'sSynthDef.dumpUGens), showing UGen types, rates, input wiring, operator names, and multi-output counts -
Envelope.compile()-- dedicated serialization path producingtuple[float, ...]directly, bypassing UGenVector/ConstantProxy; raisesTypeErroron UGen inputs.serialize()retained for UGen graph wiring -
Demo scripts
12_server_sine.py(sine wave via Server API) and13_server_pad.py(gated pad chord progression withmanaged_synth,managed_group, andserver.set()) -
EmbeddedProcessProtocol.send_packet()andsend_msg()convenience methods for sending OSC to the engine without importing_scsynthdirectly -
Auto-generated docstrings for all
@ugen-decorated classes (e.g.SinOsc -- ar, kr\n\nParameters:\n frequency (default: 440.0)) -
__slots__on core graph classes (UGen,OutputProxy,ConstantProxy,UGenVector,UGenOperable,UGenScalar,UGenSerializable) for lower memory usage -
OSC test suite now runs all 24 tests against both the C++ and pure-Python backends (48 total)
-
EmbeddedProcessProtocolstate machine tests: initial state, quit no-op, send errors when offline, callback storage, boot-when-active guard -
Test coverage for
SynthDefBuildercross-scope errors, graph optimization (_optimize/_eliminate),Envelope.linen/.triangle/.asrfactory methods, multi-channel UGens (In,PanAz,DecodeB2),compile_synthdefswith multiple SynthDefs, demand-rate UGens (Dseq,Drand,Duty, etc.), and@synthdefdecorator (trigger/audio/lag rates, complex graphs) -- 54 new tests (322 -> 376) -
qaCI job: runs ruff lint, ruff format check, mypy --strict, and pytest against the source tree on every push/PR -
Release workflow (
release.yml): tag-triggered publish to PyPI via trusted publisher,workflow_dispatchfor TestPyPI, auto-generated GitHub Release -
Docstrings on
SynthDefBuilder.build(),.add_parameter(), and.__getitem__() -
Plugin loading validation:
_options_to_world_kwargs()logs a warning when no UGen plugins path is found
Fixed¶
-
macOS CoreAudio teardown crash: registered a C-level
atexitguard in_scsynth.cpp(afterWorld_New) that calls_exit(0)before CoreAudio's static destructors run; removedos._exit(0)from all 11 demo scripts -
WorldStringsmemory leak in_scsynth.cpp: capsule destructor now frees the heap-allocated strings object -
Windows CI build failure caused by Strawberry Perl's incompatible ccache crashing MSVC (
STATUS_ENTRYPOINT_NOT_FOUND); disabled SC's ccache integration on Windows -
nanobind 2.11 compatibility: replaced capturing lambda in
_scsynth.cppcapsule constructor withWorldHandlestruct and non-capturing cleanup function -
Removed 11
type: ignore[arg-type]suppressions fromEnvGenby wideningUGen.__init__,_new_single, and_new_expandedkwargs toUGenRecursiveInput | None -
OSC decoder unbounded recursion:
_osc.cppblob/bundle recursive parsing now enforces a maximum nesting depth of 16 levels; beyond the limit, blobs are returned as raw bytes -
OSC decoder aggregate bounds checking:
decode_message_cleanpre-validates that the payload has enough bytes for all type tags before entering the decode loop -
world_send_packetconst_castremoval: OSC packet data is now copied into astd::vector<char>before passing toWorld_SendPacket, eliminating undefined behavior from casting away const on Python bytes -
scsynth_print_funcbuffer overflow: replaced fixed 4096-byte stack buffer with a two-pass approach that dynamically allocates when the formatted message exceeds the stack buffer
Changed¶
-
Protected
EmbeddedProcessProtocol._active_worldwiththreading.Lockto prevent race conditions on concurrentboot()calls -
Cleaned up
Envelope.serialize()andUGenSerializable.serialize()signatures: removed unused**kwargsparameter, added docstring documenting the wire format -
Extracted 6 enum classes (
CalculationRate,ParameterRate,BinaryOperator,UnaryOperator,DoneAction,EnvelopeShape) fromsynthdef.pyintoenums.py -
Extracted SCgf binary compiler (
_compile_*,_encode_*,compile_synthdefs) fromsynthdef.pyintocompiler.py -
Pinned
nanobind>=2.11,<3in both[build-system]and[dependency-groups] -
All
ValueErrorraises insynthdef.py(10) andenvelopes.py(2) now include descriptive messages -
Narrowed bare
except Exceptioninosc.pytoexcept (ValueError, IndexError, struct.error) -
Refactored all 11 demo scripts to use
_options_to_world_kwargs()instead of duplicated 25-line_options_kwargs()functions
[0.1.0]¶
Initial release.
Added¶
-
SynthDef compiler --
SynthDefBuildercontext manager and@synthdefdecorator for defining UGen graphs in Python, compiled to SuperCollider's SCgf binary format -
290+ UGen definitions across 18 categories: oscillators, filters, BEQ filters, noise, delays, envelopes, panning, demand, dynamics, chaos, granular, buffer I/O, physical modeling, reverb, convolution, I/O, lines, and triggers
-
Envelope system --
Envelopeclass withadsr,asr,linen,percussive, andtrianglefactory methods, plus theEnvGenUGen -
OSC codec --
OscMessageandOscBundleencode/decode with pure-Python implementation and C++ accelerated path via nanobind (_osc.cpp) -
Embedded libscsynth -- in-process SuperCollider engine via nanobind (
_scsynth.cpp), withEmbeddedProcessProtocolfor lifecycle management andOptionsfrozen dataclass for server configuration -
Vendored dependencies -- SuperCollider 3.14.1, libsndfile, and PortAudio built from source via
add_subdirectory; SC trimmed to 27MB (from 132MB) with only libscsynth, UGen plugins, and required boost headers; libsndfile tailored for WAV/AIFF only (no external codec deps). Audio backend: CoreAudio on macOS, vendored PortAudio on Linux/Windows -
Incremental builds --
make builduses--wheel --no-build-isolationwith persistent cmake build cache inbuild/; incremental rebuilds in ~3s -
Wheel repair -- platform-conditional wheel repair via delocate (macOS), auditwheel (Linux), delvewheel (Windows); SC's macOS
POST_BUILDbundle-copy commands disabled to prevent duplicate plugins leaking into the wheel -
Demos -- three example scripts: sine wave (
01_sine.py), subtractive synthesis (02_subtractive.py), and FM synthesis with melody (03_fm.py) -
Test suite -- 291 tests covering OSC round-trip encoding, SynthDef compilation, UGen instantiation and calculation rates, and server options/lifecycle
-
Full
mypy --strictcompliance with complete type annotations -
CI workflow -- GitHub Actions with cibuildwheel building wheels for CPython 3.10--3.13 across macOS ARM64, Linux x86_64, and Windows x86_64; sdist built separately; all artifacts aggregated into a single download
-
Development tooling -- Makefile with
dev,build,sdist,test,lint,format,typecheck,qa,clean, andresettargets