Polyvoice 2.0 is a complete, multichannel, polyphonic voice controller, supporting multiple channels inside a single poly~ object. With it, each voice in the poly~ object can receive different channel messages, in the same way as in multichannel MIDI keyboards and GMIDI 1.0 instruments. Any channel can play any number of voices in a single poly~ object. Each note issued to the poly~ object can make a different sound, simply by sending it to a different channel.

Polyvoice Multi Internals
Polyvoice Multi Internals

with Polyvoice 2.0, changing a channel voice parameter (say for example a filter cutoff) instantly changes the filter cutoff for all voices in the poly~ object which are on that channel, but does not change the cutoff for voices on other channels. Furthermore, the Ula design also sends preset changes upon new note-on events, without affecting existing playing notes. In MIDI terms, these are program changes, and the program changes affect new notes on a channel, but leave existing notes playing the old program until the old note ends, while simultaneously, controller changes are still instantaneous for all voices on the same channel. This is the same behavior as expected for multichannel synthesizers conforming to the GMIDI 1.0 specification.

The Polyvoice controller also contains its own voice assignment algorithm, sending new notes to the voice with the least-recently-released note by default, ensuring that new notes do not clip the release phase of playing notes when voice stealing.

The design was created on a Windows 7 64-bit workstation, in Max v6.13.

Inputs

Message Format Description
voices int Sets the number of voices across which allocation is managed, which may be any positive integer above 0. This parameter must be set prior to starting operation and has no default value. If set to 1, the allocator assigns any new incoming notes to the single available voice after creating a pitch-off message and gate-off message for that voice. When voice count changes, this allocator implements a conservative policy and turns off all playing notes, so a voice message also functions as a reset.
[chan pitch velocity]

All received messages starting with a number will be interpreted as note on/off messages for a voice on the specified channel. These messages must be three integers in the order channel pitch velocity.

There is no need for note-off commands for every note-on message; after all voices are allocated, new note-on messages will simply be assigned to the oldest voice. This version performs no checks that received values are valid.

chan Sets the channel for the note, in the range 1 to 16.
pitch Sets the pitch for the note, in the range 0 to 127.
velocity Sets the velocity for the note, in the range 0 to 127. Messages with velocity zero are interpreted as note-off commands.
Channel parameters

This group of messages sets filters, transposition, and velocity scaling for all notes on a specific channel. Changes take effect on the next received note for that channel. Commands are case sensitive.

Pilo chan pitch Sets the low pitch for the specified channel, 0 by default for all channels. Gate-on messages below the low pitch will be filtered out and discarded. Gate-off messages are still passed, in case a gate-on message was received for a lower pitch prior to changing this setting.
Pihi chan pitch Sets the high pitch for the specified channel, 127 by default for all channels. Gate-on messages above the high pitch will be filtered out and discarded. Gate-off messages are still passed, in case a gate-on message was received for a lower pitch prior to changing this setting.
Pshift chan pitch Sets a transposition applied to notes for the specified channel on the poly~ output.
RST Resets Pilo, Pihi, Pshift, Vilo, Vihi, Volo, and Vohi to default values for all channels. The command name is case sensitive.
Vilo chan vel Sets the low input velocity for the specified channel, 0 by default for all channels. Gate-on messages below the low velocity will be filtered out and discarded. Gate-off messages are still passed, in case a gate-on message was received for a lower velocity prior to changing this setting.
Vihi chan vel Sets the high input velocity for specified channel on the poly~ output. Vihi is 0 by default for all channels. Gate-on messages below the low input velocity will be filtered out and discarded. Gate-off messages are still passed, in case a gate-on message was received for a lower velocity prior to changing this setting.
Volo chan vel Sets the low velocity for the specified channel on the poly~ output, 127 by default for all channels. The allocator scales output velocities so the quietest gate-on messages will be set to this velocity.
Vohi chan vel Sets the high velocity for a channel output, 127 by default for all channels. The allocator scales output velocities so the loudest gate-on messages will be set to this velocity. Output velocities between Volo and Vohi are scaled linearly from the input range set by Vilo and Vihi.

 

Outputs

