#!/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 = < 0 body = body + < 1 # add initOutputs to overridden init: io.print <0 io.print "#{decl_buses}\n"; end io.print < 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