Use AudioBlock instead of AudioBuffer

Currently we have the AudioBuffer that owns the samples. This can be copied, which allocates.
But AudioBuffer can also refer to samples, in which case a copy doesn’t allocate.
At the same time there is no way to check on a buffer, if it owns the samples or not.

But for the rescue we have juce::dsp::AudioBlock, which doesn’t own samples. So it is safe to copy and pass around.
But then there is juce::dsp::AudioBlock(juce::HeapBlock& dataToBeUsedForAllocation, …). I haven’t checked, what happens if you copy an AudioBlock constructed with that constructor.

It’s probably a long and winding road, but could we use more AudioBlock, get rid of the owning AudioBlock and the non owning AudioBuffer?
Then it would be perfectly clear, what you can copy safely and what you cannot.
And we could also deprecate/remove the copy constructor of AudioBuffer, once this distinction is clear.

I understand this is breaking a lot of code, but will also expose many silent bugs. And it leads to a much safer API.

I actually think the copy constructor and copy assign of AudioBuffer are really useful when you want to copy the samples, which I use a lot for non-real time situations.

Whenever I want the AudioBuffer to be passed but not copied, I just use a reference or a const reference to it, which IMO makes it much clearer.

I’m not sure I’ve ever run into any bugs by copying the AudioBuffer by mistake…

BTW, I think the non-owning version of AudioBuffer is one of my most used features of it, otherwise I’d have to rewrite every function in my code twice: once for owning and once for non-owning, which doesn’t make sense as most functions don’t care about ownership and just need to read or modify samples.

1 Like

I did once and debugged two days, because I was writing into the copy. But that’s not really what I was worrying about. It is more accidently copying the buffer on the realtime thread, which often goes unnoticed. Until that day on stage with a huge audience…

Well, I would say a function should only ever take an AudioBlock, and you can create an AudioBlock from an AudioBuffer on the fly, so it is a matter of adding some curly braces around your buffer and the same function will work as expected.

While that’s annoying to debug that, AudioBuffer is used so much outside of the realtime context, so I won’t like that change.

You can quite easily make a RealTimeOnlyAudioBuffer wrapper if you want to and it helps you solve issues, but I personally think it would remove quite a lot of the usefulness of that structure as a multi purpose one for offline processing/visualizer/etc and realtime audio.

That’s only true for realtime contexts, and you certainly can enforce your functions to use this.

But in non real time contexts I have many functions that look something like:

AudioBuffer<float> readBufferFromFile(File file);

And passing those as a non-owning AudioBlock would create lifetime issues, as I really want to copy that buffer into other functions and classes.

Ok, there is a use case for copying the AudioBuffer. But at least it is clear what happens.

Currently the AudioBuffer and AudioBlock are a code duplication.
The AudioBlock was added as a structure that can be safely passed around by copy in a realtime context, and it fulfills that task perfectly. It’s just annoying that with this one HeapBlock there was a backdoor added, which destroys all that safety.

And the AudioBuffer is just that versatile, because it is the older structure. But moving forward it would be great to leverage the existence of the two different structures.

I agree, but I have a different solution:
Remove AudioBlock.
That one is completely unneeded, and also uses size_t which is horrible.

1 Like

that’s my favourite solution as well. when writing a processing method all you need is a float** for the samples and int, int for numChannels and numSamples

I’m a big fan of AudioBlock and the DSP code that we wrote in the last 2 years nearly only uses it. To me, it’s a concept that can be found in the more modern C++ standard library a lot and it feels very natural to use the AudioBlock if you are using the standard library frequently.

We find the concept of pairs of owning containers and views in the standard library. For an example, we have std::vector as an owning container and std::span as a non owning view to continuous array-like data. We have std::string as a owning string container and std::string_view as a non owning view to string data. In all those cases, passing around the views by copy, creating sub-views etc. is a cheap operation that does never involve any heap memory allocation.

Now in JUCE we have AudioBuffer as a (possibly) owning container of multichannel audio samples and AudioBlock as a non-owning view to multichannel audio samples. The big design flaw here is that AudioBuffer can also act as a view itself, which is a design decision taken long ago and probably not easy to roll back.

But the AudioBlock as a view-only class has a lot of advantages and can give you some guarantees that are important in a real-time context, which are:

  • It will never own memory, only point to some externally owned memory
  • It’s cheap to copy (as it doesn’t consist of more than a pointer and three size_t members)
  • You can create sub-views of different kinds

From my point of view, AudioBlock is the only class that you really need for passing samples to processing functions that are intended to run on the audio thread as it doesn‘t allow you to do anything stupid. Raw float pointers plus size are a lot more error prone and a lot less elegant.

This constructor will only resize the heap block in order to keep a suitable block of aligned memory and then lets the block point to that memory. After construction the block does no longer „know“ that the memory it points to is owned by a heap block. A copy of the AudioBlock will still point to the same heap block. This is why you also need to make sure that the heap blocks lifetime is long enough.

Regarding size_t vs int: I agree that there is a inconsistency inside JUCE here and I know that there are good arguments for using signed indices. Still the standard library uses size_t for sizes all over the place. Using a lot of standard library functions in DSP code, I really like that choice here as you don’t need to cast your indices everywhere which is always the case when you combine JUCE and standard library in other places.

I guess you see that I like the AudioBlock :grinning: Still I don‘t think that breaking existing interfaces like the AudioProcessor a good idea, as it would annoy a huge user base. And you can convert a buffer into a block real easy

2 Likes

I agree that the idea of a “View” into an owning AudioBuffer is really nice.

But - for this to happen in a way that I’d use it I think two things need to happen:

  1. The interface should be as identical as possible to AudioBuffer, so the two are interchangeable and you won’t need to write a function twice (similar to std::string and std::string_view).

  2. Similar to std::string and std::string_view, you need to be able to construct both from one another, that would make the non-owning version usable in cases where you do need to create a copy of the data.

Right now constructing an AudioBuffer from an AudioBlock is not trivial to do at all. And if the non-owning version of AudioBuffer would be removed, you won’t be able to interconnect the two without allocating memory or involve extra copying.

This exists:

juce::AudioBuffer buffer;
block.copyTo (buffer);

And if you really want to create a constructor for AudioBuffer:

template<typename FloatType>
AudioBuffer<FloatType>(AudioBlock<FloatType>& other)
{
    setSize (other.getNumChannels(), other.getNumSamples());
    other.copyTo (*this);
}

wasn’t too hard…? :wink:

It’s definitely not hard, if that constructor actually existed. :slight_smile:

But also - now you’ve introduced unneeded copying and allocation for an operation that until now was allocation and copy free until it was actually needed.

Well no. If I want to put something from a view into an owning structure, of course it allocates. But in this case I know, because I chose the AudioBuffer.

You’re assuming that code is either always ‘viewing’/‘modifying’ samples or always creating/owning.

While that is definitely true for realtime cases, my offline and UI use cases are not like that, and it’s very possible code will decide whether to own or not deeper in the call chain.

In those cases changing to an owning AudioBuffer will suddenly make my faster use cases slower, because I’ll be forced to allocate and copy, where as before I only copied if I absolutely had to and most of the time just passed “views” (because the interface of AudioBuffer was the same as the owned version).

No, but I am hoping that I can be clear for the reader and for the compiler what my intent is.