Gen - sharing work - antialiased OSC (similar to tri~) with flexible oversampling


    Jun 02 2021 | 9:02 am
    Since it seems there isn't to much documentation on oversampling and band-limiting OSCs in gen (or at least i couldn't found that much on those topics), I decided to share my experience on this topic and a small patch with an antialised OSC. The oversampled OSC is simple it uses a recreation of the "phasor" object and a triangle object to generate the wave, after this I used a lowpassfilter to filter out the high frequencies before going back to normal sampling-rate. The amount of oversampling used depends on the general samplingrate (higher rates need less oversampling) and the frequency of the played note (higher frequencies need more oversampling). While oversampling isn't possible in gen natively (at least as far as I know) it can be done using a for loop in a codebox, however objects that receive the samplingrate internally (like phasor) can not be oversampled, luckily phasor can be recreated quite easy so that you can make a clone of it that can be oversampled. This same method could be used to create all kind band-limited of OSCs.

    • Jun 02 2021 | 1:49 pm
      Awesome demo! :)
    • Jun 03 2021 | 2:29 pm
      Amazing! Thanks so much for this. I feel like i'm about to ask stupid questions(it's ok though: feeling stupid is what i do best in life ๐Ÿฅบ๐Ÿ˜œ), but i don't understand how you came up with the values particularly in this line:
      os = int ((48000 / samplerate) * (2 + (14 * (scale(in1, 200, 12000, 0, 1, 0.5)))));
      in particular, how did you figure out where the '14' comes into play? and since i stop understanding there, i wonder how i'd figure out the "2 +..." also... and how did you know to use "0.5" for the exponential curvature of 'scale'? i guess i can sort of see that the input to 'scale' will be an effective range in Hz?
      maybe they're all part of the same question: i don't understand how i could come up with that line of code myself to apply to other instances(i realize i probably could just copy/paste, but i don't like to do that ๐Ÿ˜‹)
      maybe a better question would be: where can i read more, in order to learn the basics of this math?
    • Jun 03 2021 | 3:05 pm
      So to the line os is the amount of how much it will be oversampled. And on how I came up with this is: 48000 is my "standartsamplerate" so if you would use a higher rate you'll need less oversampling. for the second part, it will scale the incoming frequenci to a range from 0 to 1, so evrything from 200 hertz and below will only be oversampled 2 times, and evrything obove 1200 Hertz will be oversampled 16 times. It is flexible because there is more alising on higher basfrequencis than on lower, so we can use less oversamoling on those lower frewquencies and safe some CPU. So 16 as highest oversamplingrate and 0.5 for curvature are just vThe specific values where just For the specific values I was just trying around and they did work fine. If someone has better mathknowledge this whole thing could probably be optimised a bit more. And where can you read more? Hmm I don't know, like i said it was more try and error than actual math
    • Jun 03 2021 | 3:33 pm
      perfect, that explains everything i needed ๐Ÿ™Œ (i also reread some relevant stuff in Musimathics Vol 2. just now and with your explanation it helps me realize the "2 + (14*..." leads to 16x for the highest need here... i had never seen it parsed out to different parts of the spectrum before using 'scale' etc.. everything below 200 oversampled twice, while everything above 12000 oversampled 16x ... that's genius!๐Ÿ™‡โ€โ™‚๏ธ)
      now i completely understand. thanks again so much!
    • Jun 06 2021 | 6:00 am
      Thanks a lot for sharing. Could you briefly explain what these 2 lines of code are doing: Audio = wrap(phase, 0, 1); phase = fixdenorm(Audio + (in1 / sr)); Thanks a lot!
    • Jun 06 2021 | 11:40 am
      These two lines are a recreation of the phasor object. The reoson phasor can't be used here is that it is internaly getting the samplingrate at which in which the whole patch is running. But in this situation we want it to run at a higher sampling rate, so it needs this recreatin to be abel to use a higher samplerate.
    • Jun 08 2021 | 5:05 am
      Thanks!
    • Jun 11 2021 | 3:46 pm
      Probably a stupid comment, but I will make it anyway: as far as I understand, the gen patch outputs at sample level and your for loop enables you to calculate steps in between (=oversampling). The only benefit, which could come out of the oversampling is, if any of the calculations make between two sample outputs uses historical data . Since the triangle object only reads out data from a wavetable, the generation of the triangle does not benefit (or change) from (due to) the oversampling. It is just the output after the low-pass filter that benefits from the oversampling since it makes use of historical data. For some reason I had this false impression that you could also reduce the aliasing effect if you were to oversample just the triangle object.
    • Jun 11 2021 | 3:58 pm
      Here's the exact C++ code of triangle:
      inline t_sample triangle(t_sample phase, t_sample p1) { phase = wrap(phase, 0., 1.); p1 = clamp(p1, 0., 1.); if (phase < p1) return t_sample((p1) ? phase/p1 : 0.); else return t_sample((p1==1.) ? phase : 1. - ((phase - p1) / (1. - p1))); }
      So you see it's not a wavetable. May be with the max-triangle but not the gen-triangle. And you also see, it dose not ask for a samplerate, so this is why it can be oversampled. If you look in to the patch I did make the same thing without oversampling and you can hear the difference. However the filter has to be oversampled as well, if you wouldn't do it would create the alising when going down to the original samplerate, so you have to filter out the unwanted frequencies before going to the original samplerate. But even a wavetable would benefit from the oversampling.
    • Jun 11 2021 | 4:12 pm
      Here's the exact C++ code of triangle
      ah nice! i was wondering, myself, how you knew phasor depended on sample-rate yet triangle wouldn't.
      Probably a stupid comment
      not a stupid comment at all(i had the same doubt, but couldn't find the time to verify in detail, plus could tell by the sound and spectroscope it was more than the filter that was oversampled..)... but like many things/'beautiful idiosyncracies' in Max, the docs can be a bit... confusing if not misleading:
    • Jun 11 2021 | 4:23 pm
      If you export your genpatch, you will get a file genexported.cpp in that youโ€™ll find your patch as well as the code for all used gen objects. So if you want to know more details about what exactly an object is doing or how it does what it does, this is a great way to find out.
    • Jun 11 2021 | 5:18 pm
      ya, definitely, i just hadn't used the export at all yet(stuck in a C programming mindset for so long, C++ has been difficult for me)... i'll do that more often now to double-check. Thank you!
    • Jun 13 2021 | 11:19 am
      Thanks. Need to digest this a little. But a lot of useful background information. Thanks a lot for that!
    • Sep 25 2021 | 7:22 am
      YO! Absolutely bananas stuff happens when you add "rate" after this oscillator! It's like slightly unstable oscillator sync and f'ing incredible. I'm guessing cause rate is on the original sample rate or something? Or maybe it's that it's a triangle or odd shape. Idk. But this is great.
    • Sep 25 2021 | 10:54 am
      What do you mean with adding rate? Could you share the code, so we see what you mean?
    • Sep 28 2021 | 9:30 am
      Theres no code. It's a gen object called rate. You put it after phasors for sync.