Best method for saving/restoring audio buffer between instances

This is pretty basic/general question, though I’m having trouble finding an existing answer on the forums, so apologies if I missed something!

My question is basically this: I’m making a plug-in which allows the user to record audio into it. I’d like that audio to persist between instances, so that if they reload the project, etc. that recorded audio gets restored. What’s the best way to handle this?

My first thought was to persist it similar to the parameters via getStateInformation and calling ensureSize on the provided MemoryBlock to make sure enough data is allocated. This seems technically possible, though seems kind of outside the spirit of get/setStateInformation.

The other option would basically be to write to a file and store a reference to that file in getStateInformation. I’m guessing this is the recommended way to accomplish this, but involves a bit more work than the first option, so I wanted to ask before committing :stuck_out_tongue:

I would say whatever your users record, if you just store it in memory and it gets lost in a crash or something, they will think it’s the best, most unique piece of audio the world wil never know and they will hold you personally responsible for its loss.

So… disk?

Definitely a great point! Writing to disk after every change is probably a good move, I was only thinking of the situation where the user is saving/restoring the project - thanks for catching that!

What do you expect to happen if a user moves the project to a different machine? I would have thought get/setStateInformation would indeed be the right place to store any information required to restore the state of the plugin, even if that includes audio data. I would expect the DAW to save that to disk for you as part of the project.

Good point, Anthony. As is often the case, maybe the best choice is both choices?

As a user I always prefer saving as much as possible in the state of the DAW. That means that the project will open as-is when I load the project on another computer. Plugins that store files on disk caused me to lose session data in the past and I hated it.

Due to file size, it’s not always possible. So I’m aware that some plugins do stuff like store it to a disk and only store a path in the chunk.
But I would avoid that route unless the file size has the potential to be huge.

You can of course store the data in a compressed format, like FLAC, etc.

FLACing the audio data is a really good idea! What are the size limits for plugin saved state in a DAW?

I’m not aware of any limits but performance might slow down if it’s too large.

Great points all around, yall. I think personally, as a user, I would want as much state to be tied to the DAW as possible, and as a programmer it’s nice to make storage and loading the DAW’s problem lol.

I think a user setting (“eager saves”?) could get kind of confusing, since I don’t intend the plug-in to be that robust, but maybe some sort of automatic backup system could work. The audio could be saved via the DAW, but also written to file whenever it’s changed. On load, if both exist, add a bit of logic to see which was written more recently and prefer that version?

The disk backup could even be erased whenever a save occurs to be more space efficient and avoid redundancy.

I don’t know about other DAWs but Logic’s file format is XML-based. It will take whatever data your plug-in gives it and base64-encode it (I think) inside its own XML. I’m sure other DAWs do something similar. So that’s less optimal than storing the binary data directly. Just something to keep in mind.

When considering that route, I suggest storing in the DAW chunk also a checksum of the audio file saved externally (MD5 or whatever), so that upon reload it’s possible to guarantee that the content of the file hasn’t changed.
Also, with the checksum available, it would also be possible to “scan” a directory of these external files to find the desired one, in case it has been relocated since last project save and the path stored in the DAW chunck doesn’t point to the file any more.

2 Likes

I want to do the exact same thing, but my buffer is only 1024 samples of data… did you figure it out?

I decided to go with the route for saving the audio as part of the DAW’s project file for simplicity. I plan to have an option to backup to disk as well for safety, but this seems to suffice for an MVP.

My getStateInformation is something like this (heavily condensed so might not compile/make perfect sense):

void MyProcessor::getStateInformation (juce::MemoryBlock& destData)
{
    auto parametersSize = calculateParametersSize();
    auto audioBufferSize = calculateAudioBufferSize() * buffers.size();
    auto audioBufferMetadataSize = sizeof(int) * 2; // num channels, and num samples.
    destData.ensureSize(parametersSize + audioBufferMetadataSize + audioBufferSize);

    juce::MemoryOutputStream outputStream(destData, true);

    auto state = parameters.copyState();
    std::unique_ptr<juce::XmlElement> xml (state.createXml());
    xml->writeTo(outputStream);

    outputStream.setPosition(parametersSize);

    outputStream.writeInt(playbackBuffer->getNumChannels());
    outputStream.writeInt(audioBufferLength);

    for (int channel = 0; channel < playbackBuffer->getNumChannels(); ++channel)
    {
        const float* channelData = playbackBuffer->getReadPointer(channel);
        outputStream.write(channelData, audioBufferLength * sizeof(float));
    }
}

And setStateInformation is something like this:

void MyProcessor::setStateInformation (const void* data, int sizeInBytes)
{
    // Read parameters...
    ... 

    inputStream.setPosition(parametersSize);
    int remainingDataSize = sizeInBytes - int(inputStream.getPosition());
    if (remainingDataSize > 0)
    {
            int numChannels = inputStream.readInt();
            int numSamples = inputStream.readInt();
            
            // Do some cursory checks here: if numChannels != 1 or 2, data seems wrong. Don't load.
            if (numChannels > 0 && numChannels <= 2 && numSamples > 0)
            {
                playbackBuffer->setSize(numChannels, numSamples);
   
                // Read the audio buffer data for each channel
                for (int channel = 0; channel < numChannels; ++channel)
                {
                    float* channelData = playbackBuffer->getWritePointer(channel);
                    inputStream.read(channelData, numSamples * sizeof(float));
                }
            }
    }
}

One thing to note is that the size of the parameters’ block can change if you introduce a new parameter in a future update, etc. It’s a bit hacky but I decided to make calculateParametersSize() basically return a static value that I know will be >>> the size of the parameters (between 2-4x the known/expected size of the parameters). Not totally sure if this is safe but it’s working (I haven’t released this product yet so need to do plenty of testing). Other valid options could be to:

  1. Store the size of the parameters in bytes first thing in the block, so you know how many bytes to read and expect the parameters to be.

  2. Store the audio first before the parameters, since you have to write the numChannels and numSamples so you can predict where the parameters block will start. If there’s no audio to store, make sure to store 0, 0 first.