frausto 0.2.3 → 0.2.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e6f5347160a2abe4136e948b171065dbab0d4b2816d57dc3d341a7b039c3717b
4
- data.tar.gz: fe523a91e6cc71d3de0a42fc592c1ccd21337b67d2aa2355f1da85b7428f06a8
3
+ metadata.gz: '093f5e9746f4614c8b4085708ccd9b2bf092d06c3a50f5ff08fec837a86ff978'
4
+ data.tar.gz: 4f1262a4cf6d6e9357f9315729b9628b123034c5092867b9a48f78850acef31f
5
5
  SHA512:
6
- metadata.gz: 162fe8eb9540ee3d3ca709b8ddf5554bd58bfae2188af5e74d1b9505113d29cc31cee58482d9cbf53b38cd3f4ae6d1d4dde31108862d08a552e42767131000cc
7
- data.tar.gz: c16fb85269cf137f8d55443b9e1337b56d20f46de9cd0d816bf3b24c41faa5abd343b6b648d84f27a0eea2f326223b746ea4042dcaad201717fbad703bf2dff4
6
+ metadata.gz: f2b150ac6dfedb611dd4e924b0497f6c9170122593401d13eff0e8771effc241f3512227fd592d2df6f49278c870015c6c3b7221fd9010e035a1635a4bf1464b
7
+ data.tar.gz: 7bffe90c34b3ba5a7576044fe6d2f5c0768dbffdf893ded31fb10bfb9425e57b99e1028fb2e5c37ee4eb2d6d5c28c70672b06c76cc19916a0f42a39f6f14e6f9
data/README.md CHANGED
@@ -30,24 +30,18 @@ gem 'frausto'
30
30
  require 'ruby2faust'
31
31
 
32
32
  code = Ruby2Faust.generate do
33
- freq = 60.midi >> smoo
34
- -6.db * ((osc(freq) + 0.1 * noise) >> lp(2000))
33
+ freq = hslider("freq", init: 48, min: 20, max: 100, step: 1) >> midi2hz >> smoo
34
+ amp = hslider("amp", init: -12, min: -60, max: 0, step: 1) >> db2linear >> smoo
35
+ osc(freq) >> lp(2000) >> gain(amp)
35
36
  end
36
37
 
37
38
  puts code
38
- # => import("stdfaust.lib");
39
- # process = os.osc(ba.midikey2hz(60) : si.smoo) + (no.noise : *(0.1)) : fi.lowpass(1, 2000) : *(ba.db2linear(-6));
40
-
41
- # Use pretty: true for readable output
42
- puts Ruby2Faust.generate(pretty: true) { -6.db * ((osc(60.midi >> smoo) + 0.1 * noise) >> lp(2000)) }
43
- # => import("stdfaust.lib");
44
- # process =
45
- # os.osc(
46
- # ba.midikey2hz(60)
47
- # : si.smoo
48
- # ) + (no.noise : *(0.1))
49
- # : fi.lowpass(1, 2000)
50
- # : *(ba.db2linear(-6));
39
+ # import("stdfaust.lib");
40
+ #
41
+ # process =
42
+ # os.osc(hslider("freq", 48, 20, 100, 1) : ba.midikey2hz : si.smoo)
43
+ # : fi.lowpass(1, 2000)
44
+ # : *(hslider("amp", -12, -60, 0, 1) : ba.db2linear : si.smoo);
51
45
  ```
52
46
 
53
47
  ```ruby
data/bin/faust2ruby CHANGED
@@ -18,7 +18,7 @@ parser = OptionParser.new do |opts|
18
18
  opts.separator ""
19
19
  opts.separator "Options:"
20
20
 
21
- opts.on("-o", "--output FILE", "Output Ruby file (default: stdout)") do |file|
21
+ opts.on("-o", "--output FILE", "Output Ruby file (default: <input>.rb)") do |file|
22
22
  options[:output] = file
23
23
  end
24
24
 
@@ -74,6 +74,8 @@ else
74
74
  end
75
75
  source = File.read(input_path)
76
76
  input_name = input_path
77
+ # Default output to input.dsp.rb (unless expression-only mode or explicit -o)
78
+ options[:output] ||= "#{input_path}.rb" unless options[:expression_only]
77
79
  end
78
80
 
79
81
  begin
data/faust2ruby.md CHANGED
@@ -4,8 +4,6 @@ Convert Faust DSP code to Ruby DSL code compatible with ruby2faust.
4
4
 
5
5
  ## Installation
6
6
 
7
- faust2ruby is included with the frausto gem:
8
-
9
7
  ```bash
10
8
  gem install frausto
11
9
  ```
@@ -15,29 +13,19 @@ gem install frausto
15
13
  ### Command Line
16
14
 
17
15
  ```bash
18
- # Convert a Faust file to Ruby
19
- faust2ruby input.dsp -o output.rb
16
+ # Convert a Faust file to Ruby (writes to file.dsp.rb)
17
+ faust2ruby file.dsp
18
+
19
+ # Specify output file
20
+ faust2ruby file.dsp -o other.rb
20
21
 
21
- # Output only the process expression (no boilerplate)
22
- faust2ruby -e input.dsp
22
+ # Output only the process expression (no boilerplate, to stdout)
23
+ faust2ruby -e file.dsp
23
24
 
24
25
  # Read from stdin
25
26
  echo 'process = os.osc(440) : *(0.5);' | faust2ruby -e
26
27
  ```
