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

59
run.py Normal file
View File

@@ -0,0 +1,59 @@
import argparse
import os
import wave
import numpy as np
from code_sinth import parse, build_graph, Engine
def write_wav(path, samples, sr):
pcm = np.clip(samples, -1.0, 1.0)
pcm = (pcm * 32767.0).astype(np.int16)
with wave.open(path, 'wb') as w:
w.setnchannels(1)
w.setsampwidth(2)
w.setframerate(sr)
w.writeframes(pcm.tobytes())
def main():
ap = argparse.ArgumentParser(description='code-sinth: tiny modular synth from .patch files')
ap.add_argument('patch', help='Path to .patch file')
ap.add_argument('--duration', type=float, default=6.0, help='Seconds to render/play (default 6)')
ap.add_argument('--wav', help='Render to WAV file instead of playing live')
ap.add_argument('--live', action='store_true',
help='Play indefinitely and hot-reload the patch on file changes')
ap.add_argument('--viz', action='store_true',
help='Start the inline-waveform visualizer (web UI). Implies --live.')
ap.add_argument('--sr', type=int, default=48000)
ap.add_argument('--block', type=int, default=512)
args = ap.parse_args()
def load(path):
with open(path, 'r', encoding='utf-8') as f:
src = f.read()
return build_graph(parse(src)), src
graph, src_text = load(args.patch)
engine = Engine(graph, sr=args.sr, block_size=args.block)
engine.current_patch_text = src_text
if args.wav:
print(f'Rendering {args.duration}s to {args.wav}...')
samples = engine.render_offline(args.duration)
write_wav(args.wav, samples, args.sr)
print('Done.')
elif args.viz or args.live:
if args.viz:
from code_sinth import viz
viz_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'viz')
viz.serve(engine, viz_dir)
engine.run_live(args.patch, lambda src: build_graph(parse(src)))
else:
print(f'Playing {args.patch} for {args.duration}s...')
engine.run(args.duration)
if __name__ == '__main__':
main()