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.
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.
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
20 # ======================================================================
23 # rexml is dog slow, maybe use libxml?
26 require 'rexml/document'
28 PROGRAM
= File
.basename($0)
29 PROGRAM_VERSION
= "1.1.0"
54 self[0..0].upcase
+ self[1..-1]
56 def decapitalize(greedy
=false)
58 self[0..0].downcase
+ self[1..-1]
61 (0..res
.size-1
).each
{ |i
|
75 $stderr.print("#{PROGRAM}[ERROR] #{str}")
79 $stderr.print("#{PROGRAM}[INFO] #{str}")
84 attr_reader
:type, :id, :label, :init, :min, :max, :step
86 @type = node
.attributes
["type"]
87 @id = node
.attributes
["id"]
88 if (node
.elements
["label"].text
)
89 @label = node
.elements
["label"].text
91 print_info("No label for widget ID #{id} - calling it widget#{id}\n")
92 @label = "widget#{id}"
94 if (@label =~
/^[0-9]/)
96 @label = @type + "_" + @label
97 print_info("Widget label `#{plabel}' prefixed to become `#{@label}'\n")
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
112 attr_reader
:active_widgets, :passive_widgets
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
) }
120 attr_reader
:path, :name, :author, :copyright, :license, :inputs, :outputs, :ui
121 def initialize(path
, node
)
123 %w(name author copyright license
).each
{ |name
|
124 instance_variable_set("@#{name}", node
.elements
["/faust/#{name}"].text
)
126 %w(inputs outputs
).each
{ |name
|
127 instance_variable_set("@#{name}", node
.elements
["/faust/#{name}"].text
.to_i
)
129 @ui = UI
.new(node
.elements
["/faust/ui"])
132 inputs
+ ui
.active_widgets
.size
134 def Plugin
::from_file(path
)
135 self.new(path
, REXML
::Document.new(File
.open(path
) { |io
| io
.read
}))
140 attr_reader
:plugins, :options
141 def initialize(plugins
, options
)
153 def generate_header(io
)
155 def generate_footer(io
)
157 def generate_body(io
)
158 plugins
.each_with_index
{ |plugin
,i
|
161 print_info("Generating #{lang} code for #{plugin.name} ...\n")
162 generate_plugin(io
, plugin
)
163 if i
< (plugins
.size
- 1)
167 print_error("#{$!}\n")
168 $
!.backtrace
.each
{ |l
| print_error(l
+ "\n") }
169 print_error("Omitting #{plugin.path}\n")
175 def generate_plugin(io
, plugin
)
176 raise "#{self.class}::generate_plugin() missing!"
181 IDENTIFIER_REGEXP
= /^[a-z][a-zA-Z0-9_]*[a-zA-Z0-9]?$/
182 def make_identifier(name
)
183 # gentle identifier massage
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]/)
198 print_info("Widget label `#{pname}' prefixed to become `#{name}'\n")
200 unless name
=~ IDENTIFIER_REGEXP
201 raise "invalid identifier: \"#{name}\""
205 def make_unique(list
)
210 while hd
= list
.shift
212 ids
[hd
] = id
= ids
[hd
] + 1
218 res
<< (id
? "#{hd}_#{id}" : hd
)
222 module_function
:make_identifier, :make_unique
228 def mk_function_names(plugin
)
229 fname
= plugin
.name
.decapitalize(true)
233 module_function
:mk_function_names
235 class PluginGenerator
237 def initialize(plugin
)
241 lname
, fname
= Haskell
.mk_function_names(plugin
)
242 gen_curry_func(io
, lname
, fname
)
244 gen_list_func(io
, lname
)
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"
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"
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
)
263 def mk_args(n
, x
="x")
264 (1..n
).collect
{ |i
| "#{x}#{i}" }
266 end # PluginGenerator
268 class Generator
< Faust
::Generator
269 def initialize(plugins
, options
)
270 super(plugins
, options
)
271 @module = options
["prefix"]
276 def generate_header(io
)
277 gen_module(io
, plugins
.collect
{ |p
| Haskell
.mk_function_names(p
) }.flatten1
)
282 def generate_plugin(io
, plugin
)
283 PluginGenerator
.new(plugin
).generate(io
)
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"
293 io
<< "import Sound.SC3.UGen (UGen)\n"
294 io
<< "import qualified Sound.SC3.UGen as UGen\n"
302 CLASS_REGEXP
= /^[A-Z][a-zA-Z0-9_]*[a-zA-Z0-9]?$/
304 def path_to_unitname(path
)
305 name
= File
.basename(path
)
306 if ext_index
= name
.index(".")
307 name
= name
[0..ext_index-1
]
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
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]
328 unless class_name
=~ CLASS_REGEXP
329 raise "invalid class name: \"#{class_name}\""
333 module_function
:path_to_unitname, :make_class_name
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")
349 "#{self.sc3_identifier}(#{self.init})"
355 "=(#{self.init})" # Parens make unary minus work in SC
359 class PluginGenerator
360 attr_reader
:unit_name, :class_name
361 def initialize(plugin
, options
)
364 @unit_name = plugin
.name
|| SC3
::path_to_unitname(plugin
.path
)
365 @class_name = SC3
::make_class_name(@unit_name, options
["prefix"])
374 @plugin.outputs
> 1 ? "MultiOutUGen" : "UGen"
377 (1..self.inputs
).collect
{ |i
| "in#{i}" }
380 Language
::make_unique(@plugin.ui
.active_widgets
.collect
{ |x
| x
.sc3_identifier
})
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(", ") + " |"
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(", ") + ";"
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(", ")
401 def decl_buses
# declare input buses for Synth
402 args
= self.input_names
.collect
{ |x
| \
403 " #{x}Bus = Bus.audio(s,1);\n"};
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]"
413 cnames
= self.control_names
414 cnames
.collect
! {|x
| " \\"+x
+":"}
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 ")
424 ["'%s'" % rate
] + self.input_names
+ self.control_names
427 args
= self.input_names
+ self.control_names
428 unless args
.uniq
== args
429 raise "argument list not unique"
438 def generate_decl(io
)
439 io
.print("#{@class_name} : #{self.superclass_name}\n")
440 print_info("UGen: #{@class_name}\n")
442 def generate_body(io
)
445 *ar {#{self.decl_args}
446 ^this.multiNew(#{self.new_args(:audio).join(", ")})
449 *kr {#{self.decl_args}
450 ^this.multiNew(#{self.new_args(:control).join(", ")})
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");
465 ^this.checkValidInputs
471 if @options["synthdef"]
472 generate_synthdef(io
)
474 io
.print("\n name { ^\"#{@class_name}\" }\n")
477 def generate_outputs(io
)
478 if self.outputs
> 1 # add initOutputs to overridden init:
481 init { | ... theInputs |
483 ^this.initOutputs(#{self.outputs}, rate)
488 def generate_synthdef(io
)
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}
496 SynthDef - Typical usage in SuperCollider:
499 print_info("SynthDef: have #{self.input_names.size} inputs\n")
500 if self.input_names
.size
>0
501 io
.print
"#{decl_buses}\n";
504 ~synth = Synth(\\#{sdname}, #{self.decl_args4});
509 SynthDesc.mdPlugin = TextArchiveMDPlugin;
510 // When SynthDef.writeOnce writes metadata:
511 // SynthDef.writeOnce(\\#{sdname},
512 SynthDef(\\#{sdname},
519 }, metadata: (specs:(
520 //\\controlName:[min, max, warp, step, default, units].asSpec,
521 #{self.decl_metadata}
523 // When SynthDef.writeOnce writes metadata:
529 class Generator
< Faust
::Generator
533 def generate_plugin(io
, plugin
)
534 PluginGenerator
.new(plugin
, options
).generate(io
)
542 Usage: #{File.basename($0)} [OPTION]... INPUT_FILE...
544 Generate a language module file from FAUST generated XML.
545 Currently supported languages are Haskell and SuperCollider.
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
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 ]
567 "sclang" => Faust
::SC3::Generator,
568 "haskell" => Faust
::Haskell::Generator
580 opts
.each
{ | opt
, arg
|
590 options
["prefix"] = arg
591 options
["module"] = arg
593 options
["synthdef"] = true
595 puts
"#{PROGRAM} #{PROGRAM_VERSION}"
600 if LANG_MAP
.key
?(lang
)
601 generator
= LANG_MAP
[lang
]
603 print_error("unknown output language #{lang}\n")
608 output
= File
.open(output_file
, "w")
613 plugins
= ARGV.collect
{ |file
|
615 print_info("Parsing #{file} ...\n")
616 Faust
::Plugin.from_file(file
)
618 print_error("#{$!}\n")
619 print_error("Omitting #{file}\n")
624 generator
.new(plugins
, options
).generate(output
)
626 output
.close
unless output
=== $stdout