27
28
 
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
29
  ### Ruby API
42
30
 
43
31
  ```ruby
@@ -46,106 +34,67 @@ require 'faust2ruby'
46
34
  # Convert Faust to Ruby code
47
35
  faust_code = 'process = os.osc(440) : *(0.5);'
48
36
  ruby_code = Faust2Ruby.to_ruby(faust_code)
49
- puts ruby_code
50
37
 
51
38
  # Expression only
52
39
  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)
40
+ # => "0.5 * osc(440)"
60
41
  ```
61
42
 
62
43
  ## Examples
63
44
 
64
45
  ### Simple Oscillator
65
46
 
66
- **Input (Faust):**
67
47
  ```faust
68
48
  process = os.osc(440) : *(0.5);
69
49
  ```
70
-
71
- **Output (Ruby):**
72
50
  ```ruby
73
- require 'ruby2faust'
74
- include Ruby2Faust::DSL
75
-
76
- process = (osc(440) >> gain(0.5))
77
-
78
- puts Ruby2Faust::Emitter.program(process)
51
+ 0.5 * osc(440)
79
52
  ```
80
53
 
81
54
  ### Synthesizer with Controls
82
55
 
83
- **Input (Faust):**
84
56
  ```faust
85
57
  import("stdfaust.lib");
86
- declare name "synth";
87
-
88
58
  freq = hslider("freq", 440, 20, 20000, 1);
89
- gain = hslider("gain", 0.5, 0, 1, 0.01);
90
-
91
- process = os.osc(freq) : *(gain);
59
+ amp = hslider("amp", 0.5, 0, 1, 0.01);
60
+ process = os.osc(freq) : *(amp);
92
61
  ```
93
-
94
- **Output (Ruby):**
95
62
  ```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)
63
+ freq = hslider("freq", init: 440, min: 20, max: 20000, step: 1)
64
+ amp = hslider("amp", init: 0.5, min: 0, max: 1, step: 0.01)
65
+ process = amp * osc(freq)
108
66
  ```
109
67
 
110
- ### Parallel Composition
68
+ ### Stereo Output
111
69
 
112
- **Input (Faust):**
113
70
  ```faust
114
71
  process = os.osc(440) , os.osc(880);
115
72
  ```
116
-
117
- **Output (Ruby):**
118
73
  ```ruby
119
- (osc(440) | osc(880))
74
+ osc(440) | osc(880)
120
75
  ```
121
76
 
122
- ### Feedback Loop
77
+ ### Feedback Delay
123
78
 
124
- **Input (Faust):**
125
79
  ```faust
126
80
  process = _ ~ (de.delay(44100, 22050) : *(0.5));
127
81
  ```
128
-
129
- **Output (Ruby):**
130
82
  ```ruby
131
- (wire ~ (delay(44100, 22050) >> gain(0.5)))
83
+ wire ~ (0.5 * delay(44100, 22050))
132
84
  ```
133
85
 
134
86
  ### Iteration
135
87
 
136
- **Input (Faust):**
137
88
  ```faust
138
89
  process = par(i, 4, os.osc(i * 100));
139
90
  ```
140
-
141
- **Output (Ruby):**
142
91
  ```ruby
143
- fpar(4) { |i| osc((i * 100)) }
92
+ fpar(4) { |i| osc(i * 100) }
144
93
  ```
145
94
 
146
- ## Supported Faust Constructs
95
+ ## Mapping Overview
147
96
 
148
- ### Composition Operators
97
+ ### Composition
149
98
 
150
99
  | Faust | Ruby |
151
100
  |-------|------|
@@ -154,88 +103,27 @@ fpar(4) { |i| osc((i * 100)) }
154
103
  | `a <: b` | `a.split(b)` |
155
104
  | `a :> b` | `a.merge(b)` |
156
105
  | `a ~ b` | `a ~ b` |
106
+ | `*(x)` | `x *` (idiomatic) or `gain(x)` |
157
107
 
158
- ### Arithmetic
108
+ ### Numeric Extensions
159
109
 
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
110
+ When arguments are numeric literals, faust2ruby emits idiomatic Ruby:
170
111
 
171
112
  | Faust | Ruby |
172
113
  |-------|------|
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.
114
+ | `ba.db2linear(-6)` | `-6.db` |
115
+ | `ba.midikey2hz(60)` | `60.midi` |
116
+ | `ba.sec2samp(0.1)` | `0.1.sec` |
191
117
 
192
118
  ### Library Functions
193
119
 
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 }` |
120
+ Most `os.*`, `fi.*`, `de.*`, `en.*`, `ba.*`, `si.*`, `re.*`, `co.*`, `sp.*`, `aa.*`, `an.*`, `ef.*` functions are mapped. Examples:
222
121
 
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)` |
122
+ - `os.osc(f)` → `osc(f)`
123
+ - `fi.lowpass(n, f)` → `lp(f, order: n)`
124
+ - `de.delay(m, d)` `delay(m, d)`
125
+ - `en.adsr(a,d,s,r,g)` → `adsr(a, d, s, r, g)`
126
+ - `si.smoo` `smoo`
239
127
 
