Fundamentals
Export Targets
Code Export
Patcher UI
Special Topics
RNBO Raspberry Pi OSCQuery Runner
Voice Stealing modes
Create a custom polyphonic voice stealing mode with codebox.
You might want to implement voice stealing modes for your synthesiser patch that @voicecontrol simple
does not offer. For example a voice stealing mode that opts to replace the lowest (or highest) held note with the next incoming note when all voices are currently playing.
Alternatively, you might prefer to replace the most recently played note - or the oldest. You might even want to disable voice stealing altogether so that new notes will not free any voices that are playing when the active voice count has reached the limit.
First we'll create some constants to describe the state of the note controller and the notes within it in a human readable manner, along with masks (in hexidecimal) that will be useful for assigning midi notes to channels later. Next we'll create three @state
variables, the first of which will hold the state of each voice in a 2D array. This makes it easy to find the state of each voice by looking up the first index of the array. Next, we'll create an instance of the midiparse()
operator which will handle parsing the incoming midistream and finally a variable to hold the state of the notecontroller's mode. We use @state
for these to persist them in memory between note streams.
const POLYPHONY = 4;
const MIDINoteStateOff = 0;
const MIDINoteStateOn = 1;
const MIDINoteOffMask = 0x80;
const MIDINoteOnMask = 0x90;
// 0 ... state (1 ... active, 0 ... muted)
// 1 ... note number (MIDI pitch)
// 2 ... last note on time
// 3 ... MIDI channel
@state voiceState = new FixedNumberArray(POLYPHONY, 4);
@state parser = new midiparse();
@state mode = 0;
Now we'll create some functions that will break apart the logic of our note controller into simple chunks. We can utilise the built-in codebox function listin
to handle the voicestatus list from our rnbo subpatcher. When we pass this list into the third inlet we'll search for the index of any recently muted voice in the voiceState array we made and set it to off.
function listin3(v) {
// we only react to notifications when a voice is muted
// since we assume any voice activation will be done by us
let voiceIndex = v[0] - 1;
if (v[1] == 1) {
voiceState[voiceIndex][0] = MIDINoteStateOff;
voiceState[voiceIndex][1] = 0;
voiceState[voiceIndex][2] = 0;
voiceState[voiceIndex][3] = 0;
}
}
We'll next define a function that will use our mode variable to handle some simple logic for each voice stealing mode. We can use a switch
statement to toggle whether we are comparing pitch or time depending on the mode. There are much simpler ways to write this function but verbosity helps to clarify exactly what we're doing.
function stealmode(candidate_ontime, candidate_pitch, target_time, target_pitch) {
let suitable;
switch (mode) {
case 0:
// higher note steals
suitable = candidate_pitch > target_pitch;
break;
case 1:
// lower note steals
suitable = candidate_pitch < target_pitch;
break;
case 2:
// First (FIFO)
suitable = candidate_ontime < target_time;
break;
case 3:
// Last (LIFO)
suitable = candidate_ontime > target_time;
break;
default:
// pass
suitable = false;
}
return suitable;
}
When we send a note off - we'll need to direct the message to the correct target voice in the patcher via the set object with the argument target
connected to the third outlet, then send three raw midibytes out to the midi inlet in line with the midi standard.
function sendnoteoff(target) {
if (voiceState[target - 1][0] != MIDINoteStateOff)
{
// set target
out3 = target;
let channel = voiceState[target][3];
out1 = MIDI_NoteOffMask | channel;
out1 = voiceState[target - 1][1];
out1 = 64; // default note off velocity
voiceState[target - 1][0] = MIDINoteStateOff;
}
}
The following sendnoteon function is the same - except we unmute the voice first using the second outlet. The release velocity of the noteoff function uses a default value - in the case of noteon, we pass it in.
function sendnoteon(target, velocity) {
// send mute state
listout2 = [ target, 0 ];
// set target
out3 = target;
let channel = voiceState[target][3];
// send midi values out
out1 = MIDINoteOnMask | channel;
out1 = voiceState[target - 1][1];
out1 = velocity;
}
As both of the above note on/off functions expect the values in the voiceState array to be set first, we'll create a function that will assign the voices.
function assignvoice(pitch, velocity, channel) {
let sendnoteoff = true;
let target = 1;
// get last on time of first note
let target_time = voiceState[0][2];
// get state of first note
let target_state = voiceState[0][0];
// get the pitch of the first note
let target_pitch = voiceState[0][1];
for (let i = 0; i < POLYPHONY; i++) {
let candidate_state = voiceState[i][0];
// the note is already playing, just route the note on to the voice, and do not send a note off
if (voiceState[i][1] == pitch && candidate_state == MIDINoteStateOn)
{
sendnoteoff = false;
target = i + 1;
break;
}
if (i > 0) {
// only consider this voice if it's off or if the current candidate is on
// so we get the oldest voice that's off or the oldest voice period if all are on
if ((candidate_state != MIDINoteStateOn) || (target_state == MIDINoteStateOn))
{
let candidate_ontime = voiceState[i][2];
let candidate_pitch = voiceState[i][1];
let suitable = stealmode(candidate_ontime, candidate_pitch, target_time, target_pitch);
// either this candidate is older/newer than our current target, or our current target is on
// and this candidate is off (which we always prefer)
if (suitable || (target_state == MIDINoteStateOn && candidate_state != MIDINoteStateOn)) {
target = i + 1;
if (mode > 1) target_time = candidate_ontime;
if (mode < 2) target_pitch = candidate_pitch;
target_state = candidate_state;
}
}
}
}
if (sendnoteoff) sendnoteoff(target);
// add the assigned note to voiceState array
let targetIndex = target - 1;
voiceState[targetIndex][0] = MIDINoteStateOn;
voiceState[targetIndex][1] = pitch;
voiceState[targetIndex][2] = currenttime();
voiceState[targetIndex][3] = channel;
sendnoteon(target, velocity);
}
Lastly, we create a function for incoming noteoff messages to find the voice target in the voiceStatus array - flip the state and then pass along the noteoff message.
function clearvoice(pitch, velocity, channel, status) {
// these are the note-specific messages -- need to find proper target
// and then turn voice off
let target = 0;
for (let i = 0; i < POLYPHONY; i++) {
if (voiceState[i][1] == pitch && voiceState[i][3] == channel && voiceState[i][0] == MIDINoteStateOn) {
target = i + 1;
break;
}
}
if (target > 0) {
// set target
out3 = target;
// send out MIDI
out1 = status;
out1 = pitch;
out1 = velocity;
voiceState[target - 1][0] = MIDINoteStateOff;
}
else {
// note not found -- could have been stolen -- just ignore
}
}
After all of the variable and function declarations above, we now define our main logic that will run each time input is received. We set the mode variable using in2
and read the incoming midi stream from in1
via our midiparse operator instance and pass the first value into a switch
statement. If this value is a zero - this indicates that the incoming stream contains a note event. We determine whether this is a noteon or noteoff using the velocity value - we call the assignvoice function if it is a noteon, the clearvoice function if it is a noteoff.
let midistate = parser.next(in1);
let messageType = midistate[0];
mode = in2;
let status = midistate[midistate.length - 3];
let byte1 = midistate[midistate.length - 2];
let byte2 = midistate[midistate.length - 1];
switch (messageType) {
case -1:
// nothing detected
break;
case 0:
{
// note on or off
let pitch = midistate[1];
let velocity = midistate[2];
let channel = midistate[3];
let isNoteOn = velocity != 0;
if (isNoteOn) {
assignvoice(pitch, velocity, channel);
} else {
clearvoice(pitch, velocity, channel, status);
}
break;
}
default:
// set target
out3 = 0;
// default behavior is just forwarding MIDI to everyone
out1 = status;
out1 = byte1;
out1 = byte2;
}
In the next article, we'll look at creating Monophonic Legato modes to emulate some classic synthesizer effects.
Materials in this article