porres / pd-else

ELSE - EL Locus Solus' Externals for Pure Data
Do What The F*ck You Want To Public License
297 stars 35 forks source link

add compiled band limited oscillators #1288

Open porres opened 2 years ago

porres commented 2 years ago

hopefully with support for phase modulation and hard sync

@timothyschoen is also interested in this one

timothyschoen commented 2 years ago

https://github.com/agraef/purr-data/blob/master/externals/creb/modules%2B%2B/blosc%7E.cc may be of help for figuring out the algorithms. It also appears to have sync.

Currently, it's a single object that supports all shapes. I'd rather have this replace the existing [bl.*] objects in ELSE. The good news is that this appears to contain implementations for exactly the same shapes the [bl.*] objects ELSE already has.

porres commented 2 years ago

ok, so, that's what I thought, when you mentined "purr data's band limited oscillators", these aren't really purr data's, but creb's ;) which long existed before purr data! Here is where creb lives now: https://github.com/electrickery/pd-creb

Please be carefu, when you say it's somethign from purr data it gives us the idea it's something special to them not possible in vanilla... while creb is a library that works just fine like any other in Vanilla.

It also appears to have sync

It does.

My ambition was to have something more powerful than creb but we can start by using it, there are also other algorithms out there

The good news is that this appears to contain implementations for exactly the same shapes the [bl.*] objects ELSE already has.

Not all of them, nope :/

ok, let's start by stealing from this one, I'll make it [bl.osc~] for starters

porres commented 2 years ago

there are other algorithms around we should also look into, and I'd definitely need help with that

porres commented 2 years ago

@timothyschoen , I can't build creb, I get:

"fatal error: 'iostream' file not found"

can you help me?

porres commented 2 years ago

I can't build creb

yeah, I was trying to build the whole library package and not just blosc~, but blosc~ uses this too, it's in DSPIcomplex.h

timothyschoen commented 2 years ago

I didn't know crab works with Pd-vanilla too, that's cool!

blosc.cc is a C++ file, are you compiling it as C++? If you prefer C, I can help you convert it if you want?

porres commented 2 years ago

are you compiling it as C++?

not sure what that means, I just used pd-lib-builder with "make"

If you prefer C, I can help you convert it if you want?

That's wierd cause the code for blosc~ itself really looks like a regular plain c code... what about that? but yeah, anyway, plain C is what ELSE is all about, no objects' sources are ".cc" in ELSE.

Hopefully this is easy to adapt. Should we fork creb and make it compile easily? I opened a ticket at https://github.com/electrickery/pd-creb/issues/1

timothyschoen commented 2 years ago

It appears the DSPIcomplex/DSPIfilter classes are the C++ part. It's mostly code for biquads, I'll try to convert this to C!

porres commented 2 years ago

ok, let's do it here then https://github.com/porres/pd-creb

porres commented 2 years ago

ok, let's do it here then https://github.com/porres/pd-creb

or maybe not :)

timothyschoen commented 2 years ago

Turned out to be a bit more work than I thought, converting many lines of OOP C++ code to C is no fun...

Are you planning to only copy the blosc~ part? In that case it would be much easier to just replace this filter for another stacked biquad implementation.

porres commented 2 years ago

Are you planning to only copy the blosc~ part?

yup

Turned out to be a bit more work than I thought

that's why I said "maybe not", I thought it'd be too much work to get the whole library in plain C

porres commented 2 years ago

how is this going @timothyschoen?

timothyschoen commented 2 years ago

I'm on vacation right now, I'll be back Thursday. Sorry if this is inconvenient for your ELSE schedule, I'll try to work on this as soon as I get back. I'm hoping to finish the blep oscillators before RC3, and work on store/rec and memory leaks after that, if that's okay with you?

porres commented 2 years ago

it's ok, I was gonna release it now but I can hold on the release just fine also

porres commented 2 years ago

I actually need to work more on the new filtergraph object to make it more presentable

timothyschoen commented 2 years ago

I've started working on it here:

https://github.com/timothyschoen/pd-else/blob/master/Classes/Source/blosc%7E.c

The [bl.saw~] object seems to work, square/triangle/pulse still need work. The creb library contains objects for both single-sided and two-sided pulse, how would you go about naming those? [bl.pulse~] and [bl.pulse2~]?

