Python bindings for the ChucK audio programming language using nanobind.
The pychuck 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.
pychuck is a high-performance Python wrapper for ChucK that provides:
-
Python Programmatic Access to ChucK API — Load, compile, and concurrently execute
.ckfiles and ChucK code into audio processing or generated shreds. Manage the VM using python code: configure parameters, monitor timing, and control shred lifecycles. -
Flexible Execution — Choose between real-time audio playback and recording using asynchronous RtAudio or offline input from and rendering to
numpyarrays. -
Advanced Audio Processing — Harness ChucK's complete synthesis, filtering, and DSP capabilities.
-
Live Coding — Hot-swap code, replace active shreds, and inspect VM state in real time.
-
Plugin Support — Extend functionality with ChucK chugins for additional instruments and effects.
-
Dynamic Interaction - Bidirectional communication between ChucK and Python through global variables, event triggers, and callbacks.
-
Multi-Tab Editor — Full-screen ChucK editor with syntax highlighting; use F5 to spork and F6 to replace.
-
Interactive REPL — Terminal-style interface supporting ChucK commands and code completion.
-
Automatic Versioning — Keeps track of live coding sessions (
file.ck → file-1.ck → file-1-1.ck). -
Command-Line Mode — Run ChucK files directly from the terminal, with support for duration and silent modes.
# Clone the repository
git clone <repository-url>
cd pychuck
# Build the extension
make build
# Run tests
make testpychuck provides three modes of operation:
# Launch the editor
python -m pychuck edit
# Open specific files in tabs
python -m pychuck edit bass.ck melody.ck
# Enable project versioning
python -m pychuck edit --project mymusic
# Start with audio enabled
python -m pychuck edit --start-audio --project mymusicEditor Features:
- Multi-tab editing with ChucK syntax highlighting
- F5 or Ctrl-R to spork (compile and run current buffer)
- F6 to replace running shred with current buffer
- Ctrl-O to open files with interactive dialog (Tab for path completion)
- Ctrl-S to save files
- Ctrl-T for new tab, Ctrl-W to close tab
- Ctrl-N/Ctrl-P (or Ctrl-PageDown/PageUp) to navigate tabs
- Tab names show shred IDs after sporking (e.g.,
bass-1.ck) - Project versioning: file.ck → file-1.ck → file-1-1.ck
- F1/F2/F3 for help/shreds/log windows
- Ctrl-Q to exit
# Launch the REPL
python -m pychuck repl
# Load files on startup
python -m pychuck repl bass.ck melody.ck
# Enable project versioning
python -m pychuck repl --project mymusic
# Start with audio enabled
python -m pychuck repl --start-audio
# Disable smart Enter mode
python -m pychuck repl --no-smart-enter
# Hide sidebar (can toggle with F2)
python -m pychuck repl --no-sidebarREPL Commands:
add <file>or+ <file>- Spork a fileremove <id>or- <id>- Remove a shredremove allor- all- Remove all shredsreplace <id> <file>- Replace shred with filestatus- Show VM statustime- Show ChucK time- Type
helpor press F1 for full command reference
# Execute ChucK files from command line
python -m pychuck run myfile.ck
# Run multiple files
python -m pychuck run bass.ck melody.ck
# Run for 10 seconds then exit
python -m pychuck run myfile.ck --duration 10
# Silent mode (no audio)
python -m pychuck run myfile.ck --silent
# Custom sample rate
python -m pychuck run myfile.ck --srate 48000# Show version
python -m pychuck version
# Show ChucK and pychuck info
python -m pychuck infoInterface Features:
- Full-screen layout: Professional terminal UI with multiple display areas
- Live topbar: Minimal display showing shred IDs
[1] [2] [3] - Shreds table: Detailed shred information table (F2) with ID, name (folder/file), and elapsed time since spork
- Error display bar: Red error bar shows command errors without disrupting layout
- Help window: Built-in command reference (toggle with F1)
- Log window: Scrollable ChucK VM output capture (toggle with Ctrl+L)
- Mouse support: Scroll through log output with mouse wheel
- Scrollable input: Main input area with scrollbar for long code
Editing Features:
- Smart Enter mode: Enter submits commands immediately, but allows multiline ChucK code editing
- ChucK syntax highlighting: Full Pygments lexer for ChucK language with color themes
- ChucK code completion: Tab completion for keywords, types, UGens, and standard library
- Intelligent code detection: Automatically compiles multiline ChucK code
- Tab completion: Commands,
.ckfiles, and ChucK language elements - Command history: Persistent history with Ctrl+R search
- Colored prompt:
[=>]matches ChucK logo styling
Common Keyboard Shortcuts (Editor & REPL):
F1- Toggle help windowF2- Toggle shreds table (detailed view with ID, folder/filename, elapsed time)F3- Toggle log window (ChucK VM output)Ctrl+Q- Exit applicationTab- Command and ChucK code completionUp/Down- Navigate command history
When using --project <name>, pychuck automatically versions your files as you livecode:
~/.pychuck/projects/mymusic/
bass.ck # Original file
bass-1.ck # After first spork (shred ID 1)
bass-1-1.ck # After first replace of shred 1
bass-1-2.ck # After second replace of shred 1
melody-2.ck # Second file sporked (shred ID 2)
melody-2-1.ck # After replace of shred 2This creates a complete history of your livecoding session, making it easy to:
- Review your creative process
- Recover previous versions
- Replay session timeline
- Share reproducible livecoding performances
import pychuck
import time
# Create and configure ChucK
chuck = pychuck.ChucK()
chuck.set_param(pychuck.PARAM_SAMPLE_RATE, 44100)
chuck.set_param(pychuck.PARAM_OUTPUT_CHANNELS, 2)
chuck.init()
# Compile ChucK code
chuck.compile_code('''
SinOsc s => dac;
440 => s.freq;
while(true) { 1::samp => now; }
''')
# Start real-time audio playback
pychuck.start_audio(chuck)
time.sleep(2) # Play for 2 seconds
pychuck.stop_audio()
pychuck.shutdown_audio()import pychuck
import numpy as np
# Create ChucK instance
chuck = pychuck.ChucK()
chuck.set_param(pychuck.PARAM_SAMPLE_RATE, 44100)
chuck.set_param(pychuck.PARAM_OUTPUT_CHANNELS, 2)
chuck.init()
# Compile code
chuck.compile_code('''
SinOsc s => dac;
440 => s.freq;
while(true) { 1::samp => now; }
''')
# Render to numpy array
frames = 512
output = np.zeros(frames * 2, dtype=np.float32)
chuck.run(np.zeros(0, dtype=np.float32), output, frames)__init__()- Create a new ChucK instanceinit() -> bool- Initialize ChucK with current parametersstart() -> bool- Explicitly start ChucK VM (called implicitly byrun()if needed)
set_param(name: str, value: int) -> int- Set integer parameterset_param_float(name: str, value: float) -> int- Set float parameterset_param_string(name: str, value: str) -> int- Set string parameterset_param_string_list(name: str, value: list[str]) -> int- Set string list parameterget_param_int(name: str) -> int- Get integer parameterget_param_float(name: str) -> float- Get float parameterget_param_string(name: str) -> str- Get string parameterget_param_string_list(name: str) -> list[str]- Get string list parameter
-
compile_code(code: str, args: str = "", count: int = 1, immediate: bool = False, filepath: str = "") -> tuple[bool, list[int]]- Compile ChucK code from string
- Returns:
(success, shred_ids) - Parameters:
code: ChucK code to compileargs: Additional arguments (separated by ':')count: Number of shred instances to sporkimmediate: If True, schedule immediately; if False, queue for next time stepfilepath: Optional filepath for path-related operations
-
compile_file(path: str, args: str = "", count: int = 1, immediate: bool = False) -> tuple[bool, list[int]]- Compile ChucK code from file
- Returns:
(success, shred_ids)
run(input: np.ndarray, output: np.ndarray, num_frames: int)- Process audio for specified number of frames (synchronous/offline)
input: Input buffer (1D numpy array, dtype=np.float32)- Size must be
num_frames * input_channels
- Size must be
output: Output buffer (1D numpy array, dtype=np.float32, C-contiguous)- Size must be
num_frames * output_channels
- Size must be
num_frames: Number of audio frames to process
-
start_audio(chuck: ChucK, sample_rate: int = 44100, num_dac_channels: int = 2, num_adc_channels: int = 0, dac_device: int = 0, adc_device: int = 0, buffer_size: int = 512, num_buffers: int = 8) -> bool- Start real-time audio playback using RtAudio
- Audio plays asynchronously in the background
- Returns: True if successful
-
stop_audio() -> bool- Stop real-time audio playback
- Returns: True if successful
-
shutdown_audio(msWait: int = 0)- Shutdown audio system completely
msWait: Milliseconds to wait before shutdown
-
audio_info() -> dict- Get current audio system information
- Returns dict with keys:
sample_rate,num_channels_out,num_channels_in,buffer_size
set_global_int(name: str, value: int)- Set a global int variableset_global_float(name: str, value: float)- Set a global float variableset_global_string(name: str, value: str)- Set a global string variableget_global_int(name: str, callback: Callable[[int], None])- Get a global int (async via callback)get_global_float(name: str, callback: Callable[[float], None])- Get a global float (async via callback)get_global_string(name: str, callback: Callable[[str], None])- Get a global string (async via callback)set_global_int_array(name: str, values: list[int])- Set a global int arrayset_global_float_array(name: str, values: list[float])- Set a global float arrayset_global_int_array_value(name: str, index: int, value: int)- Set array element by indexset_global_float_array_value(name: str, index: int, value: float)- Set array element by indexset_global_associative_int_array_value(name: str, key: str, value: int)- Set map value by keyset_global_associative_float_array_value(name: str, key: str, value: float)- Set map value by keyget_global_int_array(name: str, callback: Callable[[list[int]], None])- Get int array (async)get_global_float_array(name: str, callback: Callable[[list[float]], None])- Get float array (async)get_all_globals() -> list[tuple[str, str]]- Get list of all globals as (type, name) pairs
signal_global_event(name: str)- Signal a global event (wakes one waiting shred)broadcast_global_event(name: str)- Broadcast a global event (wakes all waiting shreds)listen_for_global_event(name: str, callback: Callable[[], None], listen_forever: bool = True) -> int- Listen for event, returns listener IDstop_listening_for_global_event(name: str, callback_id: int)- Stop listening using listener ID
remove_shred(shred_id: int)- Remove a shred by IDremove_all_shreds()- Remove all running shreds from VMget_all_shred_ids() -> list[int]- Get IDs of all running shredsget_ready_shred_ids() -> list[int]- Get IDs of ready (not blocked) shredsget_blocked_shred_ids() -> list[int]- Get IDs of blocked shredsget_last_shred_id() -> int- Get ID of last sporked shredget_next_shred_id() -> int- Get what the next shred ID will beget_shred_info(shred_id: int) -> dict- Get shred info (id, name, is_running, is_done)
clear_vm()- Clear the VM (remove all shreds)clear_globals()- Clear global variables without clearing the VMreset_shred_id()- Reset the shred ID counterreplace_shred(shred_id: int, code: str, args: str = "") -> int- Replace running shred with new code
is_init() -> bool- Check if ChucK is initializedvm_running() -> bool- Check if VM is runningnow() -> float- Get current ChucK time in samples
set_chout_callback(callback: Callable[[str], None]) -> bool- Capture ChucK console outputset_cherr_callback(callback: Callable[[str], None]) -> bool- Capture ChucK error outputtoggle_global_color_textoutput(onOff: bool)- Enable/disable color outputprobe_chugins()- Print info on all loaded chugins
version() -> str- Get ChucK version stringint_size() -> int- Get ChucK integer size in bitsnum_vms() -> int- Get number of active ChucK VMsset_log_level(level: int)- Set global log levelget_log_level() -> int- Get global log levelpoop()- ChucK poop compatibilityset_stdout_callback(callback: Callable[[str], None]) -> bool- Set global stdout callback (static)set_stderr_callback(callback: Callable[[str], None]) -> bool- Set global stderr callback (static)global_cleanup()- Global cleanup for all ChucK instances
PARAM_VERSION- ChucK versionPARAM_SAMPLE_RATE- Sample rate (default: 44100)PARAM_INPUT_CHANNELS- Number of input channelsPARAM_OUTPUT_CHANNELS- Number of output channels
PARAM_VM_ADAPTIVE- Adaptive VM modePARAM_VM_HALT- VM halt on errorsPARAM_OTF_ENABLE- On-the-fly programming enablePARAM_OTF_PORT- On-the-fly programming portPARAM_DUMP_INSTRUCTIONS- Dump VM instructionsPARAM_AUTO_DEPEND- Auto dependency resolutionPARAM_DEPRECATE_LEVEL- Deprecation warning level
PARAM_WORKING_DIRECTORY- Working directory pathPARAM_CHUGIN_ENABLE- Enable chugins (plugins)PARAM_USER_CHUGINS- User chugin pathsPARAM_IMPORT_PATH_SYSTEM- System import search pathsPARAM_IMPORT_PATH_PACKAGES- Package import search pathsPARAM_IMPORT_PATH_USER- User import search paths
PARAM_OTF_PRINT_WARNINGS- Print on-the-fly compiler warningsPARAM_IS_REALTIME_AUDIO_HINT- Hint for real-time audio modePARAM_COMPILER_HIGHLIGHT_ON_ERROR- Syntax highlighting in error messagesPARAM_TTY_COLOR- Enable color output in terminalPARAM_TTY_WIDTH_HINT- Terminal width hint for formatting
version() -> str- Get ChucK version (convenience function)
ChucK uses float (32-bit) for audio samples by default. Always use np.float32 for numpy arrays:
# Correct
output_buffer = np.zeros(num_frames * channels, dtype=np.float32)
# Incorrect - will produce silent output
output_buffer = np.zeros(num_frames * channels, dtype=np.float64)Audio buffers are interleaved:
- For stereo output:
[L0, R0, L1, R1, L2, R2, ...] - Buffer size =
num_frames * num_channels
ChucK code must advance time to generate audio:
# Good - infinite loop advances time
code = '''
SinOsc s => dac;
440 => s.freq;
while(true) { 1::samp => now; }
'''
# Bad - shred exits immediately, no audio
code = '''
SinOsc s => dac;
440 => s.freq;
'''import pychuck
import time
# Create and initialize ChucK
chuck = pychuck.ChucK()
chuck.set_param(pychuck.PARAM_SAMPLE_RATE, 44100)
chuck.set_param(pychuck.PARAM_OUTPUT_CHANNELS, 2)
chuck.init()
# Compile ChucK code
chuck.compile_code('''
SinOsc s => dac;
440 => s.freq;
0.5 => s.gain;
while(true) { 1::samp => now; }
''')
# Start real-time audio (plays asynchronously)
pychuck.start_audio(chuck, sample_rate=44100, num_dac_channels=2)
# Audio plays in background
time.sleep(3) # Play for 3 seconds
# Stop audio
pychuck.stop_audio()
pychuck.shutdown_audio()import pychuck
import numpy as np
chuck = pychuck.ChucK()
chuck.set_param(pychuck.PARAM_SAMPLE_RATE, 44100)
chuck.set_param(pychuck.PARAM_OUTPUT_CHANNELS, 2)
chuck.init()
chuck.compile_code('''
SinOsc s => dac;
440 => s.freq;
0.5 => s.gain;
while(true) { 1::samp => now; }
''')
# Process audio synchronously
frames = 512
output = np.zeros(frames * 2, dtype=np.float32)
chuck.run(np.zeros(0, dtype=np.float32), output, frames)
# output now contains audio samples# Get ChucK version
print(f"ChucK version: {pychuck.version()}")
# Configure VM
chuck.set_param(pychuck.PARAM_VM_HALT, 0)
chuck.set_param_string(pychuck.PARAM_WORKING_DIRECTORY, "/path/to/files")
# Check status
print(f"Initialized: {chuck.is_init()}")
print(f"Current time: {chuck.now()} samples")# Compile the same code 3 times
success, ids = chuck.compile_code(code, count=3)
print(f"Spawned shreds: {ids}") # [1, 2, 3]
# Remove all shreds
chuck.remove_all_shreds()import pychuck
chuck = pychuck.ChucK()
chuck.set_param(pychuck.PARAM_SAMPLE_RATE, 44100)
chuck.set_param(pychuck.PARAM_OUTPUT_CHANNELS, 2)
chuck.init()
# Compile from file
success, shred_ids = chuck.compile_file("examples/basic/blit2.ck")
# Start playback
pychuck.start_audio(chuck)
import time; time.sleep(2)
pychuck.stop_audio()
pychuck.shutdown_audio()import pychuck
chuck = pychuck.ChucK()
chuck.set_param(pychuck.PARAM_SAMPLE_RATE, 44100)
chuck.set_param(pychuck.PARAM_OUTPUT_CHANNELS, 2)
# Enable chugins and set search path
chuck.set_param(pychuck.PARAM_CHUGIN_ENABLE, 1)
chuck.set_param_string(pychuck.PARAM_USER_CHUGINS, "./examples/chugins")
chuck.init()
# Use a chugin in code
code = '''
SinOsc s => Bitcrusher bc => dac;
440 => s.freq;
8 => bc.bits;
while(true) { 1::samp => now; }
'''
chuck.compile_code(code)import pychuck
import numpy as np
chuck = pychuck.ChucK()
chuck.set_param(pychuck.PARAM_SAMPLE_RATE, 44100)
chuck.set_param(pychuck.PARAM_INPUT_CHANNELS, 2)
chuck.set_param(pychuck.PARAM_OUTPUT_CHANNELS, 2)
chuck.init()
chuck.start()
# Define global variables in ChucK
chuck.compile_code('''
global int tempo;
global float frequency;
global string mode;
SinOsc s => dac;
while(true) {
frequency => s.freq;
1::samp => now;
}
''')
# Helper to run audio cycles (VM processes messages during audio)
def run_cycles(count=5):
buf_in = np.zeros(512 * 2, dtype=np.float32)
buf_out = np.zeros(512 * 2, dtype=np.float32)
for _ in range(count):
chuck.run(buf_in, buf_out, 512)
# Set globals from Python
chuck.set_global_int("tempo", 120)
chuck.set_global_float("frequency", 440.0)
chuck.set_global_string("mode", "major")
run_cycles()
# Get globals via callback
result = []
chuck.get_global_float("frequency", lambda val: result.append(val))
run_cycles()
print(f"Current frequency: {result[0]} Hz")
# List all globals
globals_list = chuck.get_all_globals()
print(f"Globals: {globals_list}")import pychuck
import numpy as np
chuck = pychuck.ChucK()
chuck.set_param(pychuck.PARAM_SAMPLE_RATE, 44100)
chuck.set_param(pychuck.PARAM_INPUT_CHANNELS, 2)
chuck.set_param(pychuck.PARAM_OUTPUT_CHANNELS, 2)
chuck.init()
chuck.start()
# ChucK code with global events
chuck.compile_code('''
global Event trigger;
global Event response;
global int noteValue;
SinOsc s => dac;
fun void player() {
while(true) {
trigger => now;
Std.mtof(noteValue) => s.freq;
100::ms => now;
response.broadcast();
}
}
spork ~ player();
''')
def run_cycles(count=5):
buf_in = np.zeros(512 * 2, dtype=np.float32)
buf_out = np.zeros(512 * 2, dtype=np.float32)
for _ in range(count):
chuck.run(buf_in, buf_out, 512)
# Listen for response from ChucK
response_count = []
def on_response():
response_count.append(1)
print(f"Response received! Total: {len(response_count)}")
listener_id = chuck.listen_for_global_event("response", on_response, listen_forever=True)
# Trigger notes from Python
for note in [60, 64, 67, 72]: # C major chord
chuck.set_global_int("noteValue", note)
chuck.signal_global_event("trigger")
run_cycles(10)
# Stop listening
chuck.stop_listening_for_global_event("response", listener_id)import pychuck
import numpy as np
chuck = pychuck.ChucK()
chuck.set_param(pychuck.PARAM_SAMPLE_RATE, 44100)
chuck.set_param(pychuck.PARAM_INPUT_CHANNELS, 2)
chuck.set_param(pychuck.PARAM_OUTPUT_CHANNELS, 2)
chuck.init()
chuck.start()
# Spork multiple shreds
code = "while(true) { 100::ms => now; }"
success1, ids1 = chuck.compile_code(code)
success2, ids2 = chuck.compile_code(code)
success3, ids3 = chuck.compile_code(code)
# Introspect running shreds
all_ids = chuck.get_all_shred_ids()
print(f"Running shreds: {all_ids}")
for shred_id in all_ids:
info = chuck.get_shred_info(shred_id)
print(f"Shred {info['id']}: {info['name']}, running={info['is_running']}")
# Remove specific shred
chuck.remove_shred(ids1[0])
print(f"After removal: {chuck.get_all_shred_ids()}")
# Get next shred ID
next_id = chuck.get_next_shred_id()
print(f"Next shred ID will be: {next_id}")
# Clear all
chuck.clear_vm()
print(f"After clear_vm: {chuck.get_all_shred_ids()}")import pychuck
import numpy as np
chuck = pychuck.ChucK()
chuck.set_param(pychuck.PARAM_SAMPLE_RATE, 44100)
chuck.set_param(pychuck.PARAM_INPUT_CHANNELS, 2)
chuck.set_param(pychuck.PARAM_OUTPUT_CHANNELS, 2)
chuck.init()
chuck.start()
# Start with one sound
code_v1 = '''
SinOsc s => dac;
440 => s.freq;
while(true) { 1::samp => now; }
'''
success, ids = chuck.compile_code(code_v1)
original_id = ids[0]
# ... play for a while ...
# Hot-swap to different sound
code_v2 = '''
TriOsc t => dac;
330 => t.freq;
0.5 => t.gain;
while(true) { 1::samp => now; }
'''
new_id = chuck.replace_shred(original_id, code_v2)
print(f"Replaced shred {original_id} with {new_id}")import pychuck
chuck = pychuck.ChucK()
chuck.init()
# Capture chout (console output)
output_log = []
chuck.set_chout_callback(lambda msg: output_log.append(msg))
# Capture cherr (error output)
error_log = []
chuck.set_cherr_callback(lambda msg: error_log.append(msg))
# Run code that prints
chuck.compile_code('''
<<< "Hello from ChucK!" >>>;
<<< "Value:", 42 >>>;
''')
# Check captured output
print("ChucK output:", output_log)- Core: ChucK virtual machine and compiler (C++)
- Bindings: nanobind for efficient Python/C++ interop
- Build: CMake + scikit-build-core for modern Python packaging
- Audio:
- Real-time: RtAudio (CoreAudio on macOS, DirectSound/WASAPI on Windows, ALSA/JACK on Linux)
- Offline: Float32 sample processing, interleaved buffer format
- Plugins: Chugin support for extending ChucK functionality
- Full ChucK VM and compiler access
- Compile from strings or files
- Parameter configuration and introspection
- Advanced shred (thread) management and introspection
- Global Variables: Bidirectional data exchange between Python and ChucK
- Set/get primitives (int, float, string)
- Set/get arrays (indexed and associative)
- Async callbacks for getting values
- Global Events: Event-driven communication
- Signal/broadcast events from Python to ChucK
- Listen for events from ChucK in Python
- Persistent or one-shot event listeners
- Console Capture: Redirect ChucK output to Python callbacks
- Shred Introspection: List, query, and monitor running shreds
- Shred Control: Remove individual shreds or clear entire VM
- Hot Swapping: Replace running shred code without stopping audio
- VM Management: Clear globals, reset IDs, fine-grained control
- Real-time: Asynchronous playback through system audio
- Offline: Synchronous rendering to numpy arrays
- Load chugins (ChucK plugins)
- Configurable search paths
- Examples included in
examples/chugins/
- Basic synthesis examples in
examples/basic/ - Effect examples in
examples/effects/ - Pre-built chugins in
examples/chugins/ - Comprehensive test suite
- Python 3.8+
- CMake 3.15+
- C++17 compatible compiler
- macOS: Xcode with CoreAudio/CoreMIDI frameworks
- numpy (for audio processing)
# Build
make build
# Run tests
make test
# Clean build artifacts
make cleanChucK is licensed under the GNU General Public License v2.0.
- ChucK: Ge Wang, Perry Cook, and the ChucK team
- nanobind: Wenzel Jakob and contributors
- claude-code: Anthropic