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.
data/faust2ruby.md ADDED
@@ -0,0 +1,523 @@
1
+ # faust2ruby
2
+
3
+ Convert Faust DSP code to Ruby DSL code compatible with ruby2faust.
4
+
5
+ ## Installation
6
+
7
+ faust2ruby is included with the frausto gem:
8
+
9
+ ```bash
10
+ gem install frausto
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ### Command Line
16
+
17
+ ```bash
18
+ # Convert a Faust file to Ruby
19
+ faust2ruby input.dsp -o output.rb
20
+
21
+ # Output only the process expression (no boilerplate)
22
+ faust2ruby -e input.dsp
23
+
24
+ # Read from stdin
25
+ echo 'process = os.osc(440) : *(0.5);' | faust2ruby -e
26
+ ```
27
+
28
+ ### Options
29
+
30
+ ```
31
+ Usage: faust2ruby [options] <input.dsp>
32
+ -o, --output FILE Output Ruby file (default: stdout)
33
+ -e, --expression Output only process expression (no boilerplate)
34
+ -v, --verbose Verbose output (show parsing info)
35
+ -t, --tokens Show lexer tokens (debug mode)
36
+ -a, --ast Show AST (debug mode)
37
+ --version Show version
38
+ -h, --help Show help
39
+ ```
40
+
41
+ ### Ruby API
42
+
43
+ ```ruby
44
+ require 'faust2ruby'
45
+
46
+ # Convert Faust to Ruby code
47
+ faust_code = 'process = os.osc(440) : *(0.5);'
48
+ ruby_code = Faust2Ruby.to_ruby(faust_code)
49
+ puts ruby_code
50
+
51
+ # Expression only
52
+ ruby_expr = Faust2Ruby.to_ruby(faust_code, expression_only: true)
53
+ # => "(osc(440) >> gain(0.5))"
54
+
55
+ # Parse to AST
56
+ program = Faust2Ruby.parse(faust_code)
57
+
58
+ # Tokenize
59
+ tokens = Faust2Ruby.tokenize(faust_code)
60
+ ```
61
+
62
+ ## Examples
63
+
64
+ ### Simple Oscillator
65
+
66
+ **Input (Faust):**
67
+ ```faust
68
+ process = os.osc(440) : *(0.5);
69
+ ```
70
+
71
+ **Output (Ruby):**
72
+ ```ruby
73
+ require 'ruby2faust'
74
+ include Ruby2Faust::DSL
75
+
76
+ process = (osc(440) >> gain(0.5))
77
+
78
+ puts Ruby2Faust::Emitter.program(process)
79
+ ```
80
+
81
+ ### Synthesizer with Controls
82
+
83
+ **Input (Faust):**
84
+ ```faust
85
+ import("stdfaust.lib");
86
+ declare name "synth";
87
+
88
+ freq = hslider("freq", 440, 20, 20000, 1);
89
+ gain = hslider("gain", 0.5, 0, 1, 0.01);
90
+
91
+ process = os.osc(freq) : *(gain);
92
+ ```
93
+
94
+ **Output (Ruby):**
95
+ ```ruby
96
+ require 'ruby2faust'
97
+ include Ruby2Faust::DSL
98
+
99
+ # declare name "synth"
100
+
101
+ freq = slider("freq", init: 440, min: 20, max: 20000, step: 1)
102
+
103
+ gain = slider("gain", init: 0.5, min: 0, max: 1, step: 0.01)
104
+
105
+ process = (osc(freq) >> gain(gain))
106
+
107
+ puts Ruby2Faust::Emitter.program(process)
108
+ ```
109
+
110
+ ### Parallel Composition
111
+
112
+ **Input (Faust):**
113
+ ```faust
114
+ process = os.osc(440) , os.osc(880);
115
+ ```
116
+
117
+ **Output (Ruby):**
118
+ ```ruby
119
+ (osc(440) | osc(880))
120
+ ```
121
+
122
+ ### Feedback Loop
123
+
124
+ **Input (Faust):**
125
+ ```faust
126
+ process = _ ~ (de.delay(44100, 22050) : *(0.5));
127
+ ```
128
+
129
+ **Output (Ruby):**
130
+ ```ruby
131
+ (wire ~ (delay(44100, 22050) >> gain(0.5)))
132
+ ```
133
+
134
+ ### Iteration
135
+
136
+ **Input (Faust):**
137
+ ```faust
138
+ process = par(i, 4, os.osc(i * 100));
139
+ ```
140
+
141
+ **Output (Ruby):**
142
+ ```ruby
143
+ fpar(4) { |i| osc((i * 100)) }
144
+ ```
145
+
146
+ ## Supported Faust Constructs
147
+
148
+ ### Composition Operators
149
+
150
+ | Faust | Ruby |
151
+ |-------|------|
152
+ | `a : b` | `a >> b` |
153
+ | `a , b` | `a \| b` |
154
+ | `a <: b` | `a.split(b)` |
155
+ | `a :> b` | `a.merge(b)` |
156
+ | `a ~ b` | `a ~ b` |
157
+
158
+ ### Arithmetic
159
+
160
+ | Faust | Ruby |
161
+ |-------|------|
162
+ | `a + b` | `a + b` |
163
+ | `a - b` | `a - b` |
164
+ | `a * b` | `a * b` |
165
+ | `a / b` | `a / b` |
166
+ | `a % b` | `a % b` |
167
+ | `*(x)` | `gain(x)` |
168
+
169
+ ### Comparison
170
+
171
+ | Faust | Ruby |
172
+ |-------|------|
173
+ | `a < b` | `a < b` |
174
+ | `a > b` | `a > b` |
175
+ | `a <= b` | `a <= b` |
176
+ | `a >= b` | `a >= b` |
177
+ | `a == b` | `a.eq(b)` |
178
+ | `a != b` | `a.neq(b)` |
179
+
180
+ Note: `eq()` and `neq()` are methods because Ruby's `==` must return boolean.
181
+
182
+ ### Bitwise
183
+
184
+ | Faust | Ruby |
185
+ |-------|------|
186
+ | `a & b` | `a & b` |
187
+ | `a \| b` (bitwise) | `a.bor(b)` |
188
+ | `xor(a, b)` | `a ^ b` |
189
+
190
+ Note: `bor()` is a method because `\|` is used for parallel composition in the DSL.
191
+
192
+ ### Library Functions
193
+
194
+ | Faust | Ruby |
195
+ |-------|------|
196
+ | `os.osc(f)` | `osc(f)` |
197
+ | `os.sawtooth(f)` | `saw(f)` |
198
+ | `os.square(f)` | `square(f)` |
199
+ | `os.triangle(f)` | `triangle(f)` |
200
+ | `no.noise` | `noise` |
201
+ | `fi.lowpass(n, f)` | `lp(f, order: n)` |
202
+ | `fi.highpass(n, f)` | `hp(f, order: n)` |
203
+ | `fi.resonlp(f, q, g)` | `resonlp(f, q, g)` |
204
+ | `de.delay(m, d)` | `delay(m, d)` |
205
+ | `de.fdelay(m, d)` | `fdelay(m, d)` |
206
+ | `en.adsr(a,d,s,r,g)` | `adsr(a, d, s, r, g)` |
207
+ | `ba.db2linear(x)` | `db2linear(x)` |
208
+ | `si.smoo` | `smoo` |
209
+ | `sp.panner(p)` | `panner(p)` |
210
+
211
+ ### UI Elements
212
+
213
+ | Faust | Ruby |
214
+ |-------|------|
215
+ | `hslider("n", i, mn, mx, s)` | `slider("n", init: i, min: mn, max: mx, step: s)` |
216
+ | `vslider(...)` | `vslider(...)` |
217
+ | `nentry(...)` | `nentry(...)` |
218
+ | `button("n")` | `button("n")` |
219
+ | `checkbox("n")` | `checkbox("n")` |
220
+ | `hgroup("n", e)` | `hgroup("n") { e }` |
221
+ | `vgroup("n", e)` | `vgroup("n") { e }` |
222
+
223
+ ### Iteration
224
+
225
+ | Faust | Ruby |
226
+ |-------|------|
227
+ | `par(i, n, e)` | `fpar(n) { \|i\| e }` |
228
+ | `seq(i, n, e)` | `fseq(n) { \|i\| e }` |
229
+ | `sum(i, n, e)` | `fsum(n) { \|i\| e }` |
230
+ | `prod(i, n, e)` | `fprod(n) { \|i\| e }` |
231
+
232
+ ### Tables
233
+
234
+ | Faust | Ruby |
235
+ |-------|------|
236
+ | `waveform{v1, v2, ...}` | `waveform(v1, v2, ...)` |
237
+ | `rdtable(n, i, r)` | `rdtable(n, i, r)` |
238
+ | `rwtable(n, i, w, s, r)` | `rwtable(n, i, w, s, r)` |
239
+
240
+ ### Primitives
241
+
242
+ | Faust | Ruby |
243
+ |-------|------|
244
+ | `_` | `wire` |
245
+ | `!` | `cut` |
246
+ | `mem` | `mem` |
247
+ | `ma.SR` | `sr` |
248
+ | `ma.PI` | `pi` |
249
+
250
+ ## Round-trip Conversion
251
+
252
+ faust2ruby is designed to work with ruby2faust for round-trip conversion:
253
+
254
+ ```ruby
255
+ require 'faust2ruby'
256
+ require 'ruby2faust'
257
+
258
+ # Faust → Ruby
259
+ faust_input = 'process = os.osc(440) : *(0.5);'
260
+ ruby_code = Faust2Ruby.to_ruby(faust_input, expression_only: true)
261
+
262
+ # Ruby → Faust
263
+ include Ruby2Faust::DSL
264
+ process = eval(ruby_code)
265
+ faust_output = Ruby2Faust::Emitter.program(process)
266
+ ```
267
+
268
+ ## With Clauses
269
+
270
+ `with` clauses are converted to Ruby lambdas for proper scoping:
271
+
272
+ **Input (Faust):**
273
+ ```faust
274
+ myDSP = result with {
275
+ gain = 0.5;
276
+ result = _ * gain;
277
+ };
278
+ ```
279
+
280
+ **Output (Ruby):**
281
+ ```ruby
282
+ myDSP = -> {
283
+ gain = 0.5
284
+ result = (wire * gain)
285
+ result
286
+ }.call
287
+ ```
288
+
289
+ Function-style local definitions use `flambda`:
290
+ ```ruby
291
+ adaa = flambda(:x0, :x1) { |x0, x1| select2(...) }
292
+ ```
293
+
294
+ ## Partial Application
295
+
296
+ Faust's partial application creates reusable signal processors by providing some arguments upfront.
297
+
298
+ ### Clipping / Limiting
299
+
300
+ **Input (Faust):**
301
+ ```faust
302
+ // Clip signal to [-1, 1] range
303
+ safetyLimit = min(1) : max(-1);
304
+ process = osc(440) : safetyLimit;
305
+ ```
306
+
307
+ **Output (Ruby):**
308
+ ```ruby
309
+ safetyLimit = (flambda(:x) { |x| min_(x, 1) } >> flambda(:x) { |x| max_(x, (-1)) })
310
+ process = (osc(440) >> safetyLimit)
311
+ ```
312
+
313
+ ### Conditional Routing
314
+
315
+ **Input (Faust):**
316
+ ```faust
317
+ // Switch between two signals based on condition
318
+ useWet = checkbox("wet");
319
+ effect = _ <: _, reverb : select2(useWet);
320
+ ```
321
+
322
+ **Output (Ruby):**
323
+ ```ruby
324
+ useWet = checkbox("wet")
325
+ effect = wire.split(wire, reverb) >> flambda(:x, :y) { |x, y| select2(useWet, x, y) }
326
+ ```
327
+
328
+ ### Gain Stages
329
+
330
+ **Input (Faust):**
331
+ ```faust
332
+ // Partial application of multiplication
333
+ halfGain = *(0.5);
334
+ quarterGain = *(0.25);
335
+ process = osc(440) : halfGain;
336
+ ```
337
+
338
+ **Output (Ruby):**
339
+ ```ruby
340
+ halfGain = gain(0.5)
341
+ quarterGain = gain(0.25)
342
+ process = (osc(440) >> halfGain)
343
+ ```
344
+
345
+ ### Filter Configuration
346
+
347
+ **Input (Faust):**
348
+ ```faust
349
+ // Second-order lowpass waiting for cutoff frequency
350
+ smoothFilter = fi.lowpass(2);
351
+ process = _ : smoothFilter(1000);
352
+ ```
353
+
354
+ **Output (Ruby):**
355
+ ```ruby
356
+ smoothFilter = flambda(:x) { |x| lp(x, order: 2) }
357
+ process = (wire >> smoothFilter.call(1000)) # Note: needs .call in Ruby
358
+ ```
359
+
360
+ ### Math Functions as Processors
361
+
362
+ **Input (Faust):**
363
+ ```faust
364
+ // 0-arg functions applied to signals
365
+ softclip(x) = tanh(x);
366
+ process = osc(440) * 2 : softclip;
367
+ ```
368
+
369
+ **Output (Ruby):**
370
+ ```ruby
371
+ def softclip(x)
372
+ (x >> tanh_)
373
+ end
374
+ process = ((osc(440) * 2) >> softclip(wire))
375
+ ```
376
+
377
+ ## What Uses `literal()`
378
+
379
+ Some Faust constructs are emitted as `literal()` calls to preserve semantics:
380
+
381
+ | Construct | Example | Reason |
382
+ |-----------|---------|--------|
383
+ | Letrec blocks | `letrec { 'x = ... }` | Complex state semantics |
384
+ | Unmapped library functions | `an.amp_follower(t)` | Not in library mapper |
385
+ | Partial app (4+ args) | `route(4, 4, ...)` | Requires complex curry |
386
+
387
+ Most common Faust library functions are mapped, including `fi.*`, `os.*`, `de.*`, `en.*`, `ba.*`, `si.*`, `aa.*`, and math primitives.
388
+
389
+ ## Limitations
390
+
391
+ **Supported with limitations:**
392
+ - `with` clauses: Local definitions work, scoped via Ruby lambda
393
+ - Partial application: Works for 1-3 missing arguments
394
+ - Forward references: Functions defined later in `with` blocks are resolved
395
+
396
+ **Not supported:**
397
+ - Pattern matching on function arguments (see below)
398
+ - Foreign functions (`ffunction`)
399
+ - Component/library imports beyond path tracking
400
+
401
+ ### Case Expressions
402
+
403
+ Case expressions with integer patterns are converted to `select2` chains:
404
+
405
+ **Input (Faust):**
406
+ ```faust
407
+ process = case {
408
+ (0) => 1;
409
+ (1) => 2;
410
+ (n) => n * 2;
411
+ };
412
+ ```
413
+
414
+ **Output (Ruby):**
415
+ ```ruby
416
+ flambda(:n) { |n| select2(n.eq(0), select2(n.eq(1), (n * 2), 2), 1) }
417
+ ```
418
+
419
+ The variable pattern `(n)` becomes the default/else case, and integer patterns are checked in order.
420
+
421
+ **Limitations:**
422
+ - Only integer patterns are converted to `select2` (variable patterns become default)
423
+ - Complex patterns (tuples, nested expressions) fall back to `literal()`
424
+ - Recursive functions like `fact(0) = 1; fact(n) = n * fact(n-1)` require compile-time evaluation not available at runtime
425
+
426
+ ### Pattern Matching on Function Arguments
427
+
428
+ Multi-rule function definitions with single-parameter pattern matching are now supported:
429
+
430
+ **Input (Faust):**
431
+ ```faust
432
+ fact(0) = 1;
433
+ fact(n) = n * fact(n - 1);
434
+ ```
435
+
436
+ **Output (Ruby):**
437
+ ```ruby
438
+ fact = flambda(:n) { |n| select2(n.eq(0), (n * fact((n - 1))), 1) }
439
+ ```
440
+
441
+ Multiple definitions with the same name are automatically merged into a case expression, then converted to `select2` chains.
442
+
443
+ **Limitations:**
444
+ - Only single-parameter pattern matching is supported
445
+ - Multi-parameter patterns (e.g., `foo(0, 0) = a; foo(x, y) = b;`) are not merged
446
+ - Recursive functions like factorial require compile-time evaluation (the Ruby output is syntactically correct but may not produce the same runtime behavior)
447
+
448
+ ## Known Issues
449
+
450
+ ### letrec Blocks
451
+
452
+ `letrec` blocks are emitted as `literal()` calls because they implement complex recursive state semantics that don't have a direct Ruby DSL equivalent.
453
+
454
+ **Example Faust:**
455
+ ```faust
456
+ // Spring-damper physics simulation
457
+ follower(input) = pos letrec {
458
+ 'v = v + step * (-damping * v - stiffness * (pos - input));
459
+ 'pos = pos + step * v;
460
+ };
461
+ ```
462
+
463
+ **Generated Ruby:**
464
+ ```ruby
465
+ literal("letrec { 'v = (v + (step * ...)); 'pos = (pos + (step * v)) } pos")
466
+ ```
467
+
468
+ **Why this happens:**
469
+ - `'v` means "next value of v" (sample n+1)
470
+ - `v` means "current value of v" (sample n)
471
+ - This creates mutually recursive signals with feedback
472
+ - Ruby lacks native syntax for this pattern
473
+
474
+ **Workarounds:**
475
+ 1. **Accept the literal** - Round-trip conversion still works; the Faust code is preserved
476
+ 2. **Use simple feedback** - For single-variable recursion, use `wire ~ expr` instead
477
+ 3. **Refactor in Faust** - Sometimes letrec can be rewritten using standard feedback
478
+
479
+ **Impact:** In practice, letrec is rare. Complex DSP files like triode.lib (500+ lines) convert with only 1 literal remaining (the letrec block).
480
+
481
+ ### Unmapped Library Functions
482
+
483
+ The following Faust library namespaces are not yet mapped and will emit `literal()`:
484
+
485
+ | Namespace | Description | Status |
486
+ |-----------|-------------|--------|
487
+ | `ve.*` | Virtual analog (Moog filters, etc.) | Not mapped |
488
+ | `pm.*` | Physical modeling | Not mapped |
489
+ | `sy.*` | Synthesizers | Not mapped |
490
+ | `dx.*` | DX7 emulation | Not mapped |
491
+ | `pf.*` | Phaflangers | Not mapped |
492
+ | `dm.*` | Demos | Not mapped |
493
+
494
+ **Currently mapped:**
495
+ - `os.*` - Oscillators (osc, saw, square, triangle, phasor, lf_*)
496
+ - `no.*` - Noise (noise, pink_noise)
497
+ - `fi.*` - Filters (lowpass, highpass, resonlp, svf.*, allpass, dcblocker, peak_eq, tf1/tf2, etc.)
498
+ - `de.*` - Delays (delay, fdelay, sdelay)
499
+ - `en.*` - Envelopes (ar, asr, adsr, adsre)
500
+ - `ba.*` - Basics (db2linear, linear2db, tau2pole, midikey2hz, selectn, if, take)
501
+ - `si.*` - Signals (smooth, smoo, bus, block)
502
+ - `ma.*` - Math (SR, PI, tempo, tanh)
503
+ - `re.*` - Reverbs (mono_freeverb, zita_rev1_stereo, jpverb)
504
+ - `co.*` - Compressors (compressor_mono, limiter_1176_R4_mono)
505
+ - `sp.*` - Spatial (panner)
506
+ - `aa.*` - Antialiasing (tanh1, tanh2, arctan, softclip, hardclip, etc.)
507
+ - `an.*` - Analyzers (amp_follower, amp_follower_ar, rms_envelope_*, abs_envelope_*, peak_envelope)
508
+ - `ef.*` - Effects (gate_mono/stereo, flanger_mono/stereo, phaser2_mono/stereo, wah4, auto_wah, crybaby, echo, transpose, vocoder, speakerbp, cubicnl, dryWetMixer)
509
+ - Math primitives (sin, cos, tan, tanh, sinh, cosh, abs, min, max, pow, sqrt, exp, log, floor, ceil, etc.)
510
+
511
+ **Contributing:** To add support for unmapped functions, edit `lib/faust2ruby/library_mapper.rb` and add corresponding entries to `lib/ruby2faust/ir.rb`, `lib/ruby2faust/dsl.rb`, and `lib/ruby2faust/emitter.rb`.
512
+
513
+ ## Architecture
514
+
515
+ ```
516
+ Faust Source → Lexer → Parser → AST → Ruby Generator → Ruby DSL
517
+ ```
518
+
519
+ - **Lexer** (`lexer.rb`): StringScanner-based tokenizer
520
+ - **Parser** (`parser.rb`): Recursive descent parser
521
+ - **AST** (`ast.rb`): Abstract syntax tree nodes
522
+ - **Generator** (`ruby_generator.rb`): Produces Ruby DSL code
523
+ - **Library Mapper** (`library_mapper.rb`): Maps Faust functions to Ruby methods