Voicer
A programmatically-controlled voice-stealing node manager. Written by H. James Harkins, jamshark70@gmail.com
Voicer is not the same as sc2's Voicer.ar! The voicer class itself does not implement MIDI. After you create a voicer, however, you can place it in a VoicerMIDISocket, which includes all the necessary intelligence for note on/off, continuous controllers, and pitch bend messages. Multiple sockets can be created on the same channel with different voicers and different key ranges, allowing complex keysplit configurations to be created.
Features:
Triggering and releasing notes by frequency (no need to remember which node is playing which pitch)
Automatic gating (notes or chords can be played for specific durations--the release is scheduled using a clock of your choice, so if the clock is a TempoClock, you can specify durations in terms of beats very easily)
Arguments can be set aside as global, applying to all nodes simultaneously--great for filter cutoffs, pitch bend, etc.--the voicer's GUI window provides sliders for each global control (which also respond to MIDI, if you have it set up that way)
Easy latency setting for rock-solid timing in sequencing
GUI support based on proxies, allowing easy reuse of GUI objects. See VoicerProxy.
Support (with VoicerMIDISocket) for Instr-miditest and SynthDef-miditest, methods for testing new synths by MIDI. See the helpfile miditest.
As of 21 November 2004, compatible with event patterns (Pbind etc.). See the Sequencing section below.
Voicer presently works with synthdefs and Instr's. You should not send a patch directly to Voicer. Voicer takes the Instr with an argument list and makes its own Patches.
Synthdefs & Instr's to be used with voicer need to have a few specific things:
A variable-length envelope (Env.new with a releaseNode, Env.asr, Env.adsr)
A freq argument, for the frequency that will be triggered. Other frequencies (filter cutoffs etc.) should be specified otherwise.
A gate argument
An EnvGen whose doneAction is 2--the voicer expects that synth nodes will die after notes are released
If you use a SynthDef: an argument called outbus for the output bus index
To wit:
(
SynthDef.new("harpsi", {
arg outbus = 0, freq = 440, gate = 0;
var out;
out = EnvGen.ar(Env.adsr, gate, doneAction:2) *
Pulse.ar(freq, 0.25, 0.75);
Out.ar(outbus, [out, out]);
}).load(s);
)
// or
(
Instr.new([\harpsi], {
arg freq = 440, gate = 0;
var out;
out = EnvGen.ar(Env.adsr, gate, doneAction:2) *
Pulse.ar(freq, 0.25, 0.75);
[out, out]
});
)
Tip: If you want the instrument to be velocity-sensitive, Latch the gate argument to get the velocity:
SynthDef.new("harpsi", {
arg outbus = 0, freq = 440, gate = 0;
var out, amp;
amp = Latch.kr(gate, gate) * 0.5 + 0.5;
out = EnvGen.ar(Env.adsr, gate, doneAction:2) *
Pulse.ar(freq, 0.25, amp);
Out.ar(outbus, [out, out]);
}).load(s);
Creation
*new(voices, things, args, bus, target, addAction = \addToTail)
voices: the maximum number of voices that can be played by this instance
things: what should go into the nodes. If you supply a single thing, each node will use the same synthdef. If you supply an array or other collection, the nodes will cycle through the collection. This is another way to do the sc2 trick of the pattern that plays its successive events on different synthdefs.
args: can be a single argument array, such as [\ffreq, 10000], or an array of arg arrays: [ [\ffreq, 10000], [\ffreq, 15000] ]. If an array, the arg arrays will be referenced using wrapAt and assigned sequentially to the voicer nodes. These arguments will be stored with each node and sent to every synth that gets placed on the server. Argument arrays should always be written [name, value, name1, value1...]—even for Instr's. This is different from the standard crucial usage.
bus: the output bus to use.
target: anything that responds to .asTarget. If you supply a MixerChannel, both the bus and the target will be set appropriately. Default is Server.local.
addAction: where the synths will be placed in relation to the target. (This setting will be ignored by patch nodes.)
// with synthdef (depends on synths defined above):
v = Voicer.new(8, "harpsi"); // 8 voices, all harpsi
// with Instr & MixerChannel:
s = Server.local; s.boot;
m = MixerChannel.new("harpsi", s, 2, 2);
v = Voicer.new(8, Instr.at([\harpsi]), target:m);
// a nested Instr:
(
i = Instr([\harpsi], {
arg freq = 440, gate = 0;
var out;
out = EnvGen.ar(Env.adsr, gate, doneAction:2) *
Pulse.ar(freq, 0.25, 0.25);
[out,out]
});
f = Instr([\test, \rlpf], {
arg audio, ffreq = 500, rq = 0.1;
RLPF.ar(audio, ffreq, rq);
});
)
// If you supply an Instr as an argument, it must be followed by an argument array or nil.
// The Voicer makes a Patch for the inner Instr using the arg array immediately following.
v = Voicer(8, Instr.at([\test, \rlpf]), [\audio, Instr.at([\harpsi]), nil, \ffreq, 5000, \rq, 0.08]);
IMPORTANT: Each note that gets triggered on this last voicer will place two synths on the server: one for the harpsi, and another for the filter. These will all use the same bus, meaning that if you trigger several notes at once, the effect of the filter will be compounded for each note. This may not be the result you want. In general, you should use simple (self-contained) Instr's or synthdefs in a voicer.
With Instr, any argument you supply as a SimpleNumber will be wrapped in a KrNumberEditor so you can change its value later. This is different from Patch. If you want a SimpleNumber to serve as a fixed argument, make a Ref to it: [\fixed_filter_freq, `12500].
clock_(clock)
Now deprecated - the gate method uses thisThread.clock instead of a preset clock.
If you want to do sequencing with this voicer, you should set the clock to a TempoClock, preferably just after creation:
t = TempoClock(144/60); // 144 bpm
v = Voicer(8, "harpsi").clock_(t);
Caveat: If you do this, v.gate will only work within a Routine or Task playing on the same TempoClock. If you expect to want to gate things from the command line, leave the clock as SystemClock (its default).
free
Removes all synth objects associated with this voicer from the server, cleans up its GUI window, and disconnects the voicer from MIDI.
Playing
trigger1(freq, gate = 1, args, lat)
trigger(freq, gate = 1, args, lat)
Trigger a note (trigger1), or several notes (trigger). For trigger1, freq must be a float or int. For trigger, freq can be a single value or a collection. If you are triggering several notes, args can also be a collection of argument arrays e.g. [ [\filter, 300], [\filter, 500], [\filter, 700] ]. Each set of args will be sent in succession to the nodes as they're triggered (using wrapAt). In this case, the first node triggered will get \filter = 300, the second \filter = 500, etc.
lat is the Server latency to use for this event. The following values are allowed for all methods with a "lat" argument:
< 0 Negative number: use the Voicer's default latency (set by myVoicer.latency = 1)
>= 0 Non-negative number: use this number as the latency for this event
nil No latency: Server will play the message as soon as received
Returns the node played, or a collection of nodes played.
release1(freq, lat)
release(freq, lat)
Find the earliest-triggered note(s) with the frequency/frequencies given and send [\gate,0] to it. release1 allows only one frequency to be given; release works with either a single value or a collection. If there is no node with a given frequency, that frequency is ignored.
Returns the node(s) released.
v = Voicer.new(8, "harpsi"); // uses Server.local
f = Array.fill(5, { 1000.0.rand + 50 });
v.trigger(f); // play 5 notes
v.release(f); // release the same
gate1(freq, dur, gate = 1, args, lat)
gate(freq, dur, gate = 1, args, lat)
Triggers the notes and schedules their releases. If freq is a collection, dur and args may be the same thing for every node, or you can supply collections to have different arguments and different release times for each node.
Returns the node(s) played.
f = Array.fill(5, { 1000.0.rand + 50 });
// listen to the notes stop one by one
v.gate(f, Array.fill(5, { arg i; 2*(i+1) }));
releaseNow1(freq, sec)
releaseNow(freq, sec)
Uses a negative gate to cause an instant release. Sec determines how long the release takes. Unlike gate, you may not supply a collection for sec. (Sec is converted to the negative gate by sec.abs.neg-1)
set(args, lat)
Sends the same /n_set message to each node in this voicer. Global controls (see below) send the value to the associated kr bus.
Controlling the voicer's behavior
panic
Stops all active synths belonging to this voicer immediately.
trace
Sends the n_trace message to all active synths. Useful for debugging synthdefs. You can also trace a single playing node using the following:
aVoicer.playingNodes.choose.trace;
gui
Makes a window showing all global controls (see mapGlobal) and processes (see addProcess).
Voicer-gui uses a proxy system so that you can display different voicers without incurring the overhead of removing views, resizing the window, adding views, and resizing again. For normal use, this process is transparent to the user. To change the voicer shown in a particular GUI, drag an expression that evaluates to the voicer (usually a variable name) into the drag sink immediately to the right of the voicer GUI label.
mapGlobal(name, bus)
unmapGlobal(name, bus)
Makes an input to the synth global, by using the supplied kr bus (or creating a new one if no bus is supplied) and mapping the input to that bus in each voicer node. Newly triggered voicer nodes will be mapped automatically. This lets you control filter cutoff frequencies, pitch bends, etc. globally for all nodes in a voicer. A kr synth can be played on the bus to provide an LFO.
(
i = Instr([\harpsi], {
arg freq = 440, gate = 0;
var out;
out = EnvGen.ar(Env.adsr, gate, doneAction:2) *
Pulse.ar(freq, 0.25, 0.25);
[out,out]
}/*, [\freq, \amp]*/);
f = Instr([\test, \rlpf], {
arg audio, ffreq = 500, rq = 0.1;
RLPF.ar(audio, ffreq, rq);
});
v = Voicer(8, f, [\audio, i, nil, \ffreq, 5000, \rq, 0.08]);
)
// globalize the filter cutoff
b = v.mapGlobal(\ffreq);
(
SynthDef.new("SinLFO", { // sinewave lfo
arg outbus, freq = 1, phase = 0, mul = 1, add = 0;
ReplaceOut.kr(outbus, SinOsc.kr(freq, phase, mul, add));
}).load(Server.local);
)
l = Synth.new("SinLFO", [\freq, 0.2, \mul, 500, \add, 1400, \outbus, b.index]);
// all notes have the same filter LFO
v.trigger([60, 64, 67].midicps);
v.unmapGlobal(\ffreq); // LFO stops
v.mapGlobal(\ffreq, b); // set LFO to bus (which is still active)
v.release([60, 64, 67].midicps);
l.free;
addProcess(states, type)
Adds a VoicerProcessGroup to this Voicer. The group will be displayed as a button or pop-up menu in the voicer's GUI. For a button, specify type as \toggle in the addProcess message. See the VoicerProcessGroup help file for the correct syntax for states. Returns the new process group.
This allows you to add graphically triggered sequencers and other processes directly to the voicer GUI.
VoicerProcessGroups belong to the voicer's proxy, not to the voicer itself. This means you can change the voicer that is to play the sequence while the sequence is playing (provided both voicers use the same clock as the sequence). As with Voicer-gui, under normal circumstances this is transparent to the user.
VoicerProcesses and VoicerProcessGroups are deprecated and no longer maintained.
removeProcess(p)
Stops the process group if playing and removes it from the voicer and GUI.
stealer_
Chooses the algorithm the voicer uses to find the next node to play. Your choices are:
\preferLate: prefers nodes that were more recently played
\preferEarly: the default setting; prefers nodes that were played longer ago
\random: chooses a non-playing node at random
\cycle: cycles through the nodes in sequence, skipping nodes that are playing
\strictCycle: cycles through the nodes in sequence, always in order whether they're playing or not
latency_
Sets the default latency for this voicer. Default latency should be a positive number and can be overridden using the lat argument in trigger, release, gate, and set method calls.
Sequencing
Voicer adds two new events that can be used with event streams (Pbind):
\voicerNote: an alternate event type in the standard event framework, except that instead of including \instrument, \synthdefName in the Pbind, you should include \voicer, myVoicerObject.
v = Voicer(10, \default);
(
p = Pbind(\degree, Pseq((0..7), inf),
\delta, 0.25,
\sustain, Pwhite(1, 9, inf) * 0.25,
\amp, Pwhite(0.001, 0.15, inf),
\argKeys, #[\amp], // leave this out and the amp stream will be ignored
\voicer, v,
\type, \voicerNote // leave this out and it will try to play on \instrument, default
).play;
)
p.stop;
\voicerMIDI: a completely separate event prototype optimized for working with MIDIRecBuf. Not as flexible for general use, but easier for MIDI.
v = Voicer(10, \default);
k = VoicerMIDISocket(0, v);
m = MIDIBufManager(chan:0);
m.gui
m.initRecord; // recording starts when you start playing
m.stopRecord; // run this at the exact time you want the buffer to start looping
p = Pbind(
\note, Pseq(m[0].notes, inf),
\voicer, v,
\latency, 0.5,
\midi, true // note frequencies are MIDI note numbers...
// if true, the event will convert them to Hz -- true is the default
// if you already converted them, set this to false
).play(protoEvent: Event.makeProto(\voicerMIDI));
// note above how you retrieve the voicerMIDI event prototype
p.stop;
You may also override note parameters individually: \freq is midi note number, \delta is \delta, \length corresponds to \sustain, and \gate corresponds to velocity.
p = Pbind(
\note, Pseq(m[0].notes, inf),
// replace with different note numbers, but leave the rhythm alone
\freq, Pwhite(48, 78, inf),
\voicer, v,
\latency, 0.5
).play(protoEvent: Event.makeProto(\voicerMIDI));
p.stop;
p = Pbind(
\note, Pseq(m[0].notes, inf),
// round the original note's rhythmic value to nearest 1/32
\delta, Pfunc({ |event| event[\note].dur.round(0.125).max(0.125) }),
\voicer, v,
\latency, 0.5
).play(protoEvent: Event.makeProto(\voicerMIDI));
p.stop;