Dynamic resizing of array of waveforms

My current JUCE synth have 8 tone generators, just an array of standard C array’s each set to the same size in my case 2048 floats, which I use as a look up table (LUT) type waveform.

float tgActiveWaveform[8][2048];

My synth have the capability to import waveforms (samples) from a file, but currently if the size is different than my 2048 I scale to fit.

What I would like to do is the ability to dynamically allow the user to specify array individual size for each of the eight tone generators, meaning they can all be different. When a resize is done, I need the data in the other tone generator’s waveform array to be kept as is, not resized or re-calculated / resampled.

Playback will be stopped when resizing occurs.

The next thing I would like, but perhaps unreasonable wish thinking, is for the data in all eight arrays to b consecutive, which will ease reading through everything like a wavetable synth.

When I Google “juce array of various size audiobuffers” it shows two solutions, well at least how to create them, not to resize them without destroying data in the other buffers. Either way I’m not sure the examples are correct, as I get an errors or two trying to implement each proposal.

What would be the best approach, and thank you very much in advance!

Are you willing to have an upper limit on the size of each array? If so, you could keep your current implementation but use std::span to access the data, with a different span size argument for each array. So long as the span size is less than your upper limit (say 2048), it should work just fine, and you don’t have to actually resize anything.

Thanks for your suggestion, however I really d want to be able to load longer sample files into a tone generator.

For now I’m going with the following, first in my synthParameters.h;

std::vector<std::vector<float>> tgDynamicWaveformArray;

int tgDynamicWaveformStart[8];
int tgDynamicWaveformSize[8];
int tgDynamicWaveformEnd[8];

Then when I first create the array I do this;

synthParams.tgDynamicWaveformArray.clear ();

int tgWaveformStart = 0;

for (int module = 0; module < 8; module++)
{
	synthParams.tgDynamicWaveformStart[module] = tgWaveformStart;

	// Set new sample size for each tone generator
    // Here random, min size 16 and max size 2048, as a test, but normally it would be 2048.
	synthParams.tgDynamicWaveformSize[module] = 1 << (Random::getSystemRandom ().nextInt (8) + 4);

	synthParams.tgDynamicWaveformArray.emplace_back (synthParams.tgDynamicWaveformSize[module]);

	tgWaveformStart += synthParams.tgDynamicWaveformSize[module];

	synthParams.tgDynamicWaveformEnd[module] = tgWaveformStart - 1;
}

Then whenever I need to change any one of the eight tone generator’s waveform array I’ll do the following, note some is pseudo code or rather description for now;