240
128
  ### Primitives
241
129
 
@@ -245,164 +133,43 @@ Note: `bor()` is a method because `\|` is used for parallel composition in the D
245
133
  | `!` | `cut` |
246
134
  | `mem` | `mem` |
247
135
  | `ma.SR` | `sr` |
248
- | `ma.PI` | `pi` |
249
136
 
250
137
  ## Round-trip Conversion
251
138
 
252
- faust2ruby is designed to work with ruby2faust for round-trip conversion:
253
-
254
139
  ```ruby
255
- require 'faust2ruby'
256
- require 'ruby2faust'
140
+ require 'frausto'
257
141
 
258
- # Faust → Ruby
142
+ # Faust → Ruby → Faust
259
143
  faust_input = 'process = os.osc(440) : *(0.5);'
260
- ruby_code = Faust2Ruby.to_ruby(faust_input, expression_only: true)
144
+ ruby_expr = Faust2Ruby.to_ruby(faust_input, expression_only: true)
261
145
 
262
- # Ruby → Faust
263
146
  include Ruby2Faust::DSL
264
- process = eval(ruby_code)
147
+ process = eval(ruby_expr)
265
148
  faust_output = Ruby2Faust::Emitter.program(process)
266
149
  ```
267
150
 
268
- ## With Clauses
151
+ ## Advanced Features
269
152
 
270
- `with` clauses are converted to Ruby lambdas for proper scoping:
153
+ ### With Clauses
154
+
155
+ Local definitions are converted to Ruby lambdas:
271
156
 
272
- **Input (Faust):**
273
157
  ```faust
274
- myDSP = result with {
158
+ myDSP = _ * gain with {
275
159
  gain = 0.5;
276
- result = _ * gain;
277
160
  };
278
161
  ```
279
-
280
- **Output (Ruby):**
281
162
  ```ruby
282
163
  myDSP = -> {
283
164
  gain = 0.5
284
- result = (wire * gain)
285
- result
165
+ (wire * gain)
286
166
  }.call
287
167
  ```
288
168
 
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
169
  ### Case Expressions
402
170
 
403
- Case expressions with integer patterns are converted to `select2` chains:
171
+ Faust's `case` creates a pattern-matching function—the `(n)` pattern binds the input signal to variable `n`:
404
172
 
405
- **Input (Faust):**
406
173
  ```faust
407
174
  process = case {
408
175
  (0) => 1;
@@ -410,114 +177,35 @@ process = case {
410
177
  (n) => n * 2;
411
178
  };
412
179
  ```
413
-
414
- **Output (Ruby):**
415
180
  ```ruby
416
- flambda(:n) { |n| select2(n.eq(0), select2(n.eq(1), (n * 2), 2), 1) }
181
+ fcase(0 => 1, 1 => 2) { |n| (n * 2) }
417
182
  ```
418
183
 
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
184
+ The `fcase` DSL method handles integer patterns as a hash, with the block as the default case.
425
185
 
426
- ### Pattern Matching on Function Arguments
186
+ ### Partial Application
427
187
 
428
- Multi-rule function definitions with single-parameter pattern matching are now supported:
429
-
430
- **Input (Faust):**
431
188
  ```faust
432
- fact(0) = 1;
433
- fact(n) = n * fact(n - 1);
189
+ halfGain = *(0.5);
190
+ process = osc(440) : halfGain;
434
191
  ```
435
-
436
- **Output (Ruby):**
437
192
  ```ruby
438
- fact = flambda(:n) { |n| select2(n.eq(0), (n * fact((n - 1))), 1) }
193
+ halfGain = gain(0.5)
194
+ process = osc(440) >> halfGain
439
195
  ```
440
196
 
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
- ```
197
+ ## Limitations
462
198
 
463
- **Generated Ruby:**
464
- ```ruby
465
- literal("letrec { 'v = (v + (step * ...)); 'pos = (pos + (step * v)) } pos")
466
- ```
199
+ **Not fully supported:**
200
+ - `letrec` blocks (emitted as `literal()`)
201
+ - Foreign functions (`ffunction`)
202
+ - Multi-parameter pattern matching
203
+ - Some library namespaces: `ve.*`, `pm.*`, `sy.*`, `dx.*`
467
204
 
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`.
205
+ Unmapped functions are preserved as `literal("...")` for round-trip compatibility.
512
206
 
513
207
  ## Architecture
514
208
 
515
209
  ```
516
210
  Faust Source → Lexer → Parser → AST → Ruby Generator → Ruby DSL
517
211
  ```
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
@@ -29,6 +29,9 @@ module Faust2Ruby
29
29
  imports = program.statements.select { |s| s.is_a?(AST::Import) }.map(&:path)
30
30
  declares = program.statements.select { |s| s.is_a?(AST::Declare) }
31
31
 
32
+ # Standard Faust output names
33
+ output_names = %w[process effect]
34
+
32
35
  unless @expression_only
33
36
  lines << "require 'ruby2faust'"
