initial: code-sinth — DSL-driven modular synth (Python engine + web app)
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>
This commit is contained in:
64
code_sinth/viz.py
Normal file
64
code_sinth/viz.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user