X-Git-Url: https://scm.cri.ensmp.fr/git/Faustine.git/blobdiff_plain/1059e1cc0c2ecfa237406949aa26155b6a5b9154..66f23d4fabf89ad09adbd4dfc15ac6b5b2b7da83:/interpreter/preprocessor/faust-0.9.47mr3/tools/faust2sc-1.0.0/faust2sc diff --git a/interpreter/preprocessor/faust-0.9.47mr3/tools/faust2sc-1.0.0/faust2sc b/interpreter/preprocessor/faust-0.9.47mr3/tools/faust2sc-1.0.0/faust2sc new file mode 100755 index 0000000..375f420 --- /dev/null +++ b/interpreter/preprocessor/faust-0.9.47mr3/tools/faust2sc-1.0.0/faust2sc @@ -0,0 +1,629 @@ +#!/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