Files
code-sinth/code_sinth/viz.py
Jose Luis Montañes 7debc7436e 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>
2026-05-01 17:37:06 +02:00

65 lines
2.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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