sensorium / Mozzi

sound synthesis library for Arduino
https://sensorium.github.io/Mozzi/
GNU Lesser General Public License v2.1
1.08k stars 186 forks source link

Metaoscillators to non-aliased sounds #124

Closed tomcombriat closed 1 year ago

tomcombriat commented 3 years ago

Hi,

Because of the usually low sample rate of Mozzi, especially on AVR, wavetables of big-harmonics contents waves are very prone to aliasing. This is particularly the case of the saw and the square waves, but also triangle to a lesser extend.

This is a proposition to resolve this, even if can probably be used for other things like splitting keyboard-like kind of behavior.

The idea is to provide a MetaOscil class that can switch between different Oscil depending on the current frequency. This allows for instance to swap between different band-limited tables seamlessly in order to produce no alias: at low frequencies, tables with a lot of harmonics can be used whereas at high frequencies these harmonics have to be removed from the tables in order to avoid aliases.

The MetaOscil proposed here can, after definition, be used exactly as a standard Oscil except that it will internally change the oscillator used depending on the frequency. Transition is done at the same phase.

Note that this is still in draft state: there are very little comments and it would probably be useful to the user to have more wavetables to play with, I added only four tables of the square wave with different limiting frequencies for the sake of the example. I would be interested in your feedback before pushing it to a clean mergeable state… In particular:

Hope to hear your point on that!

tfry-git commented 3 years ago

Just a thought: This may not be the only use-case for switching oscillators. E.g. waveform selection in a synthesizer. Not sure, whether there is a nice way to cover this and your use-case, but if you have an idea on that...

Regarding your question of adding the oscillators, since the number of oscillators is fixed, I think it would make most sense to add them all in a single call, as an array (or as an initializer list), probably even in the constructor. Thus, you would call it as MetaOscil<...> BL_aSq({aSq200, aQs500,...}); Of course that implies setting the cutoffs, separately, in a similar fashion.

Regarding using tables: At first thought that would sound easier to me, too, and should save a few bytes of RAM. But I may not be aware of all implications.

Regarding table names: Agreed, all information should be included.

Regards Thomas

tomcombriat commented 3 years ago

Hi @tfry-git , Thanks for your comments!

I personally always took care of oscillator selection with defining oscillators: for a nice synthesis sometimes their relative phases need to be adjusted which is not the purpose of this… The only other use I could think of was to reproduce for instance the "split keyboard" behavior, with low frequency notes and high frequency notes being very different… Do not know how much of use that would be though…

I didn't know about initializer list, thanks! Everytime I discover new things :)! This would indeed make more sense, but is this present in Arduino? I will test…

For the tables/oscil switch: I was thinking at first that switching between tables would be a bigger overhead than switching between oscillators… After wrapping up the Oscil class, I am not really sure of that anymore… I do not think one case or the other makes much of a difference actually, the Oscil class do not have a lot of things apart from the table…

For the tables: for this to be interesting I think a good number of tables would need to be added to Mozzi (several for the square, triangle and saw waves at least…) which is maybe something you want to prevent… I think that organizing these set of tables in subfolders can help to have it clean and would not be a problem… I also did not mention in my first comment that the names are mainly targeted toward platforms working at 16k. For platforms with an AUDIO_RATE of 32k like the STM32 or the Teensy, the cutoffs can be set twice higher. Probably a good comment in the example will be enough to precise that.

So comes the question, do you think that this would be of value for Mozzi? I did it because I was annoyed by aliases on most of my projects and wanted something that I could handle like an regular Oscillator, once the setup is done. There is nothing revolutionary here, or that could not be done in the user sketch by swapping tables, but I personally think that having a good set of band-limited tables could be of value.

Best, Tom

tomcombriat commented 3 years ago

Hi @tfry-git!

So I tried initializing part of this object at construction with something like: In the class: MetaOscil(std::initializer_list<Oscil<NUM_TABLE_CELLS, UPDATE_RATE>*> Osc) Which returns a compilation error pointing at the first <. I would say that this is not in Arduino but maybe I am doing it wrong.

I also tried something like: In the class: MetaOscil(Oscil<NUM_TABLE_CELLS, UPDATE_RATE> ** Osc) In the main code: MetaOscil<SQUARE_MAX_500_AT_16384_2048_NUM_CELLS, AUDIO_RATE, 4> BL_aSq ({&aSq200,&aSq200,&aSq200,&aSq200}); for which I had great hope but this returns the error: no matching function for call to 'MetaOscil<2048u, 16384u, 4u>::MetaOscil(<brace-enclosed initializer list>)'. So it seems that the compiler actually knows what an initializer_list is, I just cannot the correct way to write it.

I think that would work to go through an intermediate array like: Oscil<NUM_TABLE_CELLS, UPDATE_RATE>** Osc = {&aSq200,&aQs500,...} and then feed that into the constructor, but that seems a bit ugly… Would you have any suggestions?