Outputs Format Description
Note Output [channel pitch velocity] Intended for display and MIDI control, these note messages are in the same format as note-message inputs. The allocator generates additional gate-off messages on this output for deallocated voices when the allocator is full, as well as gate-off messages for all active notes when the object is reset.
poly~ Interface Connects directly to a poly~ object for setting the channel, pitch, and gate for each instance, individually. Future implementations may vary pitch and gate values to support detuning and volume scaling of unisono voices. The allocator creates messages for the poly~ object as follows:
voice [c|p|g] value A value sent to the voice on the specified channel, where voice is the poly~ instance to receive data, or 0 if the data is for all voices. This is intended to set a single voice's channel, pitch, or gate.
voice c chan Sets the voice to the channel value, which may be 0-16. Initialization the voice is set to 0 (unallocated) by default. Subsequently, channel messages for the specified channel will be received by this voice instance. This is implemented instead of TARGET messages so that channel messages may be received by all instances in a poly~ which are set to that channel. On channel changes, the parameters for that channel can then be sent to the specific instance. Messages to set a channel for a voice are always followed by a pitch or gate message for that voice. To reduce message load on channel changes, the allocator only creates channel messages when the allocator is about to send a pitch or gate value to a voice which was previously on a different channel.
voice p pitch Sets the voice to the specfied pitch, in the range 0.0~127.0 (float). Pitch messages to polyphonic voices are always followed by a gate message. Future implementations, which may support monophony on each channel, may send pitch messages without gate messages to change the pitch during multi-fingered play.
voice g velocity Sets the voice to the specified velocity, a float in the range 0.0~1.0. If the gate value is zero, the allocator does not send a pitch message to the voice beforehand.
voices {integer} Sets the voice count of external objects. Before sending this message, Polyvoice sends note-off messages for all playing notes. When a poly~ instance receives this message, it creates new instances of poly~ if the number of voices are changed.
Channel Preset Signal voice c chan Provides channel-based preset control of individual voices, in the same format as sent to poly~. The allocator only generates this message when a poly~ instance has just been set to a new channel. Presets for each channel may then set the poly~ instance for that voice to new settings, so that preset changes during a performance only affect new notes.
Dump Output

Provides a set of lists indicating the object's internal state. The dump output may be connected to a route object to select specific fields, as follows.

Polyvoice Dump Ouput
Polyvoice Dump Ouput

The Dump output provides the following data upon reception of any message on the input, after any channel output message generated:

vcnt int A single number indicating the total number of voices which are currently playing.
vmax int A single number indicating the total number of voices set in the allocator.
ages [list] A list of the current age of each output voice, in order of voice number. 1 is the newest age, and the oldest age will be the same as the vmax value.
chan [list] A list indicating the current channel number for each voice, in order of voice number. The range is from 1 to 16, depending on received pitch messages.
vels [list] A list indicating the current velocity for each voice, in order of voice number, in the range 0~127, depending on received messages. Off voices will have a velocity of zero.
ptch [list] A list indicating the current or last pitch for each voice, in order of voice number, in range 0~127, depending on last received messages. The pitch is kept in the allocator after the voice is turned off, so notes which have already been turned off will still show their previous pitch in this list.

The Complex LRU Voice Allocation Algorithm

The simplest voice allocator, as for the default implementation in the Max poly~ and adsr~ objects, assigns voices cyclically. This is quite adequate if there is sufficient polyphony, and/or notes are of the same length. But restrictions on available processing power, or large scores with many notes of differing durations, can cause computational resources to be overwhelmed, and/or undesirable note clipping.In such cases, for example, fast runs of short notes truncate sustained notes. Imagine for instance there are three voices, and one is sustained while playing three more short notes; the fourth note is then typically assigned to the long-playing note, causing it to clip, as it is the next voice in cyclic order, even though other voices have been freed as the prior short notes already ended.

A simple LRU allocator cyclically assigns new notes to voices for which the note is turned off--unless all notes are playing, in which case the voice may be stolen cyclically from a voice with a playing note. Such simpler allocators work well if all notes are the same length, but can still cause notes to clip in the release phase. This is because a note message may have turned off the note, but with a classic ADSR envelope, the note may still be sounding in its release phase. If the notes are not of the same length, the stolen voice, in numeric cyclic order, may not be the oldest voice. Suppose for instance a long note is played, followed by two short notes, then a long note, on a four-voice system. Then, if the first long note is released, it is the next in cyclic order, but if selected next, its release phase will be clipped more than if one of the two short notes, that have also already been turned off by note-off messages, were selected.

To choose the best possible voice, the allocator can choose a better voice assignment by maintaining a history list of notes, and reordering the priority list whenever notes are released, which is what this patch does. This allocator implements the methodology in a four-phase assignment algorithm:

  • When receiving note-off messages, the allocator searches the playing voice list and, if the voice was already stolen for another note of a different pitch, discards the note-off message, rather than issue a pitch-off message to a voice which has already stopped playing that pitch.
  • If there are available voices which have not yet been played at all since initialization, they are selected cyclically. If all voices are played, it shifts to the next phase.
  • The voice is selected from the pool of voices which have played and been turned off, and from that set, the oldest voice is selected which was played but has already ended, regardless the order of note-release messages. That ensures that each voice obtains the longest possible release phase. If all voices are playing, it shifts to the next phase.
  • If all voices are playing, the oldest playing voice is taken for new notes. The allocator issues pitch and gate-off messages to the playing voice for the prior note, before sending new pitch and gate-on messages to it.

The poly~ Interface

To support GMIDI-style operation, the poly~ object needs to parse three types of input messages: messages for the voice, and messages for the channel. The voice messages set the channel for the voice, as well as its pitch and velocity. The channel messages may change all the parameters for the channel, or just a controller change. The following diagram illustrates a simple way for the poly~ object to support both kinds of messages.

