Skip to content

Releases: shakfu/numchuck

numchuck 0.3.0

Choose a tag to compare

@shakfu shakfu released this 22 Aug 21:23

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_listen was called with a hardcoded http://0.0.0.0:<port>, and there was no token, no origin check and no --host flag -- while the CLI printed http://localhost:<port>, understating the exposure. Since POST /api/compile runs arbitrary ChucK, and ChucK's standard library includes FileIO, anyone who could reach the port could read and write files as the user; the WebSocket REPL additionally exposed spork_file and replace_shred_file against arbitrary paths. Confirmed against a running instance. The listen address is now a property defaulting to 127.0.0.1, numchuck web gained --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 as Authorization: Bearer or ?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.url carries the token, and the CLI warns when bound wide.

  • numchuck web now 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 an Origin, and a local process sends none -- confirmed by an unauthenticated POST /api/compile from 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 a Cache-Control header, 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 on 127.0.0.1:8080 could 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 carries Cache-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 in sessionStorage, which is per-tab, so opening the IDE in a second tab authenticated nothing and its WebSocket closed immediately; it now uses localStorage, and a 401 discards the stored token so a stale one from an earlier run cannot wedge later loads. Separately, the REPL key handler called navigator.clipboard unguarded, and that API does not exist outside a secure context -- which any non-loopback bind is -- so Ctrl+C, Ctrl+X and Ctrl+V raised TypeError; 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 in sessionStorage so a reload does not lose it, and clears it from the address bar with history.replaceState so it does not linger in history or a screenshot. API calls carry it as Authorization: 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 to ws://. 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_upgrade was called with no Origin validation. WebSocket connections are not subject to the same-origin policy, so any page the user happened to visit while the IDE was running could open ws://localhost:8080/ws and drive the REPL -- which meant a loopback-only bind would not have been sufficient on its own. Every request carrying an Origin is now checked against its own Host header 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 no Origin (curl and other non-browser clients) are allowed; Origin: null, which is what sandboxed iframes and file:// pages send, does not match a Host and is refused.

  • A project-local .numchuck loaded native code without asking (paths.py, api.py, cli/main.py): get_numchuck_dir() preferred ./.numchuck whenever it existed, and Chuck.__init__ independently added ./.numchuck/chugins to 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 (on edit, repl, run, watch and web) or NUMCHUCK_LOCAL=1; enable_local_dir() and get_local_numchuck_dir() expose the switch, and Chuck.__init__ consults it before adding the local chugin path.

  • OSC server defaulted to all interfaces (osc.py, constants.py): OSCServer.host defaulted to 0.0.0.0, and OSCController maps incoming messages straight onto global writes and event signals, so the default handed VM control to anything on the network. It now defaults to DEFAULT_OSC_HOST (127.0.0.1); pass host="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() called dlopen() without RTLD_NODELETE, so ~Chuck_DLL() genuinely unmapped a chugin's text. Every chugin is deleted by Chuck_ImportRegistry::shutdown(), which ChucK::shutdown() reaches via CK_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 a std::thread per FFT block whose only joins are the next block boundary and ~ConvRev(), so the last block's worker outlives chuck.run() and can still be inside FFTConvolver::process() when the unmap lands. The next instruction fetch then faults. This was the "intermittent SIGSEGV on a background thread during ConvRev playback" tracked in TODO.md, at 7 crashes in 152 runs of pytest tests/test_api.py tests/test_examples.py. Seven captured faults were identical and unambiguous: SEGV_MAPERR with si_addr == RIP, the faulting thread never the main one, ConvRev.chug absent from /proc/self/maps while ~20 sibling chugins remained mapped, and both RIP and the return addresses on that thread's stack sitting in the hole it left -- resolving to .text+0xcda9, .text+0xcfcd and .text+0xcabe, that is ComplexMultiplyAccumulate called from ComplexMultiplyAccumulate called from FFTConvolver::process. Chugins are now opened with RTLD_NODELETE, and the Windows dlopen shim -- which ignored mode entirely -- honors it by pinning the module, since FreeLibrary() 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 never dlcloses 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 calls raise(signum), which glibc implements as tgkill(), so anything downstream -- a chained handler, AddressSanitizer -- sees si_code == SI_TKILL and an si_addr that is not a fault address at all but the si_pid/si_uid pair overlaid on the same union. That is the whole story behind the SEGV on unknown address 0x03e80002964c recorded in TODO.md: 0x3e8 is uid 1000 and 0x2964c is pid 169548. It was never a corrupted pointer. Running with -p no:faulthandler and installing a handler ahead of it yields the real SEGV_MAPERR and a usable `RI...

