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 +4 -4
- data/README.md +9 -15
- data/bin/faust2ruby +3 -1
- data/faust2ruby.md +58 -370
- data/lib/faust2ruby/ruby_generator.rb +28 -29
- data/lib/ruby2faust/dsl.rb +74 -22
- data/lib/ruby2faust/emitter.rb +28 -26
- data/lib/ruby2faust/ir.rb +2 -1
- data/lib/ruby2faust/version.rb +1 -1
- data/lib/ruby2faust.rb +8 -3
- data/ruby2faust.md +61 -26
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '093f5e9746f4614c8b4085708ccd9b2bf092d06c3a50f5ff08fec837a86ff978'
|
|
4
|
+
data.tar.gz: 4f1262a4cf6d6e9357f9315729b9628b123034c5092867b9a48f78850acef31f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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 =
|
|
34
|
-
|
|
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
|
-
#
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
#
|
|
42
|
-
|
|
43
|
-
#
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
# => "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
74
|
+
osc(440) | osc(880)
|
|
120
75
|
```
|
|
121
76
|
|
|
122
|
-
### Feedback
|
|
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
|
-
|
|
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(
|
|
92
|
+
fpar(4) { |i| osc(i * 100) }
|
|
144
93
|
```
|
|
145
94
|
|
|
146
|
-
##
|
|
95
|
+
## Mapping Overview
|
|
147
96
|
|
|
148
|
-
### Composition
|
|
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
|
-
###
|
|
108
|
+
### Numeric Extensions
|
|
159
109
|
|
|
160
|
-
|
|
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
|
-
| `
|
|
174
|
-
| `
|
|
175
|
-
| `
|
|
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
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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 '
|
|
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
|
-
|
|
144
|
+
ruby_expr = Faust2Ruby.to_ruby(faust_input, expression_only: true)
|
|
261
145
|
|
|
262
|
-
# Ruby → Faust
|
|
263
146
|
include Ruby2Faust::DSL
|
|
264
|
-
process = eval(
|
|
147
|
+
process = eval(ruby_expr)
|
|
265
148
|
faust_output = Ruby2Faust::Emitter.program(process)
|
|
266
149
|
```
|
|
267
150
|
|
|
268
|
-
##
|
|
151
|
+
## Advanced Features
|
|
269
152
|
|
|
270
|
-
|
|
153
|
+
### With Clauses
|
|
154
|
+
|
|
155
|
+
Local definitions are converted to Ruby lambdas:
|
|
271
156
|
|
|
272
|
-
**Input (Faust):**
|
|
273
157
|
```faust
|
|
274
|
-
myDSP =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
181
|
+
fcase(0 => 1, 1 => 2) { |n| (n * 2) }
|
|
417
182
|
```
|
|
418
183
|
|
|
419
|
-
The
|
|
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
|
-
###
|
|
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
|
-
|
|
433
|
-
|
|
189
|
+
halfGain = *(0.5);
|
|
190
|
+
process = osc(440) : halfGain;
|
|
434
191
|
```
|
|
435
|
-
|
|
436
|
-
**Output (Ruby):**
|
|
437
192
|
```ruby
|
|
438
|
-
|
|
193
|
+
halfGain = gain(0.5)
|
|
194
|
+
process = osc(440) >> halfGain
|
|
439
195
|
```
|
|
440
196
|
|
|
441
|
-
|
|
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
|
-
**
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
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
|
|
47
|
+
# Generate helper definitions (excluding outputs)
|
|
45
48
|
merged_statements.each do |stmt|
|
|
46
|
-
if stmt.is_a?(AST::Definition) && stmt.name
|
|
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
|
-
#
|
|
54
|
-
|
|
55
|
-
if
|
|
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(
|
|
62
|
+
lines << generate_expression(output_def.expression)
|
|
58
63
|
else
|
|
59
|
-
lines << "
|
|
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(
|
|
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
|
-
"
|
|
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:
|
|
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
|
-
#
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
|
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
|
-
|
|
724
|
+
default_expr = generate_expression(default_branch[:result])
|
|
718
725
|
else
|
|
719
|
-
# No default - use
|
|
720
|
-
|
|
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
|
-
"
|
|
730
|
+
"fcase(#{patterns}) { |#{var}| #{default_expr} }"
|
|
732
731
|
end
|
|
733
732
|
|
|
734
733
|
# Fall back to literal for complex case expressions
|
data/lib/ruby2faust/dsl.rb
CHANGED
|
@@ -732,34 +732,60 @@ module Ruby2Faust
|
|
|
732
732
|
# CONVERSION (ba.)
|
|
733
733
|
# =========================================================================
|
|
734
734
|
|
|
735
|
-
def db2linear(x)
|
|
736
|
-
|
|
737
|
-
|
|
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
|
-
|
|
742
|
-
|
|
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
|
-
|
|
747
|
-
|
|
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
|
-
|
|
752
|
-
|
|
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
|
-
|
|
757
|
-
|
|
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
|
-
|
|
762
|
-
|
|
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
|
|
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::
|
|
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
|
-
|
|
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
|
# =========================================================================
|
data/lib/ruby2faust/emitter.rb
CHANGED
|
@@ -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 << "
|
|
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
|
-
#
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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::
|
|
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
|
-
|
|
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)
|
data/lib/ruby2faust/version.rb
CHANGED
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:
|
|
26
|
+
def self.generate(pretty: true, output: "process", &block)
|
|
22
27
|
context = Object.new
|
|
23
28
|
context.extend(DSL)
|
|
24
|
-
|
|
25
|
-
Emitter.program(
|
|
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
|
-
|
|
13
|
-
|
|
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 =
|
|
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(
|
|
151
|
-
|
|
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
|
|
281
|
-
puts Ruby2Faust::Emitter.program(prog
|
|
291
|
+
# Generate Faust (pretty-printed by default)
|
|
292
|
+
puts Ruby2Faust::Emitter.program(prog)
|
|
282
293
|
|
|
283
|
-
#
|
|
284
|
-
puts Ruby2Faust.generate(pretty:
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
330
|
+
## Example: Stereo Panner
|
|
300
331
|
|
|
301
|
-
|
|
332
|
+
```ruby
|
|
333
|
+
require 'ruby2faust'
|
|
302
334
|
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
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.
|
|
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-
|
|
10
|
+
date: 2026-01-22 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: minitest
|