grame-cncm / faust

Functional programming language for signal processing and sound synthesis
http://faust.grame.fr
Other
2.58k stars 322 forks source link

monophony handling #252

Open giuliomoro opened 5 years ago

giuliomoro commented 5 years ago

use faust2xxx (e.g.: bela, caqt) with -midi -nvoices 1 and you will get the following behaviour:

This may be expected behaviour, as the docs say

When a key-on event is received, gate will be set to 1. Inversely, when a key-off event is received, gate will be set to 0. Therefore, gate is typically used to trigger an envelope, etc.

but I'd argue this is not desirable behaviour: who would want to find themselves still holding down a note and not hearing anything? Analog monosynths typically had "high" or "low" note priority, meaning the currently pressed highest (or lowest) note would be the one you'd hear. Later on, digital keyboard scanning came into play and some synthesizers started providing the "most recent" note on: the latest note being pressed (B) would be the one you'd hear, and when releasing it, while still holding down an old note A, you'd go back to hearing A. Are any of these behaviours available in FAUST?

example file:

declare options "[midi:on][nvoices:1]";
import("stdfaust.lib");
freq = hslider("freq",200,50,1000,0.01);
gain = hslider("gain",0.5,0,1,0.01);
gate = button("gate");
envelope = en.adsr(0.01,0.01,0.8,0.1,gate)*gain;
process = os.sawtooth(freq)*envelope;

built with faust2caqt -midi -nvoices 1 poly.dsp

note that the above file (mostly taken from https://faust.grame.fr/doc/manual/index.html#midi-polyphony-support ), does actually require the -nvoices command-line switch to build, even though the page says it shouldn't need it.

sletz commented 5 years ago

1) yes, polyphonic voice stealing algorithm (in https://github.com/grame-cncm/faust/blob/master-dev/architecture/faust/dsp/poly-dsp.h) is not enough sophisticated to handle correctly this monophonic case. I'll try to have a look ASAP, feel free to hack something.

2) declare options "[midi:on][nvoices:1]; stuff is already documented, but not yet fully implemented ))-; (Romain is too fast here...)

agraef commented 5 years ago

Yes, Faust just does basic voice stealing, which with nvoices=1 turns off any sounding note, which is consistent with the behavior for nvoices > 1.

The voice allocation might be improved, but that will require more sophisticated data structures than what we have now. Can you point to any online sources about the voice allocation methods that you mention? I'd be willing to look into this some time, since I'd also like to add proper multi-channel support to the polyphony component in the (hopefully) not-too-distant future.

giuliomoro commented 5 years ago

hmm, I naively implemented it in Pd once, storing one value per note in an array, assigning a count++ to the note at NoteOn, and -1 at NoteOff. Then every time there was a noteOff coming in, I'd check the array for the largest value. This requires finding the max of the array for each noteoff, so it may carry a bit of memory overhead (we are talking of an array of 128 ints, so not much stuff). Additionally, if your count variable overflows, you are screwed. This is unlikely to ever happen (and it could be reset to 0 any time there is no NoteOn), but still not very elegant.

A doubly linked list is probably the way to go?

agraef commented 5 years ago

A doubly linked list is probably the way to go?

I think that this is too slow. Linear search might be ok with a small amount of input events, but that's not guaranteed here. Remember that the voice allocation usually runs in a real-time callback under very tight timing constraints, and AFAIK the Grame implementation also does sample-accurate timing of MIDI events, so the callback might be invoked any number of times for each dsp tick. So we probably need some kind of priority queue data structure which can be accessed and updated in O(1) or at worst O(log n) time in order to not bog down the real-time thread when a lot of notes are received at the same time.

magnetophon commented 4 years ago

It would be great to have this. Any chance?

thopman commented 4 years ago

completely agree!

magnetophon commented 4 years ago

@giuliomoro Thanks for the description of your pd algorithm. I implemented it in faust, also using 2 lines of code by @josmithiii :

declare author "Bart Brouns";
declare license "GPLv3";
declare name "lastNote";
declare options "[midi:on]";
import("stdfaust.lib");
///////////////////////////////////////////////////////////////////////////////
//           give the number of the last note played                         //
///////////////////////////////////////////////////////////////////////////////

process =
  os.osc(lastNote:ba.pianokey2hz)
// increases the cpu-usage, from 7% to 11%
// * (vel(lastNote)/127)
// no velocity:
* (nrNotesPlaying>0)
;

nrNotesPlaying = 0: seq(i, nrNotes, noteIsOn(i),_:+);
noteIsOn(i) = velocity(i)>0;

vel(x) =  par(i, nrNotes, velocity(i)*(i==x)):>_ ;
velocity(i) = hslider("velocity of note %i [midi:key %i ]", 0, 0, nrNotes, 1);
nrNotes = 127; // nr of midi notes
// nrNotes = 32; // for block diagram

lastNote = par(i, nrNotes, i,index(i)):find_max_index(nrNotes):(_,!)
with {
  // an index to indicate the order of the note
  // it adds one for every additional note played
  // it resets to 0 when there are no notes playing
  // assume multiple notes can start at once
  orderIndex = ((_+((nrNotesPlaying-nrNotesPlaying'):max(0))) * (nrNotesPlaying>1))~_;

  // the order index of note i
  // TODO: when multiple notes start at the same time, give each a unique index
  index(i) = orderIndex:(select2(noteStart(i),_,_)
                         :select2(noteEnd(i)+(1:ba.impulsify),_,-1))~_;

  // we use this instead of:
  // hslider("frequency[midi:keyon 62]",0,0,nrNotes,1)
  // because keyon can come multiple times, and we only want the first
  noteStart(i) = noteIsOn(i):ba.impulsify;
  noteEnd(i) = (noteIsOn(i)'-noteIsOn(i)):max(0):ba.impulsify;
  //or do we?
  // noteStart(i) = (hslider("keyon[midi:keyon %i]",0,0,nrNotes,1)>0) :ba.impulsify;
  // at the very least, the first implementation of noteStart(i) doesn't add another 127 sliders

  // from Julius Smith's acor.dsp:
  index_comparator(n,x,m,y) = select2((x>y),m,n), select2((x>y),y,x); // compare integer-labeled signals
  // take N number-value pairs and give the number with the maximum value
  find_max_index(N) = seq(i,N-2, (index_comparator,si.bus(2*(N-i-2)))) : index_comparator;
};

It works great, though it's a bit CPU-hungry. Oddly enough the velocity function (commented out) increases the CPU usage quite a lot too.

Another downside is that there are 127 sliders on screen. Any chance of this, or something like it, being included in the compiler?

Or maybe it should go in the libraries, but in that case I think the compiler should add the option of [hidden] metadata to the gui-elements, so you can hide them.

sletz commented 4 years ago

Thanks for developing and testing, but I think it should go in a improve voice stealing algo in the C++ code...

magnetophon commented 4 years ago

Yeah, that is what I meant by "including it in the compiler". This is just a workaround / proof of concept.

I hope you'll get around to it soon! :)