Read more

numchuck 0.2.1

Choose a tag to compare

@shakfu shakfu released this 01 Aug 12:56

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) and Chuck.ugen_samples(...) read the most recent samples from a named global UGen, which taps audio mid-graph rather than the summed dac output that run() 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) and list_taps(), with Chuck.add_tap() / remove_tap() / taps. Reading a UGen buffer from Python while the audio thread writes it is a data race -- Chuck_UGen keeps an 8192-sample AccumBuffer whose write offset is a plain integer and whose get_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 after chuck->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 at add_tap() instead of silently returning possibly-torn data; offline reads are unchanged, since the VM only advances inside run(), which holds the GIL.

  • Abort the running shred (src/_numchuck.cpp, api.py): ChucK.abort_current_shred() and Chuck.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, which remove_shred cannot 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 returns False when there is nothing to abort.

  • Shred lifecycle watcher (src/_numchuck.cpp, api.py, __init__.py): ChucK.subscribe_shred_watcher(callback, options) / remove_shred_watcher() and Chuck.on_shred(callback) deliver (code, shred_id, name) as shreds are sporked, removed, suspended or activated, replacing the need to poll get_all_shred_ids(). Subscription flags SHRED_WATCH_SPORK, _REMOVE, _SUSPEND, _ACTIVATE, _ALL and _NONE are 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_value and get_global_associative_float_array_value complete 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-level Chuck class 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() and Chuck.set_adaptive() / Chuck.adaptive read 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 raises RuntimeError and ValueError respectively instead.

  • Richer shred metadata (src/_numchuck.cpp): get_shred_info now also reports is_blocked (waiting on an event), wake_time, start and the shred's args.

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 audio get_ugen_samples() would have served that stale ring instead of refusing the read. Construction now releases everything keyed to the address it claims, and init() 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=True silently did nothing (api.py, constants.py): PARAM_VM_ADAPTIVE is a maximum block size, not a flag, and ChucK treats any value <= 1 as off -- so passing True set it to 1 and left adaptive mode disabled. Chuck(vm_adaptive=...) now accepts a block size directly, with True selecting DEFAULT_ADAPTIVE_BLOCK_SIZE (512), and the vm_adaptive property reports whether the size actually enables the mode.

numchuck 0.2.0

Choose a tag to compare

@shakfu shakfu released this 20 Jun 08:30

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): The FetchContent_Declare for libpd now pins GIT_TAG 0.16.0 (with GIT_SHALLOW TRUE) instead of tracking the moving master branch. 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 where master advancing 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 in ChucK::shutdown() for Windows (WASAPI/DirectSound threads need time to terminate before memory is freed; see docs/windows_fix.md). Without it, CI crashed with 0xC0000005 (exit 3221225477) during garbage-collection of Chuck instances between tests. Restored the delay, and added scripts/patches/0001-windows-shutdown-delay.patch plus an apply_chuck_patches step in update.sh that 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 made shutdown() public (upstream keeps it protected; it is invoked by ~ChucK()). Rather than re-patch the header, the binding's shutdown method no longer calls it directly and instead only clears the Python-side chout/cherr and instance callbacks. VM teardown happens when the high-level Chuck.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

Choose a tag to compare

