parallel filter banks in gen

Alexis Baskind's icon

Dear all,

I'm working on an enhanced and more flexible version of a classic vocoder (i.e. filterbank-based, not phase-vocoder) that allows among other to define dynamically the internal filter bank. It works quite well in patch version, but since the number and values of the frequencies of the filter bank can be dynamically changed, it requires to dynamically create/delete new voices in a poly~ object, which may sometimes need several seconds to achieve.

This wouldn't be a huge deal to code this as a C++ external, but since the project needs to be shared, I'm a little bit reluctant to go this way, not because of sharing, but since I have bad experiences with externals compiled on windows on my machine and not working universally on all windows machines. Even if I provide the sources with the compiled external it's not an ideal solution, most users of my project won't be able to open Visual Studio and compile the whole themselves.

So: I'm working on a gen~ version of it, but then I'm facing a problem: each filter of the bank needs to store and retrieve its own states, and this very quickly. Currently I have internal Data arrays that allow me to do that, but because of the big number of peeks and pokes, the whole is a CPU hog.

The ideal case would be that each biquad filter (called from a function) also contains and manage its states as History variables. This seems to be indeed partly possible:
https://cycling74.com/forums/amplitude-within-parallel-filter-models-gen/
Looking at the exported C++ code, one sees that 5 instances of the filter are created, each one having its own state variables.
But in my case this won't work, since we don't know in advance the number of frequencies (and then the number of filter instances).

Any ideas?

Best

Alexis

Graham Wakefield's icon

Hi Alexis,

I would suggest that you choose the maximum number of possible filters and allocate that many, but use some kind of param variable to determine how many are active at any time. Inactive voices would either be disabled or muted (according to what is possible to patch).

To avoid Data peek/poke overhead, you could do a kind of round-robin indexing scheme the same way MSP's oscbank~ works, using some variable to identify which voice is currently being updated. (E.g. by using a [counter] to iterate over all active voices.) Then in the code for each resonator, update the parameters only if the current voice index matches the filter's index. The downside is that you have to update the filters one by one, but that's not so bad really (you would be able to update over 40 filters per millisecond, and even a small amount of [slide] would smoothen these steps). You could even do this continuously by peeking from a buffer~ and it would still be low overhead, as you are only performing two peeks per sample for the whole bank.

Below is an example, adapted from the gen~ resonator bank example, for dynamically using up to 16 voices, and using this round-robin technique. It invokes the resonators via a codebox, so that an if() statement can bypass resonators that are not active. The number of peek operations is now only one, regardless the number of voices; perhaps this will make a noticeable difference to the CPU usage.

resonator1_bank.maxpat
Max Patch

resonator1.gendsp
gendsp 18.68 KB

And here's the same but using self-muting rather than bypassing. That is, all voices are always making sound, but only "active" voices receive input excitation, so they gradually fade out. This has a much smoother sound, but doesn't have any potential CPU saving when reducing the voice count.

resonator2_bank.maxpat
Max Patch

Graham

Alexis Baskind's icon

Hi Graham,

thanks a lot for your answer! This is very informative and seems to confirm what I was afraid of: there is no way to create N instances/copies of a function within a for loop, instances have to be created manually. Considering that I may have up to ca. 100 filters in my bank, it means 100 "if" tests, which also may slow down the code in a significant way.

Well, I guess I have then indeed to code this in C++. As I said, since I'm not at all a specialist in developing for windows, I may need further help there to configure the build options in a clean and reliable way, but I'll ask this in another post in the "dev" section.

All the best,

alexis

Graham Wakefield's icon

Yes, the decision was made early in gen's lifetime to disallow dynamic allocation; in part because it can be very difficult to ensure user-level code can do this efficiently in the context of an audio sample loop, and in part because some other gen domains (e.g shader languages from jit.gl.pix) cannot do this at all. As a result, one cannot either allocate voices via a for loop; you have to have the voices spelled out in the code. In the first example I posted however, I did see that reducing the voices parameter manually (disconnect the cable from the metro to the counter to do this) also reduced the CPU load, so the principle is working.

Most code I have seen in C++ using dynamic audio voices also pre-allocates a block of voices and selectively enables/disables them. Allocating memory in an audio loop (malloc/new) is not recommended as the call to allocate memory has unpredictable duration and could cause dropouts; workarounds for that usually involve pre-allocating reusable memory through a separate thread and that also needs a guess of how much one might need in advance. The easiest implementation simply limits the maximum no. of voices and has them allocated up-front, just as in the example above. In that case, the main difference between that and carefully hand-coded C++ is likely related to memory layout of the voices, and using pointer logic to limit processing of inactive voices rather than if() logic. The difference might or might not be significant, and as you say, you may be trading off user interface and portability for this difference.

But yes, with 100 filters, it is questionable whether this would be a good way forward.

Out of interest, why would a spectral domain solution not work?

Graham

Alexis Baskind's icon

Hi Graham,

I cannot really process it in the spectral domain since I'm working on a traditional vocoder, i.e. the machine works with pure RMS and no phase, and I'd like to be able to choose the center frequencies as well as all the filters characteristics (for the bank and for RMS smoothing) myself to shape the tone color the way I want. FFT can indeed be considered as a filter bank, but then the RMS per bin cannot be that easily estimated from the amplitudes in each channel (or, I would say, the overlap needs to be high). And on top of that I cannot set the frequencies the way I want.

The principle of this vocoder is basically indeed similar to a massive bank of resonators like CNMAT's resonators~ do, but with two inputs (source and filter) and a RMS detection.

Gen~ is incredibly powerful for many things, but it seems that it's not optimized for this specific application, since it would require more object based features. Dynamic allocation is not the real issue there: as you mention, all voices can be instantiated at the startup with the maximum possible number of voices. But what is missing for my very specific purposes are a basic object-based features, which would allow for example to store the state variables of X filter instances as X structures along the routine itself. When you explicitely create 16 instances of your resonator1 function in your example, it creates behind 16 different functions named resonator1_something, each of them having its own state variables as local variables. The principle works also for 128 instances (my case), but then the internal C++ code becomes huge (I tried) and the whole takes even more CPU than with my own try (using one generic performing routine in a for loop and storing/retrieving filter states in global data objects thanks to peek/poke)

The best compromise between resources and portability I could find until now is actually a pure patch version based on poly~ where all voices are indeed allocated at startup, and only the active ones are switched on depending on the number of frequencies.

BTW: your example with the 16 resonators is very interesting. I have a question related to your architecture: with the counter and the switches on/off, I assume that there is a time-shift of n samples for the resonator instance number n. Am I correct?