--- /dev/null
+#!/usr/bin/env ruby
+# ======================================================================
+# faust2sc - Generate language modules from Faust XML.
+# Copyright (C) 2005-2008 Stefan Kersten
+# ======================================================================
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
+# USA
+# ======================================================================
+
+# TODO:
+# rexml is dog slow, maybe use libxml?
+
+require 'getoptlong'
+require 'rexml/document'
+
+PROGRAM = File.basename($0)
+PROGRAM_VERSION = "1.1.0"
+
+class Array
+ def flatten1
+ res = []
+ self.each { |l|
+ res += l
+ }
+ res
+ end
+end
+
+module REXML
+ class Element
+ def to_i
+ self.text.to_i
+ end
+ def to_f
+ self.text.to_f
+ end
+ end
+end
+
+class String
+ def encapitalize
+ self[0..0].upcase + self[1..-1]
+ end
+ def decapitalize(greedy=false)
+ unless greedy
+ self[0..0].downcase + self[1..-1]
+ else
+ res = self.clone
+ (0..res.size-1).each { |i|
+ c = res[i]
+ if 65 <= c && c <= 90
+ res[i] = c + 32
+ else
+ break
+ end
+ }
+ res
+ end
+ end
+end
+
+def print_error(str)
+ $stderr.print("#{PROGRAM}[ERROR] #{str}")
+end
+
+def print_info(str)
+ $stderr.print("#{PROGRAM}[INFO] #{str}")
+end
+
+module Faust
+ class Widget
+ attr_reader :type, :id, :label, :init, :min, :max, :step
+ def initialize(node)
+ @type = node.attributes["type"]
+ @id = node.attributes["id"]
+ if (node.elements["label"].text)
+ @label = node.elements["label"].text
+ else
+ print_info("No label for widget ID #{id} - calling it widget#{id}\n")
+ @label = "widget#{id}"
+ end
+ if (@label =~ /^[0-9]/)
+ plabel = @label
+ @label = @type + "_" + @label
+ print_info("Widget label `#{plabel}' prefixed to become `#{@label}'\n")
+ end
+ dict = node.elements
+ @init = dict["init"].to_f
+ @min = dict["min"].to_f
+ @max = dict["max"].to_f
+ @step = dict["step"].to_f
+ if (@type == "button")
+ @max = 1 # no metadata other than name for buttons
+ @step = 1
+ end
+ end
+ end
+
+ class UI
+ attr_reader :active_widgets, :passive_widgets
+ def initialize(node)
+ @active_widgets = node.get_elements("//activewidgets/widget").collect { |x| Widget.new(x) }
+ @passive_widgets = node.get_elements("//passivewidgets/widget").collect { |x| Widget.new(x) }
+ end
+ end
+
+ class Plugin
+ attr_reader :path, :name, :author, :copyright, :license, :inputs, :outputs, :ui
+ def initialize(path, node)
+ @path = path
+ %w(name author copyright license).each { |name|
+ instance_variable_set("@#{name}", node.elements["/faust/#{name}"].text)
+ }
+ %w(inputs outputs).each { |name|
+ instance_variable_set("@#{name}", node.elements["/faust/#{name}"].text.to_i)
+ }
+ @ui = UI.new(node.elements["/faust/ui"])
+ end
+ def total_inputs
+ inputs + ui.active_widgets.size
+ end
+ def Plugin::from_file(path)
+ self.new(path, REXML::Document.new(File.open(path) { |io| io.read }))
+ end
+ end
+
+ class Generator
+ attr_reader :plugins, :options
+ def initialize(plugins, options)
+ @plugins = plugins
+ @options = options
+ end
+ def lang
+ "unknown"
+ end
+ def generate(io)
+ generate_header(io)
+ generate_body(io)
+ generate_footer(io)
+ end
+ def generate_header(io)
+ end
+ def generate_footer(io)
+ end
+ def generate_body(io)
+ plugins.each_with_index { |plugin,i|
+ if plugin
+ begin
+ print_info("Generating #{lang} code for #{plugin.name} ...\n")
+ generate_plugin(io, plugin)
+ if i < (plugins.size - 1)
+ io.print("\n")
+ end
+ rescue
+ print_error("#{$!}\n")
+ $!.backtrace.each { |l| print_error(l + "\n") }
+ print_error("Omitting #{plugin.path}\n")
+ end
+ end
+ }
+ io.print("\n")
+ end
+ def generate_plugin(io, plugin)
+ raise "#{self.class}::generate_plugin() missing!"
+ end
+ end
+
+ module Language
+ IDENTIFIER_REGEXP = /^[a-z][a-zA-Z0-9_]*[a-zA-Z0-9]?$/
+ def make_identifier(name)
+ # gentle identifier massage
+ # remove quotes
+ name = name.sub(/^"([^"]*)"/, "\\1")
+ # replace invalid chars with underscores
+ name = name.downcase.gsub(/[^a-zA-Z0-9_]/, "_")
+ # reduce multiple underscores to one
+ name = name.gsub(/__+/, "_")
+ # remove leading/terminating underscores
+ name = name.sub(/(^_|_$)/, "")
+ # move leading digits to the end
+ name = name.sub(/^([0-9]+)_/, "") + ("_#{$1}" if $1).to_s
+ # if digit(s) only, prepend alpha prefix
+ if (name[0..0] =~ /^[0-9]/)
+ pname = name
+ name = "w" + name
+ print_info("Widget label `#{pname}' prefixed to become `#{name}'\n")
+ end
+ unless name =~ IDENTIFIER_REGEXP
+ raise "invalid identifier: \"#{name}\""
+ end
+ name
+ end
+ def make_unique(list)
+ # bad, bad, bad
+ list = list.clone
+ res = []
+ ids = {}
+ while hd = list.shift
+ if ids.has_key?(hd)
+ ids[hd] = id = ids[hd] + 1
+ else
+ if list.include?(hd)
+ ids[hd] = id = 0
+ end
+ end
+ res << (id ? "#{hd}_#{id}" : hd)
+ end
+ res
+ end
+ module_function :make_identifier, :make_unique
+ end
+
+ module Haskell
+ INDENT = " " * 4
+
+ def mk_function_names(plugin)
+ fname = plugin.name.decapitalize(true)
+ lname = fname + "'"
+ [lname, fname]
+ end
+ module_function :mk_function_names
+
+ class PluginGenerator
+ attr_reader :plugin
+ def initialize(plugin)
+ @plugin = plugin
+ end
+ def generate(io)
+ lname, fname = Haskell.mk_function_names(plugin)
+ gen_curry_func(io, lname, fname)
+ io << "\n"
+ gen_list_func(io, lname)
+ end
+ def gen_ugen(io, name, rate, inputs, num_outputs)
+ io << "UGen.mkUGen #{rate} \"#{name}\" #{inputs} (replicate #{num_outputs} #{rate}) (UGen.Special 0) Nothing"
+ io << "\n"
+ end
+ def gen_curry_func(io, lname, fname)
+ args = mk_args(plugin.total_inputs)
+ decs = args.collect { "UGen" } + ["UGen"] # add result type
+ io << "#{fname} :: #{decs.join(" -> ")}\n"
+ io << "#{fname} #{args.join(" ")} = #{lname} [#{args.join(',')}]\n"
+ end
+ def gen_list_func(io, fname)
+ io << "#{fname} :: [UGen] -> UGen\n"
+ io << "#{fname} args = "
+ gen_ugen(io, plugin.name, "UGen.AR", "args", plugin.outputs)
+ end
+
+ protected
+ def mk_args(n, x="x")
+ (1..n).collect { |i| "#{x}#{i}" }
+ end
+ end # PluginGenerator
+
+ class Generator < Faust::Generator
+ def initialize(plugins, options)
+ super(plugins, options)
+ @module = options["prefix"]
+ end
+ def lang
+ "haskell"
+ end
+ def generate_header(io)
+ gen_module(io, plugins.collect { |p| Haskell.mk_function_names(p) }.flatten1)
+ io << "\n"
+ gen_imports(io)
+ io << "\n"
+ end
+ def generate_plugin(io, plugin)
+ PluginGenerator.new(plugin).generate(io)
+ end
+ def gen_module(io, exports)
+ #m = @module.empty? ? "" : @module + "."
+ #io << "module #{m}#{plugin.name.encapitalize} (\n"
+ io << "module #{@module.empty? ? "Main" : @module} (\n"
+ io << exports.collect { |x| (INDENT * 1) + x }.join(",\n") << "\n"
+ io << ") where\n"
+ end
+ def gen_imports(io)
+ io << "import Sound.SC3.UGen (UGen)\n"
+ io << "import qualified Sound.SC3.UGen as UGen\n"
+ end
+ end
+ end
+
+ module SC3
+ include Language
+
+ CLASS_REGEXP = /^[A-Z][a-zA-Z0-9_]*[a-zA-Z0-9]?$/
+
+ def path_to_unitname(path)
+ name = File.basename(path)
+ if ext_index = name.index(".")
+ name = name[0..ext_index-1]
+ end
+ name
+ end
+ def make_class_name(unit_name, prefix)
+ # The SuperCollider UGen class name generated here must match
+ # that generated in the Faust architecture file supercollider.cpp
+ if prefix == ""
+ prefix = "Faust"
+ end
+ class_name = unit_name
+ class_name = class_name.sub(/^#{prefix}/i, '')
+ class_name = prefix + '_' + class_name
+ cna = class_name.split(/[^0-9a-z]/i)
+ cna.each_index { |i| cna[i] = cna[i].encapitalize } # SC name convention
+ class_name = cna.join
+ print_info(unit_name + " -> " + class_name + "\n")
+ if class_name.length > 31 # maximum SC Node ID length is 31 (hashed)
+ print_info("Class name `#{class_name}' truncated to 1st 31 chars\n")
+ class_name = class_name[0,30]
+ end
+ unless class_name =~ CLASS_REGEXP
+ raise "invalid class name: \"#{class_name}\""
+ end
+ class_name
+ end
+ module_function :path_to_unitname, :make_class_name
+
+ class Faust::Widget
+ def sc3_identifier
+ l = self.label
+ l = l.gsub(/\([^\)]*\)/, '')
+ l = Language.make_identifier(l)
+ if l.length > 30 # maximum SC ParamSpec length is 31 (hashed)
+ lt = l[0,29] # leave a final char for make_unique
+ print_info("Label `#{l}' for #{self.type} #{self.id} truncated to " +
+ "`#{lt}' (30 chars)\n")
+ l = lt
+ end
+ l
+ end
+ def sc3_arg_string
+ "#{self.sc3_identifier}(#{self.init})"
+ end
+ def sc3_default
+ "(#{self.init})"
+ end
+ def sc3_default2
+ "=(#{self.init})" # Parens make unary minus work in SC
+ end
+ end
+
+ class PluginGenerator
+ attr_reader :unit_name, :class_name
+ def initialize(plugin, options)
+ @plugin = plugin
+ @options = options
+ @unit_name = plugin.name || SC3::path_to_unitname(plugin.path)
+ @class_name = SC3::make_class_name(@unit_name, options["prefix"])
+ end
+ def inputs
+ @plugin.inputs
+ end
+ def outputs
+ @plugin.outputs
+ end
+ def superclass_name
+ @plugin.outputs > 1 ? "MultiOutUGen" : "UGen"
+ end
+ def input_names
+ (1..self.inputs).collect { |i| "in#{i}" }
+ end
+ def control_names
+ Language::make_unique(@plugin.ui.active_widgets.collect { |x| x.sc3_identifier })
+ end
+ def decl_args # "name(default)"
+ cnames = self.control_names
+ cdefaults = @plugin.ui.active_widgets.collect { |x| x.sc3_default }
+ args = self.input_names + cnames.zip(cdefaults).collect { |ary| ary[0] + ary[1] }
+ args.empty? ? "" : " | " + args.join(", ") + " |"
+ end
+ def decl_args2 # "name=default"
+ cnames = self.control_names
+ cdefaults = @plugin.ui.active_widgets.collect { |x| x.sc3_default2 }
+ args = self.input_names + cnames.zip(cdefaults).collect { |ary| ary[0] + ary[1] }
+ args.empty? ? "" : args.join(", ") + ";"
+ end
+ def decl_args3 # args for SynthDef
+ cnames = self.control_names
+ cdefaults = @plugin.ui.active_widgets.collect { |x| x.sc3_default }
+ args = self.input_names.collect { |x| "\\#{x}.ar(0)" } \
+ + cnames.zip(cdefaults).collect { |x| "\\#{x[0]}.kr#{x[1]}" }
+ args.empty? ? "" : args.join(", ")
+ end
+ def decl_buses # declare input buses for Synth
+ args = self.input_names.collect { |x| \
+ " #{x}Bus = Bus.audio(s,1);\n"};
+ end
+ def decl_args4 # args for Synth
+ cnames = self.control_names
+ cdefaults = @plugin.ui.active_widgets.collect { |x| x.sc3_default }
+ args = self.input_names.collect { |x| "#{x}:#{x}Bus.asMap"} \
+ + cnames.collect { |x| "#{x}:#{x}Var" }
+ args.empty? ? "" : "\n\t[\t" + args.join(",\n\t\t") + "\n\t]"
+ end
+ def decl_metadata
+ cnames = self.control_names
+ cnames.collect! {|x| " \\"+x+":"}
+ warp = 0
+ cmeta = @plugin.ui.active_widgets.collect \
+ { |x| "[#{x.min}, #{x.max}, #{warp}, #{x.step}, #{x.init}].asSpec" }
+ # \\name1: [minval, maxval, warp, step, default, units].asSpec,
+ # Note: could append x.units as well
+ args = cnames.zip(cmeta).collect { |ary| ary[0] + ary[1] }
+ args.empty? ? "" : args.join(",\n ")
+ end
+ def new_args(rate)
+ ["'%s'" % rate] + self.input_names + self.control_names
+ end
+ def validate
+ args = self.input_names + self.control_names
+ unless args.uniq == args
+ raise "argument list not unique"
+ end
+ self
+ end
+ def generate(io)
+ self.validate
+ generate_decl(io)
+ generate_body(io)
+ end
+ def generate_decl(io)
+ io.print("#{@class_name} : #{self.superclass_name}\n")
+ print_info("UGen: #{@class_name}\n")
+ end
+ def generate_body(io)
+ io.print("{\n")
+ body = <<EOF
+ *ar {#{self.decl_args}
+ ^this.multiNew(#{self.new_args(:audio).join(", ")})
+ }
+
+ *kr {#{self.decl_args}
+ ^this.multiNew(#{self.new_args(:control).join(", ")})
+ }
+EOF
+ if self.inputs > 0
+ body = body + <<EOF
+
+ checkInputs {
+ if (rate == 'audio', {
+ #{self.inputs}.do({|i|
+ if (inputs.at(i).rate != 'audio', {
+ ^(" input at index " + i + "(" + inputs.at(i) +
+ ") is not audio rate");
+ });
+ });
+ });
+ ^this.checkValidInputs
+ }
+EOF
+ end
+ io.print(body)
+ generate_outputs(io)
+ if @options["synthdef"]
+ generate_synthdef(io)
+ end
+ io.print("\n name { ^\"#{@class_name}\" }\n")
+ io.print("}\n")
+ end
+ def generate_outputs(io)
+ if self.outputs > 1 # add initOutputs to overridden init:
+ io.print <<EOF
+
+ init { | ... theInputs |
+ inputs = theInputs
+ ^this.initOutputs(#{self.outputs}, rate)
+ }
+EOF
+ end
+ end
+ def generate_synthdef(io)
+ s = @class_name
+ sdname = s[0,1].downcase + s[1 .. s.length]
+ print_info("SynthDef: \\#{sdname}\n")
+# if \arg.ar(0) form not used, add after out=0: ,#{self.decl_args2}
+ io.print <<EOF
+
+/*
+ SynthDef - Typical usage in SuperCollider:
+
+EOF
+ print_info("SynthDef: have #{self.input_names.size} inputs\n")
+ if self.input_names.size>0
+ io.print "#{decl_buses}\n";
+ end
+ io.print <<EOF
+ ~synth = Synth(\\#{sdname}, #{self.decl_args4});
+ */
+
+ *initClass {
+ StartUp.add {
+ SynthDesc.mdPlugin = TextArchiveMDPlugin;
+// When SynthDef.writeOnce writes metadata:
+// SynthDef.writeOnce(\\#{sdname},
+ SynthDef(\\#{sdname},
+ { |out=0|
+ Out.ar(out,
+ #{@class_name}.ar(
+ #{self.decl_args3}
+ )
+ )
+ }, metadata: (specs:(
+ //\\controlName:[min, max, warp, step, default, units].asSpec,
+ #{self.decl_metadata}
+ ) ) ).store } }
+// When SynthDef.writeOnce writes metadata:
+//) ) ) } }
+EOF
+ end
+ end
+
+ class Generator < Faust::Generator
+ def lang
+ "sclang"
+ end
+ def generate_plugin(io, plugin)
+ PluginGenerator.new(plugin, options).generate(io)
+ end
+ end
+ end # module SC3
+end # module Faust
+
+def usage
+ $stdout.print <<EOF
+Usage: #{File.basename($0)} [OPTION]... INPUT_FILE...
+
+Generate a language module file from FAUST generated XML.
+Currently supported languages are Haskell and SuperCollider.
+
+Options:
+ -h, --help Display this help
+ -l, --lang Set output language (haskell, sclang)
+ -o, --output Set output file name
+ -p, --prefix Set class or module prefix
+ -s, --synthdef Emit SynthDef including metadata (sclang)
+ -V, --version Display version information
+EOF
+end
+
+opts = GetoptLong.new(
+ [ "--help", "-h", GetoptLong::NO_ARGUMENT ],
+ [ "--lang", "-l", GetoptLong::REQUIRED_ARGUMENT ],
+ [ "--output", "-o", GetoptLong::REQUIRED_ARGUMENT ],
+ [ "--prefix", "-p", GetoptLong::REQUIRED_ARGUMENT ],
+ [ "--synthdef", "-s", GetoptLong::NO_ARGUMENT ],
+ [ "--version", "-V", GetoptLong::NO_ARGUMENT ]
+)
+
+LANG_MAP = {
+ "sclang" => Faust::SC3::Generator,
+ "haskell" => Faust::Haskell::Generator
+}
+
+lang = "sclang"
+generator = nil
+output_file = nil
+
+options = {
+ "prefix" => "",
+ "synthdef" => false
+}
+
+opts.each { | opt, arg |
+ case opt
+ when "--help"
+ usage
+ exit(0)
+ when "--lang"
+ lang = arg
+ when "--output"
+ output_file = arg
+ when "--prefix"
+ options["prefix"] = arg
+ options["module"] = arg
+ when "--synthdef"
+ options["synthdef"] = true
+ when "--version"
+ puts "#{PROGRAM} #{PROGRAM_VERSION}"
+ exit(0)
+ end
+}
+
+if LANG_MAP.key?(lang)
+ generator = LANG_MAP[lang]
+else
+ print_error("unknown output language #{lang}\n")
+ exit(1)
+end
+
+if output_file
+ output = File.open(output_file, "w")
+else
+ output = $stdout
+end
+
+plugins = ARGV.collect { |file|
+ begin
+ print_info("Parsing #{file} ...\n")
+ Faust::Plugin.from_file(file)
+ rescue
+ print_error("#{$!}\n")
+ print_error("Omitting #{file}\n")
+ end
+}
+
+begin
+ generator.new(plugins, options).generate(output)
+ensure
+ output.close unless output === $stdout
+end
+
+# EOF