5
$\begingroup$

I am implementing a Flanger using a fractional delay line. I am modulating the length of the delay line using a sin function. The delay line already uses linear interpolation to compute the delay output for the fractional sample.

When the delay length is decreasing (and pitch is increasing), the modulated length sounds just about perfect, with no artifacts. However, when the delay length is increasing (and pitch is decreasing), I am getting a kind of "zipper" effect, with intermittent clicks, that one would expect from a non-interpolated delay line, as if sharp edges are being produced.

My guess is that I need to do additional interpolation beyond the simple fractional interpolation, but I have some questions.

First, why would I only hear artifacts during the period where my modulated delay length is decreasing?

Second, what is the best method to reduce these artifacts? My first instinct is an all-pass filter, but is it typical to use an all-pass filter as well as an interpolation over a fractional delay length? I just want to make sure I'm getting it right, and not wasting unnecessary cpu cycles :).

Some code will help. In this code, config holds all delay line info, and delaySamples is the current working length of the delay line. There are two read pointers, two write pointers, and two feedback values - one for each channel. Thanks for taking a look!

void delayconfig_setDelayTime(DelayConfig *config, float delay) {
    config->delayTime = delay > 0 ? (delay <= 1 ? delay : 1) : 0;
    if (config->delayTime < 0.0001) config->delayTime = 0.0001;
    config->delaySamples = config->delayTime*SAMPLE_RATE;
    int channel;
    float *rp, *wp;
    pthread_mutex_lock( &delayMutex);
    for (channel = 0; channel < 2; channel++) {
        rp = &(config->rp[channel]);
        wp = &(config->wp[channel]);
        *rp = *wp - config->delaySamples;
        while (*rp < config->delaySamples) {
            *rp += config->delaySamples;
        }
     }
     pthread_mutex_unlock( &delayMutex);
}

float interp(float *delayBuffer, float delaySamples, float position) {
    position = fmod(position, delaySamples);
    int prev = floorf(position);
    int next = fmod(ceilf(position), delaySamples);
    float frac = position - prev;
    return delayBuffer[prev] + frac*(delayBuffer[next] - delayBuffer[prev]);
}

void delay_process(void *p, float **buffers, int size) {
    DelayConfig *config = (DelayConfig *)p;
    float out;  
    int channel, samp;
    float *wp, *rp;
    for (channel = 0; channel < 2; channel++) {
        rp = &(config->rp[channel]);
        wp = &(config->wp[channel]);
        for (samp = 0; samp < size; samp++) {
            // here is where I'm changing my delay length.  The values are tentative while I'm figuring this out, and aren't really improtant.
            // The important thing is thing is that it's a sin function
            delayconfig_setDelayTime(config, 0.0061f + 0.006f*sin(M_PI*config->count++*INV_SAMPLE_RATE));
            pthread_mutex_lock(&delayMutex);
            if (*rp >= config->delaySamples) (*rp) -= config->delaySamples;
            if (*wp >= config->delaySamples) (*wp) -= config->delaySamples;
            float interpolated = interp(config->delayBuffer[channel], config->delaySamples, (*rp)++);
            int wpi = floorf((*wp)++);
            pthread_mutex_unlock(&delayMutex);
            out = interpolated*config->wet + buffers[channel][samp]*(1 - config->wet);
            if (out > 1) out = 1;
            config->delayBuffer[channel][wpi] = buffers[channel][samp] + out*config->feedback[channel];         
            buffers[channel][samp] = out;
        }
    }
} 
$\endgroup$
  • 1
    $\begingroup$ Sounds like a bug, not an algorithm problem. If it works upwards, it should do the other way just fine as well. It's potentially in the way you calculate the split between integer and fractional delay. Code is hard to read though, very few comments and unclear types. $\endgroup$ – Hilmar Jul 17 '12 at 12:39
  • $\begingroup$ Can you show a graph or wav file of the artifacts? $\endgroup$ – endolith Jul 17 '12 at 22:55
4
$\begingroup$

It's a bug.

M_PI*config->count++

You are processing 2 channels, but incrementing your modulation in the inner loop. So at the end of processing 1 channel, you continue to increment the modulation as you process the second channel.

c1 [ 1,2,3,4]         [9,10,11,12]...
c2           [5,6,7,8]

The modulation has a discontinuity, i.e in my simplifies example - c1 modulation jumps from '4' to '9'. resulting in zipper noise. Hope that makes sense.

$\endgroup$
  • $\begingroup$ Thanks! I ended up reworking this quite a bit, and was able to get it sounding nice, but haven't gotten around to posting an answer. This was absolutely the main reason. $\endgroup$ – khiner Aug 16 '12 at 3:13
2
$\begingroup$

Some artifacts are always going to be present whenever you deviate from strict LTI (linear time invariant) systems. With time varying systems you will see FM sideband energy dispersions, which present as non removable aliasing noise. This is sadly the case even assuming bug free code, band limited interpolation and a maximally smooth modulator signal (stationary sinusoid).

Of course it helps if you keep the modulator as smooth, as low frequency and as narrow band as possible. For a standard flanger modulated by a sinusoid LFO, the FM noise might not actually be a problem, but if you want to explore a wider range of modulating options you might consider oversampling to combat aliasing. Or just accept it as part of the sound.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.