class library: DUGen - the server now handles audio-rate inputs correctly
[supercollider.git] / SCClassLibrary / Common / Audio / SynthDesc.sc
bloba1d3d740f0f7d177f1ecf72098978f3369ee10f2
1 IODesc {
2         var <>rate, <>numberOfChannels, <>startingChannel, <>type;
4         *new { arg rate, numberOfChannels, startingChannel="?", type;
5                 ^super.newCopyArgs(rate, numberOfChannels, startingChannel, type)
6         }
8         printOn { arg stream;
9                 stream << rate.asString << " " << type.name << " " << startingChannel << " " << numberOfChannels
10         }
14 SynthDesc {
15         classvar <>mdPlugin, <>populateMetadataFunc;
17         var <>name, <>controlNames, <>controlDict;
18         var <>controls, <>inputs, <>outputs;
19         var <>metadata;
21         var <>constants, <>def;
22         var <>msgFunc, <>hasGate = false, <>hasArrayArgs, <>hasVariants, <>canFreeSynth = false;
23         var <msgFuncKeepGate = false;
25         *newFrom { arg synthdef;
26                 ^synthdef.asSynthDesc
27         }
29         *initClass {
30                 mdPlugin = AbstractMDPlugin;    // override in your startup file
31         }
33         send { arg server, completionMsg;
34                 def.send(server, completionMsg);
35         }
37         printOn { arg stream;
38                 stream << "SynthDesc '" << name << "' \nControls:\n";
39                 controls.do {|control| control.printOn(stream); $\n.printOn(stream) };
40                 inputs.do {|input| stream << "   I "; input.printOn(stream); $\n.printOn(stream) };
41                 outputs.do {|output| stream << "   O "; output.printOn(stream); $\n.printOn(stream) };
42         }
44         *read { arg path, keepDefs=false, dict;
45                 dict = dict ?? { IdentityDictionary.new };
46                 path.pathMatch.do { |filename|
47                         var file, result;
48                         file = File(filename, "r");
49                         protect {
50                                 dict = this.readFile(file, keepDefs, dict, filename);
51                         }{
52                                 file.close;
53                         };
54                 };
55                 ^dict;
56         }
57                 // path is for metadata -- only this method has direct access to the new SynthDesc
58                 // really this should be a private method -- use *read instead
59         *readFile { arg stream, keepDefs=false, dict, path;
60                 var numDefs;
61                 dict = dict ?? { IdentityDictionary.new };
62                 stream.getInt32; // 'SCgf'
63                 stream.getInt32; // version
64                 numDefs = stream.getInt16;
65                 numDefs.do {
66                         var desc;
67                         desc = SynthDesc.new.readSynthDef(stream, keepDefs);
68                         dict.put(desc.name.asSymbol, desc);
69                                 // AbstractMDPlugin dynamically determines the md archive type
70                                 // from the file extension
71                         if(path.notNil) {
72                                 desc.metadata = AbstractMDPlugin.readMetadata(path);
73                         };
74                         this.populateMetadataFunc.value(desc);
75                         if(desc.def.notNil and: { stream.isKindOf(CollStream).not }) {
76                                 desc.def.metadata ?? { desc.def.metadata = () };
77                                 desc.def.metadata.put(\shouldNotSend, true)
78                                         .put(\loadPath, path);
79                         };
80                 }
81                 ^dict
82         }
83         readSynthDef { arg stream, keepDef=false;
84                 var     numControls, numConstants, numControlNames, numUGens, numVariants;
86                 protect {
88                 inputs = [];
89                 outputs = [];
90                 controlDict = IdentityDictionary.new;
92                 name = stream.getPascalString;
94                 def = SynthDef.prNew(name);
95                 UGen.buildSynthDef = def;
97                 numConstants = stream.getInt16;
98                 constants = FloatArray.newClear(numConstants);
99                 stream.read(constants);
101                 numControls = stream.getInt16;
102                 def.controls = FloatArray.newClear(numControls);
103                 stream.read(def.controls);
105                 controls = Array.fill(numControls)
106                         { |i|
107                                 ControlName('?', i, '?', def.controls[i]);
108                         };
110                 numControlNames = stream.getInt16;
111                 numControlNames.do
112                         {
113                                 var controlName, controlIndex;
114                                 controlName = stream.getPascalString.asSymbol;
115                                 controlIndex = stream.getInt16;
116                                 controls[controlIndex].name = controlName;
117                                 controlNames = controlNames.add(controlName);
118                                 controlDict[controlName] = controls[controlIndex];
119                         };
121                 numUGens = stream.getInt16;
122                 numUGens.do {
123                         this.readUGenSpec(stream);
124                 };
126                 controls.inject(nil) {|z,y|
127                         if(y.name=='?') { z.defaultValue = z.defaultValue.asArray.add(y.defaultValue); z } { y }
128                 };
130                 def.controlNames = controls.select {|x| x.name.notNil };
131                 hasArrayArgs = controls.any { |cn| cn.name == '?' };
133                 numVariants = stream.getInt16;
134                 hasVariants = numVariants > 0;
135                         // maybe later, read in variant names and values
136                         // this is harder than it might seem at first
138                 def.constants = Dictionary.new;
139                 constants.do {|k,i| def.constants.put(k,i) };
140                 if (keepDef.not) {
141                         // throw away unneeded stuff
142                         def = nil;
143                         constants = nil;
144                 };
145                 this.makeMsgFunc;
147                 } {
148                         UGen.buildSynthDef = nil;
149                 }
151         }
153         readUGenSpec { arg stream;
154                 var ugenClass, rateIndex, rate, numInputs, numOutputs, specialIndex;
155                 var inputSpecs, outputSpecs;
156                 var addIO;
157                 var ugenInputs, ugen;
158                 var control;
160                 ugenClass = stream.getPascalString.asSymbol;
161                 if(ugenClass.asClass.isNil,{
162                         Error("No UGen class found for" + ugenClass + "which was specified in synth def file: " + this.name ++ ".scsyndef").throw;
163                 });
164                 ugenClass = ugenClass.asClass;
166                 rateIndex = stream.getInt8;
167                 numInputs = stream.getInt16;
168                 numOutputs = stream.getInt16;
169                 specialIndex = stream.getInt16;
171                 inputSpecs = Int16Array.newClear(numInputs * 2);
172                 outputSpecs = Int8Array.newClear(numOutputs);
174                 stream.read(inputSpecs);
175                 stream.read(outputSpecs);
177                 ugenInputs = [];
178                 forBy (0,inputSpecs.size-1,2) {|i|
179                         var ugenIndex, outputIndex, input, ugen;
180                         ugenIndex = inputSpecs[i];
181                         outputIndex = inputSpecs[i+1];
182                         input = if (ugenIndex < 0)
183                                 {
184                                         constants[outputIndex]
185                                 }{
186                                         ugen = def.children[ugenIndex];
187                                         if (ugen.isKindOf(MultiOutUGen)) {
188                                                 ugen.channels[outputIndex]
189                                         }{
190                                                 ugen
191                                         }
192                                 };
193                         ugenInputs = ugenInputs.add(input);
194                 };
196                 rate = #[\scalar,\control,\audio][rateIndex];
197                 ugen = ugenClass.newFromDesc(rate, numOutputs, ugenInputs, specialIndex).source;
198                 ugen.addToSynth(ugen);
200                 addIO = {|list, nchan|
201                         var b = ugen.inputs[0];
202                         if (b.class == OutputProxy and: {b.source.isKindOf(Control)}) {
203                                 control = controls.detect {|item| item.index == (b.outputIndex+b.source.specialIndex) };
204                                 if (control.notNil) { b = control.name };
205                         };
206                         list.add( IODesc(rate, nchan, b, ugenClass))
207                 };
209                 if (ugenClass.isControlUGen) {
210                         // Control.newFromDesc does not set the specialIndex, since it doesn't call Control-init.
211                         // Therefore we fill it in here:
212                         ugen.specialIndex = specialIndex;
213                         numOutputs.do { |i|
214                                 controls[i+specialIndex].rate = rate;
215                         }
216                 } {
217                         case
218                         {ugenClass.isInputUGen} {inputs = addIO.value(inputs, ugen.channels.size)}
219                         {ugenClass.isOutputUGen} {outputs = addIO.value(outputs, ugen.numAudioChannels)}
220                         {
221                                 canFreeSynth = canFreeSynth or: { ugen.canFreeSynth };
222                         };
223                 };
224         }
226         makeMsgFunc {
227                 var     string, comma=false;
228                 var     names = IdentitySet.new,
229                         suffix = this.hash.asHexString(8);
230                         // if a control name is duplicated, the msgFunc will be invalid
231                         // that "shouldn't" happen but it might; better to check for it
232                         // and throw a proper error
233                 controls.do({ |controlName|
234                         var     name;
235                         if(controlName.name.asString.first.isAlpha) {
236                                 name = controlName.name.asSymbol;
237                                 if(names.includes(name)) {
238                                         "Could not build msgFunc for this SynthDesc: duplicate control name %"
239                                                 .format(name).warn;
240                                         comma = true;
241                                 } {
242                                         names.add(name);
243                                 };
244                         };
245                 });
246                         // reusing variable to know if I should continue or not
247                 if(comma) {
248 "\nYour synthdef has been saved in the library and loaded on the server, if running.
249 Use of this synth in Patterns will not detect argument names automatically because of the duplicate name(s).".postln;
250                         msgFunc = nil;
251                         ^this
252                 };
253                 comma = false;
254                 names = 0;      // now, count the args actually added to the func
256                 string = String.streamContents {|stream|
257                         stream << "#{ ";
258                         if (controlNames.size > 0) {
259                                 stream << "arg " ;
260                         };
261                         controls.do {|controlName, i|
262                                 var name, name2;
263                                 name = controlName.name.asString;
264                                 if (name != "?") {
265                                         if (name == "gate") {
266                                                 hasGate = true;
267                                                 if(msgFuncKeepGate) {
268                                                         if (comma) { stream << ", " } { comma = true };
269                                                         stream << name;
270                                                         names = names + 1;
271                                                 }
272                                         }{
273                                                 if (name[1] == $_) { name2 = name.drop(2) } { name2 = name };
274                                                 if (comma) { stream << ", " } { comma = true };
275                                                 stream << name2;
276                                                 names = names + 1;
277                                         };
278                                 };
279                         };
280                         if (controlNames.size > 0) {
281                                 stream << ";\n" ;
282                         };
283                         stream << "\tvar\tx" << suffix << " = Array.new(" << (names*2) << ");\n";
284                         comma = false;
285                         controls.do {|controlName, i|
286                                 var name, name2;
287                                 name = controlName.name.asString;
288                                 if (name != "?") {
289                                         if (msgFuncKeepGate or: { name != "gate" }) {
290                                                 if (name[1] == $_) { name2 = name.drop(2) } { name2 = name };
291                                                 stream << "\t" << name2 << " !? { x" << suffix
292                                                         << ".add('" << name << "').add(" << name2 << ") };\n";
293                                                 names = names + 1;
294                                         };
295                                 };
296                         };
297                         stream << "\tx" << suffix << "\n}"
298                 };
300                         // do not compile the string if no argnames were added
301                 if(names > 0) { msgFunc = string.compile.value };
302         }
303         msgFuncKeepGate_ { |bool = false|
304                 if(bool != msgFuncKeepGate) {
305                         msgFuncKeepGate = bool;
306                         this.makeMsgFunc;
307                 }
308         }
310         writeMetadata { arg path;
311                 if(metadata.isNil) { AbstractMDPlugin.clearMetadata(path); ^this };
312                 this.class.mdPlugin.writeMetadata(metadata, def, path);
313         }
315         // parse the def name out of the bytes array sent with /d_recv
316         *defNameFromBytes { arg int8Array;
317                 var s,n,numDefs,size;
318                 s = CollStream(int8Array);
320                 s.getInt32;
321                 s.getInt32;
322                 numDefs = s.getInt16;
323                 size = s.getInt8;
324                 n = String.newClear(size);
325                 ^Array.fill(size,{
326                   s.getChar.asAscii
327                 }).as(String)
328         }
330         outputData {
331                 var ugens = def.children;
332                 var outs = ugens.select(_.writesToBus);
333                 ^outs.collect { |outUgen|
334                         (rate: outUgen.rate, numChannels: outUgen.numAudioChannels)
335                 }
336         }
341 SynthDescLib {
342         classvar <>all, <>global;
343         var <>name, <>synthDescs, <>servers;
345         *new { arg name, servers;
346                 if (name.isNil) { "SynthDescLib must have a name".error; ^nil }
347                 ^super.new.name_(name).init(servers);
348         }
349         init { |inServers|
350                 all.put(name.asSymbol, this);
351                 synthDescs = IdentityDictionary.new;
352                 servers = IdentitySet.with(*inServers ? { Server.default })
353         }
354         *initClass {
355                 Class.initClassTree(Server);
356                 all = IdentityDictionary.new;
357                 global = this.new(\global);
359                 ServerBoot.add {|server|
360                         this.send(server)
361                 }
362         }
364         *getLib { arg libname;
365                 ^all[libname] ?? {
366                         Error("library % not found".format(libname)).throw
367                 };
368         }
370         *default {
371                 ^global
372         }
374         *send {
375                 global.send;
376         }
378         *read { arg path;
379                 global.read(path);
380         }
382         at { arg i; ^synthDescs.at(i) }
384         *at { arg i; ^global.at(i) }
386         add { |synthdesc|
387                 synthDescs.put(synthdesc.name.asSymbol, synthdesc);
388         }
390         removeAt { |name|
391                 ^synthDescs.removeAt(name.asSymbol);
392         }
394         addServer { |server|
395                 servers = servers.add(server); // IdentitySet = one server only once.
396         }
398         removeServer { |server|
399                 servers.remove(server);
400         }
402         match { |key|
403                 var     keyString = key.asString, dotIndex = keyString.indexOf($.), desc;
404                 if(dotIndex.isNil) { ^synthDescs.at(key.asSymbol) };
405                 if((desc = synthDescs[keyString[..dotIndex-1].asSymbol]).notNil
406                                 and: { desc.hasVariants })
407                         { ^desc }
408                         { ^synthDescs.at(key.asSymbol) }
409         }
410         *match { |key| ^global.match(key) }
412         send {|aServer|
413                 var targetServers;
414                 if (aServer.isNil) {
415                         targetServers = servers
416                 } {
417                         targetServers = #[aServer]
418                 };
420                 // sent to all
421                 servers.do {|server|
422                         synthDescs.do {|desc| desc.send(server.value) };
423                 };
424         }
426         read { arg path;
427                 if (path.isNil) {
428                         path = SynthDef.synthDefDir ++ "*.scsyndef";
429                 };
430                 synthDescs = SynthDesc.read(path, true, synthDescs);
431 //              postf("SynthDescLib '%' read of '%' done.\n", name, path);
432         }
437 // Basic metadata plugins
439 // to disable metadata read/write
440 AbstractMDPlugin {
441         *clearMetadata { |path|
442                 ^thisProcess.platform.clearMetadata(path)
443         }
445         *writeMetadata { |metadata, synthdef, path|
447                 this.clearMetadata(path);
448                 path = this.applyExtension(path);
449                 this.writeMetadataFile(metadata, synthdef, path);
450         }
451         *writeMetadataFile {}
453                 // clearMetadata should ensure that only one MD file ever exists
454                 // therefore we can check the subclasses in turn
455                 // and return the first MD found
456                 // every subclass should have a unique extension
457         *readMetadata { |path|
458                 var     pathTmp, classList, i;
459                 path = path.splitext[0] ++ ".";
460                 classList = this.allSubclasses;
461                         // ensure that SynthDescLib.mdPlugin is preferred for reading,
462                         // with other plugins as a fallback
463                         // it will also be possible to use Events or Protos as plugins this way
464                 if((i = classList.indexOf(SynthDesc.mdPlugin)).notNil and: { i > 0 }) {
465                         classList = classList.copy.swap(0, i);
466                 } {
467                         classList = [SynthDesc.mdPlugin] ++ classList;
468                 };
469                 classList.do({ |class|
470                         if(class.notNil and: { File.exists(pathTmp = path ++ class.mdExtension) }) {
471                                 ^class.readMetadataFile(pathTmp)
472                         }
473                 });
474                 ^nil
475         }
476         *readMetadataFile { ^nil }
478         *applyExtension { |path|
479                 ^path.splitext[0] ++ "." ++ this.mdExtension
480         }
481         *mdExtension { ^"" }            // nothing is written anyway
484 // simple archiving of the dictionary
485 TextArchiveMDPlugin : AbstractMDPlugin {
486         *writeMetadataFile { |metadata, synthdef, path|
487                 metadata.writeArchive(path)
488         }
490         *readMetadataFile { |path|
491                 ^Object.readArchive(path)
492         }
493         *mdExtension { ^"txarcmeta" }