34
37
  lines << "include Ruby2Faust::DSL"
@@ -41,26 +44,28 @@ module Faust2Ruby
41
44
 
42
45
  lines << "" if declares.any?
43
46
 
44
- # Generate helper definitions (excluding process)
47
+ # Generate helper definitions (excluding outputs)
45
48
  merged_statements.each do |stmt|
46
- if stmt.is_a?(AST::Definition) && stmt.name != "process"
49
+ if stmt.is_a?(AST::Definition) && !output_names.include?(stmt.name)
47
50
  lines << generate_definition(stmt)
48
51
  lines << ""
49
52
  end
50
53
  end
51
54
  end
52
55
 
53
- # Generate process
54
- process_def = @definitions["process"]
55
- if process_def
56
+ # Find the main output (process or effect)
57
+ output_name = output_names.find { |name| @definitions[name] }
58
+ output_def = @definitions[output_name] if output_name
59
+
60
+ if output_def
56
61
  if @expression_only
57
- lines << generate_expression(process_def.expression)
62
+ lines << generate_expression(output_def.expression)
58
63
  else
59
- lines << "process = #{generate_expression(process_def.expression)}"
64
+ lines << "#{output_name} = #{generate_expression(output_def.expression)}"
60
65
  lines << ""
61
66
 
62
67
  # Build program with imports and declares
63
- lines << "prog = Ruby2Faust::Program.new(process)"
68
+ lines << "prog = Ruby2Faust::Program.new(#{output_name})"
64
69
  imports.each do |imp|
65
70
  lines << " .import(#{imp.inspect})" unless imp == "stdfaust.lib"
66
71
  end
@@ -68,7 +73,7 @@ module Faust2Ruby
68
73
  lines << " .declare(:#{d.key}, #{d.value.inspect})"
69
74
  end
70
75
  lines << ""
71
- lines << "puts Ruby2Faust::Emitter.program(prog)"
76
+ lines << "puts Ruby2Faust::Emitter.program(prog, output: #{output_name.inspect})"
72
77
  end
73
78
  end
74
79
 
@@ -531,7 +536,7 @@ module Faust2Ruby
531
536
  min = generate_expression(node.min)
532
537
  max = generate_expression(node.max)
533
538
  step = generate_expression(node.step)
534
- "slider(#{node.label.inspect}, init: #{init}, min: #{min}, max: #{max}, step: #{step})"
539
+ "hslider(#{node.label.inspect}, init: #{init}, min: #{min}, max: #{max}, step: #{step})"
535
540
 
536
541
  when :vslider
537
542
  init = generate_expression(node.init)
@@ -677,7 +682,7 @@ module Faust2Ruby
677
682
 
678
683
  # Generate code for case expressions
679
684
  # case { (0) => a; (1) => b; (n) => c; }
680
- # converts to: flambda(:x) { |x| select2(x.eq(0), select2(x.eq(1), c, b), a) }
685
+ # converts to: fcase(0 => a, 1 => b) { |n| c }
681
686
  def generate_case_expr(node)
682
687
  branches = node.branches
683
688
 
@@ -706,29 +711,23 @@ module Faust2Ruby
706
711
  return generate_case_literal(node)
707
712
  end
708
713
 
709
- # Generate a flambda that takes the input and uses select2 chains
710
- # We need to use the variable name from default branch if present, else use 'x'
711
- var = default_branch ? default_branch[:var] : "x"
712
- var = ruby_safe_param(var)
714
+ # Get variable name from default branch if present, else use 'x'
715
+ var = default_branch ? ruby_safe_param(default_branch[:var]) : "x"
716
+
717
+ # Build the fcase pattern hash
718
+ patterns = int_branches.map do |branch|
719
+ "#{branch[:value]} => #{generate_expression(branch[:result])}"
720
+ end.join(", ")
713
721
 
714
- # Build the select2 chain from inside out
715
- # Start with the default/else case (or the last integer branch result if no default)
722
+ # Build default expression
716
723
  if default_branch
717
- inner = generate_expression(default_branch[:result])
724
+ default_expr = generate_expression(default_branch[:result])
718
725
  else
719
- # No default - use the last branch result as fallback (arbitrary choice)
720
- inner = generate_expression(int_branches.last[:result])
721
- int_branches = int_branches[0...-1]
722
- end
723
-
724
- # Wrap each integer pattern with select2
725
- # select2(cond, false_val, true_val) - condition true returns second arg
726
- int_branches.reverse_each do |branch|
727
- result = generate_expression(branch[:result])
728
- inner = "select2(#{var}.eq(#{branch[:value]}), #{inner}, #{result})"
726
+ # No default - use 0 as fallback
727
+ default_expr = "0"
729
728
  end
730
729
 
731
- "flambda(:#{var}) { |#{var}| #{inner} }"
730
+ "fcase(#{patterns}) { |#{var}| #{default_expr} }"
732
731
  end
733
732
 
734
733
  # Fall back to literal for complex case expressions
@@ -732,34 +732,60 @@ module Ruby2Faust
732
732
  # CONVERSION (ba.)
733
733
  # =========================================================================
734
734
 
