MemoryBlock to AudioBuffer

Hi forum,
I’m an absolute novice to JUCE so please bear with me.
I’m trying to send AudioBuffer from one plugin to another over InterprocessConnection and I have learnt that I need to send it as a MemoryBlock so I have a code for it below (help from another forum):

juce::MemoryBlock block;
juce::MemoryOutputStream mostream(block,false);
mostream.writeInt(buffer.getNumChannels());
mostream.writeInt(buffer.getNumSamples());
auto bufptrs = buffer.getArrayOfReadPointers();
for (int i = 0; i < buffer.getNumChannels(); ++i)
{
    for (int j = 0; j < buffer.getNumSamples(); ++j)
        {
            mostream.writeFloat(bufptrs[i][j]);
        }
 }
sendMessage(block);

Now, in order to read it back on the other end I need to decode it back to AudioBuffer from MemoryBlock. Is this code below correct?:

MemoryInputStream myInputStream (memblock.getData(), memblock.getSize(), false);
AudioBuffer<float> buffer(2,1024);
myInputStream.read(buffer.getArrayOfWritePointers(), 0);
// process the buffer further...

Can someone please give advise? Many thanks.

A few thoughts on that:

  1. Before going into depth code-wise, what is your actual goal on sending audio buffers from one plugin instance to another via InterprocessConnection? If your plan is simultaneous processing or e.g. mixing buffers from various instances this will simply fail because of multiple reasons, e.g. you cannot do it directly from the audio callback as this would first of all violate the golden rule to not perform any system call from the realtime thread. But even if we assumed that it worked, timing would probably be impossible to get right with this approach. In case both plugins run on the same thread you would not know which instance would be processed first. In case both plugins run on different threads or even in different processes this will get even more difficult to get right. So sample accurate inter-plugin communication is nearly impossible to get right.
  2. In case your use case is fine with the audio buffer arriving without strong timing constraints, your send loop is really inefficient. The memory block under the output stream will probably be re-allocated a few times during the loop.
  3. You wrote the number of channels and the number of samples as first entries to the output stream and did not read it back on the other side. Instead you just allocate a buffer with some fixed size.

Again, in case point 1 does not apply to your problem and you offload your buffer to a dedicated thread via some queue first and then send it through your IPC, I’d simply use the plain memory block, compute the size in bytes you want to send, pre-allocate it and then copy everything in one big chunk via std::memcpy. This could look e.g. like this (untested code, just written down from memory)

// A struct holding some information on that buffer. This should be known to both, the sender and 
// receive side, so better store it in a header accessible to both
struct IPCBufferHeader
{
    int numChannels;
    int numSamples;
    // you could add more relevant "metadata" here. Some kind
    // of checksum could be helpful
}



// The sender side. buffer is of type juce::AudioBuffer<float>

// The number of bytes the floats in your audio buffer take 
const auto numBytesPerChannel = buffer.getNumSamples() * sizeof (float);
const auto bufferSizeInBytes =  buffer.getNumChannels() * numBytesPerChannel;

// The memory block needs some extra space for the header
auto memBlockSizeInBytes = sizeof (IPCBufferHeader) + bufferSizeInBytes;
juce::MemoryBlock block (memBlockSizeInBytes);

// Create the metadata
IPCBufferHeader header;
header. numChannels = buffer.getNumChannels();
header.numSamples = buffer.getNumSamples();

// The access to the underlying storage we want to write to. A bit low level C++
auto* data = block.getData();

// Copy the header into the block first
std::memcpy (data, &header, sizeof (IPCBufferHeader));

// Compute the memory location right behind the header just written
data += sizeof (IPCBufferHeader));

// loop over the channels, as we can't guarantee that all channels are contiguous in memory
for (int ch = 0; ch < buffer.getNumChannels(); ++ch)
{
    std::memcpy (data, buffer.getReadPointer (ch), numBytesPerChannel);
    data += numBytesPerChannel;
}

// All has been written nicely packed, ready to send
sendMessage (block);



// The receiver side – basically do everything backwards. block is the block of data we received
auto* data = block.getData();

IPCBufferHeader header;
std::memcpy (&header, data, sizeof (IPCBufferHeader));
data += sizeof (IPCBufferHeader);

// Instead of copying the buffer, we can simply create a buffer that refers to the buffer just received.
// We need some small array that holds the pointers to all channels. If your compiler supports vla, you
// could use one here. Otherwise go for a dynamic container like juce::Array or define a maximum
// ever number of channels. You see two versions below

// VLA solution
float* channels[header.numChannels];

// Max ever num solution
constexpr int maxNumChannels = 4;
jassert (header.numChannels <= maxNumChannels)
float* channels[maxNumChannels];

// Store the channel pointers
for (int ch = 0; ch < header.numChannels; ++ch)
{
    channels[ch] = reinterpret_cast<float*> (data);
    data += header.numSamples * sizeof (float);
}

// Make an audio buffer that refers to that memory
juce::AudioBuffer<float> buffer (channels, header.numChannels, header.numSamples);

// process the buffer further

1 Like

Wow, thank you so much @PluginPenguin for the answer and help.
So in a nutshell; I’m creating a multi eq analyser, where the sender VSTs will be placed in the individual tracks (guitar, bass, vox, drums, etc…) and the the receiver VST will be placed either on a master track or a bus. And the receiver VST will visualize all the individual inputs inside one graph. There will be two plugins (senderVST, receiverVST)

I tried your code but I had many syntax errors such us:

Arithmetic on a pointer to void

Reinterpret_cast from ‘const void *’ to ‘float *’ casts away qualifiers

