Merge pull request #506 from andrewcsmith/patch-2
[supercollider.git] / SCClassLibrary / Common / Audio / SynthDesc.sc
blobba59240c052a7a821403268cfa13a0fd05a8f4e9
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, version;
61                 dict = dict ?? { IdentityDictionary.new };
62                 stream.getInt32; // 'SCgf'
63                 version = stream.getInt32; // version
64                 numDefs = stream.getInt16;
65                 numDefs.do {
66                         var desc;
67                         if(version >= 2, {
68                                 desc = SynthDesc.new.readSynthDef2(stream, keepDefs);
69                         },{
70                                 desc = SynthDesc.new.readSynthDef(stream, keepDefs);
71                         });
72                         dict.put(desc.name.asSymbol, desc);
73                                 // AbstractMDPlugin dynamically determines the md archive type
74                                 // from the file extension
75                         if(path.notNil) {
76                                 desc.metadata = AbstractMDPlugin.readMetadata(path);
77                         };
78                         this.populateMetadataFunc.value(desc);
79                         if(desc.def.notNil and: { stream.isKindOf(CollStream).not }) {
80                                 desc.def.metadata ?? { desc.def.metadata = () };
81                                 desc.def.metadata.put(\shouldNotSend, true)
82                                         .put(\loadPath, path);
83                         };
84                 }
85                 ^dict
86         }
87         readSynthDef { arg stream, keepDef=false;
88                 var     numControls, numConstants, numControlNames, numUGens, numVariants;
90                 protect {
92                 inputs = [];
93                 outputs = [];
94                 controlDict = IdentityDictionary.new;
96                 name = stream.getPascalString;
98                 def = SynthDef.prNew(name);
99                 UGen.buildSynthDef = def;
101                 numConstants = stream.getInt16;
102                 constants = FloatArray.newClear(numConstants);
103                 stream.read(constants);
105                 numControls = stream.getInt16;
106                 def.controls = FloatArray.newClear(numControls);
107                 stream.read(def.controls);
109                 controls = Array.fill(numControls)
110                         { |i|
111                                 ControlName('?', i, '?', def.controls[i]);
112                         };
114                 numControlNames = stream.getInt16;
115                 numControlNames.do
116                         {
117                                 var controlName, controlIndex;
118                                 controlName = stream.getPascalString.asSymbol;
119                                 controlIndex = stream.getInt16;
120                                 controls[controlIndex].name = controlName;
121                                 controlNames = controlNames.add(controlName);
122                                 controlDict[controlName] = controls[controlIndex];
123                         };
125                 numUGens = stream.getInt16;
126                 numUGens.do {
127                         this.readUGenSpec(stream);
128                 };
130                 controls.inject(nil) {|z,y|
131                         if(y.name=='?') { z.defaultValue = z.defaultValue.asArray.add(y.defaultValue); z } { y }
132                 };
134                 def.controlNames = controls.select {|x| x.name.notNil };
135                 hasArrayArgs = controls.any { |cn| cn.name == '?' };
137                 numVariants = stream.getInt16;
138                 hasVariants = numVariants > 0;
139                         // maybe later, read in variant names and values
140                         // this is harder than it might seem at first
142                 def.constants = Dictionary.new;
143                 constants.do {|k,i| def.constants.put(k,i) };
144                 if (keepDef.not) {
145                         // throw away unneeded stuff
146                         def = nil;
147                         constants = nil;
148                 };
149                 this.makeMsgFunc;
151                 } {
152                         UGen.buildSynthDef = nil;
153                 }
155         }
156         
157         // synthdef ver 2
158         readSynthDef2 { arg stream, keepDef=false;
159                 var     numControls, numConstants, numControlNames, numUGens, numVariants;
161                 protect {
163                 inputs = [];
164                 outputs = [];
165                 controlDict = IdentityDictionary.new;
167                 name = stream.getPascalString;
169                 def = SynthDef.prNew(name);
170                 UGen.buildSynthDef = def;
172                 numConstants = stream.getInt32;
173                 constants = FloatArray.newClear(numConstants);
174                 stream.read(constants);
176                 numControls = stream.getInt32;
177                 def.controls = FloatArray.newClear(numControls);
178                 stream.read(def.controls);
180                 controls = Array.fill(numControls)
181                         { |i|
182                                 ControlName('?', i, '?', def.controls[i]);
183                         };
185                 numControlNames = stream.getInt32;
186                 numControlNames.do
187                         {
188                                 var controlName, controlIndex;
189                                 controlName = stream.getPascalString.asSymbol;
190                                 controlIndex = stream.getInt32;
191                                 controls[controlIndex].name = controlName;
192                                 controlNames = controlNames.add(controlName);
193                                 controlDict[controlName] = controls[controlIndex];
194                         };
196                 numUGens = stream.getInt32;
197                 numUGens.do {
198                         this.readUGenSpec2(stream);
199                 };
201                 controls.inject(nil) {|z,y|
202                         if(y.name=='?') { z.defaultValue = z.defaultValue.asArray.add(y.defaultValue); z } { y }
203                 };
205                 def.controlNames = controls.select {|x| x.name.notNil };
206                 hasArrayArgs = controls.any { |cn| cn.name == '?' };
208                 numVariants = stream.getInt16;
209                 hasVariants = numVariants > 0;
210                         // maybe later, read in variant names and values
211                         // this is harder than it might seem at first
213                 def.constants = Dictionary.new;
214                 constants.do {|k,i| def.constants.put(k,i) };
215                 if (keepDef.not) {
216                         // throw away unneeded stuff
217                         def = nil;
218                         constants = nil;
219                 };
220                 this.makeMsgFunc;
222                 } {
223                         UGen.buildSynthDef = nil;
224                 }
226         }
228         readUGenSpec { arg stream;
229                 var ugenClass, rateIndex, rate, numInputs, numOutputs, specialIndex;
230                 var inputSpecs, outputSpecs;
231                 var addIO;
232                 var ugenInputs, ugen;
233                 var control;
235                 ugenClass = stream.getPascalString.asSymbol;
236                 if(ugenClass.asClass.isNil,{
237                         Error("No UGen class found for" + ugenClass + "which was specified in synth def file: " + this.name ++ ".scsyndef").throw;
238                 });
239                 ugenClass = ugenClass.asClass;
241                 rateIndex = stream.getInt8;
242                 numInputs = stream.getInt16;
243                 numOutputs = stream.getInt16;
244                 specialIndex = stream.getInt16;
246                 inputSpecs = Int16Array.newClear(numInputs * 2);
247                 outputSpecs = Int8Array.newClear(numOutputs);
249                 stream.read(inputSpecs);
250                 stream.read(outputSpecs);
252                 ugenInputs = [];
253                 forBy (0,inputSpecs.size-1,2) {|i|
254                         var ugenIndex, outputIndex, input, ugen;
255                         ugenIndex = inputSpecs[i];
256                         outputIndex = inputSpecs[i+1];
257                         input = if (ugenIndex < 0)
258                                 {
259                                         constants[outputIndex]
260                                 }{
261                                         ugen = def.children[ugenIndex];
262                                         if (ugen.isKindOf(MultiOutUGen)) {
263                                                 ugen.channels[outputIndex]
264                                         }{
265                                                 ugen
266                                         }
267                                 };
268                         ugenInputs = ugenInputs.add(input);
269                 };
271                 rate = #[\scalar,\control,\audio][rateIndex];
272                 ugen = ugenClass.newFromDesc(rate, numOutputs, ugenInputs, specialIndex).source;
273                 ugen.addToSynth(ugen);
275                 addIO = {|list, nchan|
276                         var b = ugen.inputs[0];
277                         if (b.class == OutputProxy and: {b.source.isKindOf(Control)}) {
278                                 control = controls.detect {|item| item.index == (b.outputIndex+b.source.specialIndex) };
279                                 if (control.notNil) { b = control.name };
280                         };
281                         list.add( IODesc(rate, nchan, b, ugenClass))
282                 };
284                 if (ugenClass.isControlUGen) {
285                         // Control.newFromDesc does not set the specialIndex, since it doesn't call Control-init.
286                         // Therefore we fill it in here:
287                         ugen.specialIndex = specialIndex;
288                         numOutputs.do { |i|
289                                 controls[i+specialIndex].rate = rate;
290                         }
291                 } {
292                         case
293                         {ugenClass.isInputUGen} {inputs = addIO.value(inputs, ugen.channels.size)}
294                         {ugenClass.isOutputUGen} {outputs = addIO.value(outputs, ugen.numAudioChannels)}
295                         {
296                                 canFreeSynth = canFreeSynth or: { ugen.canFreeSynth };
297                         };
298                 };
299         }
300         
301         // synthdef ver 2
302         readUGenSpec2 { arg stream;
303                 var ugenClass, rateIndex, rate, numInputs, numOutputs, specialIndex;
304                 var inputSpecs, outputSpecs;
305                 var addIO;
306                 var ugenInputs, ugen;
307                 var control;
309                 ugenClass = stream.getPascalString.asSymbol;
310                 if(ugenClass.asClass.isNil,{
311                         Error("No UGen class found for" + ugenClass + "which was specified in synth def file: " + this.name ++ ".scsyndef").throw;
312                 });
313                 ugenClass = ugenClass.asClass;
315                 rateIndex = stream.getInt8;
316                 numInputs = stream.getInt32;
317                 numOutputs = stream.getInt32;
318                 specialIndex = stream.getInt16;
320                 inputSpecs = Int32Array.newClear(numInputs * 2);
321                 outputSpecs = Int8Array.newClear(numOutputs);
323                 stream.read(inputSpecs);
324                 stream.read(outputSpecs);
326                 ugenInputs = [];
327                 forBy (0,inputSpecs.size-1,2) {|i|
328                         var ugenIndex, outputIndex, input, ugen;
329                         ugenIndex = inputSpecs[i];
330                         outputIndex = inputSpecs[i+1];
331                         input = if (ugenIndex < 0)
332                                 {
333                                         constants[outputIndex]
334                                 }{
335                                         ugen = def.children[ugenIndex];
336                                         if (ugen.isKindOf(MultiOutUGen)) {
337                                                 ugen.channels[outputIndex]
338                                         }{
339                                                 ugen
340                                         }
341                                 };
342                         ugenInputs = ugenInputs.add(input);
343                 };
345                 rate = #[\scalar,\control,\audio][rateIndex];
346                 ugen = ugenClass.newFromDesc(rate, numOutputs, ugenInputs, specialIndex).source;
347                 ugen.addToSynth(ugen);
349                 addIO = {|list, nchan|
350                         var b = ugen.inputs[0];
351                         if (b.class == OutputProxy and: {b.source.isKindOf(Control)}) {
352                                 control = controls.detect {|item| item.index == (b.outputIndex+b.source.specialIndex) };
353                                 if (control.notNil) { b = control.name };
354                         };
355                         list.add( IODesc(rate, nchan, b, ugenClass))
356                 };
358                 if (ugenClass.isControlUGen) {
359                         // Control.newFromDesc does not set the specialIndex, since it doesn't call Control-init.
360                         // Therefore we fill it in here:
361                         ugen.specialIndex = specialIndex;
362                         numOutputs.do { |i|
363                                 controls[i+specialIndex].rate = rate;
364                         }
365                 } {
366                         case
367                         {ugenClass.isInputUGen} {inputs = addIO.value(inputs, ugen.channels.size)}
368                         {ugenClass.isOutputUGen} {outputs = addIO.value(outputs, ugen.numAudioChannels)}
369                         {
370                                 canFreeSynth = canFreeSynth or: { ugen.canFreeSynth };
371                         };
372                 };
373         }
375         makeMsgFunc {
376                 var     string, comma=false;
377                 var     names = IdentitySet.new,
378                         suffix = this.hash.asHexString(8);
379                         // if a control name is duplicated, the msgFunc will be invalid
380                         // that "shouldn't" happen but it might; better to check for it
381                         // and throw a proper error
382                 controls.do({ |controlName|
383                         var     name;
384                         if(controlName.name.asString.first.isAlpha) {
385                                 name = controlName.name.asSymbol;
386                                 if(names.includes(name)) {
387                                         "Could not build msgFunc for this SynthDesc: duplicate control name %"
388                                                 .format(name).warn;
389                                         comma = true;
390                                 } {
391                                         names.add(name);
392                                 };
393                         };
394                 });
395                         // reusing variable to know if I should continue or not
396                 if(comma) {
397 "\nYour synthdef has been saved in the library and loaded on the server, if running.
398 Use of this synth in Patterns will not detect argument names automatically because of the duplicate name(s).".postln;
399                         msgFunc = nil;
400                         ^this
401                 };
402                 comma = false;
403                 names = 0;      // now, count the args actually added to the func
405                 string = String.streamContents {|stream|
406                         stream << "#{ ";
407                         if (controlNames.size > 0) {
408                                 stream << "arg " ;
409                         };
410                         controls.do {|controlName, i|
411                                 var name, name2;
412                                 name = controlName.name.asString;
413                                 if (name != "?") {
414                                         if (name == "gate") {
415                                                 hasGate = true;
416                                                 if(msgFuncKeepGate) {
417                                                         if (comma) { stream << ", " } { comma = true };
418                                                         stream << name;
419                                                         names = names + 1;
420                                                 }
421                                         }{
422                                                 if (name[1] == $_) { name2 = name.drop(2) } { name2 = name };
423                                                 if (comma) { stream << ", " } { comma = true };
424                                                 stream << name2;
425                                                 names = names + 1;
426                                         };
427                                 };
428                         };
429                         if (controlNames.size > 0) {
430                                 stream << ";\n" ;
431                         };
432                         stream << "\tvar\tx" << suffix << " = Array.new(" << (names*2) << ");\n";
433                         comma = false;
434                         controls.do {|controlName, i|
435                                 var name, name2;
436                                 name = controlName.name.asString;
437                                 if (name != "?") {
438                                         if (msgFuncKeepGate or: { name != "gate" }) {
439                                                 if (name[1] == $_) { name2 = name.drop(2) } { name2 = name };
440                                                 stream << "\t" << name2 << " !? { x" << suffix
441                                                         << ".add('" << name << "').add(" << name2 << ") };\n";
442                                                 names = names + 1;
443                                         };
444                                 };
445                         };
446                         stream << "\tx" << suffix << "\n}"
447                 };
449                         // do not compile the string if no argnames were added
450                 if(names > 0) { msgFunc = string.compile.value };
451         }
452         
453         msgFuncKeepGate_ { |bool = false|
454                 if(bool != msgFuncKeepGate) {
455                         msgFuncKeepGate = bool;
456                         this.makeMsgFunc;
457                 }
458         }
460         writeMetadata { arg path;
461                 if(metadata.isNil) { AbstractMDPlugin.clearMetadata(path); ^this };
462                 this.class.mdPlugin.writeMetadata(metadata, def, path);
463         }
465         // parse the def name out of the bytes array sent with /d_recv
466         *defNameFromBytes { arg int8Array;
467                 var stream, n, numDefs, size;
468                 stream = CollStream(int8Array);
470                 stream.getInt32;
471                 stream.getInt32;
472                 numDefs = stream.getInt16;
473                 size = stream.getInt8;
474                 n = String.newClear(size);
475                 ^Array.fill(size, {
476                   stream.getChar.asAscii
477                 }).as(String)
478         }
480         outputData {
481                 var ugens = def.children;
482                 var outs = ugens.select(_.writesToBus);
483                 ^outs.collect { |outUgen|
484                         (rate: outUgen.rate, numChannels: outUgen.numAudioChannels)
485                 }
486         }
491 SynthDescLib {
492         classvar <>all, <>global;
493         var <>name, <>synthDescs, <>servers;
495         *new { arg name, servers;
496                 if (name.isNil) { "SynthDescLib must have a name".error; ^nil }
497                 ^super.new.name_(name).init(servers);
498         }
499         init { |inServers|
500                 all.put(name.asSymbol, this);
501                 synthDescs = IdentityDictionary.new;
502                 servers = IdentitySet.with(*inServers ? { Server.default })
503         }
504         *initClass {
505                 Class.initClassTree(Server);
506                 all = IdentityDictionary.new;
507                 global = this.new(\global);
509                 ServerBoot.add { |server|
510                         // tryToLoadReconstructedDefs = false:
511                         // since this is done automatically, w/o user action,
512                         // it should not try to do things that will cause warnings
513                         // (or errors, if one of the servers is not local)
514                         this.send(server, false)
515                 }
516         }
518         *getLib { arg libname;
519                 ^all[libname] ?? {
520                         Error("library % not found".format(libname)).throw
521                 };
522         }
524         *default {
525                 ^global
526         }
528         *send { |server, tryToLoadReconstructedDefs = true|
529                 global.send(server, tryToLoadReconstructedDefs);
530         }
532         *read { arg path;
533                 global.read(path);
534         }
536         at { arg i; ^synthDescs.at(i) }
538         *at { arg i; ^global.at(i) }
540         add { |synthdesc|
541                 synthDescs.put(synthdesc.name.asSymbol, synthdesc);
542         }
544         removeAt { |name|
545                 ^synthDescs.removeAt(name.asSymbol);
546         }
548         addServer { |server|
549                 servers = servers.add(server); // IdentitySet = one server only once.
550         }
552         removeServer { |server|
553                 servers.remove(server);
554         }
556         match { |key|
557                 var     keyString = key.asString, dotIndex = keyString.indexOf($.), desc;
558                 if(dotIndex.isNil) { ^synthDescs.at(key.asSymbol) };
559                 if((desc = synthDescs[keyString[..dotIndex-1].asSymbol]).notNil
560                                 and: { desc.hasVariants })
561                         { ^desc }
562                         { ^synthDescs.at(key.asSymbol) }
563         }
564         *match { |key| ^global.match(key) }
566         send { |aServer, tryToLoadReconstructedDefs = true|
567                 // sent to all
568                 (aServer ? servers).do { |server|
569                         server = server.value;
570                         synthDescs.do { |desc|
571                                 if(desc.def.metadata.trueAt(\shouldNotSend).not) {
572                                         desc.send(server.value)
573                                 } {
574                                         if(tryToLoadReconstructedDefs) {
575                                                 desc.def.loadReconstructed(server);
576                                         };
577                                 };
578                         };
579                 };
580         }
582         read { arg path;
583                 if (path.isNil) {
584                         path = SynthDef.synthDefDir ++ "*.scsyndef";
585                 };
586                 synthDescs = SynthDesc.read(path, true, synthDescs);
587         }
592 // Basic metadata plugins
594 // to disable metadata read/write
595 AbstractMDPlugin {
596         *clearMetadata { |path|
597                 ^thisProcess.platform.clearMetadata(path)
598         }
600         *writeMetadata { |metadata, synthdef, path|
602                 this.clearMetadata(path);
603                 path = this.applyExtension(path);
604                 this.writeMetadataFile(metadata, synthdef, path);
605         }
606         *writeMetadataFile {}
608                 // clearMetadata should ensure that only one MD file ever exists
609                 // therefore we can check the subclasses in turn
610                 // and return the first MD found
611                 // every subclass should have a unique extension
612         *readMetadata { |path|
613                 var     pathTmp, classList, i;
614                 path = path.splitext[0] ++ ".";
615                 classList = this.allSubclasses;
616                         // ensure that SynthDescLib.mdPlugin is preferred for reading,
617                         // with other plugins as a fallback
618                         // it will also be possible to use Events or Protos as plugins this way
619                 if((i = classList.indexOf(SynthDesc.mdPlugin)).notNil and: { i > 0 }) {
620                         classList = classList.copy.swap(0, i);
621                 } {
622                         classList = [SynthDesc.mdPlugin] ++ classList;
623                 };
624                 classList.do({ |class|
625                         if(class.notNil and: { File.exists(pathTmp = path ++ class.mdExtension) }) {
626                                 ^class.readMetadataFile(pathTmp)
627                         }
628                 });
629                 ^nil
630         }
631         *readMetadataFile { ^nil }
633         *applyExtension { |path|
634                 ^path.splitext[0] ++ "." ++ this.mdExtension
635         }
636         *mdExtension { ^"" }            // nothing is written anyway
639 // simple archiving of the dictionary
640 TextArchiveMDPlugin : AbstractMDPlugin {
641         *writeMetadataFile { |metadata, synthdef, path|
642                 metadata.writeArchive(path)
643         }
645         *readMetadataFile { |path|
646                 ^Object.readArchive(path)
647         }
648         *mdExtension { ^"txarcmeta" }