In the meantime, I also generated a lot of wavetables, removing one harmonic at a time and calculating to maximum frequency that can acheived without alias… That makes quite a lot of tables… If we get that into Mozzi maybe we will want to limit that amount (or not?).

Best and thanks for your help…

tfry-git commented 3 years ago

Sorry, I seem to have set up a nice trap for you ;-). I've actually spent many hours fighting this problem in a different context, and so should have known better. Anyway, I think the way out will be yet another scary concept: Variadic templates. I believe the following constructor should work (but haven't tried):

  template<class... T> MetaOscil(Oscil<NUM_TABLE_CELLS, UPDATE_RATE>* first, T*... elements) :
        oscillators{first, elements...} {};

To be used as: MetaOscil myMetaOscil(&myOscilA, &myOscilB, ...);

Regarding number of tables: I don't see much of an issue with that, as none of this will be compiled in, unless explicitly requested. But @sensorium should probably comment on that.

tfry-git commented 3 years ago

... thinking about it, I wonder whether it wouldn't be possible to make the MetaOscil truly contain the Oscils (an array of them, instead of an array of pointers), with a variadic constructor that will simply take the table-names? Trying to implement that might involve a serious risk of knots in our brains, though.

tomcombriat commented 3 years ago

Hi @tfry-git ! Indeed that line nailed it, even though it took me half an hour to figure out that all your ... are C++ ellipsis and not ellipsis that I had to figure out myself… That was a nice trap for me who is more accustomed to "C with classes" rather than modern C++ but I learn new things every time I try to contribute to Mozzi and that's nice, thanks!

Now that this seems to be working I'll make the code prettier, add some comments and a good set of tables and test. I'll then convert this to a regular PR and see if we want it in or not.

I wonder whether it wouldn't be possible to make the MetaOscil truly contain the Oscils (an array of them, instead of an array of pointers),

Certainly, but personally I'm not sure this is desirable: doing so will remove all simple access to the individuals oscillators composing a meta-oscillator. Not sure it is actually useful but for instance an example I can think of is not having all the oscillators of a meta not at the same phase. This can allow, if using a fixed frequency filter (which is dephasing), to keep the phase constant over all frequencies but compensating the phase shift induced by the filter in a frequency dependent manner at the oscillator level. Maybe that can be useful…

Nothing related to the code in this PR (but related to my crusade against aliases): I wonder if it is possible to "over-sample" the signal by calling an oscillator two times in updateAudio (with the correct template parameter in the Oscil constructor) to get enough margin to filter the aliases out (in digital) before outputting the sound. That's, alongside switching tables (this PR) and BLEP oscillators the common ways to suppress aliases in digital audio… I'll test…

Will let you know when I'm done implementing these changes, that should not be very long…

Thanks for your help!

tomcombriat commented 3 years ago

Hi!

@tfry-git You opened the Pandora box: I also implemented a way to set all the cutoff frequencies using a variadic templated function! The cutoff frequencies can still be set independently as before but also "all-in-one" using

template<typename ... T >  void setCutoffFreqs(int first,T... elements)

I demonstrate the two different use in a example, put in the "Synthesis" folder. I order to remove the transitions from one Oscil to another, I made my example kind of an extreme case with a awful lot of Oscil (17), but still fits largely in a standard Arduino. It demonstrate a band limited square wave, keeping the crunchy sound at low frequencies without aliasing at high ones.

I also generated a lot of tables for the triangle, saw and square waves, of different sizes. They are arranged in folders and sub folders in the tables folder. That makes an awful lot of added files but theoretically, in order to achieve a sound containing all "samplable" frequencies without alias you have to use them all! Obviously in real life a small amount of alias of very high frequency is not a big deal and some can be dropped.

As you can see in the example, the declaration of all these Oscil is a bit tedious, however having so many Oscil does not impact performances as only one is played at a time, it just impacts the memory usage. I think that this can be very useful for synth implementation for instance where the crunchy sound of hard soundwaves is important while aliases really need to be avoided.

Thanks for your help and let me know if you have further comments! Best, Tom

tomcombriat commented 3 years ago

Hei, I am now undergoing extensive testing on a synth and I spotted a few adjustments that need to be made before considering a merging. In particular:

Except that it works great in my opinion. I'll let you know!

Best,

tomcombriat commented 3 years ago

Okay, I think this is better. As said in my previous comment I have now:

Upon test this seems to work great even if can be a bit heavy to declare, you can have a look here for an example on a personal project.

Best,

tfry-git commented 3 years ago

I have no objection against this going in as is, but a few thoughts, that you may or may not to address:

  1. I don't really have a good idea myself, but to me, "MetaOscil" sounds too generic, for what this is actually doing. Something like "BandAdjustedOscil", perhaps?
  2. As you note, this is rather heavy to define. Could this be helped with some macros or functions, perhaps? Something like makeMetaOscilBandLimitedSaw14(), which would create a "standard" MetaOscil?
  3. Is "addOscil" still needed? If it wasn't, you could do away with current_rank (ok, that's really a micro-optimization in a class that works on many kbytes of wave data, but...).
