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.
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.
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.
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.
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’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 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
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:
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).
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.
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).