here is a more extensive modification to PolyPerc (now renamed), wherein the current number of playing voices is queried before starting a new voice. if voice count is > 16, new note is ignored. (the max voice count is a classvar, which is like a static class member in C++.)
(because of the asynchronous and split-process structure of supercollider language/audio server, this is a little more complex than one might hope for - it requires setting up a query/response pattern within the engine.)
Engine_PolyPercLimited.sc
// CroneEngine_PolyPercLimited
// pulse wave with perc envelopes, triggered on freq
// now with voice limit
Engine_PolyPercLimited : CroneEngine {
classvar maxVoices = 16;
var server;
var pg;
var amp=0.3;
var release=0.5;
var pw=0.5;
var cutoff=1000;
var gain=2;
var pan = 0;
// to limit polyphony, we'll have to query the server for node count.
// this is async, so we need to store requested hz value and set up a responder.
var hz;
var queryTreeResponder;
*new { arg context, doneCallback;
^super.new(context, doneCallback);
}
playNote { arg hzArg;
hz = hzArg;
// send a queryTree message to the server; spawn a synth on response
server.sendMsg('/g_queryTree', pg.nodeID);
}
alloc {
server = Crone.server;
pg = ParGroup.tail(context.xg);
// set up a responder for node tree queries
queryTreeResponder = OSCFunc({ arg msg;
var numChildren = msg[3];
postln(numChildren);
if (numChildren < maxVoices, {
var synth;
synth = Synth("PolyPerc", [
\out, 0,
\freq, hz,
\pw, pw,
\amp, amp,
\cutoff, cutoff,
\gain, gain,
\release, release,
\pan, pan
], target:pg);
}, {
postln("too many notes!");
});
}, '/g_queryTree.reply');
SynthDef("PolyPerc", {
arg out, freq = 440, pw=pw, amp=amp, cutoff=cutoff, gain=gain, release=release, pan=pan;
var snd = Pulse.ar(freq, pw);
var filt = MoogFF.ar(snd,cutoff,gain);
var env = Env.perc(level: amp, releaseTime: release).kr(2);
Out.ar(out, Pan2.ar((filt*env), pan));
}).add;
this.addCommand("hz", "f", { arg msg;
var val = msg[1];
this.playNote(val);
});
this.addCommand("amp", "f", { arg msg;
amp = msg[1];
});
this.addCommand("pw", "f", { arg msg;
pw = msg[1];
});
this.addCommand("release", "f", { arg msg;
release = msg[1];
});
this.addCommand("cutoff", "f", { arg msg;
cutoff = msg[1];
});
this.addCommand("gain", "f", { arg msg;
gain = msg[1];
});
this.addCommand("pan", "f", { arg msg;
postln("pan: " ++ msg[1]);
pan = msg[1];
});
this.addCommand("freeAllNotes", "", {
// immediately free all currently sustaining notes.
// this simply ignores any currently sustaining envelopes, so will cause clicks.
pg.freeAll;
});
}
}
some debug statements have been added to the engine and it has been refactored a bit.
test script. similar, but slightly slower release and we now attempt to create a fast sequence of 10k notes.
polyperc_test_2.scd
e = Engine_PolyPercLimited.new(Crone.context, {});
// helper to send a command to the engine with one argument.
~com = { arg name, value; NetAddr.localAddr.sendMsg(("/command/" ++ name).asSymbol, value); };
// set medium longish release time
~com.value('release', 8);
// make a huge number of notes
// engine changes should limit polyphony
Routine {
10000.do {
arg i;
var hz = 330 * ((i*7)%5 + 1)/((i*9)%7 + 1);
var t = 0.2 * ((i*8)%7 + 1)/((i*11)%9 + 1);
[t, hz].postln;
~com.value('hz', hz);
t.wait;
};
}.play;
notice that the pattern plays the first 16 notes without pause, and subsequent note requests are ignored until older voices have died out.
probably the next step would be to add a non-periodic voice-count poll from the engine back to lua, allowing the script to perform voice allocation / ignore logic.
another approach would be to completely rearchitect PolyPerc to be paraphonic: basically, have a fixed number of static voice instances that are retriggered, and have a parallel set of commands that accepts voice ID in addition to new note frequency / other params.