Patch language with osc/noise/trig/seq/adsr/filter/delay/poly + voice templates and inline live values. Two runtimes: - code_sinth/ — Python engine (numpy + sounddevice). Hot-reload via mtime watcher. Offline render to WAV. Static-HTTP+WS visualizer (viz/) that injects waveforms next to each `node X = ...` line. - web/ — port of the engine to JS running in AudioWorklet. Single static page with CodeMirror 6 editor (line widgets for live waveforms) and a control surface on the right with knobs/faders/step_seq/piano_roll declared from the patch. State preserved across hot-reload. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
65 lines
2.2 KiB
Python
65 lines
2.2 KiB
Python
"""WebSocket + static-HTTP server for the live visualizer.
|
||
|
||
Push model: the WS handler polls engine.taps_snapshot at `fps` Hz and sends a
|
||
JSON frame to every connected client. The audio thread is never blocked — it
|
||
just replaces the snapshot dict ref each block.
|
||
|
||
Frame schema:
|
||
{ "sr": 48000,
|
||
"block_size": 512,
|
||
"patch": "<source text>",
|
||
"taps": { "<node-name>": [float, float, ...], ... } }
|
||
"""
|
||
|
||
import asyncio
|
||
import functools
|
||
import http.server
|
||
import json
|
||
import threading
|
||
|
||
|
||
def _serve_http(directory, port):
|
||
handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=directory)
|
||
server = http.server.ThreadingHTTPServer(('localhost', port), handler)
|
||
t = threading.Thread(target=server.serve_forever, daemon=True)
|
||
t.start()
|
||
return server
|
||
|
||
|
||
def _serve_ws(engine, port, fps):
|
||
import websockets
|
||
|
||
async def handler(ws):
|
||
try:
|
||
interval = 1.0 / fps
|
||
while True:
|
||
snap = engine.taps_snapshot or {}
|
||
# arr.tolist() is slow per-tap; OK for a proof. ~10 taps × 512 floats × 30 Hz
|
||
# is ~150k floats/sec, well under WS budget.
|
||
taps = {name: arr.tolist() for name, arr in snap.items()}
|
||
msg = {
|
||
'sr': engine.sr,
|
||
'block_size': engine.block_size,
|
||
'patch': engine.current_patch_text,
|
||
'taps': taps,
|
||
}
|
||
await ws.send(json.dumps(msg))
|
||
await asyncio.sleep(interval)
|
||
except websockets.exceptions.ConnectionClosed:
|
||
pass
|
||
|
||
async def run():
|
||
async with websockets.serve(handler, 'localhost', port):
|
||
await asyncio.Future() # serve forever
|
||
|
||
threading.Thread(target=lambda: asyncio.run(run()), daemon=True).start()
|
||
|
||
|
||
def serve(engine, viz_dir, http_port=9001, ws_port=9000, fps=30):
|
||
"""Start HTTP (static) + WS (taps) servers in background threads.
|
||
Returns the HTTP server so the caller can shut it down if desired."""
|
||
server = _serve_http(viz_dir, http_port)
|
||
_serve_ws(engine, ws_port, fps)
|
||
print(f'[viz] open http://localhost:{http_port}/ (ws://localhost:{ws_port})')
|
||
return server
|