Repository navigation
Releases: shakfu/numchuck
Release list
numchuck 0.3.0
Changes since the last release
Security
-
The web IDE bound every interface with no authentication (
src/_web.cpp,web/__init__.py,cli/main.py):mg_http_listenwas called with a hardcodedhttp://0.0.0.0:<port>, and there was no token, no origin check and no--hostflag -- while the CLI printedhttp://localhost:<port>, understating the exposure. SincePOST /api/compileruns arbitrary ChucK, and ChucK's standard library includesFileIO, anyone who could reach the port could read and write files as the user; the WebSocket REPL additionally exposedspork_fileandreplace_shred_fileagainst arbitrary paths. Confirmed against a running instance. The listen address is now a property defaulting to127.0.0.1,numchuck webgained--host, and any non-loopback bind is issued a random token automatically (secrets.token_urlsafe, 24 bytes) unless the caller passes one or explicitly waives it with--token "". The token is required on every/api/request and on the WebSocket upgrade, accepted asAuthorization: Beareror?token=(browsers cannot set headers on a WebSocket handshake), and compared without an early exit on the first differing byte so response latency does not leak it a character at a time.WebChuckServer.urlcarries the token, and the CLI warns when bound wide. -
numchuck webnow serves and opens the IDE in one step, authenticated (cli/main.py,web/__init__.py): the command already opened a browser, but only a non-loopback bind was issued a token, so the default run opened an unauthenticated page. That left a real hole: the origin check constrains browsers, but it only applies to requests that carry anOrigin, and a local process sends none -- confirmed by an unauthenticatedPOST /api/compilefrom a plain HTTP client, which compiled and ran ChucK. On a shared machine that is every other user on the box. A token is now minted for every bind, loopback included, and the CLI prints and opens the URL carrying it -- the same model Jupyter uses for a localhost bind.--token ""still waives auth deliberately, and now says so loudly. The CLI also reports when it could not open a browser instead of appearing to do nothing. -
Browsers served a cached page from a different server on the same port (
src/_web.cpp): no response carried aCache-Controlheader, so browsers fell back to heuristic caching (RFC 9111 4.2.2) and could reuse a response without revalidating. A loopback port is shared ground -- every local tool wants 8080 -- so this meant a document cached for an unrelated server on127.0.0.1:8080could be served in place of the IDE. Reported as "the frontend only works in Chrome": Safari was rendering a cached llama.cpp UI on numchuck's port, whose JS then called llama.cpp endpoints and reported "Server unavailable: 404 Not Found". Every response -- documents, static assets, JSON, and the 401/403/404 refusals -- now carriesCache-Control: no-store. VM state was never cacheable either, so the API needed this regardless. -
The web IDE lost its token in a new tab, and threw on copy/paste outside a secure context (
web/static/index.html): two cross-browser defects, both found while investigating a report that the frontend only worked in Chrome. The token was kept insessionStorage, which is per-tab, so opening the IDE in a second tab authenticated nothing and its WebSocket closed immediately; it now useslocalStorage, and a 401 discards the stored token so a stale one from an earlier run cannot wedge later loads. Separately, the REPL key handler callednavigator.clipboardunguarded, and that API does not exist outside a secure context -- which any non-loopback bind is -- so Ctrl+C, Ctrl+X and Ctrl+V raisedTypeError; both paths are now feature-detected with a fallback. Neither defect was browser-specific: the IDE was verified end to end in Chromium, Firefox and WebKit, and both bugs reproduced identically in all three. -
The bundled web IDE did not send the auth token (
web/static/index.html): the server enforced the token but the shipped page never presented one, so the IDE worked on loopback and was broken in exactly the configuration that requires auth -- every API call would have answered 401 and the WebSocket upgrade would have been refused. The page now takes the token from the?token=the CLI opens it with, keeps it insessionStorageso a reload does not lose it, and clears it from the address bar withhistory.replaceStateso it does not linger in history or a screenshot. API calls carry it asAuthorization: Bearer; the WebSocket URL carries it as a query parameter, since browsers cannot set headers on a handshake. A 401 or 403 is now reported in the console instead of the call silently doing nothing, and the socket scheme follows the page protocol rather than being hardcoded tows://. Static files are deliberately still served without a token, because the page has to load before it can read one. Covered by raw WebSocket handshake tests and by a contract test asserting the shipped asset satisfies what the server enforces -- the gap existed because every other test drove the API directly rather than through the page. -
Cross-site WebSocket hijacking (
src/_web.cpp):mg_ws_upgradewas called with noOriginvalidation. WebSocket connections are not subject to the same-origin policy, so any page the user happened to visit while the IDE was running could openws://localhost:8080/wsand drive the REPL -- which meant a loopback-only bind would not have been sufficient on its own. Every request carrying anOriginis now checked against its ownHostheader and refused with 403 on a mismatch, across the API, the static files and the upgrade. Comparing the request against itself rather than against a configured address needs no configuration and works identically for loopback, LAN-IP and hostname binds. Requests with noOrigin(curl and other non-browser clients) are allowed;Origin: null, which is what sandboxed iframes andfile://pages send, does not match aHostand is refused. -
A project-local
.numchuckloaded native code without asking (paths.py,api.py,cli/main.py):get_numchuck_dir()preferred./.numchuckwhenever it existed, andChuck.__init__independently added./.numchuck/chuginsto the chugin search path. Chugins are native shared libraries loaded into the process, so cloning a repository and running numchuck inside it executed whatever native code that repository shipped, with no prompt and nothing printed. The local directory is now opt-in via--local(onedit,repl,run,watchandweb) orNUMCHUCK_LOCAL=1;enable_local_dir()andget_local_numchuck_dir()expose the switch, andChuck.__init__consults it before adding the local chugin path. -
OSC server defaulted to all interfaces (
osc.py,constants.py):OSCServer.hostdefaulted to0.0.0.0, andOSCControllermaps incoming messages straight onto global writes and event signals, so the default handed VM control to anything on the network. It now defaults toDEFAULT_OSC_HOST(127.0.0.1); passhost="0.0.0.0"deliberately to accept messages from other machines.
Fixed
-
Chugins were unloaded while their own threads were still running (
thirdparty/chuck/core/chuck_dl.cpp,scripts/patches/0004-chugin-rtld-nodelete.patch):Chuck_DLL::load()calleddlopen()withoutRTLD_NODELETE, so~Chuck_DLL()genuinely unmapped a chugin's text. Every chugin is deleted byChuck_ImportRegistry::shutdown(), whichChucK::shutdown()reaches viaCK_SAFE_DELETE(m_carrier->compiler)-- but nothing guarantees the objects a chugin created are already gone by then, and a chugin may have started a thread of its own. ConvRev does:ConvRev::tick()spawns astd::threadper FFT block whose only joins are the next block boundary and~ConvRev(), so the last block's worker outliveschuck.run()and can still be insideFFTConvolver::process()when the unmap lands. The next instruction fetch then faults. This was the "intermittent SIGSEGV on a background thread during ConvRev playback" tracked inTODO.md, at 7 crashes in 152 runs ofpytest tests/test_api.py tests/test_examples.py. Seven captured faults were identical and unambiguous:SEGV_MAPERRwithsi_addr == RIP, the faulting thread never the main one,ConvRev.chugabsent from/proc/self/mapswhile ~20 sibling chugins remained mapped, and bothRIPand the return addresses on that thread's stack sitting in the hole it left -- resolving to.text+0xcda9,.text+0xcfcdand.text+0xcabe, that isComplexMultiplyAccumulatecalled fromComplexMultiplyAccumulatecalled fromFFTConvolver::process. Chugins are now opened withRTLD_NODELETE, and the Windowsdlopenshim -- which ignoredmodeentirely -- honors it by pinning the module, sinceFreeLibrary()has the same hazard. Not unmapping costs only address space, and unloading plugin code that can outlive its own unload is unsafe in general, which is why CPython neverdlcloses extension modules either. Verified over 400 runs of the reproducer. -
faulthandler's re-raise made every crash address look like a corrupted pointer: worth recording because it sent the previous investigation down a blind alley. CPython's fatal-signal handler prints its traceback and then callsraise(signum), which glibc implements astgkill(), so anything downstream -- a chained handler, AddressSanitizer -- seessi_code == SI_TKILLand ansi_addrthat is not a fault address at all but thesi_pid/si_uidpair overlaid on the same union. That is the whole story behind theSEGV on unknown address 0x03e80002964crecorded inTODO.md:0x3e8is uid 1000 and0x2964cis pid 169548. It was never a corrupted pointer. Running with-p no:faulthandlerand installing a handler ahead of it yields the realSEGV_MAPERRand a usable `RI...
numchuck 0.2.1
Changes since the last release
Added
-
Global UGen sample tap (
src/_numchuck.cpp,api.py):ChucK.get_ugen_samples(name, num_frames, num_channels=1)andChuck.ugen_samples(...)read the most recent samples from a namedglobal UGen, which taps audio mid-graph rather than the summed dac output thatrun()returns. Returns a float32 array, 1-D for mono and channel-major(channels, frames)otherwise, matching ChucK's non-interleaved multichannel layout. The ChucK side must enable buffering first (1 => tap.buffered;), otherwise ChucK fills the buffer with zeros. -
Race-free tap capture during real-time audio (
src/_numchuck.cpp,api.py):add_tap(name, num_channels, capacity_frames),remove_tap(name)andlist_taps(), withChuck.add_tap()/remove_tap()/taps. Reading a UGen buffer from Python while the audio thread writes it is a data race --Chuck_UGenkeeps an 8192-sampleAccumBufferwhose write offset is a plain integer and whoseget_most_recent()memcpy's with no synchronization. Measured on a pure sine: 831 of 274,240 reads of the full ring came back spliced (0.3%), with discontinuities up to 9x the waveform's own maximum sample-to-sample step; smaller reads had ring headroom and showed none. A registered tap is instead sampled on the audio thread immediately afterchuck->run()returns, when nothing else is writing, and appended to a per-tap ring published under a seqlock, so the audio thread never blocks and a colliding reader retries rather than returning spliced data. The same measurement through a tap: zero torn reads in 1.2 million. Taps also accumulate history across callbacks, so reads are not limited to one audio block. While real-time audio is running, reading an unregistered UGen now raises with a message pointing atadd_tap()instead of silently returning possibly-torn data; offline reads are unchanged, since the VM only advances insiderun(), which holds the GIL. -
Abort the running shred (
src/_numchuck.cpp,api.py):ChucK.abort_current_shred()andChuck.abort_shred()flag the shred currently executing in the VM. This is the only way to break out of a shred stuck in a loop that never advances time, whichremove_shredcannot reach. The VM only has a current shred while inside a compute cycle, so this applies during real-time audio, called from another thread; it returnsFalsewhen there is nothing to abort. -
Shred lifecycle watcher (
src/_numchuck.cpp,api.py,__init__.py):ChucK.subscribe_shred_watcher(callback, options)/remove_shred_watcher()andChuck.on_shred(callback)deliver(code, shred_id, name)as shreds are sporked, removed, suspended or activated, replacing the need to pollget_all_shred_ids(). Subscription flagsSHRED_WATCH_SPORK,_REMOVE,_SUSPEND,_ACTIVATE,_ALLand_NONEare exported from the package. One watcher per instance; it is unsubscribed on shutdown so no notification can arrive after the Python callable is dropped. -
Indexed and associative global array getters (
src/_numchuck.cpp,api.py):get_global_int_array_value,get_global_float_array_value,get_global_associative_int_array_valueandget_global_associative_float_array_valuecomplete the array API, whose setters already existed -- values written by key from Python could not previously be read back, since an associative array has no whole-array fetch. The high-levelChuckclass gained a global-array section covering whole arrays, elements by index and entries by key (set_int_array/get_int_array,set_int_array_value/get_int_array_value,set_assoc_int/get_assoc_int, and float equivalents). -
Runtime adaptive block size (
src/_numchuck.cpp,api.py):ChucK.set_adaptive(max_block_size)/get_adaptive()andChuck.set_adaptive()/Chuck.adaptiveread and change the shreduler's adaptive block processing on a running VM. Guarded on both sides: UGens allocate their vectorized buffers when they are created (the dac, adc and bunghole during VM init), so enabling adaptive mode on a VM that started non-adaptive would run the vectorized code path over buffers that were never allocated -- a segfault -- and raising the size past what init allocated overruns them. The binding raisesRuntimeErrorandValueErrorrespectively instead. -
Richer shred metadata (
src/_numchuck.cpp):get_shred_infonow also reportsis_blocked(waiting on an event),wake_time,startand the shred'sargs.
Fixed
-
A new ChucK instance inherited a destroyed one's taps (
src/_numchuck.cpp): Tap slots are keyed by the instance pointer, and the allocator hands a destroyed instance's address straight to the next one. An instance released by the garbage collector never runs the shutdown path that deactivates its slots, so its registrations outlived it and were adopted by whichever instance landed on that address -- measured at 18 of 20 attempts.list_taps()reported a tap the caller never registered, and during real-time audioget_ugen_samples()would have served that stale ring instead of refusing the read. Construction now releases everything keyed to the address it claims, andinit()does the same for the shred watcher, which is keyed by the VM pointer and equally reusable. The tap slot's owner became atomic, since it is now cleared while the audio thread may be scanning slots. Caught by CI on macOS x86_64: a tap leaked from one test surfaced as a stale name several tests later, on a run whose test filter changed the allocation pattern enough to reuse the address. The pointer-keyed chout/cherr maps are cleared by the same path; they were stale but unreachable, because their C++ hook has to be installed on each instance separately. -
vm_adaptive=Truesilently did nothing (api.py,constants.py):PARAM_VM_ADAPTIVEis a maximum block size, not a flag, and ChucK treats any value<= 1as off -- so passingTrueset it to 1 and left adaptive mode disabled.Chuck(vm_adaptive=...)now accepts a block size directly, withTrueselectingDEFAULT_ADAPTIVE_BLOCK_SIZE(512), and thevm_adaptiveproperty reports whether the size actually enables the mode.
numchuck 0.2.0
Changes since the last Release
Changed
-
Upgraded ChucK core to 1.5.5.9-dev (chai) (
thirdparty/chuck/): Updated the vendored ChucK to the current development build. Required one binding-level adjustment, noted under Fixed below. -
Pinned PdPatch's libpd dependency to a release tag (
thirdparty/chugins/PdPatch/CMakeLists.txt): TheFetchContent_Declarefor libpd now pinsGIT_TAG 0.16.0(withGIT_SHALLOW TRUE) instead of tracking the movingmasterbranch. libpd 0.16.0 bundles pure-data 0.56-3 via its pinned submodule. This makes the build reproducible and fixes a configure-time failure wheremasteradvancing upstream left the cached libpd checkout dirty and blocked the FetchContent git update step.
Fixed
-
Windows access violation on teardown, regressed by the ChucK update (
thirdparty/chuck/core/chuck.cpp,scripts/update.sh,scripts/patches/): The ChucK core update overwrote vendored source and dropped the numchuck local patch that adds a 50ms post-stop delay inChucK::shutdown()for Windows (WASAPI/DirectSound threads need time to terminate before memory is freed; seedocs/windows_fix.md). Without it, CI crashed with0xC0000005(exit 3221225477) during garbage-collection ofChuckinstances between tests. Restored the delay, and addedscripts/patches/0001-windows-shutdown-delay.patchplus anapply_chuck_patchesstep inupdate.shthat re-applies local chuck patches after every update and hard-fails if one no longer applies -- so this cannot silently regress again. -
Build break after ChucK core update:
ChucK::shutdown()is now protected (src/_numchuck.cpp): The update reverted numchuck's local patch that had madeshutdown()public (upstream keeps it protected; it is invoked by~ChucK()). Rather than re-patch the header, the binding'sshutdownmethod no longer calls it directly and instead only clears the Python-side chout/cherr and instance callbacks. VM teardown happens when the high-levelChuck.close()drops its sole reference to the instance, triggering the destructor -- preserving the prior teardown order (callbacks cleared before VM shutdown), and the restored 50ms delay still runs via the destructor on both the explicit-close and garbage-collection paths.
numchuck
Python bindings for the ChucK audio programming language using nanobind.
The numchuck library provides interactive control over ChucK, enabling live coding workflows, bidirectional Python/ChucK communication, and comprehensive VM introspection—all while maintaining the existing real-time and offline audio capabilities.
Changes since the last Release
Added
-
OTF shred tracking (
session.py,common.py,repl.py,editor.py): Shreds added or removed via on-the-fly programming (chuck --add,chuck --remove) are now automatically tracked in the REPL and editor. A newsync_shreds()method onChuckSessiondiffssession.shredsagainstchuck.get_all_shred_ids()and reconciles: externally-added shreds appear in the topbar,?/shreds table, and status bar count; finished or externally-removed shreds are pruned. Sync runs on each render tick (via the bottom toolbar callback in the REPL, status bar callback in the editor), guarded by_otf_enableso there is zero cost when OTF is off. OTF-discovered shreds are tagged withshred_type="otf". -
OTF CLI flags (
cli/main.py,tui/tui.py,tui/repl.py,tui/editor.py,tui/common.py): Added--otfand--otf-portflags to thereplandeditsubcommands.--otfenables the ChucK OTF listener (default port 8888), allowing externalchuckcommands to add/remove shreds. The OTF port is shown in the REPL status bar when enabled. -
REPL real-time stereo level meters (
repl.py,waveform.py): Thewavecommand now displays live L/R peak meters below the shreds panel. Uses a background daemon thread pollingget_audio_meters()at 100ms intervals withApplication(refresh_interval=0.1)for redraws. Meters appear/disappear viaConditionalContainertied tosession.show_waveform. Styled green-on-dark-green (class:waveform-area). -
REPL word aliases for symbol commands (
parser.py,lang/constants.py): Every terse symbol command now has a readable word equivalent. New aliases:shreds(?),shred <id>(? <id>),globals(?g),audio(?a),start(>),stop(||),shutdown(X),compile <file>(: <file>),exec "code"(! "code"),shell <cmd>($ <cmd>),snippet <name>(@<name>),get <var>(<var>?),set <var> <val>(<var>::<val>),signal <ev>(<ev>!),broadcast <ev>(<ev>!!). All word aliases are tab-completable. -
REPL command cheatsheet (
docs/repl-commands.md): Standalone reference documenting every REPL command organized by category, showing both symbol and word forms. -
Wheel RECORD Validation (
scripts/check_wheel_record.py,scripts/repair_wheel.py):- New standalone script validates wheel RECORD files against actual ZIP contents -- checks for smuggled files, dangling entries, hash mismatches, and size mismatches
- Addresses wheel archive confusion attacks
- Integrated into
repair_wheel.pyas a post-repair validation step for both chugin and non-chugin wheels - Added RECORD validation steps to CI wheels workflow (per-platform and combined artifact stages)
make checknow runs RECORD validation alongsidetwine check
-
CLAP, PdPatch, and VST3 Chugins (
thirdparty/chugins/{CLAP,PdPatch,VST3}/):- Added three new chugins from my-chugins: CLAP (host CLAP plugins), PdPatch (embed Pure Data patches), and VST3 (host VST3 plugins)
- All three are enabled by default via
option(CM_CLAP/CM_PDPATCH/CM_VST3 ... ON)and bundled in release wheels - External dependencies (clap SDK 1.2.6, libpd, VST3 SDK v3.8.0) fetched automatically via CMake
FetchContent - Can be individually disabled with
-DCM_CLAP=OFF,-DCM_PDPATCH=OFF, or-DCM_VST3=OFF - macOS and Linux only -- all three depend on POSIX APIs (
dlfcn.h,dirent.h, pthreads) not available on Windows - Fixed VST3 SDK option names (
SMTG_ENABLE_VST3_PLUGIN_EXAMPLES/SMTG_ENABLE_VST3_HOSTING_EXAMPLES) to correctly disable SDK samples that requiregtk+-3.0on Linux - The numchuck package now includes:
- AbletonLink.chug
- ABSaturator.chug
- AmbPan.chug
- AudioUnit.chug
- Binaural.chug
- Bitcrusher.chug
- CLAP.chug
- ConvRev.chug
- Elliptic.chug
- ExpDelay.chug
- ExpEnv.chug
- FIR.chug
- FoldbackSaturator.chug
- GVerb.chug
- KasFilter.chug
- Ladspa.chug
- Line.chug
- MagicSine.chug
- Mesh2D.chug
- MIAP.chug
- Multicomb.chug
- NHHall.chug
- Overdrive.chug
- PanN.chug
- Patch.chug
- PdPatch.chug
- Perlin.chug
- PitchTrack.chug
- PowerADSR.chug
- Random.chug
- Range.chug
- RegEx.chug
- Sigmund.chug
- Spectacle.chug
- VST3.chug
- Wavetable.chug
- WinFuncEnv.chug
- WPDiodeLadder.chug
- WPKorg35.chug
- XML.chug
Fixed
-
Wheels workflow collect job (
.github/workflows/wheels.yml):- Reordered
collectjob steps to runcheckoutbefore artifact downloads --actions/checkout@v4cleans the working directory by default, which was wiping the downloadeddist/directory
- Reordered
-
FetchContent install leaking into wheels (
thirdparty/chugins/{CLAP,PdPatch}/CMakeLists.txt):- Added
EXCLUDE_FROM_ALLtoFetchContent_Declarefor CLAP and PdPatch to prevent their SDKinstall()rules from polluting the wheel with headers, pkgconfig, and cmake config files (reduced wheel from 186 to 100 files)
- Added
-
REPL ghost line on shred removal (
common.py): Fixed terminal rendering corruption (ghost line at bottom of screen) when removing shreds while level meters were visible. Root cause: ChucK's VM writes[chuck]: (VM) removing shred: ...directly to stdout viaCK_FPRINTF_STDOUT, bypassing prompt_toolkit's full-screen renderer. Fix:setup_output_capture()now also sets the global/staticChucK.set_stdout_callback()andChucK.set_stderr_callback()to route all VM system messages through the TUI log system. Seedocs/architecture.mdfor the two-tier callback architecture. -
REPL exception handling (
repl.py):_submit_input(TUI) andprocess_line(stdin) now catch exceptions fromexecutor.execute()andspork_code(), displaying graceful error messages instead of crashing with tracebacks. -
REPL shutdown crash (
repl.py): Fixed malloc double-free / segfault on exit. Removed staticset_stdout_callback/set_stderr_callback(which outlived the Python objects they referenced) in favor of instance-level callbacks cleaned up bychuck.shutdown(). Reordered cleanup to break all reference cycles before destroying C++ objects. -
nanobind ref leak on shutdown (
common.py): Fixed "leaked 1 instances" / "leaked 1 types" nanobind warnings on exit. Root cause:setup_output_capture()registers alog_callbackclosure (capturingself) as static class-level callbacks viaChucK.set_stdout_callback()/ChucK.set_stderr_callback(). These outlived the instance and pinned the entireChuckApplication -> ChucKobject graph, preventing C++ destructor from running. Fix:cleanup()now clears static callbacks toNonebefore breaking instance references. -
Shell command execution (
commands.py):$ <cmd>now captures stdout/stderr (routed through_log()), enforces a 30-second timeout, reports nonzero exit codes, and catchesTimeoutExpired/OSErrorinstead of running unsanitized with no output capture or error handling. -
ConvRev segfault on ARM64 macOS CI (
test_examples.py): Fixed segfault intest_chugin_convrev_exampleon macos-14 (ARM64). Root cause:_check_chugin_available()created up to 2 ChucK instances without callingshutdown(), leaving ConvRev's backgroundstd::threadactive. When the test then created a new instance, concurrent ConvRev destructor/thread cleanup races caused a segfault (ARM64's stricter memory ordering exposed this). Fix: all ChucK instances intest_examples.pynow callremove_all_shreds()+shutdown()before going out of scope. -
Windows CI test failures (
test_waveform.py): Fixed 3TestREPLMeterInfrastructuretests failing on Windows CI withNoConsoleScreenBufferError. These tests instantiateChuckREPL()which creates aprompt_toolkit.Applicationrequiring a real TTY. Fix: skip the test class when stdout is not a TTY.
Changed
-
REPL help panel (
repl.py): F1 help text now showsword / symbolforms side by side for all commands that have both. -
REPL inline transcript (
repl.py): Replaced split input/output layout with a single-buffer Python/Jupyter-style transcript. Input and output are interleaved in one scrollable area -- commands echo as[=>] ..., output is indented, errors show inline with[!]prefix. Removed separate log window (F3 toggle), error bar, and transcript TextArea. Input prompt sits at the bottom of the transcript buffer.
numchuck 0.1.10
Python bindings for the ChucK audio programming language using nanobind.
The numchuck library provides interactive control over ChucK, enabling live coding workflows, bidirectional Python/ChucK communication, and comprehensive VM introspection—all while maintaining the existing real-time and offline audio capabilities.
Changes from the last Release
Added
-
Bundled Chugins in Wheels (
scripts/cmake/fn_add_chugin.cmake,pyproject.toml,src/numchuck/api.py):- Pre-built chugins (~37
.chugfiles) are now included in distributed wheels NUMCHUCK_INSTALL_CHUGINSCMake option gates installation (ON for wheel builds, OFF for local dev)cmake.args = ["-DNUMCHUCK_INSTALL_CHUGINS=ON"]inpyproject.tomlenables bundling duringpip install/uv build- Bundled chugins directory (
<package>/chugins/) automatically added to ChucK search path inChuck.__init__ - Users no longer need to build from source to use chugins
- Pre-built chugins (~37
-
numchuck infolists bundled chugins (src/numchuck/cli/main.py):- Shows count and names of all bundled
.chugfiles from the installed package - Displays "none" for editable/dev installs where chugins are in
examples/chugins/instead
- Shows count and names of all bundled
-
Cross-Platform Wheel Repair for Chugins (
scripts/repair_wheel.py):- Wheel repair tools (delocate, auditwheel, delvewheel) skip
.chugfiles since they only scan platform-standard extensions - New repair script temporarily renames
.chugto the native extension (.dylib/.so/.dll), runs the repair tool, then renames back - Ensures chugin shared library dependencies are properly bundled if any chugin gains external (non-system) dependencies
- Uses only stdlib (
zipfile,hashlib,csv) -- no dependency on thewheelCLI package - RECORD file correctly regenerated after rename operations
- Platform-specific handling:
- macOS:
.chug->.dylib, delocate-wheel with--require-archs - Linux:
.chug->.so, auditwheel repair - Windows:
.chug->.dll, delvewheel with--analyze-existing --no-mangle
- macOS:
- Wheel repair tools (delocate, auditwheel, delvewheel) skip
-
README: Bundled chugins documentation (
README.md):- Categorized table of all 37 bundled chugins (38 on macOS with AudioUnit)
- Updated "Using Chugins" section to show automatic discovery (no manual path setup)
- Plugin Support overview links to the bundled chugins table
Changed
-
CI: Chugin tests enabled in wheel builds (
.github/workflows/wheels.yml):- Removed
and not chuginfromCIBW_TEST_COMMANDfilter - All three platforms (Linux, macOS, Windows) now use the custom repair script
- Windows builds install delvewheel via
CIBW_BEFORE_BUILD_WINDOWS
- Removed
-
Chugin test helpers updated (
tests/test_examples.py):- New
_get_chugins_dir()helper checks bundled package directory first, falls back toexamples/chugins/for dev builds - All chugin tests (
test_chugin_loading,test_chugin_bitcrusher_strict,test_chugin_gverb_strict,test_chugin_convrev_example) use the unified helper
- New
Fixed
-
TUI tests skipped on Windows (
tests/conftest.py):- Added
pytest_collection_modifyitemshook to skip@pytest.mark.tuitests on Windows - prompt_toolkit raises
NoConsoleScreenBufferErroron Windows in non-console environments - 42 TUI tests now skip gracefully on Windows instead of failing
- Added
-
WAV file rendering tests (
tests/test_wavfile.py):- Fixed
test_render_sine_to_wavrun duration:chuck.run(44100)changed tochuck.run(44100 * 4)to match 4-second ChucK code duration - Added
del chuckbefore temp directory cleanup in all tests to release file handles - Fixes
PermissionErroron Windows where files can't be deleted while open
- Fixed
-
Async awaitable race condition (
src/numchuck/api.py):get_int_awaitable,get_float_awaitable,get_string_awaitablehad a race betweenrun_in_executorcompletion andcall_soon_threadsafecallback delivery- Added
await asyncio.sleep(0)afterrun_in_executorto yield to the event loop and process pending callbacks before checkingfuture.done() - Fixes intermittent "callback not invoked" failures on slower platforms (macOS x86_64 under Rosetta)
numchuck 0.1.9
This release introduces a browser-based Web IDE for ChucK, bringing live coding to the browser with full feature parity to the terminal REPL.
Highlights
Web IDE (numchuck web)
- Browser-based ChucK IDE with code editor (codemirror) and interactive REPL
(xterm.js) - Real-time audio metering with RMS/peak level visualization
- Multi-file tabs, globals panel with interactive sliders, shred management
- Works completely offline - all assets bundled locally
- Full command parity with TUI REPL
New APIs
chuck.stream()- Iterator-based audio processing for real-time applicationsrender()/to_wav()- Offline rendering tonumpyarrays or WAV filesget_audio_meters()- Real-time RMS/peak audio levels
Developer Experience
- Automatic chugin discovery from
~/.numchuck/chugins - UGen parameter autocomplete (40+ UGens)
- File watch mode with auto-reload
- Configurable themes and key bindings
- User directory template with snippets and examples
ChucK Command Alignment
- Added
abort,=(replace),^(status) shortcuts - Full compatibility with ChucK's command syntax
Breaking Changes
- Dropped Python 3.8 and 3.9 support (now requires Python 3.10+)
- Removed chump package manager integration
See the CHANGELOG.md for the complete list of changes.