How to Utilize CrossFade Utility Correctly

Hi, I am working on an output sequencer for guitar pedals and have run into an issue of “popping” or “clicking” as it is changing outputs. From some testing and analysis, it seems that this is coming from discontinuity between the original signal and the slightly delayed signal (after being processed through an external pedal). There is no pop when the external pedal is bypassed.
After seeing this post I thought that using the CrossFade utility might help me get around this. But the CrossFade is not eliminating the pop. If I write the fade over a long enough period, the pop gets quieter but then it starts delaying the signal switching outputs. I am wondering if I am not calling/utilizing it correctly, if there’s something else that I’ve missed in my code, or if a pop is just an inherent problem of this idea.

I am trying to start the crossfade as the channel change is coming up, fade to the original signal, and then fade back to the slightly delayed signal (the Return path from the external pedals).

Can you please tell me what I’m missing on the CrossFade utility? I think I have it running, but I’m not sure I understand it or am using it correctly.
(I’m running a Daisy Seed via Arduino.)

Here is my code-

#include <DaisyDuino.h>

DaisyHardware hw;

#define ledB D22 // blue led. pin 29, referenced to Daisy as D22, see Pinout
#define ledG D19 // green led, pin 26
#define ledR D21 // red led, pin 28

#define ch1 D15 // send 1, opens Channel 1. pin 22
#define ch2 D16 // pin 23
#define fsw D25 // pin 32, bypass

int byp = LOW; // LOW: running, HIGH: bypassed

long crntTime; // current Time
long prevTime; // previous Time
bool chSwitch = false; // switch between ch1 and ch2
long waitTime; // wait this long (milliseconds) until switching output

float pos; // Crossfade position
float mixOut;
float input;
float rtrns;
bool chngSoon; // channel change soon
bool chngNow; // channel change now
int chngCount; // counts up to channel change

static CrossFade cfade;
static Oscillator lfo;

size_t num_channels;

void bypass(){
    byp = !byp;
}

void chTimer(){
    crntTime = millis();

    if (crntTime - prevTime <= 96){
        chngSoon = true;
        chngCount = 0;
    }

    if (crntTime - prevTime > waitTime){
        prevTime = crntTime;
        if (chSwitch == false) chSwitch = true;
        else chSwitch = false;
    }
}

void myCallback(float **in, float **out, size_t size){
    hw.DebounceControls();

    if (byp == HIGH){
        digitalWrite(ledB, LOW);
        digitalWrite(ledG, LOW);
        digitalWrite(ledR, HIGH);
        for (size_t i = 0; i < size; i++){
                out[0][i] = in[0][i]; // write AudioIn1 (mainIn) to AudioOut1 (mainOut)
        }
    }

    if (byp == LOW){
        if (chSwitch == false){ // ch1
        // passes mainIn to Send1|Rtrn1 to mainOut
            digitalWrite(ledB, HIGH);
            digitalWrite(ledG, LOW);
            digitalWrite(ledR, LOW);
            digitalWrite(ch1, HIGH); // turns ch1 on
            digitalWrite(ch2, LOW);
        }
        else if (chSwitch == true){ // ch2
            // passes mainIn to Send2|Rtrn2 to mainOut
            digitalWrite(ledB, LOW);
            digitalWrite(ledG, HIGH);
            digitalWrite(ledR, LOW);
            digitalWrite(ch1, LOW);
            digitalWrite(ch2, HIGH); // turns ch2 on
        }

        for (size_t i = 0; i < size; i++){
                out[1][i] = in[0][i]; // write AudioIn1 (mainIn) to AudioOut2 (Sends path)

                input = in[0][i]; // main Input
                rtrns = in[1][i]; // connected to Return path of external pedals

                if (chngSoon == true && chngCount <= 48){
                    pos = i/size;
                    cfade.SetPos(pos);
                    mixOut = cfade.Process(input, rtrns);
                    chngCount++;
                    out[0][i] = mixOut; // writes CrossFade result to AudioOut1
                }
                if (chngSoon == true && chngCount > 48){
                    pos = i/size;
                    cfade.SetPos(pos);
                    mixOut = cfade.Process(rtrns, input);
                    chngCount++;
                    out[0][i] = mixOut;
                }
                if (chngSoon == false) out[0][i] = in[1][i]; // writes AudioIn2 (Return path) to AudioOut1 (mainOut)
                if (chngCount >= 96){
                    chngSoon = false;
                    chngNow = false;
                    chngCount = 0;
                }
        }
    }
    chTimer();
}
 


void setup() {
    float samplerate;
    pinMode(ledB, OUTPUT);
    pinMode(ledG, OUTPUT);
    pinMode(ledR, OUTPUT);
    pinMode(ch1, OUTPUT);
    pinMode(ch2, OUTPUT);
    attachInterrupt(digitalPinToInterrupt(fsw), bypass, RISING);
    hw = DAISY.init(DAISY_SEED, AUDIO_SR_48K);
    num_channels = hw.num_channels;
    samplerate = DAISY.get_samplerate();

    cfade.Init();
    cfade.SetCurve(CROSSFADE_LIN); // or _CPOW?

    prevTime = 0;
    waitTime = 1250;

    DAISY.begin(myCallback);
}

void loop() {}

I haven’t checked the code in detail but found something requiring a fix:

pos = i/size;

should be:

pos = float (i) / float (size);

Because both arguments are integers, the division is performed in integer, and the result will always be 0. Casting to float first ensures the division is done in floating point.

Also, you should probably use else if instead of just if for the cases in the loop. For example when chngCount == 48, the two chngSoon == true statements are executed, which is most likely not what you expected.

I don’t understand very well the logic in chTimer(). Among other, during 96 ms, chngCount will be reset at each frame, generating multiple fades (sawtooth like). I also think that for safety and clarity, you should not mix the index within the frame and chngCount to handle the crossfade. Use a variable or constant to store the crossfade length (48), and use something based on chngCount instead of the sample index i as numerator to compute pos, and the crossfade length as denominator. Or work only with frames, give a different meaning to chngCount and update it out of the loop.

Thank you. Most of this makes sense and maybe explains some of the unexpected results I’ve been getting.
Can you explain what you mean by frames? I’m not familiar with this term or its meaning in this context.

Also when fading from input to rtrns as in

cfade.SetPos(pos);
mixOut = cfade.Process(left, right); 

would pos=0 give left and pos=1 give right?

It seemed like the opposite when I was experimenting, but I think this may do to my misunderstand and maybe the logical errors in my code.