/* * SynthDef.java * JCollider * * Copyright (c) 2004-2010 Hanns Holger Rutz. All rights reserved. * * This software 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, june 1991 of the License, or (at your option) any later version. * * This software 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 (gpl.txt) along with this software; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * * For further information, please contact Hanns Holger Rutz at * contact@sciss.de , or visit http://www.sciss.de/jcollider * * * JCollider is closely modelled after SuperCollider Language, * often exhibiting a direct translation from Smalltalk to Java. * SCLang is a software originally developed by James McCartney, * which has become an Open Source project. * See http://www.audiosynth.com/ for details. * * * Changelog: * 29-Jun-05 created * 15-Oct-05 fixed a bug with non-recognized controls (LagControl, TrigControl) * 24-Feb-05 fixed bug with missing control names as produced by Control.names([ \gaga ]).kr([ 1, 2 ]) * for example (first control output is named \gaga, second is named "?" now like sclang does) * 29-Jul-06 added load() and play() */ package de.sciss.jcollider; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.io.IOException; import java.io.OutputStream; import java.io.PrintStream; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import de.sciss.net.OSCMessage; /** * This is the representation of a UGen graph, a prototype * for a synth node. While it was created to mimic most of the * behaviour of the SClang counterpart, a lot of the internals * are slightly different, including the whole idea of how * a UGen graph is represented. * <p> * While in SClang as an interpreter * language the graph is represented as a function, this is * not appropriate for java as a (semi)compiled language. Therefore, * you do not pass a graph function to the constructor, but rather * a collection of graph elements (UGens and Constants) which have * already been put together. * <p> * This also implies that there is no * function header which can be read by <code>SynthDef</code> to * automatically construct <code>Control</code> UGens from the * function's arguments. You therefore have to create <code>Control</code> * UGens explicitly yourself. <code>SynthDef</code> will find them * and construct the synth def binary object accordingly. * <p> * Note that this class includes the functionality found separately * in SClang's SynthDesc class, that is methods for reading and formatting * a synth def. Unlike SClang, when a synth def is read, UGens are * created as instances of the <code>UGen</code> class. In an earlier * version, <code>java.lang.reflect</code> was used to dynamically load * UGen subclasses. This concept was dropped because it would imply * <UL> * <LI>that the JCollider java source is modified whenever new UGens * are introduced to supercollider</LI> * <LI>a tree of some hundred classes would have to be created with all * the memory requirements and time consumption when the VM loads * the JCollider package</LI> * <LI>having to deal with all the smalltalk idiosyncratic stuff * in the UGen representations which often are different from * the actual UGens objects as known by the server</LI> * </UL> * <P> * So instead, there is one clumsy <code>UGen</code> class which * carries all the information about inlets and outlets. A synth def * file is sufficient to recreate the graph tree using this class. * On the other side, when you yourself create a UGen tree, another * objects comes in, the <code>UGenInfo</code> which acts as a lookup * table for installed UGen clases. * <p> * This class is somewhat more simple than the SClang counterpart. * For example, input rate consistency is not checked. * Tree optimization is still inferior because of the non-existing * UGen subclasses (like <code>BinaryOpUGen</code>) that could handle * context-sensitive optimization. Control-lags and Trigger-controls * are not supported. For the sake of cleanness, all the strange * interpenetration of SynthDef and UGen in the building process as * exhibited by SClang was dropped, where the def would go and write * things into the UGen and vice versa, setting up temporary fields * like the building-def and so on. So this implementation is more * stripped down but way cleaner and less spaghetti. * <p> * There seemed to be a non-finished project in SClang's SynthDef * called "variants". i don't know what this was, it * has was just been dropped. * <p> * Here is an example of building a SynthDef (comments are below): * <pre> * * GraphElem f = null; * GraphElem g, h; * Control c = Control.kr( new String[] { "resinv" }, new float[] { 0.5f }); * UGenChannel reso = c.getChannel( 0 ); * Synth synth; * Random r = new Random( System.currentTimeMillis() ); * String defName = "JNoiseBusiness1b"; * OSCBundle bndl; * SynthDef def; * long time; * * f = null; * for( int i = 0; i < 4; i++ ) { * g = UGen.ar( "*", UGen.ar( "LFSaw", UGen.kr( "midicps", UGen.kr( "MulAdd", * UGen.kr( "LFPulse", UGen.ir( 0.06f ), UGen.ir( 0 ), UGen.ir( 0.5f )), * UGen.ir( 2 ), UGen.array( UGen.ir( 34 + r.nextFloat() * 0.2f ), * UGen.ir( 34 + r.nextFloat() * 0.2f ))))), * UGen.ir( 0.01f )); * f = (f == null) ? g : UGen.ar( "+", f, g ); * } * h = UGen.kr( "LinExp", UGen.kr( "SinOsc", UGen.ir( 0.07f )), * UGen.ir( -1 ), UGen.ir( 1 ), UGen.ir( 300 ), UGen.ir( 5000 )); * f = UGen.ar( "softclip", UGen.ar( "RLPF", f, h, reso )); * f = UGen.ar( "softclip", UGen.ar( "RLPF", f, h, reso )); * def = new SynthDef( defName, UGen.ar( "Out", UGen.ir( 0 ), f )); * * synth = Synth.basicNew( defName, myServer ); * try { * def.send( myServer, synth.newMsg( myServer.asTarget(), * new String[] { "resinv" }, new float[] { 0.98f })); * time = System.currentTimeMillis(); * for( int i = 500; i < 5000; i += 250 ) { * bndl = new OSCBundle( time + i ); * bndl.addPacket( synth.setMsg( "resinv", r.nextFloat() * 0.8f + 0.015f )); * myServer.sendBundle( bndl ); * } * bndl = new OSCBundle( time + 5500 ); * bndl.addPacket( synth.freeMsg() ); * myServer.sendBundle( bndl ); * } * catch( IOException e1 ) { * System.err.println( e1 ); * } * </pre> * * Yes, it's true, the code is at least three times as big as * would be the SClang counter part, but we're definitely focussing * on an application different from jit developing synthesizers. * <p> * So some remarks on the example (the sound isn't particularly * interesting though ;-) : generally it improves readability if * me create piece of the graph in more than one line. While the * loop body is difficult to read, the statement that adds clipping * and resonance is easy. Since we have only one dead end for the * graph (Out.ar), we simply pass the result of <code>UGen.ar( "Out" ... )</code> * to the synth def constructor. to overwrite the resonance control default * value of 0.5 (as specified in the <code>Control</code> constructor), * we add control name and value parameters to the <code>synth.newMsg</code> * call. the result of this call is an <code>OSCMessage</code> (whereas * <code>new Synth( ... )</code> would have sent that message immediately), * this is passed to the synth def constructor as the completion message. * the rest shows you how to send bundles to set the resonance value * of the synth at certain times. * <p> * You will probably want to use SClang to prototype the synthesizers * and then just port them to JCollider which shouldn't be too * difficult after some practising. See the JColliderDemo for * more examples of UGen graphs. * * @todo for the same synth def, the graphs produced by SClang * and JCollider can look slightly different regarding the * ordering of the topology. this should be reviewed more * thoroughly. It doesn't seem that JCollider is less * efficient (from the CPU loads point of view), but it * makes comparison and debugging a bit tricky * * @author Hanns Holger Rutz * @version 0.32, 25-Feb-08 */ public class SynthDef implements Constants { /** * Default file suffix when writing defs to disk. */ public static final String SUFFIX = ".scsyndef"; /** * Currently supported synth def file * version (1). */ public static final int SCGF_VERSION = 1; private static final int SCGF_MAGIC = 0x53436766; // 'SCgf' private List controlDescs = new ArrayList(); private List ugens = new ArrayList(); private Set ugenSet = new HashSet(); private List constants = new ArrayList(); private Set constantSet = new HashSet(); private final String name; private List variants = new ArrayList(); private static final Object[] RATES = { kScalarRate, kControlRate, kAudioRate, kDemandRate }; private static final Comparator synthIdxComp = new SynthIndexComparator(); private static final Set ctrlUGensSet = new HashSet(); static { ctrlUGensSet.add( "Control" ); ctrlUGensSet.add( "TrigControl" ); ctrlUGensSet.add( "LagControl" ); } private SynthDef( String name ) { this.name = name; } /** * Constructs a new SynthDef from the given * graph element. * * @param name the name of the synth def * as would be used to instantiate a <code>Synth</code> * @param graph a graph element such as a <code>UGen</code> or * a collection of ugens. Basically anything that * comes out of one of the static contructor methods * of the <code>UGen</code> class. Note that when there * are several "dead ends" in the graph, those * dead ends should be collected in a <code>GraphElemArray</code> * which is then passed to <code>SynthDef</code>, otherwise the * synthdef may be incomplete. See the <code>JColliderDemo</code> * to see how to do it. */ public SynthDef( String name, GraphElem graph ) { this( name ); build( GraphElemArray.asArray( graph )); } private void build( GraphElemArray graphArray ) { // save/restore controls in case of *wrap // var saveControlNames = controlNames; // prependArgs = prependArgs.asArray; // this.addControlsFromArgsOfFunc( func, rates, prependArgs.size ); // result = func.valueArray( prependArgs ++ this.buildControls ); // controlNames = saveControlNames // collectUGens) collectUGens( graphArray ); // optimizeGraph(); // collects only those in used UGens, // therefore we do not pass graphArray as an argument collectConstants(); // XXX should do this conditionally // (using a static boolean) // checkInputs(); // re-sort graph. reindex. topologicalSort(); // indexUGens(); } // private void addUGenInput( UGenInput ui ) // { // if( ui instanceof UGenChannel ) { // addUGen( ((UGenChannel) ui).getUGen() ); // } else if( ui instanceof Constant ) { // addConstant( (Constant) ui ); // } else { // assert false : ui.getClass().getName(); // } // } private void addControlDesc( ControlDesc desc ) { //System.err.print( "me add dem desc " ); //desc.printOn( System.err ); controlDescs.add( desc ); } // includes check for Controls ! private void addUGen( UGen ugen ) { if( ugenSet.add( ugen )) { // ugen.initSetSynthIndex( ugens.size() ); ugens.add( ugen ); if( ugen instanceof Control ) { final Control ctrl = (Control) ugen; ctrl.setSpecialIndex( controlDescs.size() ); for( int i = 0; i < ctrl.getNumDescs(); i++ ) { addControlDesc( ctrl.getDesc( i )); } } } } private void addConstant( Constant value ) { if( constantSet.add( value )) { constants.add( value ); } } private void collectUGens( GraphElemArray graphArray ) { UGen ugen; GraphElem g; for( int i = 0; i < graphArray.getNumElements(); i++ ) { g = graphArray.getElement( i ); if( g instanceof UGen ) { ugen = (UGen) g; addUGen( ugen ); collectUGens( ugen.getInputs() ); } else if( g instanceof GraphElemArray ) { collectUGens( (GraphElemArray) g ); // recurse } else { collectUGens( g.asUGenInputs() ); // e.g. UGenChannel } } } private void collectUGens( UGenInput[] ins ) { UGen ugen; for( int i = 0; i < ins.length; i++ ) { if( ins[ i ] instanceof UGenChannel ) { ugen = ((UGenChannel) ins[ i ]).getUGen(); addUGen( ugen ); collectUGens( ugen.getInputs() ); } } } private void collectConstants() { UGen ugen; UGenInput ui; for( int i = 0; i < ugens.size(); i++ ) { ugen = (UGen) ugens.get( i ); for( int j = 0; j < ugen.getNumInputs(); j++ ) { ui = ugen.getInput( j ); if( ui instanceof Constant ) { addConstant( (Constant) ui ); } } } } private void topologicalSort() { // initializes the inlet/outlet lists // and collects the ugens that do not // rely on other ugens final List available = initTopoSort(); // now all ugens are collected as ugen-environments // and will be re-added according to the tree structure ugens.clear(); UGenEnv env, env2; while( !available.isEmpty() ) { env = (UGenEnv) available.remove( available.size() - 1 ); for( int i = env.collDe.size() - 1; i >= 0; i-- ) { env2 = (UGenEnv) env.collDe.get( i ); env2.collAnte.remove( env ); if( env2.collAnte.isEmpty() ) available.add( env2 ); // treated in next loop } ugens.add( env.ugen ); } // cleanupTopoSort(); } private List initTopoSort() { final int numUGens = ugens.size(); final List available = new ArrayList(); final Map mapEnv = new HashMap(); final UGenEnv[] envs = new UGenEnv[ numUGens ]; UGen ugen; UGenEnv env, env2; UGenInput ui; for( int i = 0; i < numUGens; i++ ) { ugen = (UGen) ugens.get( i ); env = new UGenEnv( ugen, i ); mapEnv.put( ugen, env ); envs[ i ] = env; } for( int i = 0; i < numUGens; i++ ) { env = envs[ i ]; ugen = env.ugen; // ugen.initTopoSort(); // this populates the descendants and antecedents for( int j = 0; j < ugen.getNumInputs(); j++ ) { ui = ugen.getInput( j ); if( ui instanceof UGenChannel ) { env2 = (UGenEnv) mapEnv.get( ((UGenChannel) ui).getUGen() ); env.collAnte.add( env2 ); env2.collDe.add( env ); } } } for( int i = numUGens - 1; i >= 0; i-- ) { env = envs[ i ]; // ugen.descendants = ugen.descendants.asArray.sort( // { arg a, b; a.synthIndex < b.synthIndex } Collections.sort( env.collDe, synthIdxComp ); // ugen.makeAvailable(); // all ugens with no antecedents are made available if( env.collAnte.isEmpty() ) { available.add( env ); } } return available; } private byte[] asBytes() throws IOException { final ByteArrayOutputStream baos = new ByteArrayOutputStream(); final DataOutputStream dos = new DataOutputStream( baos ); // don't ask me where these lines // are in sclang dos.writeInt( SCGF_MAGIC ); dos.writeInt( SCGF_VERSION ); dos.writeShort( 1 ); // number of defs in file. write( dos ); dos.flush(); dos.close(); return baos.toByteArray(); } /** * Sends the definition to * a server. * * @param server to representation of the server * to send the def to * * @throws IOException if a network error occured */ public void send( Server server ) throws IOException { server.sendMsg( recvMsg() ); } /** * Sends the definition to * a server. The server will * execute the optional completion message * when it has processed the definition. * * @param server to representation of the server * to send the def to * @param completionMsg message to execute by the server * when the synth def has become available. * typically something like <code>Synth.newMsg( ... )</code>. * may be <code>null</code> * * @throws IOException if a network error occured */ public void send( Server server, OSCMessage completionMsg ) throws IOException { server.sendMsg( recvMsg( completionMsg )); } /** * Constructs a message to sends to * a server for providing the synth def. * * @return message ready to send to a server * * @throws IOException when synth def compilation * fails (? this should never happen?) */ public OSCMessage recvMsg() throws IOException { return recvMsg( null ); } /** * Constructs a message to sends to * a server for providing the synth def. * The optional completion message is * attached to the returned message and * will be executed by the server, when * the definition has become available. * * @param completionMsg completion message, such as <code>/s_new</code> * or <code>null</code> * * @return message ready to send to a server * * @throws IOException when synth def compilation * fails (? this should never happen?) */ public OSCMessage recvMsg( OSCMessage completionMsg ) throws IOException { final Object[] args; if( completionMsg == null ) { args = new Object[] { this.asBytes() }; } else { args = new Object[] { this.asBytes(), completionMsg }; } return new OSCMessage( "/d_recv", args ); } /** * Returns the name of the synth definition */ public String getName() { return name; } /** * Stores the def in a temp file and sends a * corresponding OSC <code>/d_load</code> message to the server. * * @param s the server to send the def to * * @throws IOException if the file could not be created or the message could not be sent */ public void load( Server s ) throws IOException { load( s, null ); } /** * Stores the def in a temp file and sends a * corresponding OSC <code>/d_load</code> message to the server. * * @param s the server to send the def to * @param completionMsg an OSC message to be executed when the def was received (can be <code>null</code>) * * @throws IOException if the file could not be created or the message could not be sent */ public void load( Server s, OSCMessage completionMsg ) throws IOException { final File f = File.createTempFile( "tmp", SUFFIX ); f.deleteOnExit(); load( s, completionMsg, f ); } /** * Stores the def in a file and sends a * corresponding OSC <code>/d_load</code> message to the server. * * @param s the server to send the def to * @param completionMsg an OSC message to be executed when the def was received (can be <code>null</code>) * @param path path to a file. if a file by this name * already exists, the caller should delete it * before calling this method * * @throws IOException if the file could not be created or the message could not be sent * * @warning unlike in SClang, the path denotes the file not the * parent folder of the file */ public void load( Server s, OSCMessage completionMsg, File path ) throws IOException { final Object[] args; writeDefFile( path ); if( completionMsg == null ) { args = new Object[] { path }; } else { args = new Object[] { path, completionMsg }; } s.sendMsg( new OSCMessage( "/d_load", args )); } /** * Sends the def to the server and creates a synth from this def. * * @param target the group to whose head the node is added * @return the newly created synth * * @throws IOException if a network error occurs */ public Synth play( Group target ) throws IOException { return play( target, null, null ); } /** * Sends the def to the server and creates a synth from this def. * * @param target the group to whose head the node is added * @param argNames the names of the controls to set. can be <code>null</code> * @param argValues the values of the controls. each array element corresponds to * the element in <code>argNames</code> with the same index. the sizes of <code>argValues</code> * and <code>argNames</code> must be equal. can be <code>null</code> * @return the newly created synth * * @throws IOException if a network error occurs */ public Synth play( Group target, String[] argNames, float[] argValues ) throws IOException { return play( target, argNames, argValues, kAddToHead ); } /** * Sends the def to the server and creates a synth from this def. * * @param target the node to which the new synth is added * @param argNames the names of the controls to set. can be <code>null</code> * @param argValues the values of the controls. each array element corresponds to * the element in <code>argNames</code> with the same index. the sizes of <code>argValues</code> * and <code>argNames</code> must be equal. can be <code>null</code> * @param addAction the add action re <code>target</code> * @return the newly created synth * * @throws IOException if a network error occurs */ public Synth play( Node target, String[] argNames, float[] argValues, int addAction ) throws IOException { final Synth synth; final OSCMessage msg; synth = Synth.basicNew( getName(), target.getServer() ); msg = synth.newMsg( target, argNames, argValues, addAction ); send( target.getServer(), msg ); return synth; } /** * Prints a textual representation * of the synth def to the given stream. * This will print a list of all ugens * and their wiring. Useful for debugging. * * @param out the stream to print on, such as <code>System.out</code> * * @todo resolve alias names for specialIndex * synths such as BinaryOpUGen * (would require the use of <code>UGenInfo</code> * which in turn requires to read in all * UGen definitions, some overhead we may * not want ... ?) * * @see System#out */ public void printOn( PrintStream out ) { UGen ugen; UGenInput[] inputs; UGenChannel uch; out.println( "SynthDef(\"" + getName() + "\")" ); if( ugens.size() > 0 ) out.println( "\n ugens:" ); for( int i = 0; i < ugens.size(); i++ ) { out.print( " #" + i + " : " ); ugen = (UGen) ugens.get( i ); out.print( ugen.dumpName() + " @ " + ugen.getRate() ); if( ugen.getNumOutputs() != 1 ) out.print( ", numOuts: " + ugen.getNumOutputs() ); inputs = ugen.getInputs(); if( inputs.length > 0 ) { out.print( ", arg: [ " ); for( int j = 0; j < inputs.length; j++ ) { if( inputs[ j ] instanceof UGenChannel ) { uch = (UGenChannel) inputs[ j ]; out.print( "#" + ugens.indexOf( uch.getUGen() ) + '_' ); if( uch.getUGen().getName().equals( "Control" )) { out.print( "Control(\"" + ((ControlDesc) controlDescs.get( uch.getUGen().getSpecialIndex() + uch.getChannel() )).getName() + "\")" ); } else { out.print( uch.dumpName() ); } } else { out.print( inputs[ j ].dumpName() ); } if( j < inputs.length - 1 ) out.print( ", " ); } out.print( " ]" ); } out.println(); } if( controlDescs.size() > 0 ) out.println( "\n controls:" ); for( int i = 0; i < controlDescs.size(); i++ ) { out.print( " #" + i + " : " ); ((ControlDesc) controlDescs.get( i )).printOn( out ); } } /** * Return a list of all UGens in the graph * (in the depth-first sorted topological order). * * @return list whose elements are of class <code>UGen</code> */ public List getUGens() { return new ArrayList( ugens ); } /** * Checks to see if a given file is a * synth definition file. * * @param path to the synth def file * * @return <code>true</code> if the file starts with * the synth definition magic cookie. does not * check for the synth def file version * * @throws IOException if the file could not be read */ public static boolean isDefFile( File path ) throws IOException { final DataInputStream dis = new DataInputStream( new FileInputStream( path )); final boolean result = (dis.available() >= 10) && (dis.readInt() == SCGF_MAGIC); dis.close(); return result; } /** * Writes an array of definitions to a file. * * @param path path to a file. if a file by this name * already exists, the caller should delete it * before calling this method * @param defs array of definitions which will be written * one after another * * @throws IOException if the file cannot be opened, denotes a * directory, or if a write error occurs * * @warning unlike in SClang, the path denotes the file not the * parent folder of the file */ public static void writeDefFile( File path, SynthDef[] defs ) throws IOException { final OutputStream os = new FileOutputStream( path ); final DataOutputStream dos = new DataOutputStream( new BufferedOutputStream( os )); try { dos.writeInt( SCGF_MAGIC ); dos.writeInt( SCGF_VERSION ); dos.writeShort( defs.length ); // number of defs in file. for( int i = 0; i < defs.length; i++ ) { defs[ i ].write( dos ); } } finally { dos.close(); } } /** * Writes this def to a definition file. That it, * the resulting file will contain just one definition, that is us. * * @param path path to a file. if a file by this name * already exists, the caller should delete it * before calling this method * * @throws IOException if the file cannot be opened, denotes a * directory, or if a write error occurs * * @warning unlike in SClang, the path denotes the file not the * parent folder of the file */ public void writeDefFile( File path ) throws IOException { SynthDef.writeDefFile( path, new SynthDef[] { this }); } /** * Writes this def to an output stream (such as a file or * a memory buffer). * * @param os stream to write to. the stream will be * buffered by this method, so you do not need * to do this * * @throws IOException if a write error occurs */ public void write( OutputStream os ) throws IOException { final DataOutputStream dos = new DataOutputStream( new BufferedOutputStream( os )); write( dos ); dos.flush(); } private void write( DataOutputStream dos ) throws IOException { ControlDesc desc; SynthDef.writePascalString( dos, name ); writeConstants( dos ); dos.writeShort( controlDescs.size() ); for( int i = 0; i < controlDescs.size(); i++ ) { desc = (ControlDesc) controlDescs.get( i ); dos.writeFloat( desc.getDefaultValue() ); } dos.writeShort( controlDescs.size() ); for( int i = 0; i < controlDescs.size(); i++ ) { desc = (ControlDesc) controlDescs.get( i ); if( desc.getName() != null ) { SynthDef.writePascalString( dos, desc.getName() ); // dos.writeShort( desc.getIndex() ); dos.writeShort( i ); } else { System.err.println( "Warning: unnamed control " + i + " dropped." ); } } dos.writeShort( ugens.size() ); for( int i = 0; i < ugens.size(); i++ ) { writeUGenSpec( dos, (UGen) ugens.get( i )); } dos.writeShort( variants.size() ); if( !variants.isEmpty() ) { throw new IllegalStateException( "Variants : not supported!!" ); } } private void writeConstants( DataOutputStream dos ) throws IOException { dos.writeShort( constants.size() ); for( int i = 0; i < constants.size(); i++ ) { dos.writeFloat( ((Constant) constants.get( i )).getValue() ); } } private int getRateID( Object rate ) { for( int i = 0; i < RATES.length; i++ ) { if( rate.equals( RATES[ i ])) return i; } return -1; } private void writeUGenSpec( DataOutputStream dos, UGen ugen ) throws IOException { final UGenInput[] inputs = ugen.getInputs(); final Object[] outputRates = ugen.getOutputRates(); writePascalString( dos, ugen.getName() ); dos.writeByte( getRateID( ugen.getRate() )); dos.writeShort( ugen.getNumInputs() ); dos.writeShort( ugen.getNumOutputs() ); dos.writeShort( ugen.getSpecialIndex() ); for( int i = 0; i < inputs.length; i++ ) { writeInputSpec( dos, inputs[ i ]); } for( int i = 0; i < outputRates.length; i++ ) { dos.writeByte( getRateID( outputRates[ i ])); } } private void writeInputSpec( DataOutputStream dos, UGenInput inp ) throws IOException { if( inp instanceof UGenChannel ) { final UGenChannel uch = (UGenChannel) inp; final int synthIndex = ugens.indexOf( uch.getUGen() ); if( synthIndex == -1 ) throw new IOException( "UGen not listed in graph function : " + inp.dumpName() ); dos.writeShort( synthIndex ); dos.writeShort( uch.getChannel() ); } else if( inp instanceof Constant ) { final int constIndex = constants.indexOf( inp ); if( constIndex == -1 ) throw new IOException( "Constant not listed in synth def : " + inp.dumpName() ); dos.writeShort( -1 ); dos.writeShort( constIndex ); } else { throw new IOException( "Illegal UGen input class " + inp.getClass().getName() ); } } private static void writePascalString( DataOutputStream dos, String str ) throws IOException { dos.writeByte( str.length() ); dos.write( str.getBytes() ); } /** * Reads definitions from a synth def file. * * @param path the location of the synth def file * such as a local harddisk or remote server file * @return an array of all definitions found in the file * * @throws IOException if a read error occurs, if the * file has not a valid synth def * format or if the synth def file * version is unsupported (greater than <code>SCFG_VERSION</code>) */ public static SynthDef[] readDefFile( URL path ) throws IOException { final InputStream is = path.openStream(); try { return SynthDef.readDefFile( is ); } finally { is.close(); } } /** * Reads definitions from a synth def file. * * @param path the location of the synth def file * @return an array of all definitions found in the file * * @throws IOException if a read error occurs, if the * file has not a valid synth def * format or if the synth def file * version is unsupported (greater than <code>SCFG_VERSION</code>) */ public static SynthDef[] readDefFile( File path ) throws IOException { final InputStream is = new FileInputStream( path ); try { return SynthDef.readDefFile( is ); } finally { is.close(); } } /** * Reads definitions from an input stream * (such as a harddisk file or memory buffer). * * @param is the stream to read from * @return an array of all definitions found in the stream * * @throws IOException if a read error occurs, if the * stream has not a valid synth def * format or if the synth def file * version is unsupported (greater than <code>SCFG_VERSION</code>) */ public static SynthDef[] readDefFile( InputStream is ) throws IOException { final DataInputStream dis = new DataInputStream( new BufferedInputStream( is )); final int version; final int numDefs; final SynthDef[] defs; if( dis.readInt() != SCGF_MAGIC ) throw new IOException( "Not a SynthDef SCgf file" ); version = dis.readInt(); if( version > 1 ) throw new IOException( "Unknown SynthDef file format version : " + version ); numDefs = dis.readShort(); defs = new SynthDef[ numDefs ]; for( int i = 0; i < numDefs; i++ ) { defs[ i ] = read( dis ); } return defs; } /** * Reads a single <code>SynthDef</code> from an input stream * (such as a harddisk file or memory buffer). Please refer * to the SuperCollider document <code>Synth-Definition-File-Format.rtf</code> * to read how a synth def is constructed * (read the paragraph "a synth-definition is :"). * This assumes synth def file format version 1 as used * by SuperCollider as of september 2005. * * @param is the stream to read from with the current read * position placed at the start of a new synth def * @return the decoded synth def * * @throws IOException if a read error occurs */ public static SynthDef read( InputStream is ) throws IOException { return read( new DataInputStream( new BufferedInputStream( is ))); } private static SynthDef read( DataInputStream dis ) throws IOException { final SynthDef def = new SynthDef( readPascalString( dis )); final int numConstants; final int numParams; final int numParamNames; final int numUGens; final Constant[] constants; final ControlDesc[] controlDescs; final String[] paramName; final float[] controlDefaults; UGen ugen; String str; // UGen.buildSynthDef = def; numConstants = dis.readShort(); constants = new Constant[ numConstants ]; // inputs.clear(); // outputs.clear(); for( int i = 0; i < numConstants; i++ ) { constants[ i ] = new Constant( dis.readFloat() ); } numParams = dis.readShort(); controlDefaults = new float[ numParams ]; controlDescs = new ControlDesc[ numParams ]; for( int i = 0; i < numParams; i++ ) { // def.controls[ i ] = dis.readFloat(); controlDefaults[ i ] = dis.readFloat(); // this.controlDescs[ i ] = new ControlDesc( null, i, UGen.UNKNOWN_RATE, def.controls[ i ]); // XXX } numParamNames = dis.readShort(); paramName = new String[ numParamNames ]; for( int i = 0; i < numParamNames; i++ ) { str = readPascalString( dis ); paramName[ dis.readShort() ] = str; // this.controlDescs[ dis.readShort() ].name = str; //System.err.println( "name[ "+x+" ] == "+str ); } numUGens = dis.readShort(); for( int i = 0; i < numUGens; i++ ) { ugen = readUGenSpec( dis, def, paramName, constants ); // NOTE : the ugen is always of class UGen, even // if it's a control. therefore, we have to register // the controlDescs manually, while a user instantiated // Control will behave differently! def.addUGen( ugen ); // ugen.addToSynth(); // ugen.initFinished(); // if( ugen instanceof ControlUGen ) { // if( ugen.getName().equals( "Control" )) { if( ctrlUGensSet.contains( ugen.getName() )) { //System.err.println( "special index "+ugen.getSpecialIndex()+"; numoutputs "+ugen.getNumOutputs()+"; controlDescs.length"+controlDescs.length+"; paramName.length "+paramName.length+"; controlDefaults.length "+controlDefaults.length ); for( int k = 0, j = ugen.getSpecialIndex(); k < ugen.getNumOutputs(); k++, j++ ) { // controlDescs[ j ] = new ControlDesc( paramName[ j ], j, ugen.getRate(), def.controls[ j ]); controlDescs[ j ] = new ControlDesc( j < paramName.length ? paramName[ j ] : "?", ugen.getRate(), controlDefaults[ j ]); } } } for( int i = 0; i < controlDescs.length; i++ ) { // this is a bug in sclang // if( controlDescs[ i ].getName() != null ) def.controlDescs.add( controlDescs[ i ]); if( controlDescs[ i ] != null ) { if( controlDescs[ i ].getName() != null ) { def.addControlDesc( controlDescs[ i ]); } else { System.err.println( "Warning: unnamed control " + i + " (" + paramName[i] + ") dropped." ); } } else { System.err.println( "Warning: unreferenced control " + i + " (" + paramName[i] + ") dropped." ); } } for( int i = 0; i < constants.length; i++ ) { // this is a bug in sclang // def.constants.put( constants[ i ], new Integer( i )); def.addConstant( constants[ i ]); } // if( !keepDef ) { // def = null; // constants = null; // } // makeMsgFunc(); // UGen.buildSynthDef = null; return def; } private static UGen readUGenSpec( DataInputStream dis, SynthDef def, String[] paramName, Constant[] constants ) throws IOException { final String name = readPascalString( dis ); final Object rate = RATES[ dis.readByte() ]; final int numInputs = dis.readShort(); final int numOutputs = dis.readShort(); // specialIndex: this value is used by some unit generators for a special purpose. For example, UnaryOpUGen // and BinaryOpUGen use it to indicate which operator to perform. If not used it should be set to zero final int specialIndex = dis.readShort(); final Object[] outputRates = new Object[ numOutputs ]; final UGenInput[] ugenInputs = new UGenInput[ numInputs ]; int ugenIndex, outputIndex; final UGen ugen; for( int i = 0; i < numInputs; i++ ) { ugenIndex = dis.readShort(); outputIndex = dis.readShort(); if( ugenIndex < 0 ) { // constant input ugenInputs[ i ] = constants[ outputIndex ]; } else { // input from another ugen's output // if( ugen instanceof MultiOutUGen ) { // ugenInputs[ i ] = ((MultiOutUGen) ugen).channels[ outputIndex ]; // } else { // ugenInputs[ i ] = ugen; // } ugenInputs[ i ] = new UGenChannel( (UGen) def.ugens.get( ugenIndex ), outputIndex ); } } for( int i = 0; i < numOutputs; i++ ) { outputRates[ i ] = RATES[ dis.readByte() ]; } ugen = new UGen( name, rate, outputRates, ugenInputs, specialIndex ); return ugen; } private static String readPascalString( DataInputStream dis ) throws IOException { final byte numChars = dis.readByte(); final byte[] buf = new byte[ numChars ]; dis.readFully( buf ); return new String( buf ); } // ---------------- internal classes ---------------- private static class UGenEnv { protected final UGen ugen; protected final List collAnte; protected final List collDe; protected int synthIndex; protected UGenEnv( UGen ugen, int synthIndex ) { this.ugen = ugen; this.synthIndex = synthIndex; collAnte = new ArrayList( ugen.getNumInputs() ); collDe = new ArrayList(); } } private static class SynthIndexComparator implements Comparator { protected SynthIndexComparator() { /* empty */ } public int compare( Object env1, Object env2 ) { return( ((UGenEnv) env1).synthIndex - ((UGenEnv) env2).synthIndex ); } } }