There was no triangle implementation, but I think it shouldn't be hard to derive it from the square implementation. The square implementation is a bit strange, it currently takes a phasor as input. I'm probably going to turn it into a regular square object with PWM.

Only a small amount of the filtering code was actually used, so I just converted those functions/structs to C. I have to test it a bit more but I'm pretty sure it works. I'm doing the complex calculations with the C library, but it might be better to do these calculations directly.

porres commented 2 years ago

for reference https://cycling74.com/tools/antialiased-oscillators

see "pulseEptr1.maxpat" in this zip

synthcore24.zip

it's a gen~ patch I always wanted to port into vanilla maybe

timothyschoen commented 2 years ago

Very interesting, I'll take a look at these! I could convert the gen~ patch straight to C code, but I wonder how readable that's going to be. Could be a good starting point though. From what I've read so far, EPTR should be better than BLIT/BLEP/other algorithms.

timothyschoen commented 2 years ago

I've moved to using the polyBLEP oscillators from here: https://github.com/LabSound/LabSound/blob/dcd96dc7f3434482ec5e91fe525a3dbbca3e3664/src/extended/PolyBLEPNode.cpp

The code is a bit cleaner, and I hope polyBLEP will bring slightly better results. I have the basics working in Pd already, now I have to figure sync.

porres commented 2 years ago

Just quickly tested this https://github.com/timothyschoen/pd-else/blob/master/Classes/Source/blosc%7E.c

I would probably create a new temporary bl.osc~ object with arguments specifying which function, then split it into different objects afterwards. I see you have a float phase input for 'phase', but that's actually 'sync'. I can work on it.

If we're writting code, can we not use the gnu license? I don't do that in ELSE unless I'm using other code like in giga.rev~

I've moved to using the polyBLEP oscillators from here: https://github.com/LabSound/LabSound/blob/dcd96dc7f3434482ec5e91fe525a3dbbca3e3664/src/extended/PolyBLEPNode.cpp

The code is a bit cleaner, and I hope polyBLEP will bring slightly better results. I have the basics working in Pd already, now I have to figure sync.

Nice. I'm completely ignorant in all of this, so I can't say much. I see that blosc~ has sync already figured out, seemingly, not sure how this other technique works with hard sync and phase modulation. Phase modulation does generate partials that can fold over, so I'm not sure if these techniques take care of that, or even if this is taken care of in MAX's oscilltors.

If you need help understanding hard sync and stuff, I can help.

timothyschoen commented 2 years ago

Thanks, I'm not sure if I understand the difference between phase modulation and sync, considering that they will both affect phase. If there's an inlet to set the phase, couldn't you use that for both sync and phase modulation? If you could explain the difference, that would help a lot!

In Max, the [saw~] object only has 2 inlets, frequency and sync. The triangle and square have an extra inlet for "duty cycle".

The code from LabSound has a "syncToPhase" function, I'm hoping that will help.

The good news is that I've seen the PolyBLEP oscillators from LabSound being used in multiple projects, which gives me hope for the quality of the oscillators. This code is more liberally licensed as well, so we can get rid of the GPL license without breaking the law. Maybe @sebshader could take a listen once I finish a prototype, he seems to know more about this.

If we're unable to figure out all the features of ELSE's current bl oscillators, we could also consider copying Max' interface and putting it into cyclone instead. An advantage is that they'll be called "rect~/saw~/tri~", which makes them very easy to discover (more of a PlugData concern, admittedly). That way, there's no loss of features. But I can understand it if you'd rather make improvements to ELSE.

porres commented 2 years ago

Thanks, I'm not sure if I understand the difference between phase modulation and sync, considering that they will both affect phase. If there's an inlet to set the phase, couldn't you use that for both sync and phase modulation? If you could explain the difference, that would help a lot!

hard sync or phase sync is like the right inlet of [osc~] and [phasor~], the original [blosc~] object also has such an inlet. It syncs the phase according to a point in the phase cycle. My oscillators in ELSE provide a signal input for this as well, which is triggered by impulses, so you can actually implement the hard sync synthesis technique. The [blosc~] object has control inlets like [osc~] and [phase~] but it also provides a ready made hard sync sawtooth option, so you don't need to trigger it with impulses. I like my design better, of course.

Yeah, MAX's oscillators have hard sync and they're triggered by [phasor~] objects, which I think it's bad, I'd rather it'd be impulses.

