from .nodes import Const, BinOpNode, Negate, NODE_REGISTRY, SYMBOLIC_ARGS from .parser import Number, Ident, Call, BinOp, UnaryOp, NodeDecl, OutDecl, ListExpr, VoiceDecl # Patch-language kwargs that collide with Python keywords get renamed before # being passed into Node constructors. KWARG_RENAME = {'in': 'in_'} class Graph: def __init__(self): self.named = {} self.all = [] self.out = None self.voice_templates = {} # name -> body (list of stmts) def add(self, node): self.all.append(node) return node class VoiceInstance: """One independently-stateful copy of a voice template. Owned and processed by Poly.""" def __init__(self, freq_slot, gate_slot, order, output): self.freq_slot = freq_slot self.gate_slot = gate_slot self.order = order self.output = output self.gate_off_at = -1.0 # absolute time at which to release gate; -1 = idle self.last_on_at = -1.0 # for LRU voice stealing def build_voice_instance(template_body): """Instantiate a fresh sub-graph from a voice template. Returns a VoiceInstance whose freq_slot and gate_slot are mutable Const nodes controlled by the Poly node.""" sub = Graph() freq_slot = sub.add(Const(440.0)) gate_slot = sub.add(Const(0.0)) sub.named['freq'] = freq_slot sub.named['gate'] = gate_slot for stmt in template_body: if isinstance(stmt, NodeDecl): node = build_expr(sub, stmt.expr) if stmt.name in sub.named: raise ValueError(f'Duplicate node name in voice: {stmt.name!r}') sub.named[stmt.name] = node elif isinstance(stmt, OutDecl): sub.out = build_expr(sub, stmt.expr) else: raise ValueError(f'voice body cannot contain {type(stmt).__name__}') if sub.out is None: raise ValueError('voice block missing "out <- ..." declaration') order = topo_sort(sub.all) return VoiceInstance(freq_slot, gate_slot, order, sub.out) def build_arg(g, expr): """Like build_expr, but lists become Python lists of literal floats.""" if isinstance(expr, ListExpr): items = [] for item in expr.items: if isinstance(item, Number): items.append(item.value) elif isinstance(item, UnaryOp) and item.op == '-' and isinstance(item.operand, Number): items.append(-item.operand.value) else: raise ValueError('List elements must be numeric literals') return items return build_expr(g, expr) def build_call(g, call): func = call.func if func == 'poly': return build_poly(g, call) if func not in NODE_REGISTRY: raise ValueError(f'Unknown node function: {func!r}') cls = NODE_REGISTRY[func] symbolic = SYMBOLIC_ARGS.get(func, []) args = [] for i, arg in enumerate(call.args): if i in symbolic: if not isinstance(arg, Ident): raise ValueError(f'Argument {i} of {func}() must be a bare symbol') args.append(arg.name) else: args.append(build_arg(g, arg)) kwargs = {KWARG_RENAME.get(k, k): build_arg(g, v) for k, v in call.kwargs.items()} return g.add(cls(*args, **kwargs)) def build_poly(g, call): """poly(voice=NAME, voices=N, rate=R, gate_duration=GD, notes=[...]) — special-cased because it needs the voice template AST and instantiates sub-graphs.""" from .nodes import Poly kw = call.kwargs voice_arg = kw.get('voice') if not isinstance(voice_arg, Ident): raise ValueError('poly() requires voice=') if voice_arg.name not in g.voice_templates: raise ValueError(f'Unknown voice template: {voice_arg.name!r}') voice_body = g.voice_templates[voice_arg.name] voices_arg = kw.get('voices') if not isinstance(voices_arg, Number): raise ValueError('poly(voices=) must be a literal integer') n_voices = int(voices_arg.value) if n_voices < 1: raise ValueError('poly(voices=) must be >= 1') if 'rate' not in kw or 'gate_duration' not in kw or 'notes' not in kw: raise ValueError('poly() requires rate=, gate_duration=, notes=') rate_node = build_expr(g, kw['rate']) gd_node = build_expr(g, kw['gate_duration']) notes_arg = kw['notes'] if not isinstance(notes_arg, ListExpr): raise ValueError('poly(notes=) must be a list literal') notes = build_arg(g, notes_arg) instances = [build_voice_instance(voice_body) for _ in range(n_voices)] return g.add(Poly(rate_node, gd_node, instances, notes)) def build_expr(g, expr): if isinstance(expr, Number): return g.add(Const(expr.value)) if isinstance(expr, Ident): if expr.name not in g.named: raise ValueError(f'Unknown node reference: {expr.name!r}') return g.named[expr.name] if isinstance(expr, Call): return build_call(g, expr) if isinstance(expr, BinOp): return g.add(BinOpNode(expr.op, build_expr(g, expr.left), build_expr(g, expr.right))) if isinstance(expr, UnaryOp): return g.add(Negate(build_expr(g, expr.operand))) raise ValueError(f'Unknown expression node: {type(expr).__name__}') def build_graph(statements): g = Graph() # Voice declarations first (they can be referenced from any order in the source). for stmt in statements: if isinstance(stmt, VoiceDecl): if stmt.name in g.voice_templates: raise ValueError(f'Duplicate voice template: {stmt.name!r}') g.voice_templates[stmt.name] = stmt.body for stmt in statements: if isinstance(stmt, VoiceDecl): continue if isinstance(stmt, NodeDecl): node = build_expr(g, stmt.expr) if stmt.name in g.named: raise ValueError(f'Duplicate node name: {stmt.name!r}') g.named[stmt.name] = node elif isinstance(stmt, OutDecl): g.out = build_expr(g, stmt.expr) else: raise ValueError(f'Unknown statement: {type(stmt).__name__}') if g.out is None: raise ValueError('No "out <- ..." declaration found in patch') return g def topo_sort(nodes): visited = set() order = [] def visit(node): if id(node) in visited: return visited.add(id(node)) for inp in node.inputs: visit(inp) order.append(node) for node in nodes: visit(node) return order