Expose internal audiobuffers please

i just tried to implement juce::dsp::Oversampling in my current project. it returns a juce::dsp::AudioBlock in the upsampling method. My existing code works with juce::AudioBuffer all over the place and i would like to not rewrite it all now. However both juce::dsp::AudioBlock and juce::dsp::Oversampling have no method for exposing the underlying juce::AudioBuffer used to store the samples. I tried to find a method in juce::dsp::AudioBlock that would return float** so i can at least rewrite my stuff to this generic layout, as that would enable me to be more independent of all those custom buffer-types, but juce::dsp::AudioBlock does not provide a method for that.

Possible solutions sorted by how much i’d appreciate it:

  1. Ditch juce::dsp::AudioBlock entirely.
  2. Give it methods for returning or constructing juce::AudioBuffer that are safe to use in processBlock and don’t copy samples.
  3. Rewrite juce::dsp::Oversampling in a way that it has no internal audioBuffer, but requires the user to give it one.
  4. Add a method to juce::dsp::Oversampling for giving access to its juce::AudioBuffer. (not a good solution, because you’d have to do the same for all other juce::dsp classes as well. DRY. but i’d still appreciate it for now because it is the fastest solution and i could go on now without rewriting stuff)

Can’t you just use this where you need to get access to the raw channel pointers and overwrite the values yourself directly?

dsp::ProcessContextReplacing<float> replacing(block);
auto&& outBlock = replacing.getOutputBlock();
float* out0 = outBlock.getChannelPointer(0);
float* out1 = outBlock.getChannelPointer(1);

not sure if i understand you. when i make a ProcessContextReplacing of my block and then call getOutputBlock() on that into an auto&& i’m just getting another AudioBlock. i think i’ll just rewrite all my dsp-related files and try to find a way to make them use AudioBlock now. i was just annoyed about it because i didn’t want to spend all day with such stuff.

juce team… if you want to get anything out of this: if you ever want to make another audio buffer/block type, at least add the option somewhere to get the float** data from it so it can be used just as flexibly as AudioBuffer

I think that you are overlooking an important property of the dsp::AudioBuffer here that makes it impossible by design to expose a raw float** pointer:

The AudioBlock offers you a getSubBlock function which allows you to take an arbitrary sub-block of the original block, starting at the nth sample. This gives you great options, one of my favorite is something like this, where you need to make sure that certain parts of your processing code might not be called with a block size greater than a certain limit:

void processBlock (const juce::dsp::AudioBlock<float>& block)
{
    if (block.getNumSamples() > maxBlockSize)
    {
        auto a = block.getSubBlock (0, maxBlockSize);
        auto b = block.getSubBlock (maxBlockSize);

        processBlock (a);
        processBlock (b);

        return;
    }

    // if we get here, we can be sure that the block size is <= maxBlockSize
    // do processing...
}

Now, since the dsp::AudioBlock is guaranteed to be a super lightweight class, did you ever wonder how the sub-block thing works? The answer is that the class has basically 4 members, which are:

  • The channel count
  • The sample count
  • A pointer to an array of pointers, referencing the start of the referenced sample buffer per channel
  • A start sample offset

The implementation of getChannelPointer looks like this:

SampleType* getChannelPointer (size_t channel) const noexcept
{
    jassert (channel < numChannels);
    jassert (numSamples > 0);
    return channels[channel] + startSample;
}

So you see, that the channels array doesn’t describe the whole channel buffer, it’s the channel buffer plus the startSample offset. This can not be exposed as a simple float** and that’s probably the reason why there is no such interface. Now this is a bit of a concept shift in contrast to old-school buffers, but the same sub-buffer thing wouldn’t have been possible with the old AudioBuffer, wich is a lot heavier data structure that also handles memory management etc. along the way. With the AudioBlock you also have great options to implement sample buffers without using an AudioBlock at all, so there isn’t necessarily any internal AudioBlock to expose at all in some classes. To me the AudioBlock is way more flexible and suited for modern C++ dsp code than the AudioBuffer

