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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6175ca1a7aaec33d9d762997e3016e76be2892da7ca7b65585d7cdd8c05b6423
4
- data.tar.gz: b0e41102e41bbb150a7d6ad48b841054577715b1ca1d2507b73b6ff2793c549f
3
+ metadata.gz: e6f5347160a2abe4136e948b171065dbab0d4b2816d57dc3d341a7b039c3717b
4
+ data.tar.gz: fe523a91e6cc71d3de0a42fc592c1ccd21337b67d2aa2355f1da85b7428f06a8
5
5
  SHA512:
6
- metadata.gz: 2dc4fe2223f141e4283eab56f517dc1611844436fe9a8ef0d60f7924a7e52c6276cc15d385c16873c96c697398e0dd03c949c1fe0a00fbef69efceedbfd6ea5f
7
- data.tar.gz: 07ef3e6827676dd054b80895b8beb89a4d7394ca5c4e658494fbf3fed320b564b89749d877e8cfa54621aad4f49112bca0f76713874480d6fc597ff313f128c8
6
+ metadata.gz: 162fe8eb9540ee3d3ca709b8ddf5554bd58bfae2188af5e74d1b9505113d29cc31cee58482d9cbf53b38cd3f4ae6d1d4dde31108862d08a552e42767131000cc
7
+ data.tar.gz: c16fb85269cf137f8d55443b9e1337b56d20f46de9cd0d816bf3b24c41faa5abd343b6b648d84f27a0eea2f326223b746ea4042dcaad201717fbad703bf2dff4
data/.yardopts ADDED
@@ -0,0 +1,8 @@
1
+ --title "Frausto Documentation"
2
+ --markup markdown
3
+ --readme README.md
4
+ lib/**/*.rb
5
+ -
6
+ ruby2faust.md
7
+ faust2ruby.md
8
+ LICENSE.txt
data/README.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Frausto
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/frausto.svg)](https://rubygems.org/gems/frausto)
4
+ [![CI](https://github.com/dfl/ruby2faust/actions/workflows/ci.yml/badge.svg)](https://github.com/dfl/ruby2faust/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.0-ruby.svg)](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 Example
27
+ ## Quick Examples
23
28
 
24
29
  ```ruby
25
30
  require 'ruby2faust'
26
31
 
27
32
  code = Ruby2Faust.generate do
28
- osc(440) >> lp(800) >> gain(0.3)
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 = (os.osc(440) : fi.lowpass(1, 800) : *(0.3));
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) >> gain(0.5)"
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
- "(#{left} >> #{right})"
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]
@@ -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]
@@ -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 ? "(#{emit(node.inputs[0], indent: indent, pretty: pretty)} + #{emit(node.inputs[1], indent: indent, pretty: pretty)})" : "+"
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 ? "(#{emit(node.inputs[0], indent: indent, pretty: pretty)} * #{emit(node.inputs[1], indent: indent, pretty: pretty)})" : "*"
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 ? "(#{emit(node.inputs[0], indent: indent, pretty: pretty)} - #{emit(node.inputs[1], indent: indent, pretty: pretty)})" : "-"
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 ? "(#{emit(node.inputs[0], indent: indent, pretty: pretty)} / #{emit(node.inputs[1], indent: indent, pretty: pretty)})" : "/"
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
- left = emit(node.inputs[0], indent: indent + 1, pretty: pretty)
440
- right = emit(node.inputs[1], indent: indent + 1, pretty: pretty)
441
- if pretty
442
- "(\n#{next_sp}#{left}\n#{next_sp}: #{right}\n#{sp})"
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
- "(#{left} : #{right})"
533
+ "#{left} : #{right}"
445
534
  end
535
+ wrap(expr, my_prec, prec)
446
536
  when NodeType::PAR
447
- left = emit(node.inputs[0], indent: indent + 1, pretty: pretty)
448
- right = emit(node.inputs[1], indent: indent + 1, pretty: pretty)
449
- if pretty
450
- "(\n#{next_sp}#{left},\n#{next_sp}#{right}\n#{sp})"
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
- "(#{left}, #{right})"
543
+ "#{left}, #{right}"
453
544
  end
545
+ wrap(expr, my_prec, prec)
454
546
  when NodeType::SPLIT
455
- source = emit(node.inputs[0], indent: indent + 1, pretty: pretty)
456
- targets = node.inputs[1..].map { |n| emit(n, indent: indent + 1, pretty: pretty) }
457
- if pretty
458
- "(\n#{next_sp}#{source}\n#{next_sp}<: #{targets.join(",\n#{next_sp} ")}\n#{sp})"
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
- "(#{source} <: #{targets.join(", ")})"
553
+ "#{source} <: #{targets.join(", ")}"
461
554
  end
555
+ wrap(expr, my_prec, prec)
462
556
  when NodeType::MERGE
463
- left = emit(node.inputs[0], indent: indent + 1, pretty: pretty)
464
- right = emit(node.inputs[1], indent: indent + 1, pretty: pretty)
465
- if pretty
466
- "(\n#{next_sp}#{left}\n#{next_sp}:> #{right}\n#{sp})"
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
- "(#{left} :> #{right})"
563
+ "#{left} :> #{right}"
469
564
  end
565
+ wrap(expr, my_prec, prec)
470
566
  when NodeType::FEEDBACK
471
- left = emit(node.inputs[0], indent: indent + 1, pretty: pretty)
472
- right = emit(node.inputs[1], indent: indent + 1, pretty: pretty)
473
- if pretty
474
- "(\n#{next_sp}#{left}\n#{next_sp}~ #{right}\n#{sp})"
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
- "(#{left} ~ #{right})"
573
+ "#{left} ~ #{right}"
477
574
  end
575
+ wrap(expr, my_prec, prec)
478
576
 
479
577
  # === UTILITY ===
480
578
  when NodeType::WIRE
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ruby2Faust
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.3"
5
5
  end
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.0
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-20 00:00:00.000000000 Z
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