frausto 0.2.0 → 0.2.3
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/.yardopts +8 -0
- data/README.md +21 -4
- data/lib/faust2ruby/ruby_generator.rb +37 -1
- data/lib/ruby2faust/dsl.rb +8 -0
- data/lib/ruby2faust/emitter.rb +129 -31
- data/lib/ruby2faust/version.rb +1 -1
- data/ruby2faust.md +2 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e6f5347160a2abe4136e948b171065dbab0d4b2816d57dc3d341a7b039c3717b
|
|
4
|
+
data.tar.gz: fe523a91e6cc71d3de0a42fc592c1ccd21337b67d2aa2355f1da85b7428f06a8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 162fe8eb9540ee3d3ca709b8ddf5554bd58bfae2188af5e74d1b9505113d29cc31cee58482d9cbf53b38cd3f4ae6d1d4dde31108862d08a552e42767131000cc
|
|
7
|
+
data.tar.gz: c16fb85269cf137f8d55443b9e1337b56d20f46de9cd0d816bf3b24c41faa5abd343b6b648d84f27a0eea2f326223b746ea4042dcaad201717fbad703bf2dff4
|
data/.yardopts
ADDED
data/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Frausto
|
|
2
2
|
|
|
3
|
+
[](https://rubygems.org/gems/frausto)
|
|
4
|
+
[](https://github.com/dfl/ruby2faust/actions/workflows/ci.yml)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://www.ruby-lang.org)
|
|
7
|
+
|
|
3
8
|
A Ruby toolkit for Faust DSP: generate Faust code from Ruby, or convert Faust to Ruby.
|
|
4
9
|
|
|
5
10
|
## Installation
|
|
@@ -19,25 +24,37 @@ gem 'frausto'
|
|
|
19
24
|
- **[ruby2faust](ruby2faust.md)** - Ruby DSL that generates Faust DSP code
|
|
20
25
|
- **[faust2ruby](faust2ruby.md)** - Convert Faust DSP code to Ruby DSL
|
|
21
26
|
|
|
22
|
-
## Quick
|
|
27
|
+
## Quick Examples
|
|
23
28
|
|
|
24
29
|
```ruby
|
|
25
30
|
require 'ruby2faust'
|
|
26
31
|
|
|
27
32
|
code = Ruby2Faust.generate do
|
|
28
|
-
|
|
33
|
+
freq = 60.midi >> smoo
|
|
34
|
+
-6.db * ((osc(freq) + 0.1 * noise) >> lp(2000))
|
|
29
35
|
end
|
|
30
36
|
|
|
31
37
|
puts code
|
|
32
38
|
# => import("stdfaust.lib");
|
|
33
|
-
# process =
|
|
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));
|
|
34
51
|
```
|
|
35
52
|
|
|
36
53
|
```ruby
|
|
37
54
|
require 'faust2ruby'
|
|
38
55
|
|
|
39
56
|
ruby_code = Faust2Ruby.to_ruby('process = os.osc(440) : *(0.5);')
|
|
40
|
-
# => "osc(440)
|
|
57
|
+
# => "0.5 * osc(440)"
|
|
41
58
|
```
|
|
42
59
|
|
|
43
60
|
## License
|
|
@@ -347,7 +347,17 @@ module Faust2Ruby
|
|
|
347
347
|
|
|
348
348
|
case node.op
|
|
349
349
|
when :SEQ
|
|
350
|
-
|
|
350
|
+
# Idiomatic Ruby: signal : *(x) becomes x * signal
|
|
351
|
+
if node.right.is_a?(AST::FunctionCall) && node.right.name == "*" && node.right.args.length == 1
|
|
352
|
+
arg = generate_expression(node.right.args[0])
|
|
353
|
+
"(#{arg} * #{left})"
|
|
354
|
+
# Idiomatic Ruby: signal : /(x) becomes signal / x
|
|
355
|
+
elsif node.right.is_a?(AST::FunctionCall) && node.right.name == "/" && node.right.args.length == 1
|
|
356
|
+
arg = generate_expression(node.right.args[0])
|
|
357
|
+
"(#{left} / #{arg})"
|
|
358
|
+
else
|
|
359
|
+
"(#{left} >> #{right})"
|
|
360
|
+
end
|
|
351
361
|
when :PAR
|
|
352
362
|
"(#{left} | #{right})"
|
|
353
363
|
when :SPLIT
|
|
@@ -463,6 +473,32 @@ module Faust2Ruby
|
|
|
463
473
|
# ba.selectn(n, idx, ...) -> selectn(n, idx, ...)
|
|
464
474
|
"selectn(#{args.join(', ')})"
|
|
465
475
|
|
|
476
|
+
when :db2linear
|
|
477
|
+
# ba.db2linear(-6) -> -6.db (idiomatic Ruby)
|
|
478
|
+
# Handle both "-6" and "(-6)" forms
|
|
479
|
+
arg = args[0]&.gsub(/\A\(|\)\z/, '') # strip outer parens
|
|
480
|
+
if args.length == 1 && arg&.match?(/\A-?\d+\.?\d*\z/)
|
|
481
|
+
"#{arg}.db"
|
|
482
|
+
else
|
|
483
|
+
"db2linear(#{args.join(', ')})"
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
when :midi2hz
|
|
487
|
+
# ba.midikey2hz(60) -> 60.midi (idiomatic Ruby)
|
|
488
|
+
if args.length == 1 && args[0].match?(/\A\d+\.?\d*\z/)
|
|
489
|
+
"#{args[0]}.midi"
|
|
490
|
+
else
|
|
491
|
+
"midi2hz(#{args.join(', ')})"
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
when :sec2samp
|
|
495
|
+
# ba.sec2samp(0.1) -> 0.1.sec (idiomatic Ruby)
|
|
496
|
+
if args.length == 1 && args[0].match?(/\A\d+\.?\d*\z/)
|
|
497
|
+
"#{args[0]}.sec"
|
|
498
|
+
else
|
|
499
|
+
"sec2samp(#{args.join(', ')})"
|
|
500
|
+
end
|
|
501
|
+
|
|
466
502
|
else
|
|
467
503
|
# Standard call - check for partial application
|
|
468
504
|
expected_args = mapping[:args]
|
data/lib/ruby2faust/dsl.rb
CHANGED
|
@@ -72,6 +72,14 @@ module Ruby2Faust
|
|
|
72
72
|
"#<Ruby2Faust::DSP #{to_s}>"
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
+
# Enable numeric-on-left operations like: 0.3 * osc(440)
|
|
76
|
+
# Ruby calls coerce when left operand doesn't know how to handle right operand
|
|
77
|
+
# @param other [Numeric]
|
|
78
|
+
# @return [Array<DSP, DSP>]
|
|
79
|
+
def coerce(other)
|
|
80
|
+
[DSL.to_dsp(other), self]
|
|
81
|
+
end
|
|
82
|
+
|
|
75
83
|
# Add / mix signals (Faust +)
|
|
76
84
|
# @param other [Numeric, DSP, Symbol]
|
|
77
85
|
# @return [DSP]
|
data/lib/ruby2faust/emitter.rb
CHANGED
|
@@ -9,6 +9,41 @@ module Ruby2Faust
|
|
|
9
9
|
|
|
10
10
|
DEFAULT_IMPORTS = ["stdfaust.lib"].freeze
|
|
11
11
|
|
|
12
|
+
# Scalar types produce constant values (not signal processors)
|
|
13
|
+
SCALAR_TYPES = [
|
|
14
|
+
NodeType::DB2LINEAR, NodeType::LINEAR2DB,
|
|
15
|
+
NodeType::MIDI2HZ, NodeType::HZ2MIDI,
|
|
16
|
+
NodeType::SEC2SAMP, NodeType::SAMP2SEC
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
# Operator precedence (higher = binds tighter)
|
|
20
|
+
PREC = {
|
|
21
|
+
seq: 1, # :
|
|
22
|
+
par: 2, # ,
|
|
23
|
+
split: 3, # <:
|
|
24
|
+
merge: 3, # :>
|
|
25
|
+
rec: 4, # ~
|
|
26
|
+
add: 5, # +
|
|
27
|
+
sub: 5, # -
|
|
28
|
+
mul: 6, # *
|
|
29
|
+
div: 6, # /
|
|
30
|
+
mod: 6, # %
|
|
31
|
+
cmp: 7, # < > <= >= == !=
|
|
32
|
+
primary: 100 # literals, function calls - never need parens
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
# Check if a node represents a scalar/constant value
|
|
36
|
+
def scalar?(node)
|
|
37
|
+
return true if SCALAR_TYPES.include?(node.type)
|
|
38
|
+
return true if node.type == NodeType::LITERAL && node.args[0].to_s.match?(/\A-?\d+\.?\d*\z/)
|
|
39
|
+
false
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Wrap expression in parens if needed based on precedence
|
|
43
|
+
def wrap(expr, my_prec, parent_prec)
|
|
44
|
+
my_prec < parent_prec ? "(#{expr})" : expr
|
|
45
|
+
end
|
|
46
|
+
|
|
12
47
|
def program(process, imports: nil, declarations: {}, pretty: false)
|
|
13
48
|
if process.is_a?(Program)
|
|
14
49
|
node = process.process.is_a?(DSP) ? process.process.node : process.process
|
|
@@ -25,12 +60,12 @@ module Ruby2Faust
|
|
|
25
60
|
imports.each { |lib| lines << "import(\"#{lib}\");" }
|
|
26
61
|
lines << ""
|
|
27
62
|
|
|
28
|
-
body = emit(node, pretty: pretty)
|
|
63
|
+
body = emit(node, pretty: pretty, prec: 0)
|
|
29
64
|
lines << "process = #{body};"
|
|
30
65
|
lines.join("\n") + "\n"
|
|
31
66
|
end
|
|
32
67
|
|
|
33
|
-
def emit(node, indent: 0, pretty: false)
|
|
68
|
+
def emit(node, indent: 0, pretty: false, prec: 0)
|
|
34
69
|
sp = " " * indent
|
|
35
70
|
next_sp = " " * (indent + 1)
|
|
36
71
|
|
|
@@ -173,13 +208,64 @@ module Ruby2Faust
|
|
|
173
208
|
when NodeType::GAIN
|
|
174
209
|
"*(#{emit(node.inputs[0], indent: indent, pretty: pretty)})"
|
|
175
210
|
when NodeType::ADD
|
|
176
|
-
node.inputs.count == 2
|
|
211
|
+
if node.inputs.count == 2
|
|
212
|
+
my_prec = PREC[:add]
|
|
213
|
+
left = emit(node.inputs[0], indent: indent, pretty: pretty, prec: my_prec)
|
|
214
|
+
right = emit(node.inputs[1], indent: indent, pretty: pretty, prec: my_prec + 1)
|
|
215
|
+
wrap("#{left} + #{right}", my_prec, prec)
|
|
216
|
+
else
|
|
217
|
+
"+"
|
|
218
|
+
end
|
|
177
219
|
when NodeType::MUL
|
|
178
|
-
node.inputs.count == 2
|
|
220
|
+
if node.inputs.count == 2
|
|
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)
|
|
238
|
+
end
|
|
239
|
+
else
|
|
240
|
+
"*"
|
|
241
|
+
end
|
|
179
242
|
when NodeType::SUB
|
|
180
|
-
node.inputs.count == 2
|
|
243
|
+
if node.inputs.count == 2
|
|
244
|
+
my_prec = PREC[:sub]
|
|
245
|
+
left = emit(node.inputs[0], indent: indent, pretty: pretty, prec: my_prec)
|
|
246
|
+
right = emit(node.inputs[1], indent: indent, pretty: pretty, prec: my_prec + 1)
|
|
247
|
+
wrap("#{left} - #{right}", my_prec, prec)
|
|
248
|
+
else
|
|
249
|
+
"-"
|
|
250
|
+
end
|
|
181
251
|
when NodeType::DIV
|
|
182
|
-
node.inputs.count == 2
|
|
252
|
+
if node.inputs.count == 2
|
|
253
|
+
left_node, right_node = node.inputs
|
|
254
|
+
# Idiomatic Faust: signal : /(scalar) (uses SEQ precedence)
|
|
255
|
+
if scalar?(right_node) && !scalar?(left_node)
|
|
256
|
+
my_prec = PREC[:seq]
|
|
257
|
+
left = emit(left_node, indent: indent, pretty: pretty, prec: my_prec)
|
|
258
|
+
right = emit(right_node, indent: indent, pretty: pretty, prec: PREC[:primary])
|
|
259
|
+
wrap("#{left} : /(#{right})", my_prec, prec)
|
|
260
|
+
else
|
|
261
|
+
my_prec = PREC[:div]
|
|
262
|
+
left = emit(left_node, indent: indent, pretty: pretty, prec: my_prec)
|
|
263
|
+
right = emit(right_node, indent: indent, pretty: pretty, prec: my_prec + 1)
|
|
264
|
+
wrap("#{left} / #{right}", my_prec, prec)
|
|
265
|
+
end
|
|
266
|
+
else
|
|
267
|
+
"/"
|
|
268
|
+
end
|
|
183
269
|
when NodeType::MOD
|
|
184
270
|
"(#{emit(node.inputs[0], indent: indent, pretty: pretty)} % #{emit(node.inputs[1], indent: indent, pretty: pretty)})"
|
|
185
271
|
|
|
@@ -436,45 +522,57 @@ module Ruby2Faust
|
|
|
436
522
|
|
|
437
523
|
# === COMPOSITION ===
|
|
438
524
|
when NodeType::SEQ
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
if
|
|
442
|
-
|
|
525
|
+
my_prec = PREC[:seq]
|
|
526
|
+
# Left child: same precedence (left-associative, no parens needed)
|
|
527
|
+
# Right child: higher precedence required (would need parens if it's another SEQ)
|
|
528
|
+
left = emit(node.inputs[0], indent: indent + 1, pretty: pretty, prec: my_prec)
|
|
529
|
+
right = emit(node.inputs[1], indent: indent + 1, pretty: pretty, prec: my_prec + 1)
|
|
530
|
+
expr = if pretty
|
|
531
|
+
"\n#{next_sp}#{left}\n#{next_sp}: #{right}\n#{sp}"
|
|
443
532
|
else
|
|
444
|
-
"
|
|
533
|
+
"#{left} : #{right}"
|
|
445
534
|
end
|
|
535
|
+
wrap(expr, my_prec, prec)
|
|
446
536
|
when NodeType::PAR
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
537
|
+
my_prec = PREC[:par]
|
|
538
|
+
left = emit(node.inputs[0], indent: indent + 1, pretty: pretty, prec: my_prec)
|
|
539
|
+
right = emit(node.inputs[1], indent: indent + 1, pretty: pretty, prec: my_prec + 1)
|
|
540
|
+
expr = if pretty
|
|
541
|
+
"\n#{next_sp}#{left},\n#{next_sp}#{right}\n#{sp}"
|
|
451
542
|
else
|
|
452
|
-
"
|
|
543
|
+
"#{left}, #{right}"
|
|
453
544
|
end
|
|
545
|
+
wrap(expr, my_prec, prec)
|
|
454
546
|
when NodeType::SPLIT
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
547
|
+
my_prec = PREC[:split]
|
|
548
|
+
source = emit(node.inputs[0], indent: indent + 1, pretty: pretty, prec: my_prec)
|
|
549
|
+
targets = node.inputs[1..].map { |n| emit(n, indent: indent + 1, pretty: pretty, prec: my_prec + 1) }
|
|
550
|
+
expr = if pretty
|
|
551
|
+
"\n#{next_sp}#{source}\n#{next_sp}<: #{targets.join(",\n#{next_sp} ")}\n#{sp}"
|
|
459
552
|
else
|
|
460
|
-
"
|
|
553
|
+
"#{source} <: #{targets.join(", ")}"
|
|
461
554
|
end
|
|
555
|
+
wrap(expr, my_prec, prec)
|
|
462
556
|
when NodeType::MERGE
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
557
|
+
my_prec = PREC[:merge]
|
|
558
|
+
left = emit(node.inputs[0], indent: indent + 1, pretty: pretty, prec: my_prec)
|
|
559
|
+
right = emit(node.inputs[1], indent: indent + 1, pretty: pretty, prec: my_prec + 1)
|
|
560
|
+
expr = if pretty
|
|
561
|
+
"\n#{next_sp}#{left}\n#{next_sp}:> #{right}\n#{sp}"
|
|
467
562
|
else
|
|
468
|
-
"
|
|
563
|
+
"#{left} :> #{right}"
|
|
469
564
|
end
|
|
565
|
+
wrap(expr, my_prec, prec)
|
|
470
566
|
when NodeType::FEEDBACK
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
567
|
+
my_prec = PREC[:rec]
|
|
568
|
+
left = emit(node.inputs[0], indent: indent + 1, pretty: pretty, prec: my_prec)
|
|
569
|
+
right = emit(node.inputs[1], indent: indent + 1, pretty: pretty, prec: my_prec + 1)
|
|
570
|
+
expr = if pretty
|
|
571
|
+
"\n#{next_sp}#{left}\n#{next_sp}~ #{right}\n#{sp}"
|
|
475
572
|
else
|
|
476
|
-
"
|
|
573
|
+
"#{left} ~ #{right}"
|
|
477
574
|
end
|
|
575
|
+
wrap(expr, my_prec, prec)
|
|
478
576
|
|
|
479
577
|
# === UTILITY ===
|
|
480
578
|
when NodeType::WIRE
|
data/lib/ruby2faust/version.rb
CHANGED
data/ruby2faust.md
CHANGED
|
@@ -34,9 +34,10 @@ Ruby2Faust maps Faust's composition operators to Ruby methods and operators:
|
|
|
34
34
|
# Sequential: signal flows through a chain
|
|
35
35
|
osc(440) >> lp(800) >> gain(0.3)
|
|
36
36
|
|
|
37
|
-
# Arithmetic operators (Infix)
|
|
37
|
+
# Arithmetic operators (Infix) - work with numeric on either side
|
|
38
38
|
osc(440) + noise # Mix / Sum
|
|
39
39
|
osc(440) * 0.3 # Gain
|
|
40
|
+
0.3 * osc(440) # Gain (numeric on left)
|
|
40
41
|
osc(440) - osc(442) # Subtraction
|
|
41
42
|
-osc(440) # Negate
|
|
42
43
|
|
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.3
|
|
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-21 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: minitest
|
|
@@ -61,6 +61,7 @@ executables:
|
|
|
61
61
|
extensions: []
|
|
62
62
|
extra_rdoc_files: []
|
|
63
63
|
files:
|
|
64
|
+
- ".yardopts"
|
|
64
65
|
- LICENSE.txt
|
|
65
66
|
- README.md
|
|
66
67
|
- bin/faust2ruby
|