This is probably a good decision. I did that with all of our legacy dsp codebase approx 1 1/2 years ago and never looked back. Took me approx. a day of work, which is okay if you want to make your codebase future proof. If you don’t interface with any third party libs that expect a float**, you’ll likely find out that passing an AudioBlock wherever possible and using getChannelPointer in the most inner loops is basically all you need. And this is also a non-breaking change, if you just swap out your AudioBuffer<float>& arguments with const dsp::AudioBlock<float>&arguments. If you declare them as const reference they implicitly cast from an AudioBuffer, so the surrounding code will compile as before. Just make sure that you declare them const – even if you want to write to them. This seems a bit unintuitive at first, but with the dsp::AudioBlock the difference between a read-only and writable buffer is made by declaring the sample type const or not, so a const dsp::AudioBlock<float> will be writable, a const dsp::AudioBlock<const float> will be read-only.

Hope this helps you a bit with your transition :slight_smile:

1 Like

before i re-read your comment, let me just tell you: no. that was a bad decision. you know what happened? first of all i had to rewrite a lot of stuff. then suddenly a lot of stuff that didn’t even seem related didn’t work anymore and buffers were not allocated at the wrong places anymore. also i didn’t commit to my repository before and now i have to manually undo everything. i know. it’s my own fault. but like… this could have all not happened if the juce team had just added a little float** returner on their audioblock. i am… not really in the mood for any tips about audioblock right now.

nevertheless thank you!

1 Like

Dude, did you even read my post once? :wink:

Your experience seems quite frustrating here, but you probably shouldn‘t blame the AudioBlock class for not having saved your perfectly working code before refactoring your implementation :man_shrugging:

And even if you are not in the mood for AudioBlock advice right now, I just leave that here in case you are in the mood tomorrow or so. Reading this:

I‘m not sure if you noticed the big difference between the AudioBuffer and AudioBlock which is that the AudioBuffer can either refer to externally allocated memory or can allocate memory itself. The AudioBlock on the other hand acts only as a reference to externally managed memory and will never manage allocated memory internally. This makes the block a perfect choice for passing blocks to work on to other functions. But where ever you used the buffer as a container that owns the memory, the block is not well suited alternative, at least not on its own.

My advice: Don‘t do such a refactoring in a rush. Brew a cup of coffe, tea or another favourite drink, read the documentation, have a look at the class implementations under the hood until you feel confident that you understand the differences and then refactor your code step by step wherever it makes sense. Make sure that your unit tests pass after each change in case you have written tests. Just give the class a second chance and maybe you‘ll come to the conclusion that there are a few extremely clever design decisions in the juce dsp classes :slightly_smiling_face:

1 Like

i thought i understood AudioBlock. i really thought i did. it is a reference to audiobuffers in some way. it does not own the buffers. but i didn’t think it’s a big deal. in the end all buffers are stored in some way where pointers point to their actual storage in memory anyway. still, after i changed the code that didn’t work anymore from audioBuffer references to audioBlock references, other parts of my code that didn’t even needed to get any changes, but also had audioBuffers in them, suddenly didn’t work correctly anymore and i was so mad. now i did not only read your initial comment but also managed to get the current state of my repository back into a working state again by mindlessly copying far too big amounts of code from one commit to another until it looked like before. it’s actually incredible that this worked lol.

my conclusion is: yeah, audioblock is probably good. i can see how it is nicer to use than audioBuffer. especially because you don’t have to call getArrayOfWritePointers() everywhere anymore, but can rather immediatly start writing or reading to or from it. but it’s also an annoying thing to use in the context of projects that already use the normal audioBuffer a lot and i’d still say it would be cool if this was either forced upon us by the juce framework entirely, from the beginning of processBlock till the end, or made optional so that i can avoid dealing with it if my code base wouldn’t benefit from it. because i just wanted to know how my plugin sounds with oversampling and i just couldn’t do that all day, because of some weird buffer-type-thing. now i have to go to sleep not knowing how low the sidelobes of test-signals running through my resampler would be…

FWIW, I stick to AudioBuffer throughout my codebase. I’ve only ever used AudioBlock at the boundary between the juce::dsp module and the rest of my code. And I’d usually prefer to encapsulate each juce::dsp class into a general use class of my own, that allows you to pass an audio buffer or a float** and encapsulates the AudioBlock handling/conversion logic for you. Or you can use the juce source code as a reference point for implementing your own versions of these classes.

At the end of the day, I would probably sooner find a different solution for implementing oversampling rather than change my entire codebase’s API in order to use juce::dsp::Oversampling.

Just my two cents.

2 Likes

yes, exactly my plan now

1 Like