so I spent the whole night fixing the errors (Xcode) but now every-time I place the plugins and hit “play” the Logic Pro X crashes.

Here’s the code:

//sender
IPCBufferHeader header;
header.numChannels = buffer.getNumChannels();
header.numSamples = buffer.getNumSamples();
//
//// The access to the underlying storage we want to write to. A bit low level C++
auto* data = block.getData();

// Copy the header into the block first
std::memcpy (data, &header, sizeof (IPCBufferHeader));
//
// Compute the memory location right behind the header just written
data = sizeof (IPCBufferHeader) + (static_cast <int*> (data));
    //data += sizeof(IPCBufferHeader)

// loop over the channels, as we can't guarantee that all channels are contiguous in memory
for (int ch = 0; ch < buffer.getNumChannels(); ++ch)
{
    std::memcpy (data, buffer.getReadPointer (ch), numBytesPerChannel);
    data = numBytesPerChannel + (static_cast <int*> (data));
}

sendMessage (block);

// receiver
auto* data = memblock.getData();

IPCBufferHeader header;
std::memcpy (&header, data, sizeof (IPCBufferHeader));

data = sizeof (IPCBufferHeader) + reinterpret_cast<int*>(const_cast<void*>(data));

// VLA solution
float* channels[header.numChannels];

for (int ch = 0; ch < header.numChannels; ++ch)
{
     channels[ch] = reinterpret_cast<float*>(const_cast<void*>(data));
     data = reinterpret_cast<int*>(header.numSamples);
}

// Make an audio buffer that refers to that memory
juce::AudioBuffer<float> buffer (channels, header.numChannels, header.numSamples);
       
inputAnalyser.addAudioData (buffer, 0, getTotalNumOutputChannels()); //this is where are plot the graph

At the very least, this has to be

data += reinterpret_cast<int*>(header.numSamples);

Right now, you’re redefining data to an invalid memory address.

I’ve tried that already but that gives me this error:

Invalid operands to binary expression ('const void *' and 'int *')

It seems like you did not really understand my example and I guess that’s because you are not really safe in the pointer game yet, right? If you have not understood the basics of pointer arithmetic it’s likely that you’ll come up with crashing code. In your example you cast the pointer to int, which doesn’t make that much sense if the goal is to interpret that memory as float. I don’t have that much time for an in-depth post right now, but maybe you could outline what’s your intention behind casting the void pointer to an int pointer in the meantime?

1 Like

I get the feeling — and please forgive me and accept my apologies in advance if I’m wrong on this — that @MarcelK is leaning a bit on the “try stuff kind of at random and use whatever makes the errors go away” approach here. There’s no shame in that, and a surprising amount of perfectly useful and usable code can be written that way.

It’s impossible, however, to write pointer code in that way. Primarily that’s because pointers are a leftover from C, which offers absolutely no memory access protections or other safety checks. This means that when pointer math is involved, “this code compiles cleanly and without error” and “this code is correct and functions as expected” have absolutely no relationship to each other. None whatsoever.

As you’ve seen, when working with raw pointers you can easily write code that compiles without error, even when that code contains a statement that assigns an impossible value to a pointer variable and then proceeds to dereference it. This is guaranteed to cause an immediate crash on execution due to a memory access violation, but the compiler is perfectly fine with that code. (At a high enough warning level gcc or clang might throw up a little “this seems wrong, did you mean…?” sort of flag, but that’s the most it’ll do to protect the developer from themself in those circumstances.)

When I was in school, our first-year CS course was taught in C. (The entire CS curriculum was taught in C, back then, with just a couple of exceptions.) The faculty always made a point of structuring the course so it reached pointers before the add/drop deadline for the semester. This was 100% deliberate on their part, explicitly so that students would still have the option to drop the course, should they suddenly discover that programming wasn’t really “for them” if it meant they were going to have to understand and deal with pointers. Every semester, multiple students took them up on that.

@MarcelK,

I’m working on a prototype where I’m sending audio over an InterprocessConnection and have to unpack audio from a MemoryBlock and read it into an AudioBuffer.

Although I thought I understood pointer arithmetic enough to be dangerous, I ultimately had to build a console app to work through this on first principals. I still did iterations through the console app, first using loops to go sample by sample. Then using the implementation below which I believe illustrates the point more clearly. This implementation can definitely be improved, but it should show you what’s going on in pointer-land enough to improve it yourself.

This example shows moving between AudioBuffer → MemoryBlock → AudioBuffer:

std::unique_ptr<juce::AudioBuffer<float>> read_buffer_{nullptr};
std::unique_ptr<juce::AudioBuffer<float>> write_buffer_{nullptr};
std::unique_ptr<juce::MemoryBlock> memory_block_{nullptr};

// Read samples from buffer into memory block
for (int ch = 0; ch < read_buffer_.get()->getNumChannels(); ch++)
{
    memory_block_.get()->copyFrom(
                                    read_buffer_.get()->getArrayOfWritePointers()[ch],
                                    read_buffer_.get()->getNumSamples() * sizeof(float) * ch,
                                    read_buffer_.get()->getNumSamples() * sizeof(float));
}

// Read sample from memory block into write buffer
float *memory_block = reinterpret_cast<float*>(memory_block_.get()->getData());
for (int ch = 0; ch < write_buffer_.get()->getNumChannels(); ch++)
{
    write_buffer_.get()->copyFrom(ch, 0, memory_block, write_buffer_.get()->getNumSamples());
    memory_block += write_buffer_.get()->getNumSamples();
}
1 Like