Learn Voice Stealing modes

Getting Started

Welcome to RNBO

Quickstart

RNBO Basics

Key Differences

Why We Made RNBO

Fundamentals

Audio IO

Messages to rnbo~

Using Parameters

MIDI in RNBO

Messages and Ports

Polyphony and Voice Control

Audio Files in RNBO

Using Buffers

Using the FFT

Export Targets

Export Targets Overview

VST/AudioUnit
Max External Target
Raspberry Pi Target
The Web Export Target
The C++ Source Code Target

Code Export

Working with JavaScript
Working with C++

Codebox

Getting Started with Codebox

Understanding Storage: let, const, @state, @param

Building a Note Controller with Codebox

Building a Note Controller with Codebox

Voice Stealing modes

Monophonic Legato modes

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.

Building-a-Note-Controller-With-Codebox-01.png

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