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:
Jose Luis Montañes
2026-05-01 17:37:06 +02:00
commit 7debc7436e
19 changed files with 3260 additions and 0 deletions

64
code_sinth/viz.py Normal file
View 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