735
- def db2linear(x)
736
- x = to_dsp(x)
737
- DSP.new(Node.new(type: NodeType::DB2LINEAR, inputs: [x.node]))
735
+ def db2linear(x = nil)
736
+ if x.nil?
737
+ DSP.new(Node.new(type: NodeType::DB2LINEAR, inputs: []))
738
+ else
739
+ x = to_dsp(x)
740
+ DSP.new(Node.new(type: NodeType::DB2LINEAR, inputs: [x.node]))
741
+ end
738
742
  end
743
+ alias db2lin db2linear
739
744
 
740
- def linear2db(x)
741
- x = to_dsp(x)
742
- DSP.new(Node.new(type: NodeType::LINEAR2DB, inputs: [x.node]))
745
+ def linear2db(x = nil)
746
+ if x.nil?
747
+ DSP.new(Node.new(type: NodeType::LINEAR2DB, inputs: []))
748
+ else
749
+ x = to_dsp(x)
750
+ DSP.new(Node.new(type: NodeType::LINEAR2DB, inputs: [x.node]))
751
+ end
743
752
  end
753
+ alias lin2db linear2db
744
754
 
745
- def samp2sec(x)
746
- x = to_dsp(x)
747
- DSP.new(Node.new(type: NodeType::SAMP2SEC, inputs: [x.node]))
755
+ def samp2sec(x = nil)
756
+ if x.nil?
757
+ DSP.new(Node.new(type: NodeType::SAMP2SEC, inputs: []))
758
+ else
759
+ x = to_dsp(x)
760
+ DSP.new(Node.new(type: NodeType::SAMP2SEC, inputs: [x.node]))
761
+ end
748
762
  end
749
763
 
750
- def sec2samp(x)
751
- x = to_dsp(x)
752
- DSP.new(Node.new(type: NodeType::SEC2SAMP, inputs: [x.node]))
764
+ def sec2samp(x = nil)
765
+ if x.nil?
766
+ DSP.new(Node.new(type: NodeType::SEC2SAMP, inputs: []))
767
+ else
768
+ x = to_dsp(x)
769
+ DSP.new(Node.new(type: NodeType::SEC2SAMP, inputs: [x.node]))
770
+ end
753
771
  end
754
772
 
755
- def midi2hz(x)
756
- x = to_dsp(x)
757
- DSP.new(Node.new(type: NodeType::MIDI2HZ, inputs: [x.node]))
773
+ def midi2hz(x = nil)
774
+ if x.nil?
775
+ DSP.new(Node.new(type: NodeType::MIDI2HZ, inputs: []))
776
+ else
777
+ x = to_dsp(x)
778
+ DSP.new(Node.new(type: NodeType::MIDI2HZ, inputs: [x.node]))
779
+ end
758
780
  end
759
781
 
760
- def hz2midi(x)
761
- x = to_dsp(x)
762
- DSP.new(Node.new(type: NodeType::HZ2MIDI, inputs: [x.node]))
782
+ def hz2midi(x = nil)
783
+ if x.nil?
784
+ DSP.new(Node.new(type: NodeType::HZ2MIDI, inputs: []))
785
+ else
786
+ x = to_dsp(x)
787
+ DSP.new(Node.new(type: NodeType::HZ2MIDI, inputs: [x.node]))
788
+ end
763
789
  end
764
790
 
765
791
  def tau2pole(tau)
@@ -894,13 +920,15 @@ module Ruby2Faust
894
920
  name.to_s.include?("[") ? name.to_s : "#{meta}#{name}"
895
921
  end
896
922
 
897
- def slider(name, init:, min:, max:, step: 0.01, **meta)
923
+ def hslider(name, init:, min:, max:, step: 0.01, **meta)
898
924
  full_name = DSL.build_metadata(name, **meta)
899
- DSP.new(Node.new(type: NodeType::SLIDER, args: [full_name, init, min, max, step]))
925
+ DSP.new(Node.new(type: NodeType::HSLIDER, args: [full_name, init, min, max, step]))
900
926
  end
927
+ alias slider hslider
901
928
 
902
- def vslider(name, init:, min:, max:, step: 0.01)
903
- DSP.new(Node.new(type: NodeType::VSLIDER, args: [name, init, min, max, step]))
929
+ def vslider(name, init:, min:, max:, step: 0.01, **meta)
930
+ full_name = DSL.build_metadata(name, **meta)
931
+ DSP.new(Node.new(type: NodeType::VSLIDER, args: [full_name, init, min, max, step]))
904
932
  end
905
933
 
906
934
  def nentry(name, init:, min:, max:, step: 1)
@@ -1002,6 +1030,30 @@ module Ruby2Faust
1002
1030
  DSP.new(Node.new(type: NodeType::PARAM, args: [name]))
1003
1031
  end
1004
1032
 