tomcombriat commented 3 years ago

Hi!

Thanks for your comments, here are my thoughts about it:

  1. yes, this is very generic… Even though the obvious use of this class is to provide non-aliased oscillator I am sure someone a bit creative could use that for other purposes… For instance using different waveforms at different frequencies which could have some use for drone synths or installations? I dunno, I find "MetaOscil" a bit too generic but "BandAdjustedOscil" a bit too specific and more toward an BLEP oscillator for instance which is doing only that. I am open to any idea!
  2. It is very heavy yes but I honestly have no idea how to make something a bit lighter without sacrificing usability, especially because of the #include we do not want more than enough for obvious memory space reasons but at the same time, which ones to take is a bit user-application dependent. In particular, the given frequencies are insuring that there will be no alias at all up to this frequency. In order to save memory some aliasing can be acceptable, especially when sampled at 32k, hence the choice of Oscil to put in a MetaOscil is really dependent on the desired effect and use (for bass sound, only the wavetables non aliasing at low frequencies can be used for instance). Giving a generic function to create a MetaOscil would also be a challenge as, if you take a look in the computed wavetables, they are not spaced linearly, but logarithmicly (?) so the function would have to figure out which one to take (and include only the needed wavetables…) which is not trivial. This last in particular puzzles me: how to include only the files needed in a function? Maybe one way to go would be to have a lot of available header files, taking care of the includes but then you cannot create an arbitrary number of MetaOscil in there without dynamic memory management, which I always try to avoid on MCU… (Sorry that a big block of text to say that I have no inspiration on that…)
  3. I have mitigated feelings about this function. I removed it at some point and readded it in the end thinking that it could be used to feed the MetaOscil in a for loop during setup, thinking that it could be useful for polyphony where arrays of Oscil are created at once. In the end that did not prove useful for that so we can probably drop it… Note that, even if we drop it I am not sure we can get rid of current_rank as the functions setOscils and setCutoffFreqs being recursive functions (thanks to the variadic templates :) ) the number of time it has been called need to be incremented. Maybe there is way to do without but I did not find it…
tomcombriat commented 3 years ago

Hello! Any final though about this? I have been using for some time and it seems to be working completely fine (after the long time declaring the oscillators).

Best and long live Mozzi!

sensorium commented 3 years ago

Hi Tom, would it be worthwhile explaining in the example what the class does? Also, is it usable on 8 bit processors without much memory? Maybe document which boards it's been tested on or is suitable for, if it matters? Sorry if I've missed what you've already done!

tomcombriat commented 3 years ago

@sensorium Thanks for your comment! I will update the example to make it clear what this class does! I have tested that on Arduino (it's hidden in a comment somewhere, I had to shift+f it…) which is why I did not precise in the example that the usage is limited to "powerful" boards. Actually, the great thing is that as you can adjust the number of Oscillators in these MetaOscillators, it is easy to adjust the memory consumption to your needs, making it suitable even for low-memory platforms. Basically, if you decrease the number of Oscillators, you have the choice or to accept a bit of aliasing, or to accept that part of the higher harmonics are not present. If you think the example is not clear enough I can append it with a bit more explanations! Best,

tomcombriat commented 3 years ago

@sensorium I appended the example with some explanation about what this is capable of. I also removed a function of the class (addOscil) that could have been used non wisely (this was a suggestion from @tfry-git ).

I have used that on my own synth and it seems to be very reliable and enhancing the sound quality. I did not say it in the example by the example works on an Arduino (tested on a Mega here) without taking all memory (less than 50% of an Uno) and without any drops.

Let me know if you think something else should be amended!

Best, Tom

tomcombriat commented 1 year ago

Hi,

I am wondering if we could merge this? I am using it all the time (that the only difference between the branch I'm using and sensorium/master) and I think it is really worth it! This is obviously a bit heavy to use, but if the MCU supports it it really makes a huge difference on sounds which are in the treble range with non-sinusoidal waveforms.

Cheers,

sensorium commented 1 year ago

I'm a git fool - how can I get just the files I need to test this? Thanks...

tomcombriat commented 1 year ago

Good question, the easier might be for you to fork my repo somewhere but I'm sure @tfry-git has a better solution!

sensorium commented 1 year ago

Hey Tom, it works here with the example. Let's include it in the release.

sensorium commented 1 year ago

Woops! I meant the Wavefolder. I'll try this one...

sensorium commented 1 year ago

Tested and working on 32U4 and 328 boards.

tomcombriat commented 1 year ago

The end of a long story! I am using it on nearly all my projects, which usually involves other boards so I think it is proofed. Having the possibility to remove aliasing on some time of waves can be useful, but was a bit reluctant to merge such a big one by myself. Thank both for trying and improving the PR!