The phase modulation input is just another input for changing and modulating frequency, but operating on the running phase itself. The oscillators from SuperCollider have a phase input for this (but not for sync). Phase modulation is important for implementing Frequency Modulation like in DX7.

The code from LabSound has a "syncToPhase" function, I'm hoping that will help.

Looks like hard sync :)

If we're unable to figure out all the features of ELSE's current bl oscillators, we could also consider copying Max' interface and putting it into cyclone instead.

Hard sync is the tricky part I guess, a phase modulation input should be trivial. I just wonder if the partials created by the frequency/phase modulation should also have anti aliasing filtering, but I guess that is not the case, for instance, in SuperCollider or even elsewhere (DX7). This is something we should check, but I am really positive there's no such concern, because the foldover also happens at 0Hz... anyway, the current oscillators from ELSE just have this oversampling + filtering, so they do have an anti alising filter for frequency/phase modulation and maybe we should keep them as an alternative version because of this. They're CPU expensive, but it's an interesting thing and ELSE is also the basis of my didactical work, so it would serve at least as a didactical example.

By the way, I do provide a band limited sinewave in ELSE just because of hard sync, because hard sync generates lots of aliasing. Apparently these other oscillators don't care for bandlimiting hard synced sine waves (MAX doesn't have it, for instance).

Anyway, for last, I hate MAX and Cyclone with all my guts. At one point I had abstractions for cyclone, but gladly removed them. I almost I started working with cyclone :)

timothyschoen commented 2 years ago

I tested this, Max' oscillators work with both phasors and impulses. I guess it only pays attention to the jump part of the phasor?

I have working objects for saw/square/triangle, implemented with polyBLEP now, they sound pretty good imo. The hard sync doesn't sound like what I would expect yet, so that still needs work. I do see a serious performance improvement compared to the oversampled oscillators, which was to be expected. I'll work on phase modulation as well, thanks for your explanation!

I agree that it might be good to keep the oversampled ones, because their simplicity makes them very reliable for things like FM synthesis and other complicated tasks. These oscillators on the other hand, would be better for subtractive (poly)synths.

I'll push these new versions to GitHub soon!

Screenshot 2022-07-15 at 17 59 26
porres commented 2 years ago

as for phase modulation, I guess you have a running phase inside the oscillator. I do in my objects, check https://github.com/porres/pd-else/blob/master/Classes/Source/saw~.c#L47

input frequency determines a phase step and the phase modulation is a simple phase deviation on the phase step counter, that's it

porres commented 2 years ago

From what I've read so far, EPTR should be better than BLIT/BLEP/other algorithms.

I should add information about this in my live electronics tutorial, about all the algorithms out there, how they work and everything. I hope you and @sebshader can help me on this one too

porres commented 2 years ago

This code is more liberally licensed as well, so we can get rid of the GPL license without breaking the law.

BSD is great :) since this would not be code we created, it'd be ok to end up with a GNU license, I meant only if we were writing code then i'd like to use a more permissive license. But if it uses BSD, then it's almost ideal and perfect.

timothyschoen commented 2 years ago

From what I've read so far, EPTR should be better than BLIT/BLEP/other algorithms.

I should add information about this in my live electronics tutorial, about all the algorithms out there, how they work and everything. I hope you and @sebshader can help me on this one too

I've since learned that this is not true, I posted the source in a Facebook comment.

sebshader commented 2 years ago

