surge-synthesizer / shortcircuit-xt

Will be a sampler when its done!
GNU General Public License v3.0
259 stars 32 forks source link

Oversampling: The thoughts before vacation #837

Closed baconpaul closed 7 months ago

baconpaul commented 7 months ago

In the voice and group path there are lots of places where we upsample, process, and downsample.

Right now we oversample the generator if the speedup factor is more than about 1.05 roughly (although that calculation has a magic constant from SC1/2 with no explanation) and then downsample it post generator pre zone effects. (see the use of useOversampling in voice.cpp/h)

But then some of the effects upsample, process, and downsample again. Basically the voice chain is this mis-mash of up and down, and peeking in a profiler, I can actually end up seeing some time going to all these runs of HalfRateFilter (not to mention the various sound and phase effects which I bet they introduce).

This kinda has me thinking: we can do a lot better than this if a voice 'retains' oversampling down its pipe.

In voice.cpp you can see where we check if the generator has oversampled and downsample from there. We could instead just leave this oversampling through the entire voice.

This would require the voice processors to deal with oversampled data, but they can. And it would require the zone to deal with oversampled voice output.

But the question banging around my head is: do we even need to do all this 'either or' stuff. If we just run the entire pipeline up 2x the end of group oversampled we can save a whole bunch of up/down work.

In the no-effects case it would be more efficient in most cases anyway. The end of group would do one downsample per group, not per voice. Unless you were in a situation where the generator oversampling never kicked in ... We would do twice as many sums of course but they are already sse-ified and efficient.

In the effects on case, quite a few effects do up/down now, and some of the ones in #836 do it also. things like wave shaper and filter headroom would be useful and common cases; the generators we use now when we run them in surge run oversampled too so we would be matching that.

So just dropping this here because I'm slowly concluding "for almost every use case except for a very large library playback where you never go away from root key on any sample, oversampling the entire pipeline would be lower CPU and more coherent audio output". Totally understand why you wouldn't do that in 2005, but in 2024 I think it makes sense.

Leaving this here for comments.

mkruselj commented 7 months ago

I think it probably makes the most sense to have oversampling an option at group level, so that user can decide how clean they want their stuff to be.

baconpaul commented 7 months ago

That’s a good idea

baconpaul commented 7 months ago

TL;DR

OK so some thoughts

  1. To the GroupOutputInfo add a uint32_t oversampleFactor{2} or a bool (but there's a weird reason why a uint32 is more convenient and future flexible). Then basically copy https://github.com/surge-synthesizer/shortcircuit-xt/commit/1708e6166e10b28d4f8f8cbc17c640618e48874b to get it bound to a multi switch rather than a slider in the output area and streaming. Cool.

  2. Next do voices without effects. Here's how I would do it

    1. Add a blocksize_os constant = block size << 1 in utils.h if we don't have it
    2. make the default voice output size blocksize_os
    3. add a bool to voice 'isOversampled'
    4. In voice initiation do a parentGroup->oversampleration == 2 to set it for now
    5. then in the generator reset the 'is oversampled' based on this and skip downsampling if it is true

I think to do that properly you will need to make voice::process get a template argument for oversampling so you can make sure you use the right block size AEG and lipols so on also. Those details are gonna be tricky. But once it is done you can

  1. In group.cpp :: process after you sum downsample

at that point a zone without effects can do oversampling or not. Hardest part of the above is definitely the block sizes on the envelopes which are compile time. You may just want two envelope generators on the voice type which you can swap between because really we want as much of this to be compile time as possible

So now you have to turn to the voice effects. Here's how I would do it

  1. In processor_defs we create voices based on the sst-voice-effects with a config. There's a macro
    DEFINE_PROC(GenSaw, sst::voice_effects::generator::GenSaw<SCXTVFXConfig>, proct_osc_saw, "Saw",
            "Generators", "osc-saw");

which makes a creditable class. Unpack that macro and you can see what it is doing.

So I think we change that macro so that it looks like this

DEFINE_PROC(GenSaw, 
     sst::voice_effects::generator::GenSaw<SCXTVFXConfig<1>>,
     sst::voice_effects::generator::GenSaw<SCXTVFXConfig<2>>,
     proct_osc_saw, "Saw",
            "Generators", "osc-saw");

namely the config gets a factor. This allows the config to constexpr answer things like block size and sample rate with a factor. Cool.

Then the define proc makes a factory. This uses some template trickery in spawnProcessor but if spawnProcessor gets an oversample arg then we can make the (block-virtual) effects with either a 1x or a 2x. Basically you do it by having the processor type look like

template <ProcessorType ft> struct ProcessorImplementor
{
    typedef unimpl_t T;
    typedef unimple_t T_oversample;
};

and then factory that through in the details section.

Then you go and visit each voice effect and if they implement internal oversampling, strip it out.

and then you should be able to spawn an effect chain with either an OS or a non-OS version.

With all of that in place, you should be good. Quite a bit of work. but that's how I'd tackle it.

baconpaul commented 7 months ago

some other details

baconpaul commented 7 months ago

OK back from vaca am going to tackle this this weekend

baconpaul commented 7 months ago

Just my running extras as I go