Using a DelayLine as an effect "send" causes noise

,

This one is esoteric enough that I am submitting this fully expecting the issue to be sitting in the chair typing up this bug report, but I've got a very constrained and reproducible bit of code here, so if I'm doing something wrong, I'm truly lost, and would love a bit of guidance.

My goal is to use a DelayLine as an effect send where in one source can be shipped off to multiple different Channels where each is processed differently. According to the docs, this Rube Goldberg should be conceptually possible, and perhaps even intended:

Note that DelayLineTap is a SoundSource, not a SoundEffect. A delay line tap can be added to any channel, not only the channel the delay line is on.

In putting this together, I noticed that my effects sounded really bad, and I traced it back to the DelayLine itself (data point supporting this is in the comments of the code.)

The minimal example, in prose is:

  1. have one channel, muted, with a short DelayLine as its effect, set to full wet.
  2. use that DelayLine to create a DelayLineTap set to 0 samples, but it doesn't matter where this is set, the outcome is effectively the same.
  3. have a second channel with an effect on it (in this case it is a "mute")
  4. take the tap, `add it to the effected channel
  5. Play a SynthVoice directly into the first channel which is muted and has the DelayLine applied as its effect.

Here is my example in code. (As you consider the following, please forgive the odd bits of C++yness scattered about, it is simply a visual distraction, no real C++ was used in the making of this example):

#include <pd_api.h>
#include <pdcpp/pdnewlib.h>

PDSynth* source;
SoundEffect* mute;

DelayLine* multiplexer;
DelayLineTap* send_output;
SoundChannel* main_input_chan;
SoundChannel* effected_output_chan;

// This is our effect. It *ought* to zero-out all data coming into the function
int mute_func(SoundEffect*, int32_t* left, int32_t* right, int nsamples, int bufactive)
{
    while(--nsamples)
    {
        *left = 0;
        *right = 0;
        left++, right++;
    }
    return 1;
}


// Constants for easy manipulation later
// Note, going below 256 causes at best bad sounds, and at worst, crashes. This
// makes sense, the buffer size is 256, so it would stand to reason shorter
// delays would cause a mess. However, this also controls how long it takes for
// the messy noise to start, so delay length *is* implicated here somehow.
static const auto kDelaySize = 256;



static int update(void* userdata)
{
    PlaydateAPI* pd = (PlaydateAPI*) userdata;
    if (pd->system->getElapsedTime() > 2.0f)
    {
        pd->sound->synth->noteOff(source, 0);
    }
    return 1;
};

#ifdef __cplusplus
extern "C" {
#endif

#ifdef _WINDLL
__declspec(dllexport)
#endif
int eventHandler(PlaydateAPI* pd, PDSystemEvent event, uint32_t arg)
{
    eventHandler_pdnewlib(pd, event, arg);
    if (event == kEventInit)
    {
        // Set up the allocations
        source = pd->sound->synth->newSynth();
        main_input_chan = pd->sound->channel->newChannel();
        effected_output_chan = pd->sound->channel->newChannel();
        multiplexer = pd->sound->effect->delayline->newDelayLine(kDelaySize, 1);
        send_output = pd->sound->effect->delayline->addTap(multiplexer, 0);
        mute = pd->sound->effect->newEffect(&mute_func, nullptr);

        // Set up the main source, a simple synth voice. We're only going to
        // play one note here, the effect is *very* audible.
        pd->sound->synth->setWaveform(source, SoundWaveform::kWaveformPOPhase);
        auto env = pd->sound->synth->getEnvelope(source);
        pd->sound->envelope->setDecay(env, 2);
        pd->sound->envelope->setSustain(env, 0.25f);
        pd->sound->envelope->setRelease(env, 2);

        auto default_chan = pd->sound->getDefaultChannel();

        // Mute the output from the main input channel
        pd->sound->channel->setVolume(main_input_chan, 0);

        // Set up the routing.
        pd->sound->channel->addEffect(main_input_chan, (SoundEffect*) multiplexer);

        // multiplexer takes 100% of the audio
        pd->sound->effect->setMix((SoundEffect*) multiplexer, 1.0f);

        // Attach the send
        pd->sound->channel->addSource(effected_output_chan, (SoundSource*) send_output);

        // mute the send by effect
        pd->sound->channel->addEffect(effected_output_chan, mute);
        pd->sound->effect->setMix(mute, 1.0f);

        // reset the timer so that the "update" function can turn off the note eventually.
        pd->system->resetElapsedTime();

        // We have to start the source before setting its channel. Also, the
        // note selected, does impact the timbre of the noise in the output.
        pd->sound->synth->playMIDINote(source, 65, 0.5, -1, 0);
        pd->sound->channel->removeSource(default_chan, (SoundSource*) source);
        pd->sound->channel->addSource(main_input_chan, (SoundSource*) source);

        pd->system->setUpdateCallback(update, pd);
    }

    // Destroy the global state to prevent memory leaks
    if (event == kEventTerminate)
    {
         // This is a quick and dirty demo, please forgive my sins.
    }
    return 0;
}
#ifdef __cplusplus
}
#endif

If my understanding of the signal flow here is correct, (and if there are no other bugs in my processing I'm missing,) the result should not be audible at all, but it is.

To me, it sounds like there's some crumb leftover in some buffer somewhere that I can't reach from my effect, that's somehow leaking through. It follows the amplitude of the synth's input, and even caries a hint of its pitch and timbre.

This reproduces on both the Windows Simulator as well as on the Playdate itself.