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,285 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module Ruby2Faust
6
+ # Intermediate Representation node for DSP graphs.
7
+ # Nodes are immutable value objects representing DSP operations.
8
+ Node = Struct.new(:type, :args, :inputs, :channels, keyword_init: true) do
9
+ def initialize(type:, args: [], inputs: [], channels: 1)
10
+ super(type: type, args: args.freeze, inputs: inputs.freeze, channels: channels)
11
+ end
12
+
13
+ def fingerprint
14
+ content = [type, args, inputs.map(&:fingerprint)].inspect
15
+ Digest::SHA1.hexdigest(content)
16
+ end
17
+
18
+ def same_structure?(other)
19
+ fingerprint == other.fingerprint
20
+ end
21
+ end
22
+
23
+ # Node type constants - comprehensive Faust library coverage
24
+ module NodeType
25
+ # === Comments/Documentation ===
26
+ COMMENT = :comment # // line comment
27
+ DOC = :doc # /* inline comment */ attached to node
28
+
29
+ # === Oscillators (os.) ===
30
+ OSC = :osc # os.osc(freq) - sine
31
+ SAW = :saw # os.sawtooth(freq)
32
+ SQUARE = :square # os.square(freq)
33
+ TRIANGLE = :triangle # os.triangle(freq)
34
+ PHASOR = :phasor # os.phasor(tablesize, freq)
35
+ LF_SAW = :lf_saw # os.lf_sawpos(freq) - low-freq sawtooth 0-1
36
+ LF_TRIANGLE = :lf_triangle
37
+ LF_SQUARE = :lf_square
38
+ IMPTRAIN = :imptrain # os.lf_imptrain(freq) - impulse train
39
+ PULSETRAIN = :pulsetrain # os.lf_pulsetrain(freq, duty)
40
+
41
+ # === Noise (no.) ===
42
+ NOISE = :noise # no.noise - white
43
+ PINK_NOISE = :pink_noise # no.pink_noise
44
+
45
+ # === Filters (fi.) ===
46
+ LP = :lp # fi.lowpass(order, freq)
47
+ HP = :hp # fi.highpass(order, freq)
48
+ BP = :bp # fi.bandpass(order, freq, q)
49
+ RESONLP = :resonlp # fi.resonlp(freq, q, gain)
50
+ RESONHP = :resonhp
51
+ RESONBP = :resonbp
52
+ ALLPASS = :allpass # fi.allpass_comb(maxdelay, delay, feedback)
53
+ DCBLOCK = :dcblock # fi.dcblocker
54
+ PEAK_EQ = :peak_eq # fi.peak_eq(freq, q, gain_db)
55
+
56
+ # SVF (State Variable Filter)
57
+ SVF_LP = :svf_lp # fi.svf.lp(freq, q)
58
+ SVF_HP = :svf_hp # fi.svf.hp(freq, q)
59
+ SVF_BP = :svf_bp # fi.svf.bp(freq, q)
60
+ SVF_NOTCH = :svf_notch # fi.svf.notch(freq, q)
61
+ SVF_AP = :svf_ap # fi.svf.ap(freq, q)
62
+ SVF_BELL = :svf_bell # fi.svf.bell(freq, q, gain)
63
+ SVF_LS = :svf_ls # fi.svf.ls(freq, q, gain) - low shelf
64
+ SVF_HS = :svf_hs # fi.svf.hs(freq, q, gain) - high shelf
65
+
66
+ # Other filters
67
+ LOWPASS3E = :lowpass3e # fi.lowpass3e(freq) - 3rd order elliptic
68
+ HIGHPASS3E = :highpass3e # fi.highpass3e(freq)
69
+ LOWPASS6E = :lowpass6e # fi.lowpass6e(freq) - 6th order elliptic
70
+ HIGHPASS6E = :highpass6e # fi.highpass6e(freq)
71
+ BANDSTOP = :bandstop # fi.bandstop(order, freq, q)
72
+ NOTCHW = :notchw # fi.notchw(freq, width)
73
+ LOW_SHELF = :low_shelf # fi.low_shelf(freq, q, gain)
74
+ HIGH_SHELF = :high_shelf # fi.high_shelf(freq, q, gain)
75
+ PEAK_EQ_CQ = :peak_eq_cq # fi.peak_eq_cq(freq, q, gain)
76
+ FI_POLE = :fi_pole # fi.pole(p)
77
+ FI_ZERO = :fi_zero # fi.zero(z)
78
+ TF1 = :tf1 # fi.tf1(b0, b1, a1)
79
+ TF2 = :tf2 # fi.tf2(b0, b1, b2, a1, a2)
80
+ TF1S = :tf1s # fi.tf1s(b0, b1, a1)
81
+ TF2S = :tf2s # fi.tf2s(b0, b1, b2, a1, a2)
82
+ IIR = :iir # fi.iir(bcoeffs, acoeffs)
83
+ FIR = :fir # fi.fir(coeffs)
84
+ CONV = :conv # fi.conv(impulse, size)
85
+ FBCOMBFILTER = :fbcombfilter # fi.fbcombfilter(maxdel, del, fb)
86
+ FFCOMBFILTER = :ffcombfilter # fi.ffcombfilter(maxdel, del)
87
+
88
+ # === Delays (de.) ===
89
+ DELAY = :delay # de.delay(maxdelay, delay)
90
+ FDELAY = :fdelay # de.fdelay(maxdelay, delay) - fractional
91
+ SDELAY = :sdelay # de.sdelay(maxdelay, interp, delay) - smooth
92
+
93
+ # === Envelopes (en.) ===
94
+ AR = :ar # en.ar(attack, release, gate)
95
+ ASR = :asr # en.asr(attack, sustain_level, release, gate)
96
+ ADSR = :adsr # en.adsr(attack, decay, sustain, release, gate)
97
+ ADSRE = :adsre # en.adsre with exponential segments
98
+
99
+ # === Math (primitives + ma.) ===
100
+ GAIN = :gain # *(x)
101
+ ADD = :add # +
102
+ MUL = :mul # *
103
+ SUB = :sub # -
104
+ DIV = :div # /
105
+ NEG = :neg # 0 - x
106
+ ABS = :abs # abs
107
+ MIN = :min # min(a, b)
108
+ MAX = :max # max(a, b)
109
+ CLIP = :clip # max(min_val, min(max_val, x))
110
+ POW = :pow # pow(base, exp)
111
+ SQRT = :sqrt # sqrt
112
+ EXP = :exp # exp
113
+ LOG = :log # log
114
+ LOG10 = :log10 # log10
115
+ SIN = :sin # sin
116
+ COS = :cos # cos
117
+ TAN = :tan # tan
118
+ ASIN = :asin
119
+ ACOS = :acos
120
+ ATAN = :atan
121
+ ATAN2 = :atan2
122
+ TANH = :tanh # ma.tanh - saturating
123
+ SINH = :sinh
124
+ COSH = :cosh
125
+ ASINH = :asinh
126
+ ACOSH = :acosh
127
+ ATANH = :atanh
128
+ FLOOR = :floor
129
+ CEIL = :ceil
130
+ RINT = :rint # round to int
131
+ FMOD = :fmod # fmod(x, y)
132
+ REMAINDER = :remainder
133
+ MOD = :mod # %
134
+
135
+ # === Comparison ===
136
+ LT = :lt # <
137
+ GT = :gt # >
138
+ LE = :le # <=
139
+ GE = :ge # >=
140
+ EQ = :eq # ==
141
+ NEQ = :neq # !=
142
+
143
+ # === Bitwise ===
144
+ BAND = :band # &
145
+ BOR = :bor # | (bitwise, not parallel)
146
+ XOR = :xor # xor
147
+
148
+ # === Conversion (ba.) ===
149
+ DB2LINEAR = :db2linear # ba.db2linear
150
+ LINEAR2DB = :linear2db # ba.linear2db
151
+ SAMP2SEC = :samp2sec # ba.samp2sec
152
+ SEC2SAMP = :sec2samp # ba.sec2samp
153
+ MIDI2HZ = :midi2hz # ba.midikey2hz
154
+ HZ2MIDI = :hz2midi # ba.hz2midikey
155
+ TAU2POLE = :tau2pole # ba.tau2pole
156
+ POLE2TAU = :pole2tau # ba.pole2tau
157
+ BA_IF = :ba_if # ba.if(cond, then, else)
158
+ SELECTOR = :selector # ba.selector(n, sel, inputs)
159
+ BA_TAKE = :ba_take # ba.take(idx, tuple)
160
+
161
+ # === Smoothing (si.) ===
162
+ SMOOTH = :smooth # si.smooth(ba.tau2pole(tau))
163
+ SMOO = :smoo # si.smoo - default 5ms smooth
164
+ POLYSMOOTH = :polysmooth # si.polySmooth(s, n)
165
+
166
+ # === Selectors ===
167
+ SELECT2 = :select2 # select2(cond, a, b)
168
+ SELECTN = :selectn # ba.selectn(n, idx, ...)
169
+
170
+ # === Routing (si./ro.) ===
171
+ BUS = :bus # si.bus(n) - n parallel wires
172
+ BLOCK = :block # si.block(n) - terminate n signals
173
+
174
+ # === Reverbs (re.) ===
175
+ FREEVERB = :freeverb # re.mono_freeverb(fb1, fb2, damp, spread)
176
+ ZITA_REV = :zita_rev # re.zita_rev1_stereo(...)
177
+ JPVERB = :jpverb # re.jpverb(...)
178
+
179
+ # === Compressors (co.) ===
180
+ COMPRESSOR = :compressor # co.compressor_mono(ratio, thresh, attack, release)
181
+ LIMITER = :limiter # co.limiter_1176_R4_mono
182
+
183
+ # === Spatial (sp.) ===
184
+ PANNER = :panner # sp.panner(pan) - stereo pan
185
+
186
+ # === UI Controls ===
187
+ SLIDER = :slider
188
+ VSLIDER = :vslider
189
+ NENTRY = :nentry
190
+ BUTTON = :button
191
+ CHECKBOX = :checkbox
192
+ HGROUP = :hgroup
193
+ VGROUP = :vgroup
194
+ TGROUP = :tgroup
195
+
196
+ # === Composition ===
197
+ SEQ = :seq # :
198
+ PAR = :par # ,
199
+ SPLIT = :split # <:
200
+ MERGE = :merge # :>
201
+ FEEDBACK = :feedback # ~
202
+ REC = :rec # letrec style
203
+ LETREC = :letrec # letrec { 'x = expr; 'y = expr; } result
204
+
205
+ # === Iteration ===
206
+ FPAR = :fpar # par(i, n, expr)
207
+ FSEQ = :fseq # seq(i, n, expr)
208
+ FSUM = :fsum # sum(i, n, expr)
209
+ FPROD = :fprod # prod(i, n, expr)
210
+
211
+ # === Lambda ===
212
+ LAMBDA = :lambda # \(x).(body)
213
+ PARAM = :param # Parameter reference
214
+
215
+ # === Tables ===
216
+ RDTABLE = :rdtable # rdtable(n, init, ridx)
217
+ RWTABLE = :rwtable # rwtable(n, init, widx, wsig, ridx)
218
+ WAVEFORM = :waveform # waveform{...}
219
+
220
+ # === Additional Routing ===
221
+ ROUTE = :route # route(ins, outs, connections)
222
+ SELECT3 = :select3 # select3(sel, a, b, c)
223
+
224
+ # === Utility ===
225
+ WIRE = :wire # _
226
+ CUT = :cut # !
227
+ LITERAL = :literal # raw Faust expression
228
+ MEM = :mem # mem (1-sample delay)
229
+ INT = :int # int(x)
230
+ FLOAT = :float # float(x)
231
+
232
+ # === Constants ===
233
+ SR = :sr # ma.SR
234
+ PI = :pi # ma.PI
235
+ TEMPO = :tempo # ma.tempo
236
+
237
+ # === Antialiasing (aa.) ===
238
+ AA_TANH1 = :aa_tanh1 # aa.tanh1
239
+ AA_TANH2 = :aa_tanh2 # aa.tanh2
240
+ AA_ARCTAN = :aa_arctan # aa.arctan
241
+ AA_SOFTCLIP = :aa_softclip # aa.softclip
242
+ AA_HARDCLIP = :aa_hardclip # aa.hardclip
243
+ AA_PARABOLIC = :aa_parabolic # aa.parabolic
244
+ AA_SIN = :aa_sin # aa.sin
245
+ AA_CUBIC1 = :aa_cubic1 # aa.cubic1
246
+ AA_CUBIC2 = :aa_cubic2 # aa.cubic2
247
+
248
+ # === Analyzers (an.) ===
249
+ AMP_FOLLOWER = :amp_follower # an.amp_follower(t)
250
+ AMP_FOLLOWER_AR = :amp_follower_ar # an.amp_follower_ar(attack, release)
251
+ AMP_FOLLOWER_UD = :amp_follower_ud # an.amp_follower_ud(up, down)
252
+ RMS_ENVELOPE_RECT = :rms_envelope_rect # an.rms_envelope_rect(period)
253
+ RMS_ENVELOPE_TAU = :rms_envelope_tau # an.rms_envelope_tau(tau)
254
+ ABS_ENVELOPE_RECT = :abs_envelope_rect # an.abs_envelope_rect(period)
255
+ ABS_ENVELOPE_TAU = :abs_envelope_tau # an.abs_envelope_tau(tau)
256
+ MS_ENVELOPE_RECT = :ms_envelope_rect # an.ms_envelope_rect(period)
257
+ MS_ENVELOPE_TAU = :ms_envelope_tau # an.ms_envelope_tau(tau)
258
+ PEAK_ENVELOPE = :peak_envelope # an.peak_envelope(t)
259
+
260
+ # === Effects (ef.) ===
261
+ CUBICNL = :cubicnl # ef.cubicnl(drive, offset)
262
+ GATE_MONO = :gate_mono # ef.gate_mono(thresh, att, hold, rel)
263
+ GATE_STEREO = :gate_stereo # ef.gate_stereo(thresh, att, hold, rel)
264
+ EF_COMPRESSOR_MONO = :ef_compressor_mono # ef.compressor_mono
265
+ EF_COMPRESSOR_STEREO = :ef_compressor_stereo # ef.compressor_stereo
266
+ EF_LIMITER_1176_MONO = :ef_limiter_1176_mono # ef.limiter_1176_R4_mono
267
+ EF_LIMITER_1176_STEREO = :ef_limiter_1176_stereo # ef.limiter_1176_R4_stereo
268
+ ECHO = :echo # ef.echo(maxdel, del, fb)
269
+ TRANSPOSE = :transpose # ef.transpose(w, x, s)
270
+ FLANGER_MONO = :flanger_mono # ef.flanger_mono(...)
271
+ FLANGER_STEREO = :flanger_stereo # ef.flanger_stereo(...)
272
+ PHASER2_MONO = :phaser2_mono # ef.phaser2_mono(...)
273
+ PHASER2_STEREO = :phaser2_stereo # ef.phaser2_stereo(...)
274
+ WAH4 = :wah4 # ef.wah4(fr)
275
+ AUTO_WAH = :auto_wah # ef.auto_wah(level)
276
+ CRYBABY = :crybaby # ef.crybaby(wah)
277
+ VOCODER = :vocoder # ef.vocoder(bands, range)
278
+ SPEAKERBP = :speakerbp # ef.speakerbp(flo, fhi)
279
+ DRY_WET_MIXER = :dry_wet_mixer # ef.dryWetMixer(mix)
280
+ DRY_WET_MIXER_CP = :dry_wet_mixer_cp # ef.dryWetMixerConstantPower(mix)
281
+
282
+ # === Metadata ===
283
+ DECLARE = :declare
284
+ end
285
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ir"
4
+ require_relative "emitter"
5
+
6
+ module Ruby2Faust
7
+ # Live reload support: graph diffing, compilation, crossfade.
8
+ module Live
9
+ module_function
10
+
11
+ # Check if two graphs have different structure
12
+ #
13
+ # @param old_graph [DSP, Node] Previous graph
14
+ # @param new_graph [DSP, Node] New graph
15
+ # @return [Boolean] True if structures differ
16
+ def changed?(old_graph, new_graph)
17
+ old_node = old_graph.is_a?(DSP) ? old_graph.node : old_graph
18
+ new_node = new_graph.is_a?(DSP) ? new_graph.node : new_graph
19
+ !old_node.same_structure?(new_node)
20
+ end
21
+
22
+ # Compile a DSP graph to a Faust file
23
+ #
24
+ # @param graph [DSP] The DSP graph to compile
25
+ # @param output [String] Output file path (.dsp)
26
+ # @param imports [Array<String>] Libraries to import
27
+ # @return [String] Path to the output file
28
+ def compile(graph, output:, imports: Emitter::DEFAULT_IMPORTS)
29
+ code = Emitter.program(graph, imports: imports)
30
+ File.write(output, code)
31
+ output
32
+ end
33
+
34
+ # Generate crossfade DSP code for smooth transitions
35
+ # Creates a Faust program that crossfades between old and new DSP
36
+ #
37
+ # @param old_process [String] Faust expression for old process
38
+ # @param new_process [String] Faust expression for new process
39
+ # @param duration [Float] Crossfade duration in seconds (default 0.05)
40
+ # @return [String] Faust source with crossfade
41
+ def crossfade_dsp(old_process, new_process, duration: 0.05)
42
+ <<~FAUST
43
+ import("stdfaust.lib");
44
+
45
+ // Crossfade envelope
46
+ xfade = hslider("xfade", 0, 0, 1, 0.001) : si.smoo;
47
+
48
+ // Old and new processes
49
+ old = #{old_process};
50
+ new = #{new_process};
51
+
52
+ // Crossfade: (1-x)*old + x*new
53
+ process = old * (1 - xfade), new * xfade :> _;
54
+ FAUST
55
+ end
56
+
57
+ # Run the Faust compiler on a .dsp file
58
+ # Requires faust to be in PATH
59
+ #
60
+ # @param dsp_file [String] Path to .dsp file
61
+ # @param target [Symbol] Compilation target (:wasm, :cpp, :llvm)
62
+ # @param output_dir [String] Output directory (default: same as input)
63
+ # @return [Boolean] True if compilation succeeded
64
+ def faust_compile(dsp_file, target: :cpp, output_dir: nil)
65
+ output_dir ||= File.dirname(dsp_file)
66
+ basename = File.basename(dsp_file, ".dsp")
67
+
68
+ cmd = case target
69
+ when :wasm
70
+ "faust2wasm #{dsp_file} -o #{output_dir}/#{basename}.wasm"
71
+ when :cpp
72
+ "faust -a minimal.cpp #{dsp_file} -o #{output_dir}/#{basename}.cpp"
73
+ when :llvm
74
+ "faust -lang llvm #{dsp_file} -o #{output_dir}/#{basename}.ll"
75
+ else
76
+ raise ArgumentError, "Unknown target: #{target}"
77
+ end
78
+
79
+ system(cmd)
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruby2Faust
4
+ VERSION = "0.2.0"
5
+ end
data/lib/ruby2faust.rb ADDED
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ruby2faust/version"
4
+ require_relative "ruby2faust/ir"
5
+ require_relative "ruby2faust/dsl"
6
+ require_relative "ruby2faust/emitter"
7
+ require_relative "ruby2faust/live"
8
+
9
+ module Ruby2Faust
10
+ class Error < StandardError; end
11
+
12
+ # Convenience method to generate Faust code from a block
13
+ #
14
+ # @example
15
+ # code = Ruby2Faust.generate do
16
+ # osc(440).then(gain(0.3))
17
+ # end
18
+ #
19
+ # @yield Block that returns a DSP
20
+ # @return [String] Faust source code
21
+ def self.generate(pretty: false, &block)
22
+ context = Object.new
23
+ context.extend(DSL)
24
+ process = context.instance_eval(&block)
25
+ Emitter.program(process, pretty: pretty)
26
+ end
27
+ end