1033
+ # Case expression with integer patterns
1034
+ # @param patterns [Hash{Integer => Object}] Integer patterns mapped to results
1035
+ # @yield Block for default case, receives the input variable
1036
+ # @return [DSP]
1037
+ # @example
1038
+ # fcase(0 => 1, 1 => 2) { |n| n * 2 }
1039
+ # # generates: case { (0) => 1; (1) => 2; (n) => n * 2; }
1040
+ def fcase(patterns = {}, &block)
1041
+ raise ArgumentError, "fcase requires a block for the default case" unless block_given?
1042
+
1043
+ # Get variable name from block parameter
1044
+ var_name = block.parameters.first&.last || :x
1045
+ var = param(var_name)
1046
+
1047
+ # Evaluate the block to get the default expression
1048
+ default_expr = to_dsp(block.call(var))
1049
+
1050
+ # Convert pattern values to DSP nodes
1051
+ pattern_nodes = patterns.transform_values { |v| to_dsp(v).node }
1052
+
1053
+ # Create CASE node: args = [var_name, patterns_hash, default_expr]
1054
+ DSP.new(Node.new(type: NodeType::CASE, args: [var_name, pattern_nodes, default_expr.node]))
1055
+ end
1056
+
1005
1057
  # =========================================================================
1006
1058
  # TABLES
1007
1059
  # =========================================================================
@@ -44,7 +44,7 @@ module Ruby2Faust
44
44
  my_prec < parent_prec ? "(#{expr})" : expr
45
45
  end
46
46
 
47
- def program(process, imports: nil, declarations: {}, pretty: false)
47
+ def program(process, imports: nil, declarations: {}, pretty: false, output: "process")
48
48
  if process.is_a?(Program)
49
49
  node = process.process.is_a?(DSP) ? process.process.node : process.process
50
50
  imports ||= process.imports
@@ -59,9 +59,9 @@ module Ruby2Faust
59
59
  lines << "" if declarations.any?
60
60
  imports.each { |lib| lines << "import(\"#{lib}\");" }
61
61
  lines << ""
62
-
62
+
63
63
  body = emit(node, pretty: pretty, prec: 0)
64
- lines << "process = #{body};"
64
+ lines << "#{output} = #{body};"
65
65
  lines.join("\n") + "\n"
66
66
  end
67
67
 
@@ -219,23 +219,17 @@ module Ruby2Faust
219
219
  when NodeType::MUL
220
220
  if node.inputs.count == 2
221
221
  left_node, right_node = node.inputs
222
- # Normalize: signal : *(scalar) - idiomatic Faust gain (uses SEQ precedence)
223
- if scalar?(right_node) && !scalar?(left_node)
224
- my_prec = PREC[:seq]
225
- left = emit(left_node, indent: indent, pretty: pretty, prec: my_prec)
226
- right = emit(right_node, indent: indent, pretty: pretty, prec: PREC[:primary])
227
- wrap("#{left} : *(#{right})", my_prec, prec)
228
- elsif scalar?(left_node) && !scalar?(right_node)
229
- my_prec = PREC[:seq]
230
- left = emit(right_node, indent: indent, pretty: pretty, prec: my_prec)
231
- right = emit(left_node, indent: indent, pretty: pretty, prec: PREC[:primary])
232
- wrap("#{left} : *(#{right})", my_prec, prec)
233
- else
234
- my_prec = PREC[:mul]
235
- left = emit(left_node, indent: indent, pretty: pretty, prec: my_prec)
236
- right = emit(right_node, indent: indent, pretty: pretty, prec: my_prec + 1)
237
- wrap("#{left} * #{right}", my_prec, prec)
222
+ # Always emit idiomatic Faust: signal : *(gain)
223
+ # Normalize scalar-on-left to signal : *(scalar)
224
+ if scalar?(left_node) && !scalar?(right_node)
225
+ # Swap: put signal on left
226
+ left_node, right_node = right_node, left_node
238
227
  end
228
+ my_prec = PREC[:seq]
229
+ left = emit(left_node, indent: indent, pretty: pretty, prec: my_prec)
230
+ # Don't require high precedence for gain arg - *(x) already groups it
231
+ right = emit(right_node, indent: indent, pretty: pretty, prec: 0)
232
+ wrap("#{left} : *(#{right})", my_prec, prec)
239
233
  else
240
234
  "*"
241
235
  end
@@ -352,17 +346,17 @@ module Ruby2Faust
352
346
 
353
347
  # === CONVERSION ===
354
348
  when NodeType::DB2LINEAR
355
- "ba.db2linear(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
349
+ node.inputs.empty? ? "ba.db2linear" : "ba.db2linear(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
356
350
  when NodeType::LINEAR2DB
357
- "ba.linear2db(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
351
+ node.inputs.empty? ? "ba.linear2db" : "ba.linear2db(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
358
352
  when NodeType::SAMP2SEC
359
- "ba.samp2sec(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
353
+ node.inputs.empty? ? "ba.samp2sec" : "ba.samp2sec(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
360
354
  when NodeType::SEC2SAMP
361
- "ba.sec2samp(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
355
+ node.inputs.empty? ? "ba.sec2samp" : "ba.sec2samp(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
362
356
  when NodeType::MIDI2HZ
363
- "ba.midikey2hz(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
357
+ node.inputs.empty? ? "ba.midikey2hz" : "ba.midikey2hz(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
364
358
  when NodeType::HZ2MIDI
365
- "ba.hz2midikey(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
359
+ node.inputs.empty? ? "ba.hz2midikey" : "ba.hz2midikey(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
366
360
  when NodeType::TAU2POLE
