SHARING: poly object in gen codebox


    Jul 10 2019 | 9:36 am
    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.

    • Jul 11 2019 | 8:38 am
      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.
    • Jul 12 2019 | 1:23 am
      Very cool!
    • Jul 13 2019 | 4:16 pm
      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.
    • Jul 16 2019 | 12:37 am
      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.