frausto 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,255 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strscan"
4
+
5
+ module Faust2Ruby
6
+ # Lexer for Faust DSP source code.
7
+ # Uses StringScanner for efficient tokenization.
8
+ class Lexer
9
+ # Token struct for lexer output
10
+ Token = Struct.new(:type, :value, :line, :column, keyword_init: true)
11
+
12
+ # Keywords in Faust
13
+ KEYWORDS = %w[
14
+ import declare process with letrec where
15
+ par seq sum prod
16
+ case of
17
+ environment library component
18
+ inputs outputs
19
+ ].freeze
20
+
21
+ # Multi-character operators (must check before single-char)
22
+ MULTI_CHAR_OPS = {
23
+ "<:" => :SPLIT,
24
+ ":>" => :MERGE,
25
+ "==" => :EQ,
26
+ "!=" => :NEQ,
27
+ "<=" => :LE,
28
+ ">=" => :GE,
29
+ "<<" => :LSHIFT,
30
+ ">>" => :RSHIFT,
31
+ "=>" => :ARROW,
32
+ }.freeze
33
+
34
+ # Single-character operators and punctuation
35
+ SINGLE_CHAR_OPS = {
36
+ ":" => :SEQ,
37
+ "," => :PAR,
38
+ "~" => :REC,
39
+ "+" => :ADD,
40
+ "-" => :SUB,
41
+ "*" => :MUL,
42
+ "/" => :DIV,
43
+ "%" => :MOD,
44
+ "^" => :POW,
45
+ "@" => :DELAY,
46
+ "'" => :PRIME,
47
+ "=" => :DEF,
48
+ ";" => :ENDDEF,
49
+ "(" => :LPAREN,
50
+ ")" => :RPAREN,
51
+ "{" => :LBRACE,
52
+ "}" => :RBRACE,
53
+ "[" => :LBRACKET,
54
+ "]" => :RBRACKET,
55
+ "<" => :LT,
56
+ ">" => :GT,
57
+ "&" => :AND,
58
+ "|" => :OR,
59
+ "!" => :CUT,
60
+ "_" => :WIRE,
61
+ "." => :DOT,
62
+ "\\" => :LAMBDA,
63
+ }.freeze
64
+
65
+ attr_reader :tokens, :errors
66
+
67
+ def initialize(source)
68
+ @source = source
69
+ @scanner = StringScanner.new(source)
70
+ @tokens = []
71
+ @errors = []
72
+ @line = 1
73
+ @line_start = 0
74
+ end
75
+
76
+ def tokenize
77
+ until @scanner.eos?
78
+ token = next_token
79
+ @tokens << token if token
80
+ end
81
+ @tokens << Token.new(type: :EOF, value: nil, line: @line, column: current_column)
82
+ @tokens
83
+ end
84
+
85
+ private
86
+
87
+ def current_column
88
+ @scanner.pos - @line_start + 1
89
+ end
90
+
91
+ def next_token
92
+ skip_whitespace_and_comments
93
+
94
+ return nil if @scanner.eos?
95
+
96
+ start_line = @line
97
+ start_col = current_column
98
+
99
+ # Try to match each token type
100
+ token = try_string ||
101
+ try_number ||
102
+ try_multi_char_op ||
103
+ try_single_char_op ||
104
+ try_identifier
105
+
106
+ unless token
107
+ # Unknown character - report error and skip
108
+ char = @scanner.getch
109
+ @errors << "Unknown character '#{char}' at line #{start_line}, column #{start_col}"
110
+ return nil
111
+ end
112
+
113
+ token
114
+ end
115
+
116
+ def skip_whitespace_and_comments
117
+ loop do
118
+ # Skip whitespace, tracking newlines
119
+ while @scanner.scan(/[ \t]+/) || @scanner.scan(/\r?\n/)
120
+ if @scanner.matched.include?("\n")
121
+ @line += 1
122
+ @line_start = @scanner.pos
123
+ end
124
+ end
125
+
126
+ # Skip line comments
127
+ if @scanner.scan(%r{//[^\n]*})
128
+ next
129
+ end
130
+
131
+ # Skip block comments
132
+ if @scanner.scan(%r{/\*})
133
+ depth = 1
134
+ until depth.zero? || @scanner.eos?
135
+ if @scanner.scan(%r{/\*})
136
+ depth += 1
137
+ elsif @scanner.scan(%r{\*/})
138
+ depth -= 1
139
+ elsif @scanner.scan(/\n/)
140
+ @line += 1
141
+ @line_start = @scanner.pos
142
+ else
143
+ @scanner.getch
144
+ end
145
+ end
146
+ next
147
+ end
148
+
149
+ break
150
+ end
151
+ end
152
+
153
+ def try_string
154
+ start_line = @line
155
+ start_col = current_column
156
+
157
+ # Double-quoted string
158
+ if @scanner.scan(/"/)
159
+ value = String.new
160
+ until @scanner.eos?
161
+ if @scanner.scan(/\\(.)/)
162
+ # Escape sequence
163
+ case @scanner[1]
164
+ when "n" then value << "\n"
165
+ when "t" then value << "\t"
166
+ when "r" then value << "\r"
167
+ when "\\" then value << "\\"
168
+ when '"' then value << '"'
169
+ else value << @scanner[1]
170
+ end
171
+ elsif @scanner.scan(/"/)
172
+ return Token.new(type: :STRING, value: value, line: start_line, column: start_col)
173
+ elsif @scanner.scan(/[^"\\]+/)
174
+ value << @scanner.matched
175
+ else
176
+ break
177
+ end
178
+ end
179
+ @errors << "Unterminated string at line #{start_line}, column #{start_col}"
180
+ return Token.new(type: :STRING, value: value, line: start_line, column: start_col)
181
+ end
182
+
183
+ nil
184
+ end
185
+
186
+ def try_number
187
+ start_line = @line
188
+ start_col = current_column
189
+
190
+ # Float with optional exponent
191
+ if @scanner.scan(/\d+\.\d+([eE][+-]?\d+)?/)
192
+ return Token.new(type: :FLOAT, value: @scanner.matched.to_f, line: start_line, column: start_col)
193
+ end
194
+
195
+ # Float with exponent (no decimal point)
196
+ if @scanner.scan(/\d+[eE][+-]?\d+/)
197
+ return Token.new(type: :FLOAT, value: @scanner.matched.to_f, line: start_line, column: start_col)
198
+ end
199
+
200
+ # Integer
201
+ if @scanner.scan(/\d+/)
202
+ return Token.new(type: :INT, value: @scanner.matched.to_i, line: start_line, column: start_col)
203
+ end
204
+
205
+ nil
206
+ end
207
+
208
+ def try_multi_char_op
209
+ start_line = @line
210
+ start_col = current_column
211
+
212
+ MULTI_CHAR_OPS.each do |op, type|
213
+ if @scanner.scan(Regexp.new(Regexp.escape(op)))
214
+ return Token.new(type: type, value: op, line: start_line, column: start_col)
215
+ end
216
+ end
217
+
218
+ nil
219
+ end
220
+
221
+ def try_single_char_op
222
+ start_line = @line
223
+ start_col = current_column
224
+
225
+ char = @scanner.peek(1)
226
+ if SINGLE_CHAR_OPS.key?(char)
227
+ @scanner.getch
228
+ return Token.new(type: SINGLE_CHAR_OPS[char], value: char, line: start_line, column: start_col)
229
+ end
230
+
231
+ nil
232
+ end
233
+
234
+ def try_identifier
235
+ start_line = @line
236
+ start_col = current_column
237
+
238
+ # Identifiers: start with letter or underscore (but not just _)
239
+ # Allow dots for qualified names like os.osc
240
+ if @scanner.scan(/[a-zA-Z_][a-zA-Z0-9_]*/)
241
+ value = @scanner.matched
242
+
243
+ # Check if it's a keyword
244
+ if KEYWORDS.include?(value)
245
+ type = value.upcase.to_sym
246
+ return Token.new(type: type, value: value, line: start_line, column: start_col)
247
+ end
248
+
249
+ return Token.new(type: :IDENT, value: value, line: start_line, column: start_col)
250
+ end
251
+
252
+ nil
253
+ end
254
+ end
255
+ end
@@ -0,0 +1,249 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Faust2Ruby
4
+ # Maps Faust library function calls to Ruby2Faust NodeTypes and DSL methods.
5
+ module LibraryMapper
6
+ # Mapping from Faust function names to Ruby DSL info
7
+ # Each entry: faust_name => { type: NodeType, dsl: method_name, args: arg_count }
8
+ MAPPINGS = {
9
+ # Oscillators (os.)
10
+ "os.osc" => { dsl: :osc, args: 1 },
11
+ "os.sawtooth" => { dsl: :saw, args: 1 },
12
+ "os.square" => { dsl: :square, args: 1 },
13
+ "os.triangle" => { dsl: :triangle, args: 1 },
14
+ "os.phasor" => { dsl: :phasor, args: 2 },
15
+ "os.lf_sawpos" => { dsl: :lf_saw, args: 1 },
16
+ "os.lf_trianglepos" => { dsl: :lf_triangle, args: 1 },
17
+ "os.lf_squarewavepos" => { dsl: :lf_square, args: 1 },
18
+ "os.lf_imptrain" => { dsl: :imptrain, args: 1 },
19
+ "os.lf_pulsetrain" => { dsl: :pulsetrain, args: 2 },
20
+
21
+ # Noise (no.)
22
+ "no.noise" => { dsl: :noise, args: 0 },
23
+ "no.pink_noise" => { dsl: :pink_noise, args: 0 },
24
+
25
+ # Filters (fi.)
26
+ "fi.lowpass" => { dsl: :lp, args: 2, opts: { order: 0 } },
27
+ "fi.highpass" => { dsl: :hp, args: 2, opts: { order: 0 } },
28
+ "fi.bandpass" => { dsl: :bp, args: 3 },
29
+ "fi.resonlp" => { dsl: :resonlp, args: 3 },
30
+ "fi.resonhp" => { dsl: :resonhp, args: 3 },
31
+ "fi.resonbp" => { dsl: :resonbp, args: 3 },
32
+ "fi.allpass_comb" => { dsl: :allpass, args: 3 },
33
+ "fi.dcblocker" => { dsl: :dcblock, args: 0 },
34
+ "fi.peak_eq" => { dsl: :peak_eq, args: 3 },
35
+
36
+ # SVF (State Variable Filter) (fi.svf.)
37
+ "fi.svf.lp" => { dsl: :svf_lp, args: 2 },
38
+ "fi.svf.hp" => { dsl: :svf_hp, args: 2 },
39
+ "fi.svf.bp" => { dsl: :svf_bp, args: 2 },
40
+ "fi.svf.notch" => { dsl: :svf_notch, args: 2 },
41
+ "fi.svf.ap" => { dsl: :svf_ap, args: 2 },
42
+ "fi.svf.bell" => { dsl: :svf_bell, args: 3 },
43
+ "fi.svf.ls" => { dsl: :svf_ls, args: 3 },
44
+ "fi.svf.hs" => { dsl: :svf_hs, args: 3 },
45
+
46
+ # Other filters (fi.)
47
+ "fi.lowpass3e" => { dsl: :lowpass3e, args: 1 },
48
+ "fi.highpass3e" => { dsl: :highpass3e, args: 1 },
49
+ "fi.lowpass6e" => { dsl: :lowpass6e, args: 1 },
50
+ "fi.highpass6e" => { dsl: :highpass6e, args: 1 },
51
+ "fi.bandstop" => { dsl: :bandstop, args: 3 },
52
+ "fi.notchw" => { dsl: :notchw, args: 2 },
53
+ "fi.low_shelf" => { dsl: :low_shelf, args: 3 },
54
+ "fi.high_shelf" => { dsl: :high_shelf, args: 3 },
55
+ "fi.peak_eq_cq" => { dsl: :peak_eq_cq, args: 3 },
56
+ "fi.pole" => { dsl: :fi_pole, args: 1 },
57
+ "fi.zero" => { dsl: :fi_zero, args: 1 },
58
+ "fi.tf1" => { dsl: :tf1, args: 3 },
59
+ "fi.tf2" => { dsl: :tf2, args: 5 },
60
+ "fi.tf1s" => { dsl: :tf1s, args: 3 },
61
+ "fi.tf2s" => { dsl: :tf2s, args: 5 },
62
+ "fi.iir" => { dsl: :iir, args: 2 },
63
+ "fi.fir" => { dsl: :fir, args: 1 },
64
+ "fi.conv" => { dsl: :conv, args: 2 },
65
+ "fi.fbcombfilter" => { dsl: :fbcombfilter, args: 3 },
66
+ "fi.ffcombfilter" => { dsl: :ffcombfilter, args: 2 },
67
+
68
+ # Delays (de.)
69
+ "de.delay" => { dsl: :delay, args: 2 },
70
+ "de.fdelay" => { dsl: :fdelay, args: 2 },
71
+ "de.sdelay" => { dsl: :sdelay, args: 3 },
72
+
73
+ # Envelopes (en.)
74
+ "en.ar" => { dsl: :ar, args: 3 },
75
+ "en.asr" => { dsl: :asr, args: 4 },
76
+ "en.adsr" => { dsl: :adsr, args: 5 },
77
+ "en.adsre" => { dsl: :adsre, args: 5 },
78
+
79
+ # Conversion (ba.)
80
+ "ba.db2linear" => { dsl: :db2linear, args: 1 },
81
+ "ba.linear2db" => { dsl: :linear2db, args: 1 },
82
+ "ba.samp2sec" => { dsl: :samp2sec, args: 1 },
83
+ "ba.sec2samp" => { dsl: :sec2samp, args: 1 },
84
+ "ba.midikey2hz" => { dsl: :midi2hz, args: 1 },
85
+ "ba.hz2midikey" => { dsl: :hz2midi, args: 1 },
86
+ "ba.selectn" => { dsl: :selectn, args: :variadic },
87
+ "ba.tau2pole" => { dsl: :tau2pole, args: 1 },
88
+ "ba.pole2tau" => { dsl: :pole2tau, args: 1 },
89
+ "ba.if" => { dsl: :ba_if, args: 3 },
90
+ "ba.selector" => { dsl: :selector, args: 3 },
91
+ "ba.selectmulti" => { dsl: :selectmulti, args: :variadic },
92
+ "ba.count" => { dsl: :ba_count, args: :variadic },
93
+ "ba.take" => { dsl: :ba_take, args: 2 },
94
+
95
+ # Smoothing (si.)
96
+ "si.smooth" => { dsl: :smooth, args: 1 },
97
+ "si.smoo" => { dsl: :smoo, args: 0 },
98
+ "si.bus" => { dsl: :bus, args: 1 },
99
+ "si.block" => { dsl: :block, args: 1 },
100
+
101
+ # Reverbs (re.)
102
+ "re.mono_freeverb" => { dsl: :freeverb, args: 4 },
103
+ "re.zita_rev1_stereo" => { dsl: :zita_rev, args: 6 },
104
+ "re.jpverb" => { dsl: :jpverb, args: 11 },
105
+
106
+ # Compressors (co.)
107
+ "co.compressor_mono" => { dsl: :compressor, args: 4 },
108
+ "co.limiter_1176_R4_mono" => { dsl: :limiter, args: 0 },
109
+
110
+ # Spatial (sp.)
111
+ "sp.panner" => { dsl: :panner, args: 1 },
112
+
113
+ # Math (ma.)
114
+ "ma.SR" => { dsl: :sr, args: 0 },
115
+ "ma.PI" => { dsl: :pi, args: 0 },
116
+ "ma.tempo" => { dsl: :tempo, args: 0 },
117
+ "ma.tanh" => { dsl: :tanh_, args: 0 },
118
+
119
+ # Antialiasing (aa.)
120
+ "aa.tanh1" => { dsl: :aa_tanh1, args: 0 },
121
+ "aa.tanh2" => { dsl: :aa_tanh2, args: 0 },
122
+ "aa.arctan" => { dsl: :aa_arctan, args: 0 },
123
+ "aa.softclip" => { dsl: :aa_softclip, args: 0 },
124
+ "aa.hardclip" => { dsl: :aa_hardclip, args: 0 },
125
+ "aa.parabolic" => { dsl: :aa_parabolic, args: 0 },
126
+ "aa.sin" => { dsl: :aa_sin, args: 0 },
127
+ "aa.cubic1" => { dsl: :aa_cubic1, args: 0 },
128
+ "aa.cubic2" => { dsl: :aa_cubic2, args: 0 },
129
+
130
+ # Analyzers (an.)
131
+ "an.amp_follower" => { dsl: :amp_follower, args: 1 },
132
+ "an.amp_follower_ar" => { dsl: :amp_follower_ar, args: 2 },
133
+ "an.amp_follower_ud" => { dsl: :amp_follower_ud, args: 2 },
134
+ "an.rms_envelope_rect" => { dsl: :rms_envelope_rect, args: 1 },
135
+ "an.rms_envelope_tau" => { dsl: :rms_envelope_tau, args: 1 },
136
+ "an.abs_envelope_rect" => { dsl: :abs_envelope_rect, args: 1 },
137
+ "an.abs_envelope_tau" => { dsl: :abs_envelope_tau, args: 1 },
138
+ "an.ms_envelope_rect" => { dsl: :ms_envelope_rect, args: 1 },
139
+ "an.ms_envelope_tau" => { dsl: :ms_envelope_tau, args: 1 },
140
+ "an.peak_envelope" => { dsl: :peak_envelope, args: 1 },
141
+
142
+ # Effects (ef.)
143
+ "ef.cubicnl" => { dsl: :cubicnl, args: 2 },
144
+ "ef.gate_mono" => { dsl: :gate_mono, args: 4 },
145
+ "ef.gate_stereo" => { dsl: :gate_stereo, args: 4 },
146
+ "ef.compressor_mono" => { dsl: :ef_compressor_mono, args: 4 },
147
+ "ef.compressor_stereo" => { dsl: :ef_compressor_stereo, args: 4 },
148
+ "ef.limiter_1176_R4_mono" => { dsl: :ef_limiter_1176_mono, args: 0 },
149
+ "ef.limiter_1176_R4_stereo" => { dsl: :ef_limiter_1176_stereo, args: 0 },
150
+ "ef.echo" => { dsl: :echo, args: 3 },
151
+ "ef.transpose" => { dsl: :transpose, args: 3 },
152
+ "ef.flanger_mono" => { dsl: :flanger_mono, args: 5 },
153
+ "ef.flanger_stereo" => { dsl: :flanger_stereo, args: 7 },
154
+ "ef.phaser2_mono" => { dsl: :phaser2_mono, args: 6 },
155
+ "ef.phaser2_stereo" => { dsl: :phaser2_stereo, args: 6 },
156
+ "ef.wah4" => { dsl: :wah4, args: 1 },
157
+ "ef.auto_wah" => { dsl: :auto_wah, args: 1 },
158
+ "ef.crybaby" => { dsl: :crybaby, args: 1 },
159
+ "ef.vocoder" => { dsl: :vocoder, args: 2 },
160
+ "ef.speakerbp" => { dsl: :speakerbp, args: 2 },
161
+ "ef.dryWetMixer" => { dsl: :dry_wet_mixer, args: 1 },
162
+ "ef.dryWetMixerConstantPower" => { dsl: :dry_wet_mixer_cp, args: 1 },
163
+ }.freeze
164
+
165
+ # Primitive functions that map directly to DSL
166
+ PRIMITIVES = {
167
+ # Math operators (used as functions)
168
+ "abs" => { dsl: :abs_, args: 0 },
169
+ "min" => { dsl: :min_, args: 2 },
170
+ "max" => { dsl: :max_, args: 2 },
171
+ "pow" => { dsl: :pow, args: 2 },
172
+ "sqrt" => { dsl: :sqrt_, args: 0 },
173
+ "exp" => { dsl: :exp_, args: 0 },
174
+ "log" => { dsl: :log_, args: 0 },
175
+ "log10" => { dsl: :log10_, args: 0 },
176
+ "sin" => { dsl: :sin_, args: 0 },
177
+ "cos" => { dsl: :cos_, args: 0 },
178
+ "tan" => { dsl: :tan_, args: 0 },
179
+ "tanh" => { dsl: :tanh_, args: 0 },
180
+ "asin" => { dsl: :asin_, args: 0 },
181
+ "acos" => { dsl: :acos_, args: 0 },
182
+ "atan" => { dsl: :atan_, args: 0 },
183
+ "atan2" => { dsl: :atan2, args: 2 },
184
+ "sinh" => { dsl: :sinh_, args: 0 },
185
+ "cosh" => { dsl: :cosh_, args: 0 },
186
+ "asinh" => { dsl: :asinh_, args: 0 },
187
+ "acosh" => { dsl: :acosh_, args: 0 },
188
+ "atanh" => { dsl: :atanh_, args: 0 },
189
+ "floor" => { dsl: :floor_, args: 0 },
190
+ "ceil" => { dsl: :ceil_, args: 0 },
191
+ "rint" => { dsl: :rint_, args: 0 },
192
+ "fmod" => { dsl: :fmod, args: 2 },
193
+ "int" => { dsl: :int_, args: 0 },
194
+ "float" => { dsl: :float_, args: 0 },
195
+
196
+ # Tables
197
+ "rdtable" => { dsl: :rdtable, args: 3 },
198
+ "rwtable" => { dsl: :rwtable, args: 5 },
199
+
200
+ # Selectors
201
+ "select2" => { dsl: :select2, args: 3 },
202
+ "select3" => { dsl: :select3, args: 4 },
203
+
204
+ # Primitives
205
+ "mem" => { dsl: :mem, args: 0 },
206
+ }.freeze
207
+
208
+ # UI element types
209
+ UI_ELEMENTS = {
210
+ "hslider" => :slider,
211
+ "vslider" => :vslider,
212
+ "nentry" => :nentry,
213
+ "button" => :button,
214
+ "checkbox" => :checkbox,
215
+ }.freeze
216
+
217
+ UI_GROUPS = {
218
+ "hgroup" => :hgroup,
219
+ "vgroup" => :vgroup,
220
+ "tgroup" => :tgroup,
221
+ }.freeze
222
+
223
+ module_function
224
+
225
+ def lookup(name)
226
+ MAPPINGS[name] || PRIMITIVES[name]
227
+ end
228
+
229
+ def ui_element?(name)
230
+ UI_ELEMENTS.key?(name)
231
+ end
232
+
233
+ def ui_group?(name)
234
+ UI_GROUPS.key?(name)
235
+ end
236
+
237
+ def ui_element_method(name)
238
+ UI_ELEMENTS[name]
239
+ end
240
+
241
+ def ui_group_method(name)
242
+ UI_GROUPS[name]
243
+ end
244
+
245
+ def known_function?(name)
246
+ MAPPINGS.key?(name) || PRIMITIVES.key?(name)
247
+ end
248
+ end
249
+ end