processBlock with float vs double?

Hi friends,

I’m looking through the AudioProcessor base class, and I see that there are two versions of the virtual void processBlock(): one that takes an AudioBuffer<float>, and one that takes an AudioBuffer<double>.

If I want my plugin to work with either 32-bit or 64-bit systems, should I implement both of these methods? Does everything have to be written twice, with two versions of any internal buffers…?

or is there some way of doing a function template…?

Thanks in advance for help!

That’s what I usually do:

void MyPluginProcessor::processBlock(AudioBuffer< float >& buffer, MidiBuffer& midiMessages)
{
	processSamples(buffer, midiMessages);
}

void MyPluginProcessor::processBlock(AudioBuffer< double >& buffer, MidiBuffer& midiMessages)
{
	processSamples(buffer, midiMessages);
}

template< typename Type >
void MyPluginProcessor::processSamples(AudioBuffer< Type >& buffer, MidiBuffer& midiMessages)
{
1 Like

awesome, thanks!

So if I have any internal buffers as members anywhere, would I need to create two versions, one for float and one for double…? Or is there some way to template/conditional those too?

The used audio sample type doesn’t have anything to do with the CPU or operating system type. That is, you don’t for example need to have support for 64 bit floating point on a 64 bit OS.

1 Like

so if I were to only implement the <float> version of processBlock and not the <double> version, it would still run on any computer & any operating system…?

Yes. (Of course you need to build 32 bit binaries for 32 bit systems and 64 bit binaries for 64 bit systems.)

but either 32-bit binaries or 64-bit binaries can be built from the <float> version of processBlock?

so is it correct to say that the <double> version is just like an extra layer of precision for some systems that may happen to support it, but the <float> version provides basically universal compatibility/usability?

Yes, 32 and 64 bit binaries can be built from code that only supports the float processing.

The 64 bit float processing is optional, and you can implement it for marketing reasons or such. Not all hosts even support it, though.

2 Likes

Gotcha. I guess I was assuming that 64-but processing = doubles and 32-bit = floats ¯_(ツ)_/¯

Thanks for the clarification!

@rory if I’m going to wrap my processing like this and redirect both versions of processBlock to a templated function, how should I deal with internal buffers I need to use for processing?

Is there a way to convert their type depending on the input, or maybe re-initialize them during prepareToPlay if the type changes…?

If I understand correctly you’re asking about how to deal with buffers that might be members of your AudioProcessor class, and how best to use them in the two template functions? I’m not sure what the best approach is here. I would just use floats throughout. Is there anything in the FloatVectorOperations class that might do buffer conversions?

1 Like

They can be converted, but that might undo all efforts (not discussing the necessity or uselessness of double precision processing here).

Another thing to point out: there is no need for a 64 bit architecture to support double precision processing. All permutations will work, 64 bit processing on a 32 bit OS and vice versa.

What you can do is write your processSamples method to accept the internal buffer as third parameter. That way you call it in the not templated calls with the correct variant of the internal buffer.

You’re correct, I’m talking about internal buffers that are members of my AudioProcessor class.

I got it to work doing what I think @daniel just suggested - I have made duplicate versions of my buffers, like so:

AudioBuffer<float> wetBufferFloat;
AudioBuffer<double> wetBufferDouble;
AudioBuffer<float> dryBufferFloat;
AudioBuffer<float> dryBufferDouble;

and then doing this:

processBlock(AudioBuffer<float>& buffer)
{
    processBlockWrapped(buffer, dryBufferFloat, wetBufferFloat);
}

processBlock(AudioBuffer<double>& buffer)
{
    processBlockWrapped(buffer, dryBufferDouble, wetBufferDouble);
}

template<typename SampleType>
processBlockWrapped(AudioBuffer<SampleType>& input, AudioBuffer<SampleType>& dryBufferToUse, AudioBuffer<SampleType>& wetBufferToUse)
{
   // do stuff
}

it works, but it sucks having to make two versions of everything and have a ton of arguments to my wrapping functions…

You can also create a templated “engine” class, so everything is wrapped there:

template<typename FloatType>
class Engine
{
public:
    Engine (MyProcessor& p) : processor (p) {}
    void process (juce::AudioBuffer<FloatType>& buffer);
private:
    // use a back reference to access functions of the AudioProcessor
    MyProcessor& processor;

    // only one buffer to write
    juce::AudioBuffer<FloatType> wetBuffer;
    juce::AudioBuffer<FloatType> dryBuffer;
};

// processor members
Engine<float>  singleMachine  { *this };
Engine<double> doubleMachine  { *this };
2 Likes

This is a good solution!

It would still require twice as much memory though, to have two of these Engine objects, right?

I don’t suppose there’s any way to only allocate/initialize the double version if the host asks for it…?

The AudioBuffer would be there in any case. But you will probably need something like setup() in the Engine, that is called from prepareToPlay(). There you can ask there for isUsingDoublePrecision() and only setup the engine you are going to use.

This way the audio buffers in the unused engine remain at zero size.

2 Likes

cool - so the Engine class’s internal buffers would remain at 0 size unless & until its prepare() method is called in the top-level processor’s prepareToPlay, correct?

and there could also be an engine.releaseResources() to resize its buffers to 0, in case the user switches precision processing modes, so it won’t still have memory allocated for both instances of engine…

I was going to suggest the same thing.

Something that’s worth baring in mind is that you’ll need to make sure that every process that’s editing your samples (filters, compressors, etc.) will also need to be templated to handle either data type. There’s no point in using double samples in your Engine class if your filters only use floats as you’ll just lose the extra precision.

1 Like

Also, I try to avoid using juce::AudioBuffer for storing samples passing through processBlock.

It’s really easy to write something like:

void processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer&)
{
    dryBuffer = buffer;
    // do some processing...
    wetBuffer = buffer;
}

The issue is that if the buffer passed to processBlock is not the same size as dryBuffer or wetBuffer then these buffers will be resized by the copy which will involved some memory allocation.

Instead I’d suggest using a fixed-size FIFO for storing audio data, or even a ring buffer. You could always store some write indicies along with your wet/dry buffers and treat them as FIFOs but your code will be a lot cleaner if you use/write a dedicated class for the job (which could use a fixed-size AudioBuffer internally if you really wanted).

Exactly. You can also put the Engine into std::unique_ptrs.

If you want to go over the top, you can use a technique called type erasing:

class EngineBase
{
public:
    EngineBase() = default;
    virtual ~EngineBase() = default;

    virtual setup (int samplesPerBlock, double sampleRare) = 0;
};

template<typename FloatType>
class Engine : public EngineBase
{
// ...
};

Now you can put the Engine in a

std::unique_ptr<EngineBase> engine;

But then you face the problem again, how to call the templated variant, since the FloatType is not known in the EngineBase.

Anyway, I just wanted to introduce that concept, but IMHO it is overkill in that instance.

1 Like