Ula Poly~ Example
Ula Poly~ Example

When the poly~ instance starts up, loadbang unmutes the instance though thispoly~. This also causes thispoly~ to output its instance number. The instance numbr is set on the first router, so messages to its voice are only received by its instance. Messages to set the poly~ to a different channel are parsed by the second router, which sets the second input on the first router to receive channel control parameters sent to cx, where x is the channel number.

As well as note on/off messages, this design subsequently handles two kinds of messages: controller messages sent to all voices on a particular channel, and messages sent to a specific voice to change all its parameters at once, for program changes.

The Channel Interface

The following control path provides supports both program changes for later notes and instantaneous controller changes for channel 1.

  • The three inputs on the right are from panel controls for channel 1: when they are changed, the channel sends a c1 [list] of the channel parameters to all instances of poly~ which have been set to channel 1, via the ULA ring messaging protocol (described earlier on this site).
  • The single input on the left receives a channel message from Polyvoice via the Ula ring. The message is here unpacked. If the message's third parameter states a new voice is using channel 1, then this appends a packed list of all channel parameters to the supplied voice number and sends it to poly~ via the ULA ring. The poly~ instance which has been assigned to a new channel then is updated with all parameters at once.

Overall the process is complex, as for each note, as not only must the poly~ instance be given a channel number, but also it must load parameters for that channel and get updates when they change.

Ula Channel Control
Ula Channel Control

The Ula ring messaging system provides a way to sequence all of the events and observe them in the Max console window in the exact order as they occur, because all the messages are routed through the same ring. The following example shows the sequence of events. There are eight steps:

  1. First, the keyboard is sending a note on channel 2, of pitch 76 and velocity 110, to the arpeggiator (in the 'Arp' module).
  2. The arpeggiator then forwards the same note message to Polyvoice (which is in the 'Multi' module).
  3. Polyvoice internally allocates voice 1 to the note, which wasn't previously playing channel 2, so it sends a message to the 1st instance of poly~ commanding it to be part of channel 2.
  4. Then Polyvoice sends the same message to the channel 2 controller (in the 'Waves2' module) to let it know that channel 2 now controls voice 1.
  5. The Waves2 module immediately sends a complete list of synthesizer parameters to instance 1 of poly~ so it sounds the same as other synthesizers on channel 2.
  6. Polyvoice then lets the keyboard know it can highlight the appropriate key as playing...
  7. before finally sending the pitch-on...
  8. and gate-on messages to voice 1 in poly~.

Ula 1.2 Event List
Ula 1.2 Event List

Examples

Yofiel is providing three examples of Polyvoice in the public domain as Max patches.

Polyvoice 1

The Polyvoice 1 zipfile in the Cycling74 toolbox contains the main patch, and a demo poly~ patcher instance. It simply illustrates the LRU allocator and does not implement multiple channels. The example contains the allocator in one flat subpatch, with no UI or preset values. The design provides a simple panel with small glue I/O subpatchers for the kslider object, monophonic ADSR controls, a number box to change the number of allocated voices, and a reset button.

kslider Adapter

Polyvoice is supplied on the cycling74 site with one adapter for the kslider object. The allocator also synchronizes the keyboard display with its internal state. When the allocator turns off notes, the keyboard display also turns off the notes.

Additional adapters are available.

Polyvoice Duo

Polyoice Duo expands on the Polyvoice LRU allocator by providing two independent channels in one poly~. The input is from the kslider object, which is also updated as notes are taken for other voices.

  • In SPLIT mode, notes below the split pitch go to channel 1. Notes above the split pitch go to channel 2.
  • In UNISONO mode, each incoming note triggers a note in both channels.
  • Notes can also be sent either to channel 1 or channel 2.

Changing channels in the panel does not clear the allocator of playing notes, so switching modes can leave notes sustained while playing others.

Future Possibilities

It is possible to extend this design to support other polyphonic voice assignments. For example, when playing the same note repeatedly on a piano sound, it may actually be desirable to truncate the notes, for which the best algorithm is most-recently-used (MRU), rather than LRU. Other options can be to keep the highest or lost playing note. For monophony, it is also desirable to maintain voice history in the design, so if for example a musician holds one key down while tapping others, the held key causes new pitch. Optionally, one envelope, for example a filter envelope, can trigger when more than one note is sounding; or, the amplitude envelope may not retrigger unless all notes are released and a new note played.While the patch contains the design necessary for voice history, such alternate polyphonic assignment algorithms and selective envelope gating methods are not directly implemented.

Version History

  • Version 2a, 1pm 10/24/2013 - Cleaned up presentation and added notes.
  • Version 2.0 adds support for any number of voices on up to 16 channels in a single poly~. Version 2.0 also adds parameterized multichannel support, with pitch input range, velocity input range, and output velocity scaling settable independently for each of the 16 channels. Version 2.0 also adds three separate outputs, one of which provides note data for display update, one of which connects directly to a poly~ object, and one of which provides a dump list of the multichannel data state after each received message. The object does not require any externals.