Skip to content
Permalink
main
Go to file
 
 
Cannot retrieve contributors at this time
545 lines (491 sloc) 16.5 KB
CroneGenEngine : CroneEngine {
classvar <>debug = true;
classvar defaultPolyphony = 6;
var mainGroup;
var voiceGroup;
var metadata;
var <controlBusses; // TODO: make private
var synths;
var buffers;
var savedArgs;
var polyphony;
*new { |context, callback|
^super.new(context, callback).initCroneGenEngine;
}
initCroneGenEngine {
metadata = this.class.parseMetadata;
if (metadata[\inControlName].isNil and: metadata[\outControlName].isNil) {
Error("a mono or stereo in or out must be defined.").throw;
};
}
alloc {
// TODO: remove var controlsNotToExposeAsCommands;
var controlBusArgs;
var args;
var specs;
var inControlName = metadata[\inControlName];
var outControlName = metadata[\outControlName];
var type = metadata[\type];
/*
TODO: remove
controlsNotToExposeAsCommands = [
'gate',
'freq',
inControlName,
outControlName, // TODO: was 'out', instead check output_data and filter out correct out argument
'amp_env0', // TODO: test if this actually works
'amp_env1', // TODO: test if this actually works
'pitch_trk0', // TODO: test if this actually works
'pitch_trk1' // TODO: test if this actually works
] ++ if (inControlName.notNil, [inControlName], []) ++ if (outControlName.notNil, [outControlName], []);
controlsToExposeAsCommands = synthDesc.controls.reject { |control|
controlsNotToExposeAsCommands.includes(control.name);
};
*/
var controlsToExposeAsCommands = metadata[\controlsToExposeAsCommands];
mainGroup = Group.tail(context.xg);
voiceGroup = ParGroup.tail(mainGroup);
controlBusses = IdentityDictionary.new;
args = this.autorouteInputs(inControlName, args);
this.class.trace(\autorouteInputs, args);
args = this.autorouteOutputs(outControlName, args);
this.class.trace(\autorouteOutputs, args);
args = this.autorouteAmplitudeEnvelope(args);
this.class.trace(\autorouteAmp, args);
args = this.autoroutePitchTracker(args);
this.class.trace(\autoroutePitch, args);
this.class.trace(\type, type);
switch (type)
{\persistent} {
// 1:1. one synth is spawned until engine is changed
// context.server.sync;
// synths = [ Synth(synthDef.name, args: args, target: context.xg) ];
// context.server.sync;
this.addCommand(
"run",
"i",
{ |msg| synths.first.run(msg[1]); }
);
}
{\polyphonicReuseSynths} {
// a synth having a gate argument is considered polyphonic, but synths cannot free themselves hence [polyphony] number of synths are spawned and reused (this is good for deterministic engine performance characteristics)
// args = args.addAll([\gate, 0]);
// context.server.sync;
this.addCommand(
"gate",
"ii",
{ |msg|
var voicenum = msg[1];
var gate = msg[2];
if (voicenum < polyphony) {
synths[voicenum].set(\gate, gate);
} {
"gate command ignored: voicenum % referred, only % voices available".format(voicenum, polyphony).warn;
};
}
);
if (this.class.synthDesc.hasControlNamed(\freq)) {
// TODO: lookout for command name clashes w/ autogenerated commands
this.addCommand(
"on",
"i",
{ |msg|
synths[msg[1]].set(\gate, 1);
}
);
this.addCommand(
"off",
"i",
{ |msg|
synths[msg[1]].set(\gate, 0);
}
);
this.addCommand(
"noteOn",
"if",
{ |msg|
synths[msg[1]].set(\gate, 1, \freq, msg[2].midicps);
}
);
this.addCommand(
"freqOn",
"if",
{ |msg|
synths[msg[1]].set(\gate, 1, \freq, msg[2]);
}
);
};
this.addCommand(
"polyphony",
"i",
{ |msg|
this.polyphonicReuseSynthsRespawnSynths(polyphony = msg[1]);
}
);
};
/*
TODO: sc-side voice allocation
{\polyphonicFreeSelf} {
// gate and freq means synth is considered polyphonic, synths are spawned on note ons and released on note offs
// TODO: how to handle polyphony / voice allocation?
// synths = Array.fill(128); // TODO: number of midi notes
// TODO: gate and freq reserved for noteOns/Offs
controlsToExposeAsCommands = controlsToExposeAsCommands.reject { |control| [\gate, \freq].includes(control.name) };
// TODO: also free controlBusses
// TODO: currently, amp gets reserved for midi note on velocity
if (this.class.synthDesc.hasControlNamed(\amp)) {
// TODO: or multiply amp with command main volume (??)
// TODO: if excluded from commands also free controlBus
controlsToExposeAsCommands = controlsToExposeAsCommands.reject { |control| control.name == \amp };
};
this.addCommand(
"noteOn",
"ii", // midinote + velocity
{ |msg|
var midinote = msg[1];
var velocity = msg[2];
var noteOnArgs = [\gate, 1, \freq, midinote.midicps].addAll(args);
if (this.class.synthDesc.hasControlNamed(\amp)) {
noteOnArgs = [\amp, \midivelocity.asSpec.unmap(velocity)].addAll(noteOnArgs); // TODO: fix amp curve
};
this.class.trace(\noteOnArgs, noteOnArgs);
context.server.makeBundle(nil, {
synths[midinote] !? _.release;
synths[midinote] = Synth(
this.class.synthDesc.name,
args: noteOnArgs,
target: voiceGroup
);
});
}
);
this.addCommand(
"noteOff",
"i", // midinote
{ |msg|
var midinote = msg[1];
synths[midinote] !? _.release;
synths[midinote] = nil;
}
);
this.addCommand(
"allNotesOff",
"",
{ |msg|
synths do: _.release;
}
);
this.addCommand(
"polyphony",
"i",
{ |msg|
// if polyphony is set to less than before, kill currentrunning - polyphony synths (the "oldest")
}
);
this.addCommand(
"unison",
"",
{ |msg|
// kill currentrunning synths - all
}
);
this.addCommand(
"unisonDetune",
"f", // detune
{ |msg|
// TODO: checkout \detune.asSpec -20 +20 hz
// detune all currentrunning synths ?
}
);
if (this.class.synthDesc.hasControlNamed(\pan)) {
this.addCommand(
"unisonSpread",
"f", // stereo spread
{ |msg|
// spread currentrunning synths ?
}
);
};
};
*/
if (this.class.synthDesc.hasControlNamed(\bufnum)) {
this.addCommand(
"loadSample", // TODO: naming? what about channel count: mono/stereo? delegated to SynthDef / Engine creator?
"is",
{ |msg|
// TODO: bufnum implies buffers are used, so command to load buffers is exposed
// TODO: bufnum index must be offset / made correct i -> real bufnum
}
);
this.addCommand(
"buffers", // TODO: naming? what about channel count: mono/stereo? delegated to SynthDef / Engine creator?
"i",
{ |msg|
// TODO: free / allocate buffers as needed, probably free all also
}
);
};
context.server.sync;
specs = this.class.synthDesc.metadata !? { |metadata| metadata.specs } ? ();
args = this.addCommandsForControlsToExposeAsCommands(controlsToExposeAsCommands, specs, args);
this.class.trace(\argsComplete, args);
context.server.sync; // TODO: don't think this is needed, test
switch (type)
{\persistent} {
// 1:1 synth and engine, synth is spawned until engine is changed
synths = [ Synth(this.class.synthDesc.name, args: args, target: mainGroup) ];
}
/*
{\polyphonicFreeSelf} {
synths = Array.fill(128); // TODO: number of midi notes - should be limited by polyphony
}
*/
{\polyphonicReuseSynths} {
// args = args.addAll([\gate, 0]); TODO: shouldn't be needed, there's a controlBus for this probably set to 0
savedArgs = args;
this.polyphonicReuseSynthsRespawnSynths(polyphony = defaultPolyphony);
};
context.server.sync;
}
autorouteInputs { |controlName, args|
^if (controlName.notNil) { args.addAll([controlName, context.in_b]) } { args }
}
autorouteOutputs { |controlName, args|
^if (controlName.notNil) { args.addAll([controlName, context.out_b]) } { args } // TODO: fix mono -> stereo ?
}
autorouteAmplitudeEnvelope { |args|
// autoroute input amp envelope (would be better to allocate adjacent stereo busses in AudioContext and bundle these into one for SynthDesc control named \amp_env
if (this.class.synthDesc.hasControlNamed(\amp_env0)) {
args = args.addAll([\amp_env0, context.amp_in_b[0].asMap]);
};
if (this.class.synthDesc.hasControlNamed(\amp_env1)) {
args = args.addAll([\amp_env1, context.amp_in_b[1].asMap]);
};
^args;
}
autoroutePitchTracker { |args|
// autoroute pitch tracker (would be better to allocate adjacent stereo busses in AudioContext and bundle these into one for SynthDesc control named \pitch
if (this.class.synthDesc.hasControlNamed(\pitch_trk0)) {
args = args.addAll([\pitch_trk0, context.pitch_in_b[0].asMap]);
};
if (this.class.synthDesc.hasControlNamed(\pitch_trk1)) {
args = args.addAll([\pitch_trk1, context.pitch_in_b[1].asMap]);
};
^args;
}
addCommandsForControlsToExposeAsCommands { |controls, specs, args|
controls.do { |control|
var controlName = control.name;
var spec;
var busArgs;
this.class.trace(\controlName, controlName);
spec = if (specs[controlName].notNil) { specs[controlName].asSpec } { controlName.asSpec };
controlBusses[controlName] = Bus.control;
if (spec.notNil) {
spec.default !? { |default|
controlBusses[controlName].set(default); // TODO: right now spec default is picked up, but not argument default (ie. index = 3 above)
};
};
busArgs = [controlName, controlBusses[controlName].asMap];
args = args.addAll(busArgs);
this.class.trace(\controlBusArg, busArgs);
this.addCommand( // TODO: these controls are suited as possible direct-to-scsynth commands
controlName.asString,
"f",
if (spec.notNil) {
{ |msg|
var value = msg[1];
var constrainedValue = spec.constrain(value);
this.class.trace(\speccedControlBusCommand, [controlName, controlBusses[controlName], value, constrainedValue]);
controlBusses[controlName].set(constrainedValue);
}
} {
{ |msg|
var value = msg[1];
this.class.trace(\unspeccedControlBusCommand, [controlName, controlBusses[controlName], value]);
controlBusses[controlName].set(value);
}
}
);
};
^args;
}
polyphonicReuseSynthsRespawnSynths { |polyphony|
synths do: _.free;
synths = polyphony.collect { Synth.tail(voiceGroup, this.class.synthDesc.name, savedArgs) };
}
free {
(thisMethod.ownerClass.asString ++ "." ++ thisMethod.name.asString).debug(\pre);
synths do: _.free;
controlBusses do: _.free;
buffers do: _.free;
(thisMethod.ownerClass.asString ++ "." ++ thisMethod.name.asString).debug(\post);
}
/*
(make CroneGenEngine a subclass of CroneEngine)
*new { |context, callback| ^super.new(context, callback).initCroneGenEngine }
initCroneGenEngine {
var synthDef, synthDesc;
synthDef = this.synthDef;
if (synthDef.name == 'nil') { // TODO: even needed, or should this simply be overwritten to classname?
synthDef.name = this.asString.asSymbol;
};
synthDef.add;
synthDesc=SynthDescLib.global[synthDef.name];
CroneSynthDefIntrospectionEngine.new(context, synthDesc);
^super.new.initCroneGenEngine(context);
}
*/
*parseMetadata {
^CroneSynthDefIntrospectionUtil.inspectSynthDesc(this.synthDesc);
}
*synthDesc {
var synthDef, synthDesc;
synthDef = this.synthDef;
if (synthDef.name == 'nil') { // TODO: even needed, or should this simply be overwritten to classname?
synthDef.name = this.asString.asSymbol;
};
synthDef.add;
synthDesc=SynthDescLib.global[synthDef.name];
^synthDesc;
}
*synthDef {
^if (this.ugenGraphFunc.notNil) {
var synthDef = this.wrapOut(
// TODO: effectively filter out synthdefs from automatic lookup in Crone?
("No_" ++ this.name.asString).asSymbol, // TODO: assumes class name has Engine_ prefix (this could be validated)
this.ugenGraphFunc,
this.rates, // TODO: remove this, assume all rates kr
this.prependArgs // TODO: needed at all?
);
synthDef.metadata = this.specs !? { |specs| (specs: specs.asDict) };
synthDef;
/*
TODO: below is support for in-memory and on-disk synthdefs based on symbol in *defName
} {
this.defName !? { |defName|
var synthDesc;
synthDesc = SynthDescLib.global.synthDescs[defName];
if (synthDesc.notNil) {
"synthdef % found in memory".format(defName).inform;
^synthDesc.def;
} {
var path = thisProcess.platform.userAppSupportDir + "synthdefs" +/+ defName ++ ".scsyndef";
if (PathName(path).isFile) {
"synthdef % found on disk".format(defName).inform;
SynthDescLib.global.read(path);
"synthdef % read from file %".format(defName, path).inform;
if (synthDesc.notNil) {
^synthDesc.def;
};
} {
"synthdef % not found on disk".format(defName).inform;
}
}
}
*/
};
}
*ugenGraphFunc { ^nil }
*specs { ^nil }
*rates { ^nil } // TODO: remove this, assume all rates kr
*prependArgs { ^nil } // TODO: needed at all?
// adapted from *wrapOut in GraphBuilder
*wrapOut { arg name, func, rates, prependArgs, outClass=\Out, fadeTime;
^SynthDef.new(name, { arg i_out=0;
var result, rate, env;
result = SynthDef.wrap(func, rates, prependArgs).asUGenInput;
rate = result.rate;
if(rate.isNil or: { rate === \scalar }) {
// Out, SendTrig, [ ] etc. probably a 0.0
result
} {
if(fadeTime.notNil) {
result = this.makeFadeEnv(fadeTime) * result;
};
outClass = outClass.asClass;
outClass.replaceZeroesWithSilence(result.asArray);
outClass.multiNewList([rate, i_out]++result)
}
})
}
*trace { |what, args|
if (debug) { args.debug(what) };
}
*generateLuaEngineModuleSpecsSection {
var specs = this.synthDesc.metadata !? { |metadata| metadata.specs } ? ();
^"local specs = {}\n\n" ++ this.parseMetadata[\controlsToExposeAsCommands].collect { |control|
var controlName = control.name;
var spec;
spec = if (specs[controlName].notNil) { specs[controlName] } { controlName };
"specs."++controlName++" = " ++
if (spec.notNil) {
if (spec.class == Symbol) {
"\\" ++ spec.asString
} {
spec.asSpecifier !? { |specifier| "ControlSpec."++specifier.asString.toUpper } ? ("ControlSpec.new("++[spec.minval, spec.maxval, spec.warp.asSpecifier.asString.quote, spec.step, spec.default, spec.units.quote].join(", ")++")")
}
} {
"nil";
}
}.join($\n);
}
}
CroneSynthDefIntrospectionUtil {
*inspectSynthDesc { |synthDesc|
var type = (case
{ synthDesc.hasControlNamed(\gate) and: synthDesc.canFreeSynth.not } {
\polyphonicReuseSynths
}
{ synthDesc.hasControlNamed(\gate) and: synthDesc.canFreeSynth } {
\polyphonicFreeSelf
} ? \persistent).debug(\type);
var inControlName = this.retrieveInputControlName(synthDesc).debug(\inControlName);
var outControlName = this.retrieveOutputControlName(synthDesc).debug(\outControlName);
var controlsToExposeAsCommands = synthDesc.controls.reject { |control|
[
'gate',
'freq',
inControlName,
outControlName, // TODO: was 'out', instead check output_data and filter out correct out argument
'amp_env0', // TODO: test if this actually works
'amp_env1', // TODO: test if this actually works
'pitch_trk0', // TODO: test if this actually works
'pitch_trk1' // TODO: test if this actually works
].includes(control.name)
}.debug(\controlsToExposeAsCommands);
^(
type: type,
inControlName: inControlName,
outControlName: outControlName,
ampenvControls: [],
pitchtrkControls: [],
controlsToExposeAsCommands: controlsToExposeAsCommands
);
}
*retrieveInputControlName { |synthDesc|
^this.detectMonoOrStereoAudioIO(synthDesc.inputs) !? { |iodesc| iodesc.startingChannel }
}
*retrieveOutputControlName { |synthDesc|
^this.detectMonoOrStereoAudioIO(synthDesc.outputs) !? { |iodesc| iodesc.startingChannel }
}
*detectMonoOrStereoAudioIO { |iodescs|
^iodescs.detect { |iodesc|
(iodesc.rate == 'audio')
and:
(iodesc.numberOfChannels < 3)
and:
(iodesc.startingChannel.class == Symbol)
}
}
}
+ SynthDesc {
hasControlNamed { |controlName|
^this.controls.any {|control| control.name == controlName};
}
}
+ ControlSpec {
asSpecifier {
^specs.findKeyForValue(this)
}
}