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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +45 -0
- data/bin/faust2ruby +124 -0
- data/bin/ruby2faust +129 -0
- data/faust2ruby.md +523 -0
- data/lib/faust2ruby/ast.rb +315 -0
- data/lib/faust2ruby/ir_builder.rb +413 -0
- data/lib/faust2ruby/lexer.rb +255 -0
- data/lib/faust2ruby/library_mapper.rb +249 -0
- data/lib/faust2ruby/parser.rb +596 -0
- data/lib/faust2ruby/ruby_generator.rb +708 -0
- data/lib/faust2ruby/version.rb +5 -0
- data/lib/faust2ruby.rb +82 -0
- data/lib/frausto.rb +8 -0
- data/lib/ruby2faust/dsl.rb +1332 -0
- data/lib/ruby2faust/emitter.rb +599 -0
- data/lib/ruby2faust/ir.rb +285 -0
- data/lib/ruby2faust/live.rb +82 -0
- data/lib/ruby2faust/version.rb +5 -0
- data/lib/ruby2faust.rb +27 -0
- data/ruby2faust.md +334 -0
- metadata +106 -0
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
|