void tgResizeWaveformArray(int module, int newSize, bool keepExistingData)
{
  std::vector<float> tgTempWaveformArray;

  if (keepExistingData)
  {
    // Copying existing data to a temporary array
    tgTempWaveformArray.emplace_back (synthParams.tgDynamicWaveformSize[module]);

    for (int i = 0; i < synthParams.tgDynamicWaveformSize[module]; i++
      tgTempWaveformArray[i] = synthParams.tgDynamicWaveformArray[module][i];
  }

  // Resizing array
  synthParams.tgDynamicWaveformArray[module].resize (newSize);

  // Squeeze or stretch old waveform data to fit new size
  if (keepExistingData)
  {
    if (newSize < synthParams.tgDynamicWaveformSize[module])
      // Squeeze old waveform to fit new smaller size
      ..
    else if (newSize > synthParams.tgDynamicWaveformSize[module])
      // Stretch old waveform to fit new larger size
      ..
  }

  // Set new size variable
  synthParams.tgDynamicWaveformSize[module] = newSize;
}

It looks like you’re allocating on the audio thread here with all of those emplace_back calls. You really want to avoid allocating or using locks on the audio thread, which is very tricky in situations like this.

If it were me, I’d take an approach, as follows:

  1. Create a struct or class S which contains the std::vector<float>, size, start and any other member data, all in one place.
  2. For thread safety, mark all instances of S which the audio thread accesses as const, or access them through a wrapper which marks them as const. (You can skip this if you need to, but then you lose the guarantee).
  3. When you want to resize them, copy the original (not on the audio thread), resize the copy, and then swap the new copy into the original place in a thread-safe way.

The last step is not easy (i.e. swapping it in in a thread safe way). You should take a look at these talks to see what you’re up against, and then look at the Farbot library for some prepackaged solutions.

I’ve tried to design something simpler than Farbot’s RealtimeObject before, which I’ll copy below in case it’s useful. If it works (it might not), it would allow you to do something like this:

struct S
{
  std::vector<float> data;
  int size, start, end; // whatever
}

std::array<ThreadSafe<S>, 8> sampleData;


// on audio thread
const auto reader = sampleData[0].get();  // must keep reader in scope!

 for (const float sample : reader->data)
  ...


// on message thread
  const auto copy = *sampleData[0].get();
  copy.data->resize(); // ...
  sampleData[0].set(copy);

Here is my ThreadSafe class. In theory it allows you to set and get from different threads at the time lock free, and as long you’re not setting from multiple threads, it shouldn’t crash. I’ve tested it a bit, but it’s not battle tested, and I can’t guarantee that there aren’t edge cases. (I’m eager for feedback, so if anyone spots any problems with it, please let me know!)

#include <atomic>


template <typename T>
class ThreadSafe
{
public:

    class Reader
    {
    public:
        ~Reader() { --parent.readCount; }

        const T& operator*() const
        {
            return writingToPrimary ? parent.secondary : parent.primary;
        }

        const T* operator->() const
        {
            return writingToPrimary ? &parent.secondary : &parent.primary;
        }

    private:
        Reader(const ThreadSafe& p, const bool w) :
            parent(p), writingToPrimary(w) {
        }

        const ThreadSafe& parent;
        const bool writingToPrimary;

        friend class ThreadSafe;
    };

    ThreadSafe(const T& t = T()) : primary(t), secondary(t),
        writingToPrimary(true), readCount(0) {}

    Reader get() const
    {
        ++readCount;
        return Reader(*this, writingToPrimary);
    }

    void set(const T& t)
    {
        if (writingToPrimary)
            primary = t;
        else
            secondary = t;

        writingToPrimary.exchange(!writingToPrimary);

        while (readCount); // spin until all readers have finished
    }

private:
    T primary, secondary;
    mutable std::atomic_bool writingToPrimary;
    mutable std::atomic_int readCount;

    friend class Reader;
};

easiest solution: every time the user performs a specific action (gui / message thread), you call suspendProcessing on the processor and resize the array

Exactly, I was going to do that anyways, just forgot to mention it.

What if the audio processor is in the middle of an action when you call suspendProcess() and the process doesn’t actually suspend until some time later? Then you have a data race.

suspendProcessing blocks the calling thread by using a lock to ensure the current processBlock call finishes.

void AudioProcessor::suspendProcessing (const bool shouldBeSuspended)
{
    const ScopedLock sl (callbackLock);
    suspended = shouldBeSuspended;
}

Obviously it’s a rough way to deal with the issue and is pretty much guaranteed to cause glitches in the audio because the processor can suddenly switch to outputting silence and back to outputting audio when suspendProcessing(false) is called.

I highly appreciate all thoughts on the subject.

I also forgot to mention that I have a flag in SynthVoice that I set when I access my wavetable data, and clear when I’m done. I then check for it and wait when I modify same, which have worked without a hitch since I started the project many years ago.

Either way I do need to change my approach a bit, not just to make it safer, but to get back to my initial requirement, which is I need all eight “arrays’“ or data areas to be consecutive.

What are you all thinking of using juce::HeapBlock instead? Is it faster, meaning accessing data, in my case samples, than vector? I know that standard C arrays are a bit faster than vector.