367
361
  "ba.tau2pole(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
368
362
  when NodeType::POLE2TAU
@@ -424,7 +418,7 @@ module Ruby2Faust
424
418
  "sp.panner(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
425
419
 
426
420
  # === UI CONTROLS ===
427
- when NodeType::SLIDER
421
+ when NodeType::HSLIDER
428
422
  name, init, min, max, step = node.args
429
423
  "hslider(\"#{name}\", #{init}, #{min}, #{max}, #{step})"
430
424
  when NodeType::VSLIDER
@@ -490,6 +484,14 @@ module Ruby2Faust
490
484
  "\\(#{param_str}).(#{emit(body.node, indent: indent, pretty: pretty)})"
491
485
  when NodeType::PARAM
492
486
  node.args[0].to_s
487
+ when NodeType::CASE
488
+ var_name, patterns, default_node = node.args
489
+ branches = patterns.map do |val, result_node|
490
+ "(#{val}) => #{emit(result_node, indent: indent, pretty: pretty)}"
491
+ end
492
+ default_expr = emit(default_node, indent: indent, pretty: pretty)
493
+ branches << "(#{var_name}) => #{default_expr}"
494
+ "case { #{branches.join('; ')}; }"
493
495
 
494
496
  # === TABLES ===
495
497
  when NodeType::RDTABLE
data/lib/ruby2faust/ir.rb CHANGED
@@ -184,7 +184,7 @@ module Ruby2Faust
184
184
  PANNER = :panner # sp.panner(pan) - stereo pan
185
185
 
186
186
  # === UI Controls ===
187
- SLIDER = :slider
187
+ HSLIDER = :hslider
188
188
  VSLIDER = :vslider
189
189
  NENTRY = :nentry
190
190
  BUTTON = :button
@@ -211,6 +211,7 @@ module Ruby2Faust
211
211
  # === Lambda ===
212
212
  LAMBDA = :lambda # \(x).(body)
213
213
  PARAM = :param # Parameter reference
214
+ CASE = :case # case { (0) => a; (n) => b; }
214
215
 
215
216
  # === Tables ===
216
217
  RDTABLE = :rdtable # rdtable(n, init, ridx)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ruby2Faust
4
- VERSION = "0.2.3"
4
+ VERSION = "0.2.4"
5
5
  end
data/lib/ruby2faust.rb CHANGED
@@ -16,12 +16,17 @@ module Ruby2Faust
16
16
  # osc(440).then(gain(0.3))
17
17
  # end
18
18
  #
19
+ # @example Generate an effect instead of process
20
+ # code = Ruby2Faust.generate(output: "effect") do
21
+ # wire >> delay(48000, 0.5.sec) * 0.5
22
+ # end
23
+ #
19
24
  # @yield Block that returns a DSP
20
25
  # @return [String] Faust source code
21
- def self.generate(pretty: false, &block)
26
+ def self.generate(pretty: true, output: "process", &block)
22
27
  context = Object.new
23
28
  context.extend(DSL)
24
- process = context.instance_eval(&block)
25
- Emitter.program(process, pretty: pretty)
29
+ result = context.instance_eval(&block)
30
+ Emitter.program(result, pretty: pretty, output: output)
26
31
  end
27
32
  end
data/ruby2faust.md CHANGED
@@ -7,13 +7,10 @@ A Ruby DSL that generates Faust DSP code. Ruby describes the graph; Faust compil
7
7
  ```ruby
8
8
  require 'ruby2faust'
9
9
 
10
- # Idiomatic Ruby style
11
10
  code = Ruby2Faust.generate do
12
- # Use numeric extensions: .midi, .hz, .db
13
- freq = 60.midi >> smoo
14
-
15
- # Arithmetic operators for signal mixing
16
- (osc(freq) + noise) * -6.db
11
+ freq = hslider("freq", init: 48, min: 20, max: 100, step: 1) >> midi2hz >> smoo
12
+ amp = hslider("amp", init: -12, min: -60, max: 0, step: 1) >> db2linear >> smoo
13
+ osc(freq) >> lp(2000) >> gain(amp)
17
14
  end
18
15
 
19
16
  puts code
@@ -23,7 +20,10 @@ Output:
23
20
  ```faust
24
21
  import("stdfaust.lib");
25
22
 
26
- process = ((os.osc(ba.midikey2hz(60)) + no.noise) * ba.db2linear(-6));
23
+ process =
24
+ os.osc(hslider("freq", 48, 20, 100, 1) : ba.midikey2hz : si.smoo)
25
+ : fi.lowpass(1, 2000)
26
+ : *(hslider("amp", -12, -60, 0, 1) : ba.db2linear : si.smoo);
27
27
  ```
28
28
 
29
29
  ## Composition
@@ -146,15 +146,21 @@ floor_, ceil_, rint_
146
146
  ```
147
147
 
148
148
  ### Conversion (ba.)
149
+
150
+ With argument (wrapping):
149
151
  ```ruby
150
- db2linear(x) # dB to linear
151
- linear2db(x) # Linear to dB
152
- midi2hz(x) # MIDI note to Hz
153
- hz2midi(x) # Hz to MIDI note
154
- samp2sec(x) # Samples to seconds
155
- sec2samp(x) # Seconds to samples
152
+ db2linear(-6) # ba.db2linear(-6)
153
+ midi2hz(60) # ba.midikey2hz(60)
156
154
  ```
