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:
59
run.py
Normal file
59
run.py
Normal 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()
|
||||
Reference in New Issue
Block a user