Garbage buffer data when using dsp::ProcessContextReplacing + dsp::DelayLine in Renoise

I am seeing some very weird behavior regarding corruption/incorrect data in an audio buffer when using juce::dsp::ProcessContextReplacing, specifically in Renoise.

So this is a weird one that I am struggling to understand. Right now this is only tested on Windows 10 and JUCE 7.0.4, and Renoise 3.4.3. I started noticing this when using juce::dps::DelayLine in conjunction with juce::dsp::ProcessContextReplacing and a submix buffer. This isn’t happening in the other DAWs I tested so far, so I doubt it’s a flawed implementation on my part.

I managed to implement a very simple test plug-in that exhibits this behavior, the implementation looks something like this (using float as the numeric type, everything is stereo/2 channels):

  • Started with standard audio plug-in project from Projucer, using all of the defaults, and adding the juce_dsp module.
  • Added a single audio parameter via juce::AudioProcessorValueTreeState; in this case a simple gain parameter for testing
  • The plug-in processor adds an audio buffer as a member used for submixing dsp: juce::AudioSampleBuffer delayBuffer;
  • The plug-in processor also adds a juce::dsp::DelayLine as a member.
  • in prepareToPlay() the processor initializes the delayBuffer and DelayLine, max delay is set to 2 seconds, delay is set to 1 second
  • in processBlock() the delayBuffer is cleared:
  • the first two input channels from the processBlock buffer are copied into the delayBuffer:
    delayBuffer.copyFrom(0, 0, buffer, 0, 0, numSamples); delayBuffer.copyFrom(1, 0, buffer, 1, 0, numSamples);
  • A dsp processing context is setup for the delayBuffer:
    juce::dsp::AudioBlock<float> block(delayBuffer); juce::dsp::ProcessContextReplacing<float> procContext(block);
  • The delay line is then processed:
  • Finally, the delayBuffer is then copied back to the outputs of the main buffer:
    buffer.copyFrom(0, 0, delayBuffer, 0, 0, numSamples); buffer.copyFrom(1, 0, delayBuffer, 1, 0, numSamples);

So in this case, the DelayLine just ends up delaying the input by a certain amount, and works fine for testing (no wet, feedback, etc.).

In Ableton 11 and in AudioPluginHost, this works fine, even with audio plug-in parameters being automated.

In Renoise, using an ASIO driver, this works fine if none of the plug-in parameters are automated. But as soon as any of the plug-in’s parameters are automated, the buffer has some of the correct signal in it, but it also has a bunch of random garbage data mixed in as well and sounds terrible.

Keep in mind, I don’t even have to be using the automated parameter values anywhere in code, just simply the fact that they exist, and are being automated by the host causes this buffer corruption. As soon as you stop automating parameters, even while it’s running, the buffer corruption stops after the DelayLine gets through a full cycle (in this case 1 second).

Now here’s where it gets really strange (this is all specifically Renoise behavior):

ProcessContextReplacing w/ ASIO driver

  • Not automating any parameters produces the expected output
  • Automating any parameters produces bad output while the parameter is being automated only

ProcessContextReplacing w/ WASAPI driver

  • Not automating any parameters produces bad output
  • Automating any parameters produces bad output

ProcessContextReplacing w/ DirectSound driver

  • No output is produced at all (silence) regardless of parameter automation (weird?)

Now, I also tried using juce::dsp::ProcessContextNonReplacing as well:

delayBuffer.copyFrom(0, 0, buffer, 0, 0, numSamples);
delayBuffer.copyFrom(1, 0, buffer, 1, 0, numSamples);

juce::dsp::AudioBlock<float> inputBlock(delayBuffer.getArrayOfWritePointers(), 2, 0, numSamples);
juce::dsp::AudioBlock<float> outputBlock(delayBuffer.getArrayOfWritePointers(), 2, 0, numSamples);
juce::dsp::ProcessContextNonReplacing<float> procContext(inputBlock, outputBlock);

buffer.copyFrom(0, 0, delayBuffer, 0, 0, numSamples);
buffer.copyFrom(1, 0, delayBuffer, 1, 0, numSamples);

Now, I know this isn’t actually recommended to do, and I had to comment out the jassert in the constructor of ProcessContextNonReplacing:

// If the input and output blocks are the same then you should use
// ProcessContextReplacing instead.
jassert (input != output);

But, oddly enough, it works as expected with all audio driver types: ASIO, WASAPI, DirectSound.

In addition, I also just used the DelayLine manually, more or less recreating what is happening inside its process() method:


auto in0 = buffer.getReadPointer(0);
auto in1 = buffer.getReadPointer(1);
auto delayW0 = delayBuffer.getWritePointer(0);
auto delayW1 = delayBuffer.getWritePointer(1);

for (size_t i = 0; i < numSamples; ++i)
    delayLine.pushSample(0, in0[i]);
    delayLine.pushSample(1, in1[i]);
    delayW0[i] = delayLine.popSample(0, -1, true);
    delayW1[i] = delayLine.popSample(1, -1, true);

buffer.copyFrom(0, 0, delayBuffer, 0, 0, numSamples);
buffer.copyFrom(1, 0, delayBuffer, 1, 0, numSamples);

This also works correctly with all driver types.

Finally, if I use dsp::ProcessContextReplacing, but instead of using it on delayBuffer, just apply it to the processBlock() buffer directly:

juce::dsp::AudioBlock<float> block(buffer);
juce::dsp::ProcessContextReplacing<float> procContext(block);

This also works as expected with all driver types.

This appears to be the result of some kind of undefined behavior, and only when using ProcessContextReplacing + a separate submix buffer. The fact that the behavior changes based on driver type further mystifies the results.

I’m just looking for any insights into what it is specifically about dsp::AudioBlock, dsp::ProcessContextReplacing that could lead to such an issue (but dsp::ProcessContextNonReplacing is fine when used incorrectly in my testing).

Happy to attach the source files for the simple demo plug-in if it’s helpful.