frausto 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,599 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ir"
4
+
5
+ module Ruby2Faust
6
+ # Emitter generates Faust source code from an IR graph.
7
+ module Emitter
8
+ module_function
9
+
10
+ DEFAULT_IMPORTS = ["stdfaust.lib"].freeze
11
+
12
+ def program(process, imports: nil, declarations: {}, pretty: false)
13
+ if process.is_a?(Program)
14
+ node = process.process.is_a?(DSP) ? process.process.node : process.process
15
+ imports ||= process.imports
16
+ declarations = process.declarations.merge(declarations)
17
+ else
18
+ node = process.is_a?(DSP) ? process.node : process
19
+ imports ||= DEFAULT_IMPORTS
20
+ end
21
+
22
+ lines = []
23
+ declarations.each { |k, v| lines << "declare #{k} \"#{v}\";" }
24
+ lines << "" if declarations.any?
25
+ imports.each { |lib| lines << "import(\"#{lib}\");" }
26
+ lines << ""
27
+
28
+ body = emit(node, pretty: pretty)
29
+ lines << "process = #{body};"
30
+ lines.join("\n") + "\n"
31
+ end
32
+
33
+ def emit(node, indent: 0, pretty: false)
34
+ sp = " " * indent
35
+ next_sp = " " * (indent + 1)
36
+
37
+ case node.type
38
+
39
+ # === COMMENTS ===
40
+ when NodeType::COMMENT
41
+ "// #{node.args[0]}\n"
42
+ when NodeType::DOC
43
+ # Inline comment wrapped around the inner expression
44
+ "/* #{node.args[0]} */ #{emit(node.inputs[0], indent: indent, pretty: pretty)}"
45
+
46
+ # === OSCILLATORS ===
47
+ when NodeType::OSC
48
+ "os.osc(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
49
+ when NodeType::SAW
50
+ "os.sawtooth(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
51
+ when NodeType::SQUARE
52
+ "os.square(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
53
+ when NodeType::TRIANGLE
54
+ "os.triangle(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
55
+ when NodeType::PHASOR
56
+ "os.phasor(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)})"
57
+ when NodeType::LF_SAW
58
+ "os.lf_sawpos(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
59
+ when NodeType::LF_TRIANGLE
60
+ "os.lf_trianglepos(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
61
+ when NodeType::LF_SQUARE
62
+ "os.lf_squarewavepos(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
63
+ when NodeType::IMPTRAIN
64
+ "os.lf_imptrain(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
65
+ when NodeType::PULSETRAIN
66
+ "os.lf_pulsetrain(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)})"
67
+
68
+ # === NOISE ===
69
+ when NodeType::NOISE
70
+ "no.noise"
71
+ when NodeType::PINK_NOISE
72
+ "no.pink_noise"
73
+
74
+ # === FILTERS ===
75
+ when NodeType::LP
76
+ "fi.lowpass(#{node.args[0] || 1}, #{emit(node.inputs[0], indent: indent, pretty: pretty)})"
77
+ when NodeType::HP
78
+ "fi.highpass(#{node.args[0] || 1}, #{emit(node.inputs[0], indent: indent, pretty: pretty)})"
79
+ when NodeType::BP
80
+ "fi.bandpass(1, #{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)})"
81
+ when NodeType::RESONLP
82
+ "fi.resonlp(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)}, #{emit(node.inputs[2], indent: indent, pretty: pretty)})"
83
+ when NodeType::RESONHP
84
+ "fi.resonhp(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)}, #{emit(node.inputs[2], indent: indent, pretty: pretty)})"
85
+ when NodeType::RESONBP
86
+ "fi.resonbp(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)}, #{emit(node.inputs[2], indent: indent, pretty: pretty)})"
87
+ when NodeType::ALLPASS
88
+ "fi.allpass_comb(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)}, #{emit(node.inputs[2], indent: indent, pretty: pretty)})"
89
+ when NodeType::DCBLOCK
90
+ "fi.dcblocker"
91
+ when NodeType::PEAK_EQ
92
+ "fi.peak_eq(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)}, #{emit(node.inputs[2], indent: indent, pretty: pretty)})"
93
+
94
+ # SVF (State Variable Filter)
95
+ when NodeType::SVF_LP
96
+ "fi.svf.lp(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)})"
97
+ when NodeType::SVF_HP
98
+ "fi.svf.hp(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)})"
99
+ when NodeType::SVF_BP
100
+ "fi.svf.bp(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)})"
101
+ when NodeType::SVF_NOTCH
102
+ "fi.svf.notch(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)})"
103
+ when NodeType::SVF_AP
104
+ "fi.svf.ap(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)})"
105
+ when NodeType::SVF_BELL
106
+ "fi.svf.bell(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)}, #{emit(node.inputs[2], indent: indent, pretty: pretty)})"
107
+ when NodeType::SVF_LS
108
+ "fi.svf.ls(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)}, #{emit(node.inputs[2], indent: indent, pretty: pretty)})"
109
+ when NodeType::SVF_HS
110
+ "fi.svf.hs(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)}, #{emit(node.inputs[2], indent: indent, pretty: pretty)})"
111
+
112
+ # Other filters
113
+ when NodeType::LOWPASS3E
114
+ "fi.lowpass3e(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
115
+ when NodeType::HIGHPASS3E
116
+ "fi.highpass3e(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
117
+ when NodeType::LOWPASS6E
118
+ "fi.lowpass6e(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
119
+ when NodeType::HIGHPASS6E
120
+ "fi.highpass6e(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
121
+ when NodeType::BANDSTOP
122
+ "fi.bandstop(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)}, #{emit(node.inputs[2], indent: indent, pretty: pretty)})"
123
+ when NodeType::NOTCHW
124
+ "fi.notchw(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)})"
125
+ when NodeType::LOW_SHELF
126
+ "fi.low_shelf(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)}, #{emit(node.inputs[2], indent: indent, pretty: pretty)})"
127
+ when NodeType::HIGH_SHELF
128
+ "fi.high_shelf(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)}, #{emit(node.inputs[2], indent: indent, pretty: pretty)})"
129
+ when NodeType::PEAK_EQ_CQ
130
+ "fi.peak_eq_cq(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)}, #{emit(node.inputs[2], indent: indent, pretty: pretty)})"
131
+ when NodeType::FI_POLE
132
+ "fi.pole(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
133
+ when NodeType::FI_ZERO
134
+ "fi.zero(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
135
+ when NodeType::TF1
136
+ "fi.tf1(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)}, #{emit(node.inputs[2], indent: indent, pretty: pretty)})"
137
+ when NodeType::TF2
138
+ "fi.tf2(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)}, #{emit(node.inputs[2], indent: indent, pretty: pretty)}, #{emit(node.inputs[3], indent: indent, pretty: pretty)}, #{emit(node.inputs[4], indent: indent, pretty: pretty)})"
139
+ when NodeType::TF1S
140
+ "fi.tf1s(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)}, #{emit(node.inputs[2], indent: indent, pretty: pretty)})"
141
+ when NodeType::TF2S
142
+ "fi.tf2s(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)}, #{emit(node.inputs[2], indent: indent, pretty: pretty)}, #{emit(node.inputs[3], indent: indent, pretty: pretty)}, #{emit(node.inputs[4], indent: indent, pretty: pretty)})"
143
+ when NodeType::IIR
144
+ "fi.iir(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)})"
145
+ when NodeType::FIR
146
+ "fi.fir(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
147
+ when NodeType::CONV
148
+ "fi.conv(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)})"
149
+ when NodeType::FBCOMBFILTER
150
+ "fi.fbcombfilter(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)}, #{emit(node.inputs[2], indent: indent, pretty: pretty)})"
151
+ when NodeType::FFCOMBFILTER
152
+ "fi.ffcombfilter(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)})"
153
+
154
+ # === DELAYS ===
155
+ when NodeType::DELAY
156
+ "de.delay(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)})"
157
+ when NodeType::FDELAY
158
+ "de.fdelay(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)})"
159
+ when NodeType::SDELAY
160
+ "de.sdelay(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)}, #{emit(node.inputs[2], indent: indent, pretty: pretty)})"
161
+
162
+ # === ENVELOPES ===
163
+ when NodeType::AR
164
+ "en.ar(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)}, #{emit(node.inputs[2], indent: indent, pretty: pretty)})"
165
+ when NodeType::ASR
166
+ "en.asr(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)}, #{emit(node.inputs[2], indent: indent, pretty: pretty)}, #{emit(node.inputs[3], indent: indent, pretty: pretty)})"
167
+ when NodeType::ADSR
168
+ "en.adsr(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)}, #{emit(node.inputs[2], indent: indent, pretty: pretty)}, #{emit(node.inputs[3], indent: indent, pretty: pretty)}, #{emit(node.inputs[4], indent: indent, pretty: pretty)})"
169
+ when NodeType::ADSRE
170
+ "en.adsre(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)}, #{emit(node.inputs[2], indent: indent, pretty: pretty)}, #{emit(node.inputs[3], indent: indent, pretty: pretty)}, #{emit(node.inputs[4], indent: indent, pretty: pretty)})"
171
+
172
+ # === MATH ===
173
+ when NodeType::GAIN
174
+ "*(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
175
+ when NodeType::ADD
176
+ node.inputs.count == 2 ? "(#{emit(node.inputs[0], indent: indent, pretty: pretty)} + #{emit(node.inputs[1], indent: indent, pretty: pretty)})" : "+"
177
+ when NodeType::MUL
178
+ node.inputs.count == 2 ? "(#{emit(node.inputs[0], indent: indent, pretty: pretty)} * #{emit(node.inputs[1], indent: indent, pretty: pretty)})" : "*"
179
+ when NodeType::SUB
180
+ node.inputs.count == 2 ? "(#{emit(node.inputs[0], indent: indent, pretty: pretty)} - #{emit(node.inputs[1], indent: indent, pretty: pretty)})" : "-"
181
+ when NodeType::DIV
182
+ node.inputs.count == 2 ? "(#{emit(node.inputs[0], indent: indent, pretty: pretty)} / #{emit(node.inputs[1], indent: indent, pretty: pretty)})" : "/"
183
+ when NodeType::MOD
184
+ "(#{emit(node.inputs[0], indent: indent, pretty: pretty)} % #{emit(node.inputs[1], indent: indent, pretty: pretty)})"
185
+
186
+ # Comparison operators
187
+ when NodeType::LT
188
+ "(#{emit(node.inputs[0], indent: indent, pretty: pretty)} < #{emit(node.inputs[1], indent: indent, pretty: pretty)})"
189
+ when NodeType::GT
190
+ "(#{emit(node.inputs[0], indent: indent, pretty: pretty)} > #{emit(node.inputs[1], indent: indent, pretty: pretty)})"
191
+ when NodeType::LE
192
+ "(#{emit(node.inputs[0], indent: indent, pretty: pretty)} <= #{emit(node.inputs[1], indent: indent, pretty: pretty)})"
193
+ when NodeType::GE
194
+ "(#{emit(node.inputs[0], indent: indent, pretty: pretty)} >= #{emit(node.inputs[1], indent: indent, pretty: pretty)})"
195
+ when NodeType::EQ
196
+ "(#{emit(node.inputs[0], indent: indent, pretty: pretty)} == #{emit(node.inputs[1], indent: indent, pretty: pretty)})"
197
+ when NodeType::NEQ
198
+ "(#{emit(node.inputs[0], indent: indent, pretty: pretty)} != #{emit(node.inputs[1], indent: indent, pretty: pretty)})"
199
+
200
+ # Bitwise operators
201
+ when NodeType::BAND
202
+ "(#{emit(node.inputs[0], indent: indent, pretty: pretty)} & #{emit(node.inputs[1], indent: indent, pretty: pretty)})"
203
+ when NodeType::BOR
204
+ "(#{emit(node.inputs[0], indent: indent, pretty: pretty)} | #{emit(node.inputs[1], indent: indent, pretty: pretty)})"
205
+ when NodeType::XOR
206
+ "(#{emit(node.inputs[0], indent: indent, pretty: pretty)} xor #{emit(node.inputs[1], indent: indent, pretty: pretty)})"
207
+
208
+ when NodeType::NEG
209
+ "0 - #{emit(node.inputs[0], indent: indent, pretty: pretty)}"
210
+ when NodeType::ABS
211
+ "abs"
212
+ when NodeType::MIN
213
+ "min(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)})"
214
+ when NodeType::MAX
215
+ "max(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)})"
216
+ when NodeType::CLIP
217
+ "max(#{emit(node.inputs[0], indent: indent, pretty: pretty)}) : min(#{emit(node.inputs[1], indent: indent, pretty: pretty)})"
218
+ when NodeType::POW
219
+ "pow(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)})"
220
+ when NodeType::SQRT
221
+ "sqrt"
222
+ when NodeType::EXP
223
+ "exp"
224
+ when NodeType::LOG
225
+ "log"
226
+ when NodeType::LOG10
227
+ "log10"
228
+ when NodeType::SIN
229
+ "sin"
230
+ when NodeType::COS
231
+ "cos"
232
+ when NodeType::TAN
233
+ "tan"
234
+ when NodeType::TANH
235
+ "ma.tanh"
236
+ when NodeType::SINH
237
+ "sinh"
238
+ when NodeType::COSH
239
+ "cosh"
240
+ when NodeType::ASINH
241
+ "asinh"
242
+ when NodeType::ACOSH
243
+ "acosh"
244
+ when NodeType::ATANH
245
+ "atanh"
246
+ when NodeType::ASIN
247
+ "asin"
248
+ when NodeType::ACOS
249
+ "acos"
250
+ when NodeType::ATAN
251
+ "atan"
252
+ when NodeType::ATAN2
253
+ "atan2(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)})"
254
+ when NodeType::FLOOR
255
+ "floor"
256
+ when NodeType::CEIL
257
+ "ceil"
258
+ when NodeType::RINT
259
+ "rint"
260
+ when NodeType::FMOD
261
+ "fmod(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)})"
262
+ when NodeType::INT
263
+ "int"
264
+ when NodeType::FLOAT
265
+ "float"
266
+
267
+ # === CONVERSION ===
268
+ when NodeType::DB2LINEAR
269
+ "ba.db2linear(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
270
+ when NodeType::LINEAR2DB
271
+ "ba.linear2db(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
272
+ when NodeType::SAMP2SEC
273
+ "ba.samp2sec(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
274
+ when NodeType::SEC2SAMP
275
+ "ba.sec2samp(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
276
+ when NodeType::MIDI2HZ
277
+ "ba.midikey2hz(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
278
+ when NodeType::HZ2MIDI
279
+ "ba.hz2midikey(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
280
+ when NodeType::TAU2POLE
281
+ "ba.tau2pole(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
282
+ when NodeType::POLE2TAU
283
+ "ba.pole2tau(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
284
+ when NodeType::BA_IF
285
+ cond = emit(node.inputs[0], indent: indent, pretty: pretty)
286
+ then_val = emit(node.inputs[1], indent: indent, pretty: pretty)
287
+ else_val = emit(node.inputs[2], indent: indent, pretty: pretty)
288
+ "ba.if(#{cond}, #{then_val}, #{else_val})"
289
+ when NodeType::SELECTOR
290
+ n = node.args[0]
291
+ sel = emit(node.inputs[0], indent: indent, pretty: pretty)
292
+ inputs = node.inputs[1..].map { |i| emit(i, indent: indent, pretty: pretty) }.join(", ")
293
+ "ba.selector(#{n}, #{sel}, #{inputs})"
294
+ when NodeType::BA_TAKE
295
+ idx = emit(node.inputs[0], indent: indent, pretty: pretty)
296
+ tuple = emit(node.inputs[1], indent: indent, pretty: pretty)
297
+ "ba.take(#{idx}, #{tuple})"
298
+
299
+ # === SMOOTHING ===
300
+ when NodeType::SMOOTH
301
+ "si.smooth(ba.tau2pole(#{emit(node.inputs[0], indent: indent, pretty: pretty)}))"
302
+ when NodeType::SMOO
303
+ "si.smoo"
304
+
305
+ # === SELECTORS ===
306
+ when NodeType::SELECT2
307
+ "select2(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)}, #{emit(node.inputs[2], indent: indent, pretty: pretty)})"
308
+ when NodeType::SELECTN
309
+ n = node.args[0]
310
+ idx = emit(node.inputs[0], indent: indent, pretty: pretty)
311
+ signals = node.inputs[1..].map { |i| emit(i, indent: indent, pretty: pretty) }.join(", ")
312
+ "ba.selectn(#{n}, #{idx}, #{signals})"
313
+
314
+ # === ROUTING ===
315
+ when NodeType::BUS
316
+ "si.bus(#{node.args[0]})"
317
+ when NodeType::BLOCK
318
+ "si.block(#{node.args[0]})"
319
+
320
+ # === REVERBS ===
321
+ when NodeType::FREEVERB
322
+ "re.mono_freeverb(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)}, #{emit(node.inputs[2], indent: indent, pretty: pretty)}, #{emit(node.inputs[3], indent: indent, pretty: pretty)})"
323
+ when NodeType::ZITA_REV
324
+ args = node.args.join(", ")
325
+ "re.zita_rev1_stereo(#{args})"
326
+ when NodeType::JPVERB
327
+ args = node.args.join(", ")
328
+ "re.jpverb(#{args})"
329
+
330
+ # === COMPRESSORS ===
331
+ when NodeType::COMPRESSOR
332
+ "co.compressor_mono(#{emit(node.inputs[0], indent: indent, pretty: pretty)}, #{emit(node.inputs[1], indent: indent, pretty: pretty)}, #{emit(node.inputs[2], indent: indent, pretty: pretty)}, #{emit(node.inputs[3], indent: indent, pretty: pretty)})"
333
+ when NodeType::LIMITER
334
+ "co.limiter_1176_R4_mono"
335
+
336
+ # === SPATIAL ===
337
+ when NodeType::PANNER
338
+ "sp.panner(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
339
+
340
+ # === UI CONTROLS ===
341
+ when NodeType::SLIDER
342
+ name, init, min, max, step = node.args
343
+ "hslider(\"#{name}\", #{init}, #{min}, #{max}, #{step})"
344
+ when NodeType::VSLIDER
345
+ name, init, min, max, step = node.args
346
+ "vslider(\"#{name}\", #{init}, #{min}, #{max}, #{step})"
347
+ when NodeType::NENTRY
348
+ name, init, min, max, step = node.args
349
+ "nentry(\"#{name}\", #{init}, #{min}, #{max}, #{step})"
350
+ when NodeType::BUTTON
351
+ "button(\"#{node.args[0]}\")"
352
+ when NodeType::CHECKBOX
353
+ "checkbox(\"#{node.args[0]}\")"
354
+ when NodeType::HGROUP
355
+ content = emit(node.inputs[0], indent: indent + 1, pretty: pretty)
356
+ if pretty
357
+ "hgroup(\"#{node.args[0]}\",\n#{next_sp}#{content}\n#{sp})"
358
+ else
359
+ "hgroup(\"#{node.args[0]}\", #{content})"
360
+ end
361
+ when NodeType::VGROUP
362
+ content = emit(node.inputs[0], indent: indent + 1, pretty: pretty)
363
+ if pretty
364
+ "vgroup(\"#{node.args[0]}\",\n#{next_sp}#{content}\n#{sp})"
365
+ else
366
+ "vgroup(\"#{node.args[0]}\", #{content})"
367
+ end
368
+ when NodeType::TGROUP
369
+ content = emit(node.inputs[0], indent: indent + 1, pretty: pretty)
370
+ if pretty
371
+ "tgroup(\"#{node.args[0]}\",\n#{next_sp}#{content}\n#{sp})"
372
+ else
373
+ "tgroup(\"#{node.args[0]}\", #{content})"
374
+ end
375
+
376
+ # === ITERATION ===
377
+ when NodeType::FPAR
378
+ var, count, block = node.args
379
+ # Evaluate the block with each index to get the expression
380
+ # For emission, we generate the Faust par() syntax
381
+ body = emit_iteration_body(var, block, indent, pretty)
382
+ "par(#{var}, #{count}, #{body})"
383
+ when NodeType::FSEQ
384
+ var, count, block = node.args
385
+ body = emit_iteration_body(var, block, indent, pretty)
386
+ "seq(#{var}, #{count}, #{body})"
387
+ when NodeType::FSUM
388
+ var, count, block = node.args
389
+ body = emit_iteration_body(var, block, indent, pretty)
390
+ "sum(#{var}, #{count}, #{body})"
391
+ when NodeType::FPROD
392
+ var, count, block = node.args
393
+ body = emit_iteration_body(var, block, indent, pretty)
394
+ "prod(#{var}, #{count}, #{body})"
395
+
396
+ # === LAMBDA ===
397
+ when NodeType::LAMBDA
398
+ params, block = node.args
399
+ param_str = params.map(&:to_s).join(", ")
400
+ # Create param DSP nodes for each parameter
401
+ param_dsps = params.map { |p| DSL.param(p) }
402
+ body = block.call(*param_dsps)
403
+ body = DSL.send(:to_dsp, body)
404
+ "\\(#{param_str}).(#{emit(body.node, indent: indent, pretty: pretty)})"
405
+ when NodeType::PARAM
406
+ node.args[0].to_s
407
+
408
+ # === TABLES ===
409
+ when NodeType::RDTABLE
410
+ size = emit(node.inputs[0], indent: indent, pretty: pretty)
411
+ init = emit(node.inputs[1], indent: indent, pretty: pretty)
412
+ ridx = emit(node.inputs[2], indent: indent, pretty: pretty)
413
+ "rdtable(#{size}, #{init}, #{ridx})"
414
+ when NodeType::RWTABLE
415
+ size = emit(node.inputs[0], indent: indent, pretty: pretty)
416
+ init = emit(node.inputs[1], indent: indent, pretty: pretty)
417
+ widx = emit(node.inputs[2], indent: indent, pretty: pretty)
418
+ wsig = emit(node.inputs[3], indent: indent, pretty: pretty)
419
+ ridx = emit(node.inputs[4], indent: indent, pretty: pretty)
420
+ "rwtable(#{size}, #{init}, #{widx}, #{wsig}, #{ridx})"
421
+ when NodeType::WAVEFORM
422
+ values = node.args.join(", ")
423
+ "waveform{#{values}}"
424
+
425
+ # === ADDITIONAL ROUTING ===
426
+ when NodeType::ROUTE
427
+ ins, outs, connections = node.args
428
+ conn_str = connections.map { |from, to| "(#{from}, #{to})" }.join(", ")
429
+ "route(#{ins}, #{outs}, #{conn_str})"
430
+ when NodeType::SELECT3
431
+ sel = emit(node.inputs[0], indent: indent, pretty: pretty)
432
+ a = emit(node.inputs[1], indent: indent, pretty: pretty)
433
+ b = emit(node.inputs[2], indent: indent, pretty: pretty)
434
+ c = emit(node.inputs[3], indent: indent, pretty: pretty)
435
+ "select3(#{sel}, #{a}, #{b}, #{c})"
436
+
437
+ # === COMPOSITION ===
438
+ when NodeType::SEQ
439
+ left = emit(node.inputs[0], indent: indent + 1, pretty: pretty)
440
+ right = emit(node.inputs[1], indent: indent + 1, pretty: pretty)
441
+ if pretty
442
+ "(\n#{next_sp}#{left}\n#{next_sp}: #{right}\n#{sp})"
443
+ else
444
+ "(#{left} : #{right})"
445
+ end
446
+ when NodeType::PAR
447
+ left = emit(node.inputs[0], indent: indent + 1, pretty: pretty)
448
+ right = emit(node.inputs[1], indent: indent + 1, pretty: pretty)
449
+ if pretty
450
+ "(\n#{next_sp}#{left},\n#{next_sp}#{right}\n#{sp})"
451
+ else
452
+ "(#{left}, #{right})"
453
+ end
454
+ when NodeType::SPLIT
455
+ source = emit(node.inputs[0], indent: indent + 1, pretty: pretty)
456
+ targets = node.inputs[1..].map { |n| emit(n, indent: indent + 1, pretty: pretty) }
457
+ if pretty
458
+ "(\n#{next_sp}#{source}\n#{next_sp}<: #{targets.join(",\n#{next_sp} ")}\n#{sp})"
459
+ else
460
+ "(#{source} <: #{targets.join(", ")})"
461
+ end
462
+ when NodeType::MERGE
463
+ left = emit(node.inputs[0], indent: indent + 1, pretty: pretty)
464
+ right = emit(node.inputs[1], indent: indent + 1, pretty: pretty)
465
+ if pretty
466
+ "(\n#{next_sp}#{left}\n#{next_sp}:> #{right}\n#{sp})"
467
+ else
468
+ "(#{left} :> #{right})"
469
+ end
470
+ when NodeType::FEEDBACK
471
+ left = emit(node.inputs[0], indent: indent + 1, pretty: pretty)
472
+ right = emit(node.inputs[1], indent: indent + 1, pretty: pretty)
473
+ if pretty
474
+ "(\n#{next_sp}#{left}\n#{next_sp}~ #{right}\n#{sp})"
475
+ else
476
+ "(#{left} ~ #{right})"
477
+ end
478
+
479
+ # === UTILITY ===
480
+ when NodeType::WIRE
481
+ "_"
482
+ when NodeType::CUT
483
+ "!"
484
+ when NodeType::MEM
485
+ "mem"
486
+ when NodeType::LITERAL
487
+ node.args[0].to_s
488
+
489
+ # === CONSTANTS ===
490
+ when NodeType::SR
491
+ "ma.SR"
492
+ when NodeType::PI
493
+ "ma.PI"
494
+ when NodeType::TEMPO
495
+ "ma.tempo"
496
+
497
+ # === ANTIALIASING ===
498
+ when NodeType::AA_TANH1
499
+ "aa.tanh1"
500
+ when NodeType::AA_TANH2
501
+ "aa.tanh2"
502
+ when NodeType::AA_ARCTAN
503
+ "aa.arctan"
504
+ when NodeType::AA_SOFTCLIP
505
+ "aa.softclip"
506
+ when NodeType::AA_HARDCLIP
507
+ "aa.hardclip"
508
+ when NodeType::AA_PARABOLIC
509
+ "aa.parabolic"
510
+ when NodeType::AA_SIN
511
+ "aa.sin"
512
+ when NodeType::AA_CUBIC1
513
+ "aa.cubic1"
514
+ when NodeType::AA_CUBIC2
515
+ "aa.cubic2"
516
+
517
+ # Analyzers (an.)
518
+ when NodeType::AMP_FOLLOWER
519
+ "an.amp_follower(#{emit_args(node, indent, pretty)})"
520
+ when NodeType::AMP_FOLLOWER_AR
521
+ "an.amp_follower_ar(#{emit_args(node, indent, pretty)})"
522
+ when NodeType::AMP_FOLLOWER_UD
523
+ "an.amp_follower_ud(#{emit_args(node, indent, pretty)})"
524
+ when NodeType::RMS_ENVELOPE_RECT
525
+ "an.rms_envelope_rect(#{emit_args(node, indent, pretty)})"
526
+ when NodeType::RMS_ENVELOPE_TAU
527
+ "an.rms_envelope_tau(#{emit_args(node, indent, pretty)})"
528
+ when NodeType::ABS_ENVELOPE_RECT
529
+ "an.abs_envelope_rect(#{emit_args(node, indent, pretty)})"
530
+ when NodeType::ABS_ENVELOPE_TAU
531
+ "an.abs_envelope_tau(#{emit_args(node, indent, pretty)})"
532
+ when NodeType::MS_ENVELOPE_RECT
533
+ "an.ms_envelope_rect(#{emit_args(node, indent, pretty)})"
534
+ when NodeType::MS_ENVELOPE_TAU
535
+ "an.ms_envelope_tau(#{emit_args(node, indent, pretty)})"
536
+ when NodeType::PEAK_ENVELOPE
537
+ "an.peak_envelope(#{emit_args(node, indent, pretty)})"
538
+
539
+ # Effects (ef.)
540
+ when NodeType::CUBICNL
541
+ "ef.cubicnl(#{emit_args(node, indent, pretty)})"
542
+ when NodeType::GATE_MONO
543
+ "ef.gate_mono(#{emit_args(node, indent, pretty)})"
544
+ when NodeType::GATE_STEREO
545
+ "ef.gate_stereo(#{emit_args(node, indent, pretty)})"
546
+ when NodeType::EF_COMPRESSOR_MONO
547
+ "ef.compressor_mono(#{emit_args(node, indent, pretty)})"
548
+ when NodeType::EF_COMPRESSOR_STEREO
549
+ "ef.compressor_stereo(#{emit_args(node, indent, pretty)})"
550
+ when NodeType::EF_LIMITER_1176_MONO
551
+ "ef.limiter_1176_R4_mono"
552
+ when NodeType::EF_LIMITER_1176_STEREO
553
+ "ef.limiter_1176_R4_stereo"
554
+ when NodeType::ECHO
555
+ "ef.echo(#{emit_args(node, indent, pretty)})"
556
+ when NodeType::TRANSPOSE
557
+ "ef.transpose(#{emit_args(node, indent, pretty)})"
558
+ when NodeType::FLANGER_MONO
559
+ "ef.flanger_mono(#{emit_args(node, indent, pretty)})"
560
+ when NodeType::FLANGER_STEREO
561
+ "ef.flanger_stereo(#{emit_args(node, indent, pretty)})"
562
+ when NodeType::PHASER2_MONO
563
+ "ef.phaser2_mono(#{emit_args(node, indent, pretty)})"
564
+ when NodeType::PHASER2_STEREO
565
+ "ef.phaser2_stereo(#{emit_args(node, indent, pretty)})"
566
+ when NodeType::WAH4
567
+ "ef.wah4(#{emit_args(node, indent, pretty)})"
568
+ when NodeType::AUTO_WAH
569
+ "ef.auto_wah(#{emit_args(node, indent, pretty)})"
570
+ when NodeType::CRYBABY
571
+ "ef.crybaby(#{emit_args(node, indent, pretty)})"
572
+ when NodeType::VOCODER
573
+ "ef.vocoder(#{emit_args(node, indent, pretty)})"
574
+ when NodeType::SPEAKERBP
575
+ "ef.speakerbp(#{emit_args(node, indent, pretty)})"
576
+ when NodeType::DRY_WET_MIXER
577
+ "ef.dryWetMixer(#{emit_args(node, indent, pretty)})"
578
+ when NodeType::DRY_WET_MIXER_CP
579
+ "ef.dryWetMixerConstantPower(#{emit_args(node, indent, pretty)})"
580
+
581
+ else
582
+ raise ArgumentError, "Unknown node type: #{node.type}"
583
+ end
584
+ end
585
+
586
+ # Helper to emit iteration body for par/seq/sum/prod
587
+ # Creates a symbolic parameter to capture the iterator variable
588
+ def emit_iteration_body(var, block, indent, pretty)
589
+ # Create a context that provides the iterator variable as a symbol
590
+ context = Object.new
591
+ context.extend(DSL)
592
+ # The block receives a symbolic representation of the iterator
593
+ iter_param = DSL.param(var)
594
+ body = block.call(iter_param)
595
+ body = DSL.send(:to_dsp, body)
596
+ emit(body.node, indent: indent, pretty: pretty)
597
+ end
598
+ end
599
+ end