New Makefile for libsndfile-ocaml library integration; Makefiles chain modified.
[Faustine.git] / interpretor / preprocessor / faust-0.9.47mr3 / tools / faust2sc-1.0.0 / faust2sc
1 #!/usr/bin/env ruby
2 # ======================================================================
3 # faust2sc - Generate language modules from Faust XML.
4 # Copyright (C) 2005-2008 Stefan Kersten
5 # ======================================================================
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
19 # USA
20 # ======================================================================
21
22 # TODO:
23 # rexml is dog slow, maybe use libxml?
24
25 require 'getoptlong'
26 require 'rexml/document'
27
28 PROGRAM = File.basename($0)
29 PROGRAM_VERSION = "1.1.0"
30
31 class Array
32 def flatten1
33 res = []
34 self.each { |l|
35 res += l
36 }
37 res
38 end
39 end
40
41 module REXML
42 class Element
43 def to_i
44 self.text.to_i
45 end
46 def to_f
47 self.text.to_f
48 end
49 end
50 end
51
52 class String
53 def encapitalize
54 self[0..0].upcase + self[1..-1]
55 end
56 def decapitalize(greedy=false)
57 unless greedy
58 self[0..0].downcase + self[1..-1]
59 else
60 res = self.clone
61 (0..res.size-1).each { |i|
62 c = res[i]
63 if 65 <= c && c <= 90
64 res[i] = c + 32
65 else
66 break
67 end
68 }
69 res
70 end
71 end
72 end
73
74 def print_error(str)
75 $stderr.print("#{PROGRAM}[ERROR] #{str}")
76 end
77
78 def print_info(str)
79 $stderr.print("#{PROGRAM}[INFO] #{str}")
80 end
81
82 module Faust
83 class Widget
84 attr_reader :type, :id, :label, :init, :min, :max, :step
85 def initialize(node)
86 @type = node.attributes["type"]
87 @id = node.attributes["id"]
88 if (node.elements["label"].text)
89 @label = node.elements["label"].text
90 else
91 print_info("No label for widget ID #{id} - calling it widget#{id}\n")
92 @label = "widget#{id}"
93 end
94 if (@label =~ /^[0-9]/)
95 plabel = @label
96 @label = @type + "_" + @label
97 print_info("Widget label `#{plabel}' prefixed to become `#{@label}'\n")
98 end
99 dict = node.elements
100 @init = dict["init"].to_f
101 @min = dict["min"].to_f
102 @max = dict["max"].to_f
103 @step = dict["step"].to_f
104 if (@type == "button")
105 @max = 1 # no metadata other than name for buttons
106 @step = 1
107 end
108 end
109 end
110
111 class UI
112 attr_reader :active_widgets, :passive_widgets
113 def initialize(node)
114 @active_widgets = node.get_elements("//activewidgets/widget").collect { |x| Widget.new(x) }
115 @passive_widgets = node.get_elements("//passivewidgets/widget").collect { |x| Widget.new(x) }
116 end
117 end
118
119 class Plugin
120 attr_reader :path, :name, :author, :copyright, :license, :inputs, :outputs, :ui
121 def initialize(path, node)
122 @path = path
123 %w(name author copyright license).each { |name|
124 instance_variable_set("@#{name}", node.elements["/faust/#{name}"].text)
125 }
126 %w(inputs outputs).each { |name|
127 instance_variable_set("@#{name}", node.elements["/faust/#{name}"].text.to_i)
128 }
129 @ui = UI.new(node.elements["/faust/ui"])
130 end
131 def total_inputs
132 inputs + ui.active_widgets.size
133 end
134 def Plugin::from_file(path)
135 self.new(path, REXML::Document.new(File.open(path) { |io| io.read }))
136 end
137 end
138
139 class Generator
140 attr_reader :plugins, :options
141 def initialize(plugins, options)
142 @plugins = plugins
143 @options = options
144 end
145 def lang
146 "unknown"
147 end
148 def generate(io)
149 generate_header(io)
150 generate_body(io)
151 generate_footer(io)
152 end
153 def generate_header(io)
154 end
155 def generate_footer(io)
156 end
157 def generate_body(io)
158 plugins.each_with_index { |plugin,i|
159 if plugin
160 begin
161 print_info("Generating #{lang} code for #{plugin.name} ...\n")
162 generate_plugin(io, plugin)
163 if i < (plugins.size - 1)
164 io.print("\n")
165 end
166 rescue
167 print_error("#{$!}\n")
168 $!.backtrace.each { |l| print_error(l + "\n") }
169 print_error("Omitting #{plugin.path}\n")
170 end
171 end
172 }
173 io.print("\n")
174 end
175 def generate_plugin(io, plugin)
176 raise "#{self.class}::generate_plugin() missing!"
177 end
178 end
179
180 module Language
181 IDENTIFIER_REGEXP = /^[a-z][a-zA-Z0-9_]*[a-zA-Z0-9]?$/
182 def make_identifier(name)
183 # gentle identifier massage
184 # remove quotes
185 name = name.sub(/^"([^"]*)"/, "\\1")
186 # replace invalid chars with underscores
187 name = name.downcase.gsub(/[^a-zA-Z0-9_]/, "_")
188 # reduce multiple underscores to one
189 name = name.gsub(/__+/, "_")
190 # remove leading/terminating underscores
191 name = name.sub(/(^_|_$)/, "")
192 # move leading digits to the end
193 name = name.sub(/^([0-9]+)_/, "") + ("_#{$1}" if $1).to_s
194 # if digit(s) only, prepend alpha prefix
195 if (name[0..0] =~ /^[0-9]/)
196 pname = name
197 name = "w" + name
198 print_info("Widget label `#{pname}' prefixed to become `#{name}'\n")
199 end
200 unless name =~ IDENTIFIER_REGEXP
201 raise "invalid identifier: \"#{name}\""
202 end
203 name
204 end
205 def make_unique(list)
206 # bad, bad, bad
207 list = list.clone
208 res = []
209 ids = {}
210 while hd = list.shift
211 if ids.has_key?(hd)
212 ids[hd] = id = ids[hd] + 1
213 else
214 if list.include?(hd)
215 ids[hd] = id = 0
216 end
217 end
218 res << (id ? "#{hd}_#{id}" : hd)
219 end
220 res
221 end
222 module_function :make_identifier, :make_unique
223 end
224
225 module Haskell
226 INDENT = " " * 4
227
228 def mk_function_names(plugin)
229 fname = plugin.name.decapitalize(true)
230 lname = fname + "'"
231 [lname, fname]
232 end
233 module_function :mk_function_names
234
235 class PluginGenerator
236 attr_reader :plugin
237 def initialize(plugin)
238 @plugin = plugin
239 end
240 def generate(io)
241 lname, fname = Haskell.mk_function_names(plugin)
242 gen_curry_func(io, lname, fname)
243 io << "\n"
244 gen_list_func(io, lname)
245 end
246 def gen_ugen(io, name, rate, inputs, num_outputs)
247 io << "UGen.mkUGen #{rate} \"#{name}\" #{inputs} (replicate #{num_outputs} #{rate}) (UGen.Special 0) Nothing"
248 io << "\n"
249 end
250 def gen_curry_func(io, lname, fname)
251 args = mk_args(plugin.total_inputs)
252 decs = args.collect { "UGen" } + ["UGen"] # add result type
253 io << "#{fname} :: #{decs.join(" -> ")}\n"
254 io << "#{fname} #{args.join(" ")} = #{lname} [#{args.join(',')}]\n"
255 end
256 def gen_list_func(io, fname)
257 io << "#{fname} :: [UGen] -> UGen\n"
258 io << "#{fname} args = "
259 gen_ugen(io, plugin.name, "UGen.AR", "args", plugin.outputs)
260 end
261
262 protected
263 def mk_args(n, x="x")
264 (1..n).collect { |i| "#{x}#{i}" }
265 end
266 end # PluginGenerator
267
268 class Generator < Faust::Generator
269 def initialize(plugins, options)
270 super(plugins, options)
271 @module = options["prefix"]
272 end
273 def lang
274 "haskell"
275 end
276 def generate_header(io)
277 gen_module(io, plugins.collect { |p| Haskell.mk_function_names(p) }.flatten1)
278 io << "\n"
279 gen_imports(io)
280 io << "\n"
281 end
282 def generate_plugin(io, plugin)
283 PluginGenerator.new(plugin).generate(io)
284 end
285 def gen_module(io, exports)
286 #m = @module.empty? ? "" : @module + "."
287 #io << "module #{m}#{plugin.name.encapitalize} (\n"
288 io << "module #{@module.empty? ? "Main" : @module} (\n"
289 io << exports.collect { |x| (INDENT * 1) + x }.join(",\n") << "\n"
290 io << ") where\n"
291 end
292 def gen_imports(io)
293 io << "import Sound.SC3.UGen (UGen)\n"
294 io << "import qualified Sound.SC3.UGen as UGen\n"
295 end
296 end
297 end
298
299 module SC3
300 include Language
301
302 CLASS_REGEXP = /^[A-Z][a-zA-Z0-9_]*[a-zA-Z0-9]?$/
303
304 def path_to_unitname(path)
305 name = File.basename(path)
306 if ext_index = name.index(".")
307 name = name[0..ext_index-1]
308 end
309 name
310 end
311 def make_class_name(unit_name, prefix)
312 # The SuperCollider UGen class name generated here must match
313 # that generated in the Faust architecture file supercollider.cpp
314 if prefix == ""
315 prefix = "Faust"
316 end
317 class_name = unit_name
318 class_name = class_name.sub(/^#{prefix}/i, '')
319 class_name = prefix + '_' + class_name
320 cna = class_name.split(/[^0-9a-z]/i)
321 cna.each_index { |i| cna[i] = cna[i].encapitalize } # SC name convention
322 class_name = cna.join
323 print_info(unit_name + " -> " + class_name + "\n")
324 if class_name.length > 31 # maximum SC Node ID length is 31 (hashed)
325 print_info("Class name `#{class_name}' truncated to 1st 31 chars\n")
326 class_name = class_name[0,30]
327 end
328 unless class_name =~ CLASS_REGEXP
329 raise "invalid class name: \"#{class_name}\""
330 end
331 class_name
332 end
333 module_function :path_to_unitname, :make_class_name
334
335 class Faust::Widget
336 def sc3_identifier
337 l = self.label
338 l = l.gsub(/\([^\)]*\)/, '')
339 l = Language.make_identifier(l)
340 if l.length > 30 # maximum SC ParamSpec length is 31 (hashed)
341 lt = l[0,29] # leave a final char for make_unique
342 print_info("Label `#{l}' for #{self.type} #{self.id} truncated to " +
343 "`#{lt}' (30 chars)\n")
344 l = lt
345 end
346 l
347 end
348 def sc3_arg_string
349 "#{self.sc3_identifier}(#{self.init})"
350 end
351 def sc3_default
352 "(#{self.init})"
353 end
354 def sc3_default2
355 "=(#{self.init})" # Parens make unary minus work in SC
356 end
357 end
358
359 class PluginGenerator
360 attr_reader :unit_name, :class_name
361 def initialize(plugin, options)
362 @plugin = plugin
363 @options = options
364 @unit_name = plugin.name || SC3::path_to_unitname(plugin.path)
365 @class_name = SC3::make_class_name(@unit_name, options["prefix"])
366 end
367 def inputs
368 @plugin.inputs
369 end
370 def outputs
371 @plugin.outputs
372 end
373 def superclass_name
374 @plugin.outputs > 1 ? "MultiOutUGen" : "UGen"
375 end
376 def input_names
377 (1..self.inputs).collect { |i| "in#{i}" }
378 end
379 def control_names
380 Language::make_unique(@plugin.ui.active_widgets.collect { |x| x.sc3_identifier })
381 end
382 def decl_args # "name(default)"
383 cnames = self.control_names
384 cdefaults = @plugin.ui.active_widgets.collect { |x| x.sc3_default }
385 args = self.input_names + cnames.zip(cdefaults).collect { |ary| ary[0] + ary[1] }
386 args.empty? ? "" : " | " + args.join(", ") + " |"
387 end
388 def decl_args2 # "name=default"
389 cnames = self.control_names
390 cdefaults = @plugin.ui.active_widgets.collect { |x| x.sc3_default2 }
391 args = self.input_names + cnames.zip(cdefaults).collect { |ary| ary[0] + ary[1] }
392 args.empty? ? "" : args.join(", ") + ";"
393 end
394 def decl_args3 # args for SynthDef
395 cnames = self.control_names
396 cdefaults = @plugin.ui.active_widgets.collect { |x| x.sc3_default }
397 args = self.input_names.collect { |x| "\\#{x}.ar(0)" } \
398 + cnames.zip(cdefaults).collect { |x| "\\#{x[0]}.kr#{x[1]}" }
399 args.empty? ? "" : args.join(", ")
400 end
401 def decl_buses # declare input buses for Synth
402 args = self.input_names.collect { |x| \
403 " #{x}Bus = Bus.audio(s,1);\n"};
404 end
405 def decl_args4 # args for Synth
406 cnames = self.control_names
407 cdefaults = @plugin.ui.active_widgets.collect { |x| x.sc3_default }
408 args = self.input_names.collect { |x| "#{x}:#{x}Bus.asMap"} \
409 + cnames.collect { |x| "#{x}:#{x}Var" }
410 args.empty? ? "" : "\n\t[\t" + args.join(",\n\t\t") + "\n\t]"
411 end
412 def decl_metadata
413 cnames = self.control_names
414 cnames.collect! {|x| " \\"+x+":"}
415 warp = 0
416 cmeta = @plugin.ui.active_widgets.collect \
417 { |x| "[#{x.min}, #{x.max}, #{warp}, #{x.step}, #{x.init}].asSpec" }
418 # \\name1: [minval, maxval, warp, step, default, units].asSpec,
419 # Note: could append x.units as well
420 args = cnames.zip(cmeta).collect { |ary| ary[0] + ary[1] }
421 args.empty? ? "" : args.join(",\n ")
422 end
423 def new_args(rate)
424 ["'%s'" % rate] + self.input_names + self.control_names
425 end
426 def validate
427 args = self.input_names + self.control_names
428 unless args.uniq == args
429 raise "argument list not unique"
430 end
431 self
432 end
433 def generate(io)
434 self.validate
435 generate_decl(io)
436 generate_body(io)
437 end
438 def generate_decl(io)
439 io.print("#{@class_name} : #{self.superclass_name}\n")
440 print_info("UGen: #{@class_name}\n")
441 end
442 def generate_body(io)
443 io.print("{\n")
444 body = <<EOF
445 *ar {#{self.decl_args}
446 ^this.multiNew(#{self.new_args(:audio).join(", ")})
447 }
448
449 *kr {#{self.decl_args}
450 ^this.multiNew(#{self.new_args(:control).join(", ")})
451 }
452 EOF
453 if self.inputs > 0
454 body = body + <<EOF
455
456 checkInputs {
457 if (rate == 'audio', {
458 #{self.inputs}.do({|i|
459 if (inputs.at(i).rate != 'audio', {
460 ^(" input at index " + i + "(" + inputs.at(i) +
461 ") is not audio rate");
462 });
463 });
464 });
465 ^this.checkValidInputs
466 }
467 EOF
468 end
469 io.print(body)
470 generate_outputs(io)
471 if @options["synthdef"]
472 generate_synthdef(io)
473 end
474 io.print("\n name { ^\"#{@class_name}\" }\n")
475 io.print("}\n")
476 end
477 def generate_outputs(io)
478 if self.outputs > 1 # add initOutputs to overridden init:
479 io.print <<EOF
480
481 init { | ... theInputs |
482 inputs = theInputs
483 ^this.initOutputs(#{self.outputs}, rate)
484 }
485 EOF
486 end
487 end
488 def generate_synthdef(io)
489 s = @class_name
490 sdname = s[0,1].downcase + s[1 .. s.length]
491 print_info("SynthDef: \\#{sdname}\n")
492 # if \arg.ar(0) form not used, add after out=0: ,#{self.decl_args2}
493 io.print <<EOF
494
495 /*
496 SynthDef - Typical usage in SuperCollider:
497
498 EOF
499 print_info("SynthDef: have #{self.input_names.size} inputs\n")
500 if self.input_names.size>0
501 io.print "#{decl_buses}\n";
502 end
503 io.print <<EOF
504 ~synth = Synth(\\#{sdname}, #{self.decl_args4});
505 */
506
507 *initClass {
508 StartUp.add {
509 SynthDesc.mdPlugin = TextArchiveMDPlugin;
510 // When SynthDef.writeOnce writes metadata:
511 // SynthDef.writeOnce(\\#{sdname},
512 SynthDef(\\#{sdname},
513 { |out=0|
514 Out.ar(out,
515 #{@class_name}.ar(
516 #{self.decl_args3}
517 )
518 )
519 }, metadata: (specs:(
520 //\\controlName:[min, max, warp, step, default, units].asSpec,
521 #{self.decl_metadata}
522 ) ) ).store } }
523 // When SynthDef.writeOnce writes metadata:
524 //) ) ) } }
525 EOF
526 end
527 end
528
529 class Generator < Faust::Generator
530 def lang
531 "sclang"
532 end
533 def generate_plugin(io, plugin)
534 PluginGenerator.new(plugin, options).generate(io)
535 end
536 end
537 end # module SC3
538 end # module Faust
539
540 def usage
541 $stdout.print <<EOF
542 Usage: #{File.basename($0)} [OPTION]... INPUT_FILE...
543
544 Generate a language module file from FAUST generated XML.
545 Currently supported languages are Haskell and SuperCollider.
546
547 Options:
548 -h, --help Display this help
549 -l, --lang Set output language (haskell, sclang)
550 -o, --output Set output file name
551 -p, --prefix Set class or module prefix
552 -s, --synthdef Emit SynthDef including metadata (sclang)
553 -V, --version Display version information
554 EOF
555 end
556
557 opts = GetoptLong.new(
558 [ "--help", "-h", GetoptLong::NO_ARGUMENT ],
559 [ "--lang", "-l", GetoptLong::REQUIRED_ARGUMENT ],
560 [ "--output", "-o", GetoptLong::REQUIRED_ARGUMENT ],
561 [ "--prefix", "-p", GetoptLong::REQUIRED_ARGUMENT ],
562 [ "--synthdef", "-s", GetoptLong::NO_ARGUMENT ],
563 [ "--version", "-V", GetoptLong::NO_ARGUMENT ]
564 )
565
566 LANG_MAP = {
567 "sclang" => Faust::SC3::Generator,
568 "haskell" => Faust::Haskell::Generator
569 }
570
571 lang = "sclang"
572 generator = nil
573 output_file = nil
574
575 options = {
576 "prefix" => "",
577 "synthdef" => false
578 }
579
580 opts.each { | opt, arg |
581 case opt
582 when "--help"
583 usage
584 exit(0)
585 when "--lang"
586 lang = arg
587 when "--output"
588 output_file = arg
589 when "--prefix"
590 options["prefix"] = arg
591 options["module"] = arg
592 when "--synthdef"
593 options["synthdef"] = true
594 when "--version"
595 puts "#{PROGRAM} #{PROGRAM_VERSION}"
596 exit(0)
597 end
598 }
599
600 if LANG_MAP.key?(lang)
601 generator = LANG_MAP[lang]
602 else
603 print_error("unknown output language #{lang}\n")
604 exit(1)
605 end
606
607 if output_file
608 output = File.open(output_file, "w")
609 else
610 output = $stdout
611 end
612
613 plugins = ARGV.collect { |file|
614 begin
615 print_info("Parsing #{file} ...\n")
616 Faust::Plugin.from_file(file)
617 rescue
618 print_error("#{$!}\n")
619 print_error("Omitting #{file}\n")
620 end
621 }
622
623 begin
624 generator.new(plugins, options).generate(output)
625 ensure
626 output.close unless output === $stdout
627 end
628
629 # EOF