Idk that much about polyblep or PTR specifically yet. (I've only used Blit). hard sync is easier to anti-alias than phase or frequency modulation, the reason being that you only need to jump to points in the wave, rather than traveling through it. (In order to avoid that aliasing you would have to put an anti-alias filter after the frequency modulation but before sampling. That will give a different result than putting one after sampling which is what happens when you just travel to the next point according to input frequency)

But that is a very difficult issue to solve I think (even for sine waves and that's for linear FM, not even considering exponential FM). Of course oversampling goes a long way so some oversampled polyblep might be the best case.

You could also just use wave tables but those have their own issues of course..

One thing to consider regarding phasor~ vs impulse triggering for hard sync is that if you use phasor~ you could hypothetically know the fractional part of the sample the jump happens which would/could be more accurate I think

Edit: polyblep can also be higher order afaik, might be worth it to use more than 2 points..

timothyschoen commented 2 years ago

I think the current function is first order, I could use higher orders, but from what I've read online the difference is minimal. Oversampling is probably a better bet if you want them to sound perfect.

I've implemented sync and phase modulation, and it appears to be anti-aliased pretty well. I've compared the polyBLEP sawtooth with an oversampled and a raw sawtooth. Sync and FM sound very good, with little to no aliasing. Phase modulation will cause some aliasing, especially at higher rates, but still less than a raw sawtooth. I guess this is because of how BLEP works. It detects the discontinuities that cause aliasing, and inserts a band-limited pulse instead. You can apply whatever modulation you want, because it applies this to the final phase value. That said: I've been listening to beeps all day, so I might need to check again with a fresh ear.

I could implement oversampling, but I'm not sure yet if it's worth it. Right now, the oscillators are very lightweight while still having significantly reduced aliasing. Adding oversampling might further reduce aliasing, but since it already works so well, I'm wondering if it's worth sacrificing performance for. Especially because oversampled oscillators will also be included.

It might be an option to replace the default saw/square/triangle with these oscillators. When these oscillators are used inside the oversampled oscillator abstractions, you'll only need 2x or 4x oversampling to get a perfect result. A pre-requisite would be some more testing to make sure that these are completely correct. I think @porres can decide on the best way to organise these new oscs.

Also: the tri~ oscillator has an option to morph between triangle and saw, I think it's pretty cool (Max has this as well).

My new version: https://github.com/timothyschoen/pd-else/blob/master/Classes/Source/blosc%7E.c

Some sources: The method we're using: https://core.ac.uk/download/pdf/297014559.pdf Good explanation of BLIT/BLEP: https://www.metafunction.co.uk/post/all-about-digital-oscillators-part-2-blits-bleps

(sorry for this essay of a comment!)

porres commented 2 years ago

question: what technique is j09 from audio examples?

sebshader commented 2 years ago

@porres BLEP

timothyschoen commented 2 years ago

It's similar to what I'm doing now, except I use a polynomial instead of a table.

porres commented 2 years ago

My new version: https://github.com/timothyschoen/pd-else/blob/master/Classes/Source/blosc%7E.c

seems that pulse width and sync is not working yet. I can't see it yet in the code where the magic happens, where's the polynomial...

Can we adapt the code for now so the object is created as [bl.osc~] and the shape is given by the first argument? Similar to [creb/blosc~]. This might be the best solution and I might just keep it, but I'm not sure yet.

Note that [bl.tri~] with a variable width should actually be renamed to [bl.vsaw~]. Check [else/vsaw~] for more details on the waveshape. [bl.tri~] should just have a simple triangular waveform.

So we have [bl.square~], [bl.saw~], [bl.tri~], [bl.vsaw~]. the [bl.saw2~] should be easy to adapt as well. We'd be mostly missing the impulse~ and impulse2~ versions, that the original creb/blosc~ also has. Any idea on that?

Else also has a [bl.wavetable~] and I'm not sure if we can do something like this, like having arbitrary waveforms from tables in Pd and use them. That'd be great. We would need at least a sine wave for [bl.sine~] and I guess a wavetable is best for that, what do you say?

porres commented 2 years ago

But we can include [bl.osc~] as it is, just with the saw, square, tri and vsaw options right away, as soon as we have the pulse width and hard sync working

porres commented 2 years ago

Can we adapt the code for now so the object is created as [bl.osc~] and the shape is given by the first argument? Similar to [creb/blosc~].

I can do that once you figure out the sync/pwm

sebshader commented 2 years ago

edit: whoops, didn't see blamp was already implemented but just a small note on blosc~.c @timothyschoen: in my experience using macros for clipping are faster than using fmin() and fmax() iirc it's bc of macros being more type-generic (fmin/fmax have to have their arguments converted to doubles or whatever and then might be a library call)

porres commented 2 years ago

blamp?

sebshader commented 2 years ago

@porres triangle waves use polyBLAMP instead of polyBLEP. It's like an integrated BLEP, in order to make a bandlimited corner

porres commented 2 years ago

I went ahead and adapted the code. I'm gonna be working a bit on this now https://github.com/porres/pd-else/blob/master/Classes/Source/bl.osc~.c

I see that the square waveform is also a pulse wave, not a sqaure wave

porres commented 2 years ago

I see that the square waveform is also a pulse wave, not a square wave

can't see that no more, anyway, fixed the pulse width for the square wave in my last commit

porres commented 2 years ago

Note that [bl.tri~] with a variable width should actually be renamed to [bl.vsaw~]. Check [else/vsaw~] for more details on the waveshape. [bl.tri~] should just have a simple triangular waveform.

my last commit makes 'tri' a regular triangle waveform and now I have 'vsaw' for a 'variable sawtooth'

porres commented 2 years ago

added 'saw2' in my last commit

porres commented 2 years ago

fixed sync input, bu for floats only for now (will include signal as well later)

porres commented 2 years ago

https://github.com/porres/pd-else/commit/71a2707b4485bcf3c22e51b81e62cee8877baba1 includes signal for hard sync but for some reason ir doesn't work for no signal input and just outputs 'nan'. Maybe @brbrofsvl can help figure out what's going on, but on second though, my existing objects like bl.sine~ can't sync with float input and this doesn't really make much sense for bandlimited oscillators, so I'm gonna just have signal input instead, which will make the code cleaner

porres commented 2 years ago

https://github.com/porres/pd-else/commit/7af6bf6f20fde644a1f2c230f3ac8fe263c06163 removes float sync, so we can just use impulses for hard sync. This is pretty much done! I can revise and test some more, please also test this.

What's missing now is other waveforms, like impulses (one side and two sided with pwm) and maybe wavetables, what do you say @timothyschoen and @sebshader ? Is it possible?

sebshader commented 2 years ago

bandlimited wavetables are another entire topic/implementation imo (personally I've been wanting to implement this one.. I think serum uses a variation of it http://www.mp3-tech.org/programmer/docs/resampler.pdf) you already kind of have bandlimited impulses with gbuzz and buzz, right? I guess there's some way to generalize it to bipolar dsf..you could also use blit (direct dirichlet kernel) from that JOS article.. here's mine (and there's also regular buzz~.c): https://github.com/sebshader/shadylib/blob/master/bpbuzz%7E.c

porres commented 2 years ago

you already kind of have bandlimited impulses with gbuzz and buzz

I only have gbuzz~ but I don't have hard sync and phase modulation and no bipolar impulses with pulse width - creb/bloc~ has these, I wonder if we can steal from it... but creb uses a different technique, right? Which one again? Doesn't polyBLEP offer us bandlimited impulses?

you could also use blit (direct dirichlet kernel) from that JOS article.. here's mine (and there's also regular buzz~.c): https://github.com/sebshader/shadylib/blob/master/bpbuzz%7E.c

cool, will check it out

brbrofsvl commented 2 years ago

I can make a buzz easily enough if you want it

On Tue, Jul 19, 2022, 12:51 AM porres @.***> wrote:

you already kind of have bandlimited impulses with gbuzz and buzz

I only have gbuzz~ but I don't have hard sync and phase modulation and no bipolar impulses with pulse width - creb/bloc~ has these, I wonder if we can steal from it... but creb uses a different technique, right? Which one again? Doesn't polyBLEP offer us bandlimited impulses?

you could also use blit (direct dirichlet kernel) from that JOS article.. here's mine (and there's also regular buzz~.c): https://github.com/sebshader/shadylib/blob/master/bpbuzz%7E.c https://github.com/sebshader/shadylib/blob/master/bpbuzz~.c

cool, will check it out

— Reply to this email directly, view it on GitHub https://github.com/porres/pd-else/issues/1288#issuecomment-1188596585, or unsubscribe https://github.com/notifications/unsubscribe-auth/ADSF4MQGI4MRI4DLA6L53ZTVUYX6PANCNFSM53CQ3XZA . You are receiving this because you were mentioned.Message ID: @.***>

timothyschoen commented 2 years ago

7af6bf6 removes float sync, so we can just use impulses for hard sync. This is pretty much done! I can revise and test some more, please also test this.

What's missing now is other waveforms, like impulses (one side and two sided with pwm) and maybe wavetables, what do you say @timothyschoen and @sebshader ? Is it possible?

I'm not sure how to do this, but if you'd like I could create a very minimal copy of pd-creb's impulse shape? I think BLIT might be more suited for this than polyBLEP.

If you're going to continue to work on it, I've also noticed that the PolyBLEP oscillators don't take negative frequencies yet.