polyphony (what happens after note off)? was: Killing thread from without
Hans, Eduard, community at large,
I think what Hans is asking is "If you want a note to continue after a
note-off event, what's a good way to program this in ChucK?"
Attached is how I do it.
The philosophy is simple: *Every* note-on event spawns a thread. That
thread patches in the UGs and plays sound until there's a note-off
event, at which it *begins* to shut down the note. Only after the
note actually ends does the the thread unpatch the UGs and kill itself.
That's the *philosophy*, but the implementation shown here deviates in
an important way: A thread (and related UGs) are created on demand,
but it is never killed: it blocks rather than terminates. And the
object associated with the thread is never discarded: it is put into a
resource pool and reused for the next note.
If you hit C4 twice in a row, it will actually spawn two threads --
this is exactly what you want for something with a long decay.
Don't let the length of the code scare you - most of it is comments.
- Rob
// File: ShredPoolExample2.ck
// ================================================================
//
// This program is free software: you can redistribute it and/or
modify it under
// the terms of the GNU General Public License as published by the
Free Software
// Foundation, either version 3 of the License, or (at your option)
any later
// version.
//
// This program is distributed in the hope that it will be useful, but
WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS
// FOR A PARTICULAR PURPOSE. See the GNU General Public License for
more
// details.
//
// You should have received a copy of the GNU General Public License
along with
// this program. If not, see http://www.gnu.org/licenses/.
//
// ================================================================
// About ShredPoolExample2:
//
// ShredPoolExample2.ck is a example for the ChucK audio programming
// and processing environment. It requires a MIDI keyboard or
// equivalent source of midi note on/note off events. It demonstrates
// three separate idioms:
//
// * How to implement a general purpose "resource pool". The basic
// idea is that if you need an object, and there's one in the pool,
// then you remove it from the pool and use it. If there isn't one
// in the pool, only then do you allocate one from memory. When you
// are done with the object, you return it to the pool. This way,
// you will initially allocate some objects from memory, but
// (hopefully) most of the time you'll be reusing ones you already
// allocated. This techique is useful in ChucK since it lacks a
// full garbage collection system.
//
// * How to create a Shred (thread) that can be started and stopped
// for reuse. This is useful because ChucK does not garbage collect
// shreads after they terminate, and memory will silt up.
//
// * One way to implement polyphony (including dynamic patching) and how
// to handle "overhang" after a midi note ends.
//
// ================================================================
// USAGE:
// chuck ShredPoolExample2.ck
//
// ================================================================
// Revision History
// 1.00 22-April-2009: Robert Poor
; }
// ================ // "forever" loop starts here while (true) { midi_in => now; // block here until a midi event arrives while (midi_in.recv(msg)) { // ignore everything but key press messages if (isKeyPress(msg)) { // extract key number and velocity from the midi message msg.data2 => int midi_key; msg.data3 => int midi_velocity; // Theoretically, we can't get two note on events without // seeing an intermediate note off event. But sometimes // midi events are dropped (notably note off events). If // two note on events happen in a row for the same key, we // need to turn off the previous note, so we conditionally // do it here: note_off(midi_key); if (midi_velocity > 0) { // Here with a note-on event. We allocate a Mistral // from the pool, start it playing, and save a handle // to it in the _active_notes[] array so we can find // it when it's time to call note_off() note_on(midi_key, midi_velocity); } else { // Here with a note-off event. But we've already // called note_off() above. So nothing to do here. } } } } fun int isKeyPress(MidiMsg msg) { return (msg.data1 == 144); } // Allocate a Mistral from the pool, send it a note_on message and // stash a pointer to it in the _active_notes[] list so we know how // to send it a note_off message when the midi event arrives. fun void note_on(int midi_key, int midi_velocity) { resource_pool.allocate_element() $ Mistral @=> Mistral @ mistral; mistral.note_on(midi_key, midi_velocity); mistral @=> _active_notes[midi_key]; } // If there is an active note on this key, ask it do to a note_off() // and remove it from the list of active notes. The mistral will take // care of stopping its own processing and returning itself to the // resource pool when the note actually completes. // fun void note_off(int midi_key) { if (_active_notes[midi_key] != null) { _active_notes[midi_key].note_off(); null @=> _active_notes[midi_key]; } } // EOF
On 23 Apr 2009, at 18:53, Robert Poor wrote:
The philosophy is simple: *Every* note-on event spawns a thread. That thread patches in the UGs and plays sound until there's a note- off event, at which it *begins* to shut down the note. Only after the note actually ends does the the thread unpatch the UGs and kill itself. ... If you hit C4 twice in a row, it will actually spawn two threads -- this is exactly what you want for something with a long decay.
So what happens in your code if the same key is hit again before the decay has ended: do you get another generator, or is the already sounding generator of the key reset? Hans
do you get another generator, or is the already sounding generator of the key reset?
The answer is "yes" -- it behaves one of those two ways. :) But it's a trivial change -- about four lines of code -- to make it behave the other way. I don't want to presume your proficiency in coding, but I wrote this as an example for people to look at and adapt as appropriate. Let me know off-list if you want more help. - Rob On 24 Apr 2009, at 00:47, Hans Aberg wrote:
On 23 Apr 2009, at 18:53, Robert Poor wrote:
The philosophy is simple: *Every* note-on event spawns a thread. That thread patches in the UGs and plays sound until there's a note- off event, at which it *begins* to shut down the note. Only after the note actually ends does the the thread unpatch the UGs and kill itself. ... If you hit C4 twice in a row, it will actually spawn two threads -- this is exactly what you want for something with a long decay.
So what happens in your code if the same key is hit again before the decay has ended: do you get another generator, or is the already sounding generator of the key reset?
Hans
On 26 Apr 2009, at 04:38, Robert Poor wrote:
do you get another generator, or is the already sounding generator of the key reset?
The answer is "yes" -- it behaves one of those two ways. :) But it's a trivial change -- about four lines of code -- to make it behave the other way.
These are different musical effects, I think. I want to have one generator per key. I think of keeping it when doing transpositions. I also spawn one thread for each key - at the release. Then fundamental problem is that Chuck cannot schedule and re-schedule events in the future; this must be done by creating a new thread.
I don't want to presume your proficiency in coding, but I wrote this as an example for people to look at and adapt as appropriate.
The code was too massive too extract just the things I wanted to know, though examples are nice. (Note that in ISO/ANSI C/C++, names starting with '_' are reserved for the implementation of the compiler, though Chuck is a different language.) Also, the idea of keeping threads alive in order prevent memory leaks seems an unwanted workaround, though perhaps it may be necessary if not possible to do it otherwise. As for killing threads, when dialing a phone number, already one and a half decade ago, the computer could create thousands of threads, each one doing a search path. When one is found and reporting back, it would be a waste of resources letting the others hanging around. So I think killing threads would be normal programming.
Let me know off-list if you want more help.
On the lists I know (for example the Bison lists), most everything are kept on-list. Hans
Hans;
The answer is "yes" -- it behaves one of those two ways. :) But it's a trivial change -- about four lines of code -- to make it behave the other way.
These are different musical effects, I think. I want to have one generator per key. I think of keeping it when doing transpositions.
Yes, and you may simply want to code in a way that suits your perspective on the instrument. For what it's worth; commercial synths tend to cycle (or steal...) the voice that was already associated with this key the last time it was played if we re-trigger a voice before the decay has finished, even when they don't have a absolute link between voices and keys in general.
I also spawn one thread for each key - at the release. Then fundamental problem is that Chuck cannot schedule and re-schedule events in the future; this must be done by creating a new thread.
Another way of phrasing that would be saying that the only thing ChucK can shredule is ChucK code and that this code inherently needs to be in a shred. I really don't see how that is a "fundamental problem", it's just a perspective and a syntax. We need to express anything we want done in code anyway. If you absolutely must re-schedule events then the only solution I see right now comes down to homebrew functor style objects with timestamps and making your own scheduler. That would be a interesting experiment though I don't think there is any need for that here.
Also, the idea of keeping threads alive in order prevent memory leaks seems an unwanted workaround, though perhaps it may be necessary if not possible to do it otherwise.
Well, as you are basically using a single STKInstrument per voice here I don't think there is a need to have a shred per voice at all in this case. Just a array of a dozen or so UGens with some infrastructure to keep track of which ones are free and similar voice-cycling infrastructure should do the trick. That would mean a single shred and no garbage at all.
As for killing threads, when dialing a phone number, already one and a half decade ago, the computer could create thousands of threads, each one doing a search path. When one is found and reporting back, it would be a waste of resources letting the others hanging around. So I think killing threads would be normal programming.
That's a quite different sort of system for a very different type of application though. I don't think ChucK is particularly well suited for managing phone company centres. Kas.
I wrote;
Well, as you are basically using a single STKInstrument per voice here I don't think there is a need to have a shred per voice at all in this case. Just a array of a dozen or so UGens with some infrastructure to keep track of which ones are free and similar voice-cycling infrastructure should do the trick. That would mean a single shred and no garbage at all.
I'm pasting a edit of the last version Hans send to the list based on this strategy below. the basic strategy is starting a few voices, hopefully a amount our cpu can take. When a new key is pressed it will be assigned to the voice that already had that voice linked to it. If no such voice is found it'll take the voice that's not currently playing and gone longest since the last note off. When all voices are currently playing it'll steal the one that has gone longest since the last note on. No sporking and probably no garbage (I didn't verify the second part of that claim). There are likely some issues with this; while the strategy should be sound I wrote this quite quickly. I also changed the indentation style after getting confused about a "else" in the main loop. This is roughly the strategy hardware synths take; the amount of cpu used should be more or less static, clearly something more advanced is in order when we need to run several instruments on the same computer but I took this setup to be meant for a solo instrument. No warranties, no refunds, please mind your speakers and neighbours. Cheers, Kas. //======================8<============================= //set this to what your computer can take 5 => int NR_OF_VOICES; // The device number to open 1 => int deviceNum; //voice cycling by Kas, everything else by Hans 2009 /* Example of a generalized diatonic (or extended mentone) key map. Transposition takes place by keyboard translation, so each scale and chord need only one fingering pattern. The keyboard layout is altering pitches as follows: ^ # / . -> M / \ b v v m where M (resp. m) is the major (resp. minor) second, and the the sharp # (resp. flat b) raises (resp lower) with the interval M - m, that is, the difference between the major and minor seconds. Resulting key pattern: C# D# E# C D E F# G# A# B# Cb Db Eb F G A B Fb Gb Ab Bb C' M is set to 2^(5/31) and m = 2^(3/31), the E31 approximation of Renaissance meantone. Middle A = 440 Hz, and is in US keyboard layout on key "J". */ // keyboard Hid kb; // hid message HidMsg msg; // Open keyboard. if(!kb.openKeyboard(deviceNum)) me.exit(); // Successful! Print name of device. <<< "Keyboard '", kb.name(), "' ready." >>>; // Key x-axis coordinate int x[256]; 0 => x[100] => x[53]; 1 => x[41] => x[49] => x[30] => x[20] => x[4] => x[29]; 2 => x[58] => x[31] => x[26] => x[22] => x[27]; 3 => x[59] => x[32] => x[8] => x[7] => x[6]; 4 => x[60] => x[33] => x[21] => x[9] => x[25]; 5 => x[61] => x[34] => x[23] => x[10] => x[5]; 6 => x[62] => x[35] => x[28] => x[11] => x[17]; 7 => x[63] => x[36] => x[24] => x[13] => x[16]; 8 => x[64] => x[37] => x[12] => x[14] => x[54]; 9 => x[65] => x[38] => x[18] => x[15] => x[55]; 10 => x[66] => x[39] => x[19] => x[51] => x[56]; 11 => x[67] => x[45] => x[47] => x[52] => x[229]; 12 => x[68] => x[46] => x[48] => x[49]; // Key y-axis coordinate int y[256]; 0 => y[49] => y[58] => y[59] => y[60] => y[61] => y[62] => y[63] => y[64] => y[65] => y[66] => y[67] => y[68] => y[69]; 1 => y[100] => y[30] => y[31] => y[32] => y[33] => y[34] => y[35] => y[36] => y[37] => y[38] => y[39] => y[45] => y[46]; 2 => y[20] => y[26] => y[8] => y[21] => y[23] => y[28] => y[24] => y[12] => y[18] => y[19] => y[47] => y[48]; 3 => y[4] => y[22] => y[7] => y[9] => y[10] => y[11] => y[13] => y[14] => y[15] => y[51] => y[52] => y[49]; 4 => y[53] => y[29] => y[27] => y[6] => y[25] => y[5] => y[17] => y[16] => y[54] => y[55] => y[56] => y[229]; // Key serving as origin 7 => int x0; 3 => int y0; int init[256]; // Tuning frequency 440.0 => float f0; Voice voices[NR_OF_VOICES]; JCRev r => dac; r => Echo e => Echo e2 => dac; // set delays 240::ms => e.max => e.delay; 480::ms => e2.max => e2.delay; // set gains .6 => e.gain; .3 => e2.gain; .05 => r.mix; for(int n; n < NR_OF_VOICES; n++) { voices[n].signal => r; 1 => voices[n].signal.noteOff; } while(true) { kb => now; while(kb.recv(msg)) { // Print. /* not printing as writing to the screen can cause a delay as debated on the list if(msg.isButtonDown()) <<< " key", msg.which, "down (", x[msg.which], ",", y[msg.which], ")" >>>; else <<< " key", msg.which, "up (", x[msg.which], ",", y[msg.which], ")" >>>; */ // Play a note. if (!((x[msg.which] == 0) && (y[msg.which] == 0))) { if(msg.isButtonDown()) { int assigned; //check whether one of our voices already had this pitch assigned for(int n; n< NR_OF_VOICES; n++) { if (voices[n].key == msg.which) { f0 * Math.pow(2, (5*(x[msg.which] - x0) + 3*(y[msg.which] - y0))/31.0) => voices[n].signal.freq; 1 => voices[n].playing; msg.which => voices[n].key; now => voices[n].last_NoteOn; 1 => voices[n].signal.noteOn; 1 => assigned; break; } } //otherwise take the voice that has been silent for longest. if(!assigned) { now => time voice_age; int oldest_voice; for (int n; n< NR_OF_VOICES; n++) { if ( !voices[n].playing && voices[n].last_NoteOff < voice_age) { voices[n].last_NoteOff => voice_age; n => oldest_voice; } } f0 * Math.pow(2, (5*(x[msg.which] - x0) + 3*(y[msg.which] - y0))/31.0) => voices[oldest_voice].signal.freq; 1 => voices[oldest_voice].playing; msg.which => voices[oldest_voice].key; now => voices[oldest_voice].last_NoteOn; 1 => voices[oldest_voice].signal.noteOn; 1 => assigned; } //if that too fails we'll need to "steal" a voice //we'll take the one that has been playing for the longest time //"real" synths tend to reserve the highest and/or lowest pitch //such cleverness is left as a excersise for the reader if(!assigned) { now => time voice_age; int oldest_voice; for (int n; n< NR_OF_VOICES; n++) { if ( voices[n].last_NoteOff < voice_age) { voices[n].last_NoteOff => voice_age; n => oldest_voice; } } f0 * Math.pow(2, (5*(x[msg.which] - x0) + 3*(y[msg.which] - y0))/31.0) => voices[oldest_voice].signal.freq; 1 => voices[oldest_voice].playing; msg.which => voices[oldest_voice].key; now => voices[oldest_voice].last_NoteOn; 1 => voices[oldest_voice].signal.noteOn; 1 => assigned; } } else { for (int n; n< NR_OF_VOICES; n++) { if(voices[n].playing && voices[n].key == msg.which) { 1 => voices[n].signal.noteOff; now => voices[n].last_NoteOff; 0 => voices[n].playing; } } } } else { <<<"presumably keys reserved for control go here?", "">>>; } } } class Voice { BeeThree signal; int playing; int key; time last_NoteOn; time last_NoteOff; }
On 26 Apr 2009, at 20:15, Kassen wrote:
I'm pasting a edit of the last version Hans send to the list based on this strategy below.
Thank you. I have been focusing on implementing octave shifts, transpositions, and different tuning systems, but I add a copy here of what I currently have. The playing area has been extended with a few keys, and some bug fixing. The caps lock is a octave up shift, as to extend the keyboard when playing. The left shift key plus a key on the number row sets an octave shift; key 4 = middle octave, though written as 0. The option key plus a playing key center the tuning frequency on that key. For reference, the key next to the left shift key always plays the tuning frequency (currently 440 Hz). The left shift key plus a key on the two rows closest to it sets various tuning systems, with increasing minor second m from left to right. It is interesting to play C# Db, and listen to the changes. E12 should have no beats, of course.
the basic strategy is starting a few voices, hopefully a amount our cpu can take. When a new key is pressed it will be assigned to the voice that already had that voice linked to it. If no such voice is found it'll take the voice that's not currently playing and gone longest since the last note off. When all voices are currently playing it'll steal the one that has gone longest since the last note on.
No sporking and probably no garbage (I didn't verify the second part of that claim).
There are likely some issues with this; while the strategy should be sound I wrote this quite quickly.
So its good with input on voices.
I also changed the indentation style after getting confused about a "else" in the main loop.
I use a fairly common, more compact C/C++ style. I also avoid using tabs, instead using two spaces, which are expressive enough. Unix tabs are traditionally set at 8 spaces, which is too much, and there seems to be no good way to avoid it set at that in various circumstances.
This is roughly the strategy hardware synths take; the amount of cpu used should be more or less static, clearly something more advanced is in order when we need to run several instruments on the same computer but I took this setup to be meant for a solo instrument.
I haven't had any problems with CPU overload in the code in the attached code. I have tried it for extended periods of time. Hans
Hans;
Thank you.
Your're welcome. i hope this solves some of the issues you've been having.
I have been focusing on implementing octave shifts, transpositions, and different tuning systems, but I add a copy here of what I currently have. The playing area has been extended with a few keys, and some bug fixing.
Sound good!
I use a fairly common, more compact C/C++ style.
Yes, I know you style is more common, I find I benefit from taking more space for added (subjective) clarity so I find that way more clear. I wanted to stick to your style but I got mixed up in some nesting so I edited it a bit (ok... a lot). No critique was implied; it was just me being lazy.
I also avoid using tabs, instead using two spaces, which are expressive enough. Unix tabs are traditionally set at 8 spaces, which is too much, and there seems to be no good way to avoid it set at that in various circumstances.
Yes, I understand. I like tabs as they save on the keystrokes though I do set my editors to use 4 spaces per tab for display. Sometimes I use two spaces when writing directly in emails or forum posts as web browsers like to use the tab for different purposes but this was a bit too much editing and I wanted to make sure it ran.
I haven't had any problems with CPU overload in the code in the attached code. I have tried it for extended periods of time.
You'll probably be able to turn the number of voices up a bit; 5 is really quite modest. Kas.
On 26 Apr 2009, at 22:30, Kassen wrote:
Thank you.
Your're welcome. i hope this solves some of the issues you've been having.
I will have a look at it later. I was planning to take it easy - that's why I din't post a new version.
I have been focusing on implementing octave shifts, transpositions, and different tuning systems, but I add a copy here of what I currently have. The playing area has been extended with a few keys, and some bug fixing.
Sound good!
You might try to play something you know well in E12, meantone (MM), and Pythagorean (P) and see if you hear the difference.
I use a fairly common, more compact C/C++ style.
Yes, I know you style is more common, I find I benefit from taking more space for added (subjective) clarity so I find that way more clear. I wanted to stick to your style but I got mixed up in some nesting so I edited it a bit (ok... a lot). No critique was implied; it was just me being lazy.
OK. Of course, you are free to write in your own style :-).
I also avoid using tabs, instead using two spaces, which are expressive enough. Unix tabs are traditionally set at 8 spaces, which is too much, and there seems to be no good way to avoid it set at that in various circumstances.
Yes, I understand. I like tabs as they save on the keystrokes though I do set my editors to use 4 spaces per tab for display. Sometimes I use two spaces when writing directly in emails or forum posts as web browsers like to use the tab for different purposes but this was a bit too much editing and I wanted to make sure it ran.
It is simplest to use an editor that replaces tabs with spaces. I used tab on 4 spaces for some years, until I finally felt 2 was expressive enough. One can sometimes get a lot of indentations, especially in some languages.
I haven't had any problems with CPU overload in the code in the attached code. I have tried it for extended periods of time.
You'll probably be able to turn the number of voices up a bit; 5 is really quite modest.
I do not have any limit in the code I posted. One can typically only have five or so simultaneous keys on my keyboard due to limitation of key roll over, fingers, and so on. With an extra keyboard, one gets up to ten voices or so. With the BeeThree, decay can be set as short as 11 ms, though I use the more ample 16 ms. This is about the time of the shortest perceptible time bends (which is shorter than the MIDI time resolution, and possibly a reason why MIDI timing may not good enough). Hans
On 26 Apr 2009, at 18:26, Kassen wrote:
These are different musical effects, I think. I want to have one generator per key. I think of keeping it when doing transpositions.
Yes, and you may simply want to code in a way that suits your perspective on the instrument.
Sure.
For what it's worth; commercial synths tend to cycle (or steal...) the voice that was already associated with this key the last time it was played if we re-trigger a voice before the decay has finished, even when they don't have a absolute link between voices and keys in general.
Since there are no such limits in Chuck, I want in principle enable the same generator to do note-on effects emulating real instruments.
I also spawn one thread for each key - at the release. Then fundamental problem is that Chuck cannot schedule and re-schedule events in the future; this must be done by creating a new thread.
Another way of phrasing that would be saying that the only thing ChucK can shredule is ChucK code and that this code inherently needs to be in a shred. I really don't see how that is a "fundamental problem", it's just a perspective and a syntax. We need to express anything we want done in code anyway.
By that I mean that it Chuck does not have a built-in mechanism to schedule and reschedule future events. If you want that, it must be built by hand via halted threads.
If you absolutely must re-schedule events then the only solution I see right now comes down to homebrew functor style objects with timestamps and making your own scheduler. That would be a interesting experiment though I don't think there is any need for that here.
You mean apart from scheduling decay cutoffs and the like.
Also, the idea of keeping threads alive in order prevent memory leaks seems an unwanted workaround, though perhaps it may be necessary if not possible to do it otherwise.
Well, as you are basically using a single STKInstrument per voice here I don't think there is a need to have a shred per voice at all in this case. Just a array of a dozen or so UGens with some infrastructure to keep track of which ones are free and similar voice-cycling infrastructure should do the trick. That would mean a single shred and no garbage at all.
It seems that problem that folks try to program around is that Chuck returns references of class objects, without having a root tracing mechanism that could be used for a GC.
As for killing threads, when dialing a phone number, already one and a half decade ago, the computer could create thousands of threads, each one doing a search path. When one is found and reporting back, it would be a waste of resources letting the others hanging around. So I think killing threads would be normal programming.
That's a quite different sort of system for a very different type of application though. I don't think ChucK is particularly well suited for managing phone company centres.
Chuck probably just gets its threads from the OS rather implementing in effect an OS on top of the one already there, so that determines what it should be capable of. Hans
participants (3)
-
Hans Aberg
-
Kassen
-
Robert Poor