@shakfu shakfu released this 26 Feb 00:45

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 new sync_shreds() method on ChuckSession diffs session.shreds against chuck.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_enable so there is zero cost when OTF is off. OTF-discovered shreds are tagged with shred_type="otf".

  • OTF CLI flags (cli/main.py, tui/tui.py, tui/repl.py, tui/editor.py, tui/common.py): Added --otf and --otf-port flags to the repl and edit subcommands. --otf enables the ChucK OTF listener (default port 8888), allowing external chuck commands 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): The wave command now displays live L/R peak meters below the shreds panel. Uses a background daemon thread polling get_audio_meters() at 100ms intervals with Application(refresh_interval=0.1) for redraws. Meters appear/disappear via ConditionalContainer tied to session.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.py as 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 check now runs RECORD validation alongside twine 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 require gtk+-3.0 on 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 collect job steps to run checkout before artifact downloads -- actions/checkout@v4 cleans the working directory by default, which was wiping the downloaded dist/ directory
  • FetchContent install leaking into wheels (thirdparty/chugins/{CLAP,PdPatch}/CMakeLists.txt):

    • Added EXCLUDE_FROM_ALL to FetchContent_Declare for CLAP and PdPatch to prevent their SDK install() rules from polluting the wheel with headers, pkgconfig, and cmake config files (reduced wheel from 186 to 100 files)
  • 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 via CK_FPRINTF_STDOUT, bypassing prompt_toolkit's full-screen renderer. Fix: setup_output_capture() now also sets the global/static ChucK.set_stdout_callback() and ChucK.set_stderr_callback() to route all VM system messages through the TUI log system. See docs/architecture.md for the two-tier callback architecture.

  • REPL exception handling (repl.py): _submit_input (TUI) and process_line (stdin) now catch exceptions from executor.execute() and spork_code(), displaying graceful error messages instead of crashing with tracebacks.

  • REPL shutdown crash (repl.py): Fixed malloc double-free / segfault on exit. Removed static set_stdout_callback/set_stderr_callback (which outlived the Python objects they referenced) in favor of instance-level callbacks cleaned up by chuck.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 a log_callback closure (capturing self) as static class-level callbacks via ChucK.set_stdout_callback() / ChucK.set_stderr_callback(). These outlived the instance and pinned the entire ChuckApplication -> ChucK object graph, preventing C++ destructor from running. Fix: cleanup() now clears static callbacks to None before 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 catches TimeoutExpired/OSError instead of running unsanitized with no output capture or error handling.

  • ConvRev segfault on ARM64 macOS CI (test_examples.py): Fixed segfault in test_chugin_convrev_example on macos-14 (ARM64). Root cause: _check_chugin_available() created up to 2 ChucK instances without calling shutdown(), leaving ConvRev's background std::thread active. 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 in test_examples.py now call remove_all_shreds() + shutdown() before going out of scope.

  • Windows CI test failures (test_waveform.py): Fixed 3 TestREPLMeterInfrastructure tests failing on Windows CI with NoConsoleScreenBufferError. These tests instantiate ChuckREPL() which creates a prompt_toolkit.Application requiring a real TTY. Fix: skip the test class when stdout is not a TTY.

Changed

  • REPL help panel (repl.py): F1 help text now shows word / symbol forms 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

Choose a tag to compare

@shakfu shakfu released this 24 Feb 00:46

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 .chug files) are now included in distributed wheels
    • NUMCHUCK_INSTALL_CHUGINS CMake option gates installation (ON for wheel builds, OFF for local dev)
    • cmake.args = ["-DNUMCHUCK_INSTALL_CHUGINS=ON"] in pyproject.toml enables bundling during pip install / uv build
    • Bundled chugins directory (<package>/chugins/) automatically added to ChucK search path in Chuck.__init__
    • Users no longer need to build from source to use chugins
  • numchuck info lists bundled chugins (src/numchuck/cli/main.py):

    • Shows count and names of all bundled .chug files from the installed package
    • Displays "none" for editable/dev installs where chugins are in examples/chugins/ instead
  • Cross-Platform Wheel Repair for Chugins (scripts/repair_wheel.py):

    • Wheel repair tools (delocate, auditwheel, delvewheel) skip .chug files since they only scan platform-standard extensions
    • New repair script temporarily renames .chug to 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 the wheel CLI 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
  • 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 chugin from CIBW_TEST_COMMAND filter
    • All three platforms (Linux, macOS, Windows) now use the custom repair script
    • Windows builds install delvewheel via CIBW_BEFORE_BUILD_WINDOWS
  • Chugin test helpers updated (tests/test_examples.py):

    • New _get_chugins_dir() helper checks bundled package directory first, falls back to examples/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

Fixed

  • TUI tests skipped on Windows (tests/conftest.py):

    • Added pytest_collection_modifyitems hook to skip @pytest.mark.tui tests on Windows
    • prompt_toolkit raises NoConsoleScreenBufferError on Windows in non-console environments
    • 42 TUI tests now skip gracefully on Windows instead of failing
  • WAV file rendering tests (tests/test_wavfile.py):

    • Fixed test_render_sine_to_wav run duration: chuck.run(44100) changed to chuck.run(44100 * 4) to match 4-second ChucK code duration
    • Added del chuck before temp directory cleanup in all tests to release file handles
    • Fixes PermissionError on Windows where files can't be deleted while open
  • Async awaitable race condition (src/numchuck/api.py):

    • get_int_awaitable, get_float_awaitable, get_string_awaitable had a race between run_in_executor completion and call_soon_threadsafe callback delivery
    • Added await asyncio.sleep(0) after run_in_executor to yield to the event loop and process pending callbacks before checking future.done()
    • Fixes intermittent "callback not invoked" failures on slower platforms (macOS x86_64 under Rosetta)

numchuck 0.1.9

Choose a tag to compare

@shakfu shakfu released this 27 Jan 20:50

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 applications
  • render() / to_wav() - Offline rendering to numpy arrays or WAV files
  • get_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.