157
155
 
156
+ Without argument (chainable):
157
+ ```ruby
158
+ hslider("amp", ...) >> db2linear >> smoo # hslider(...) : ba.db2linear : si.smoo
159
+ hslider("freq", ...) >> midi2hz >> smoo # hslider(...) : ba.midikey2hz : si.smoo
160
+ ```
161
+
162
+ All conversion functions: `db2linear`, `linear2db`, `midi2hz`, `hz2midi`, `samp2sec`, `sec2samp`
163
+
158
164
  ### Smoothing (si.)
159
165
  ```ruby
160
166
  smooth(tau) # Smooth with time constant
@@ -185,6 +191,11 @@ fpar(4) { osc((it + 1) * 100) } # par(it, 4, osc((it+1)*100))
185
191
  flambda(:x) { |x| x * 2 } # \(x).(x * 2)
186
192
  ```
187
193
 
194
+ ### Case (Pattern Matching)
195
+ ```ruby
196
+ fcase(0 => 1, 1 => 2) { |n| n * 2 } # case { (0) => 1; (1) => 2; (n) => n : *(2); }
197
+ ```
198
+
188
199
  ### Tables
189
200
  ```ruby
190
201
  waveform(0, 0.5, 1, 0.5) # waveform{0, 0.5, 1, 0.5}
@@ -277,11 +288,11 @@ prog = Ruby2Faust::Program.new do
277
288
  osc(440) * 0.5
278
289
  end
279
290
 
280
- # Generate pretty-printed Faust with indentation and newlines
281
- puts Ruby2Faust::Emitter.program(prog, pretty: true)
291
+ # Generate Faust (pretty-printed by default)
292
+ puts Ruby2Faust::Emitter.program(prog)
282
293
 
283
- # The generate helper also supports pretty: true
284
- puts Ruby2Faust.generate(pretty: true) do
294
+ # Use pretty: false for compact single-line output
295
+ puts Ruby2Faust.generate(pretty: false) do
285
296
  hgroup("Synth") { osc(440) + noise }
286
297
  end
287
298
  ```
@@ -290,20 +301,44 @@ end
290
301
 
291
302
  ```ruby
292
303
  require 'ruby2faust'
293
- include Ruby2Faust::DSL
294
304
 
295
- gate = button("gate")
296
- freq = slider("freq", init: 220, min: 20, max: 2000, style: :knob) >> smoo
297
- cutoff = slider("cutoff", init: 1000, min: 100, max: 8000, style: :knob) >> smoo
305
+ code = Ruby2Faust.generate do
306
+ gate = button("gate")
307
+ freq = hslider("freq", init: 48, min: 20, max: 100, step: 1) >> midi2hz >> smoo
308
+ cutoff = hslider("cutoff", init: 2000, min: 100, max: 10000, step: 1) >> smoo
309
+ res = hslider("res", init: 0.5, min: 0, max: 1, step: 0.01)
310
+
311
+ env = adsr(0.01, 0.1, 0.7, 0.3, gate)
312
+ saw(freq) >> resonlp(cutoff, res, 1) * env
313
+ end
314
+ ```
315
+
316
+ ## Example: Feedback Delay
317
+
318
+ ```ruby
319
+ require 'ruby2faust'
320
+
321
+ code = Ruby2Faust.generate do
322
+ dtime = hslider("delay", init: 0.3, min: 0.01, max: 1, step: 0.01) >> sec2samp >> smoo
323
+ fback = hslider("feedback", init: 0.5, min: 0, max: 0.95, step: 0.01) >> smoo
324
+ wet = hslider("wet", init: 0.5, min: 0, max: 1, step: 0.01) >> smoo
325
+
326
+ wire.feedback(delay(48000, dtime) * fback) * wet
327
+ end
328
+ ```
298
329
 
299
- env = adsr(0.01, 0.2, 0.6, 0.3, gate)
330
+ ## Example: Stereo Panner
300
331
 
301
- process = saw(freq) >> resonlp(cutoff, 4, 1) >> gain(env) >> panner(0.5)
332
+ ```ruby
333
+ require 'ruby2faust'
302
334
 
303
- prog = Ruby2Faust::Program.new(process)
304
- .declare(:name, "SubSynth")
335
+ code = Ruby2Faust.generate do
336
+ freq = hslider("freq", init: 48, min: 20, max: 100, step: 1) >> midi2hz >> smoo
337
+ pan = hslider("pan", init: 0.5, min: 0, max: 1, step: 0.01) >> smoo
338
+ amp = hslider("amp", init: -12, min: -60, max: 0, step: 1) >> db2linear >> smoo
305
339
 
306
- puts Ruby2Faust::Emitter.program(prog)
340
+ osc(freq) * amp >> panner(pan)
341
+ end
307
342
  ```
308
343
 
309
344
  ## CLI
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: frausto
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Lowenfels
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-01-21 00:00:00.000000000 Z
10
+ date: 2026-01-22 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: minitest