Repository navigation
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.