SHARING: poly object in gen codebox
When assigning polyphonic voices in codebox, additional control and external signal assignments are possible, because the internal pitch/gate/age data arrays and methods are editable. For example, the voice age is accessible for devices such as arpeggiators. Also, the exact manner of voice stealing, voice clear, and voice-count change are directly controllable.
This example patch shows stealing with corrected-least-recently used (CLRU) voice replacement. Upon overflow with a CLRU algorithm, new notes after overflow are less likely to truncate any playing voices that are gated off, but still in any envelope release phase. Internally, three separate pitch, gate, and voice-age arrays are ordered by voice number, and stored in a buffer~ object for external inspection and access.
As with the original poly object, positive integers on the first MIDI-pitch input create a MIDI note-on or note-off event, depending on the second input's MIDI-velocity value.
0 on the first input causes the object to issue GATE-OFF events for any playing voices, then resets the object's internal data.
It has four other enhancements, described below the screenshot
Dynamic voice-count changing. A negative value on the first input sets the number of voices, for example, -64 sets the number of voices to 64. after reset and GATE-OFF events for any playing voices. When setting the number of voices with the first input, the second input is ignored. Note, if the voice count on other objects is higher, the GATE-OFF events can simply trigger a note-off event on other objects, allowing playing voice amplitudes to decay slowly. However, If lowering the voice count on other objects, some playing voices may click off.
Direct kslider input: This gen implementation interfaces directly with a kslider object in poly mode. With the kslider object, velocities for note-on events are 1 at the bottom and 127 at the top of the keyboard. Therefore the current implementation subtracts velocities in the 1~127 range from 128, also scaling velocity values to 0-1 to gated output amplitude on the third output. Then the MIDI note frequency (in Hz) is output on the second output, then finally, the first output sends a voice number for the new voice-on or voice-off event. Voice-off events are the same as voice-on events, except the third output has a value of 0.
kslider fix, to turn off notes after voice overflow. On overflow, a fourth output sends the pitch for a MIDI note-off event just prior to the voice-on event, or 0 if there is no overflow. You can pass this value through a "SET $1 0" messagebox to create an input event for the kslider object.
Connection to mc.gen~ inputs. To send the gen outputs to a mc.gen~ object's frequency and gate inputs, the example packs the voice, frequency, and gate outputs together, then uses a "SETVALUE $1 $2" message to set a voice's frequency, and a "SETVALUE $1 $3" message to set a voice's gate.
Here is the first demo patch:
On reset, the allocator primes the age buffer, empties the others, and sets static variables to default values. VCNT is the maximum number of available voices (edit as desired) and NUMV is the number of active voices (default 0).
Data pch(257), vel(257), age(257);
History numv(0), vcnt(257);
if (in1 <=0){
...
for (i=1; i<=vcnt; i+=1){
vel.poke(0, i);
pch.poke(0, i);
age.poke(i, i);
numv = 0;
}
Note, gen can only output one event for each input event, so multiple voice-off events for a single reset signal is not possible without enabling the metro . However, enabling the metro also prevents the gen object from receiving more than one input event in any metro interval.
On note-on events, the allocator first checks to see if all voices are on for overflow handling. Then it increments the age of all voices by one and puts the new note, with age of 1, in the oldest slot. If the note is not found, aoff remains 0 and nothing happens.
if(in2 >0){
for(i=1;i<=vcnt; i+=1){
out3 = in2;
out2 = in1;
x = age.peek(i);
if(x==vcnt){
if(numv==vcnt){
...(//kslider fix code)...
}
}else{
numv +=1;
}
age.poke(1, i);
pch.poke(in1,i);
vel.poke(in2,i);
out1 = i;
}else{
age.poke(x+1,i);
}
}
}
On note-off events, the allocator looks for a note with the same pitch. If found, it sets its age to the current number of voices, decrements numv, and shuffles older notes up in the age queue.
of (in2==0){
for(i=1;i<=vcnt; i+=1){
if(pch.peek(i) ==in1){
aoff = age.peek(i);
age.poke(numv, i);
pch.poke(0,i);
vel.poke(0,i);
numv -=1;
out4 = 0; //kb fix
out3 = 0; //vel 0
out2 = in1; //fr off
out1 = i; //voice off
// break;
}
}
for(i=1;i<=vcnt; i+=1){
x = age.peek(i);
if(x>aoff && x<numv){
age.poke(x+1);
}
}
}
Note, when the kslider object receives a note-off message while the user is pressing an on-screen note, it resends the last note-on event. While this could be useful for implementing aftertouch, currently the implementation filters out consecutive note-on events with the same pitch value using a History object called p0 to store the last received pitch:
History p0;
...
if (in1 <= 0) {
...(reset routines)...
} else if (in1 != p0 && in2 >0) {
...(voice-on routines)...
p0 = in1;
} else if (in2 == 0) {
..(voice off routines)...
p0 = 0;
}
Without the filter on consecutive note-on events with the same pitch, the kslider sets all the output voices to the same pitch while the mouse is clicked and dragged on it. With this filter, the user can click and drag on the kslider object to turn on multiple neighboring notes,. Whenever the voice capacity overflows, the replaced notes are immediately turned off on the kslider object.
You can change the output scaling in the first two statements of a note-on event (also there is an mtof() conversion in the reset routine):
... }else if (in1 != p0 && in2 >0) {
out3 = 1 -(in2/127); //gate out 0-1
out2 = mtof(in1); //fc out, Hz
... }
The allocator can contain multiple voices with the same pitch. Currently, NOTE-OFF messages turn off all voices with the same pitch. You can change this behavior by uncommenting the BREAK statement at the end of the note-off routine.
...
}else if (in2 ==0) {
...
// break;
}
If a voice has already been stolen, note-off messages for are simply discarded.
On overflow, this implementation does not add and latency to voice-on events after overflow. If you overflow the voice capacity, then this example may create an audible click on the retriggered voice, which you are free to fix in gen.
The second demo simplifies the top-level patch by using Data() rather than Buffer~ objects in gen:
When changing the number of voices, mc.*~ objects require an audio restart, with a DEFERLOW on the START message immediately after the STOP message. When changing the number of voices in the gen object, but not changing the number of voices in the patch's mc.*~ objects, audio restart is not required.
Limitation: without switching gen to active metro, I don't think it can output more than one event at a time. but with active enabled, I think more than incoming event within the same interval are ignored. At least that's how I think it works, but it's not specified in the documentation.
This simpler demo, updated July 13, replaces the Buffer() objects with internal Data arrays in gen. Also, it uses kslider FLUSH to send multiple voice-off messages on reset and voice count change. Additionally, it adds a 'SEL 0.' object on the gen object's first output, to block zero messages when the allocator receives note-off messages for pitches that have already been turned off. Finally, it outputs midi pitch rather than frequency,
The gen code is as follows:
Data pch(257), vel(257), age(257);
History numv(0), p0(0), g0(0), vcnt(64);
x, aoff = 0;
if (in1 <=0){ // RESETS
if (in1 <0){
vcnt = neg(in1);
}
for (i=1; i<=vcnt; i+=1){
vel.poke(0, i);
pch.poke(0, i);
age.poke(i, i);
}
p0 = 0;
numv =0;
}else if (in1 != p0 && in2 >0) { // NOTE ON
out3 = 1 -(in2/127); //gate out 0-1
out2 = in1; //fc out, Hz
for(i=1;i<=vcnt; i+=1){
x = age.peek(i);
if(x==vcnt){
if(numv==vcnt){
if(vel.peek(i)>0){
out4 = pch.peek(i); //pkb fix
}
}else{
numv +=1;
out4= 0;
}
age.poke(1, i);
pch.poke(in1,i);
vel.poke(in2,i);
out1 = i;
}else{
age.poke(x+1,i);
}
}
p0 = in1;
} else if (in2 ==0){ //NOTE OFF
for(i=1;i<=vcnt; i+=1){
if(pch.peek(i) ==in1){
aoff = age.peek(i);
age.poke(numv, i);
pch.poke(0,i);
vel.poke(0,i);
numv -=1;
out4 = 0; //kb fix
out3 = 0; //vel 0
out2 = in1; //fr off
out1 = i; //voice off
for(i=1;i<=vcnt; i+=1){
x = age.peek(i);
if(x>aoff && x<numv){
age.poke(x+1);
}
}
break;
}
}
p0 = 0;
}
When changing the number of voices in the gen object, but not changing the number of voices in the patch's mc.*~ objects, audio restart is not required. However, when changing the number of voices on mc.*~ objects, there must be an audio restart, with a DEFERLOW on the START message immediately after the STOP message. Note, restarting audio can causes error on DirectSound drivers, depending whether Max is already started when the patch is opened.
Very cool!
Thanks, Graham. Can we mute mc.*~ voices with messages directly in some way, equivalent to 'MUTE X X' for poly~ ?
The best I can figure out is including an mc.noteallocator~ object on the gen object output and setting voices for the internal note allocation system with mpe messages, and enabling the internal busy map.
Ran into a problem. Can't assign voices with mpeevent, the mc.noteallocator~ still uses its own voice assignment. Verified it works on Macs though.
Polyphonic voice handling can be done in gen~, but there isn't a general solution, and there are a few subproblems to figure out, e.g.:
- First, how will you get new note events into a param or signal format that gen~ can respond to?
- The first thought might be a [param pitch] and [param gate] or [param velocity], and/or maybe a [param duration], but what if you wanted to trigger two notes in a row with the same pitch? Do you need a separate [in 1 trig] to start a new note? Every one of these variants means patching it in a slighty different way.
- For a polyphonic patch, how do you assign voices? Does every new event go to the next available (non-busy) voice? Or do events with the same pitch not trigger a new note? What if all voices are busy, does it replace a playing voice (and if so, which? The first, the last, the highest, the lowest, etc.?) Again, each of these requires a different kind of patching solution.
- If you want to handle chords it gets even trickier, since an [in] can only handle one event per sample, and a [param] will only handle one change per vector size... One possible workaround is to feed events by writing into a circular buffer~, but that's another layer of complexity...
---
Here's an example of a relatively simpler event allocator -- new events are triggered by [in 1 trig], and go to the next available non-busy voice. If all voices are busy, the event is ignored. New events sample [params] for their configuration (e.g. duration & envelope shape in this patcher). So, it cannot play synchronous chords, has no voice-stealing, can play the same pitch twice overlapping, and assumes the duration is determined when the event starts. Because triggers are audio rate it could get fairly granular, though at some point you might want to add sub-sample accurate grain scheduling...
I'm trying to do something similar. I used a similar idea as in the many voices patch.
However it dosen't work in every situation. If I make a midiloop in Ableton to trigger it, evrything works fine. But if I play notes with a Midi-keyboard fast or Chords it sometimes does not get every note change, which will result in notes not stopping to play. To be honest, I have no clue why this happens, and why it dose not happen if the notes are played by Ableton.
Hi, I got this code to work properly and its now available for download at https://www.yofiel.com/max.php
and the second youtube video shows it in operation.