Hello, I'm playing around with the code for the delay lecture, and trying to reduce the clicking/zipping sound using 4-sample cubic interpolation. Equations 2.13 and 2.14 in Reiss & McPherson (2015) seem to be the answer, and I'm most of the way to implementing it, but I wasn't sure about the "a0" (coefficient? multiplier?) in the second line of equation 2.14. If anyone has a hint or explanation, I would appreciate it!

Cheers,
Michael

    this is the relevant passage, for reference:

    alt text

    This is the reference [8]: http://yehar.com/blog/wp-content/uploads/2009/08/deip.pdf

    I never managed to make sense of that formula, but I have seen several typographical errors on the pdf version of this book, so maybe this is one of those. It would seem that the signs of the coefficients are the same as the "4-point, 3rd-order Hermite" (section 6.4 of the pdf linked above, and the same as this), but the fractional coefficients have magically disappeared.

    I'd recommend using the formula from the second link I provide (where p0, p1, p2, p3 are x[n-1], x[n],x[n+1], x[n+2] respectively).

    mww Hello, I'm playing around with the code for the delay lecture, and trying to reduce the clicking/zipping sound using 4-sample cubic interpolation.

    Now, if what you are trying to achieve is reduce zippering noise, then be aware that cubic interpolation is not particularly needed, nor is it sufficient on its own. The first thing you want to do to avoid clicks when changing the delay time is to smooth the change in delay time so that it doesn't happen instantaneously, but it happens progressively over time. For instance, say that your current delay time is 10000 samples and you want to change this to 20000 samples in response to a GUI event: if you suddenly change the delay time from 10000 to 20000 samples, you will get a click, regardless of what interpolation you have in place. What you should do, instead, is change the delay time in small intervals, and very frequently. For instance, you could change the delay by 1 sample every sample. Pseudo-code (assuming delay time is in samples):

    unsigned int gTargetDelay;
    unsigned int gActualDelay;
    
    void render(...)
    {
      gTargetDelay = gui.getSliderValue(0);
      for(unsigned int n = 0; n < context->audioFrames; ++n)
      {
        if(gActualDelay < gTargetDelay)
          setDelay(++gActualDelay);
        else if (gActualDelay > gTargetDelay)
          setDelay(--gActualDelay);
        // do more stuff ...
      }
    }

    with something like the above, you would get a drastic improvement on the reduction of zippering noise, without need for any interpolation. There are several (and probably better) ways of smoothing values, e.g.: with a one-pole filter. Some of these will generate fractional delay times, and there a cubic interpolator could help, although it's not strictly necessary.

    • mww likes this.

    On the topic of reducing zipper noise another method is to limit the rate of change of the delay time with a one pole low pass filter.

    This is basically a slew limiter on the control signal so if you rapidly change delay time the control signal will always smoothly ramp between values at a rate determined by the filters cutoff.

    I've found this works like a charm and sounds very "analog" even just using linear interpolated reads

    • mww likes this.

    Thank you for the fast and thorough reply, Guilio. I sympathize with anyone involved in checking and editing a book with so many formulas! Anyway, it's been a handy resource to the online course, typos and all.

    I've tried the approach in your pseudocode first. Indeed, it reduces the zipping sound, but only when I place the ++/-- code outside of the buffer-updating loop, like so:

    void render(...)
    {
      gTargetDelay = gui.getSliderValue(0);
    
      if(gActualDelay < gTargetDelay)
          setDelay(++gActualDelay);
      else if (gActualDelay > gTargetDelay)
          setDelay(--gActualDelay);
    
      for(unsigned int n = 0; n < context->audioFrames; ++n)
      {
      }
    }

    This violates the "very frequently" part of your instructions, but it yields a fun-sounding effect, slowly moving to the intended delay rate, which is accentuated nicely with feedback.

    When I placed the code inside the loop updating the samples, as in your pseudocode, it changed the nature of the zipping rather than reducing it. Specifically, what was previously a subtle clicking noise became a high pitch noise when increasing the delay interval, and a glitchy click when decreasing the delay interval. The length of time that the noise persists seems to match the amount of time adjusted (e.g., going from 0.01 to 0.49 s, the high pitch lasts ~0.48 s). I must be making some sort of mistake with reading the contents of the circular buffer, or maybe wiping it out momentarily until it gets repopulated, or something more basic.

    Below is the glitchy code, to reproduce and/or illustrate my error. Here I used gTargetOffset and gActualOffset as variable names in place of gTargetDelay and gActualDelay in your pseudocode:

    /*
     ____  _____ _        _    
    | __ )| ____| |      / \   
    |  _ \|  _| | |     / _ \  
    | |_) | |___| |___ / ___ \ 
    |____/|_____|_____/_/   \_\
    
    http://bela.io
    
    C++ Real-Time Audio Programming with Bela - Lecture 11: Circular buffers
    circular-buffer: template code for implementing delays
    */
    
    #include <Bela.h>
    #include <vector>
    #include "MonoFilePlayer.h"
    #include <libraries/Gui/Gui.h>      				 // Need this to use the GUI
    #include <libraries/GuiController/GuiController.h>   // Need this to use the GUI
    
    // Name of the sound file (in project folder)
    std::string gFilename = "slow-drum-loop.wav";
    
    // Object that handles playing sound from a buffer
    MonoFilePlayer gPlayer;
    
    // Browser-based GUI to adjust parameters
    Gui gui;
    GuiController controller;
    
    // TODO: declare variables for circular buffer
    unsigned int gWritePointer = 0;
    unsigned int gReadPointer = 0;
    float gMaxDelay = 0.5;
    std::vector<float> gDelayBuffer (0);
    unsigned int gActualOffset = 0;
    unsigned int gTargetOffset = 0;
    
    bool setup(BelaContext *context, void *userData)
    {
    	// Load the audio file
    	if(!gPlayer.setup(gFilename)) {
        	rt_printf("Error loading audio file '%s'\n", gFilename.c_str());
        	return false;
    	}
    
    	// Set up the GUI
    	gui.setup(context->projectName);
    	controller.setup(&gui, "Delay");
    	
    	// Arguments: name, default value, minimum, maximum, increment
    	controller.addSlider("Delay", 0.25, 0.01, .49, 0);
    	controller.addSlider("Feedback", 0.5, 0.0, .95, 0);
    
    	// set the size of the buffer to the max delay length
    	gDelayBuffer.resize(context->audioSampleRate * gMaxDelay);
    
    	// Print some useful info
            rt_printf("Loaded the audio file '%s' with %d frames (%.1f seconds)\n", 
        			gFilename.c_str(), gPlayer.size(),
        			gPlayer.size() / context->audioSampleRate);
    
    	return true;
    }
    
    void render(BelaContext *context, void *userData)
    {
    
    	float delay = controller.getSliderValue(0);	// delay (seconds) is first slider 
    	float feedback = controller.getSliderValue(1); // feedback is second slider
    	
    	gTargetOffset = (int)(delay * context->audioSampleRate); // convert delay seconds to samples
    
    	gReadPointer = (gWritePointer - gActualOffset + gDelayBuffer.size()) % gDelayBuffer.size();
    	
        for(unsigned int n = 0; n < context->audioFrames; n++) {
            
        	float in = gPlayer.process();
    
        	// circular buffer code goes here
            float out = gDelayBuffer[gReadPointer];
            
            gReadPointer++;
            if (gReadPointer >= gDelayBuffer.size()) {
            	gReadPointer = 0;
            }
            
            gDelayBuffer[gWritePointer] = in + out * feedback;
            
            gWritePointer++;
            if (gWritePointer >= gDelayBuffer.size()) {
            	gWritePointer = 0;
            }
            
            // slowly and frequently update the delay offset
    	    if(gActualOffset < gTargetOffset) {
    			gActualOffset++;
    	    }
    		else if (gActualOffset > gTargetOffset) {
    			gActualOffset--;
    	    }
    		
            // Write the input and output to different channels
        	audioWrite(context, n, 0, in);
        	audioWrite(context, n, 1, out);
        }
    }
    
    void cleanup(BelaContext *context, void *userData)
    {
    
    }

    Cheers and thanks again,
    Michael

    matt Thank you as well for these tips, Matt! I somehow only noticed your posts after replying to Guilio, my bad.

    Cheers,
    Michael

    Not sure what issues you were having. The below works fine for me. I added a couple of sliders. One to select the transition mode:

    • NONE (slider <1 ) for the default non-smoothing behaviout
    • INCREMENT (1 <= SLIDER <2) for the behaviour I described above
    • ONEPOLE (SLIDER > 2) for the one pole smoothing behaviour

    Another one alpha is for the smoothing factor of the ONEPOLE mode: smaller values (e.g.: 0.999) will result in a quicker transition, larger values (e.g.: 0.99999) will result in a longer transition. Note that gActualInterval is now a float (because that's required for ONEPOLE to work), however it gets rounded to an int before being used to reset gReadPointer. When doing that, the + 0.5 is used to round it to the nearest integer instead of being truncated down (this is sometimes called "nearest neighbour interpolation"). Also note that gReadPointer is no longer incremented and wrapped individually, but it is simply computed bases on gWritePointer at every sample based on the gActualOffset (in fact gReadPointer wouldn't actually need to be global any longer).
    With the updated GUI, you can compare the three methods easily. It sounds pretty good in ONEPOLE mode, and that's even without interpolation !

    Ultimately when smoothing the transition you are trading off an instantaneous click for a temporary pitch shifting. The alpha parameter allows you to fine tune that.

    #include <Bela.h>
    #include <vector>
    #include "MonoFilePlayer.h"
    #include <libraries/Gui/Gui.h>      				 // Need this to use the GUI
    #include <libraries/GuiController/GuiController.h>   // Need this to use the GUI
    
    // Name of the sound file (in project folder)
    std::string gFilename = "ed.wav";
    
    // Object that handles playing sound from a buffer
    MonoFilePlayer gPlayer;
    
    // Browser-based GUI to adjust parameters
    Gui gui;
    GuiController controller;
    
    // TODO: declare variables for circular buffer
    unsigned int gWritePointer = 0;
    unsigned int gReadPointer = 0;
    float gMaxDelay = 0.5;
    std::vector<float> gDelayBuffer (0);
    unsigned int gTargetOffset = 0;
    
    bool setup(BelaContext *context, void *userData)
    {
    	// Load the audio file
    	if(!gPlayer.setup(gFilename)) {
        	rt_printf("Error loading audio file '%s'\n", gFilename.c_str());
        	return false;
    	}
    
    	// Set up the GUI
    	gui.setup(context->projectName);
    	controller.setup(&gui, "Delay");
    	
    	// Arguments: name, default value, minimum, maximum, increment
    	controller.addSlider("Delay", 0.25, 0.01, .49, 0);
    	controller.addSlider("Feedback", 0.5, 0.0, .95, 0);
    	controller.addSlider("Transition Mode", 0, 0, 3, 0);
    	controller.addSlider("Alpha", 0.999, 0.99, 0.99999, 0.0001);
    
    	// set the size of the buffer to the max delay length
    	gDelayBuffer.resize(context->audioSampleRate * gMaxDelay);
    
    	// Print some useful info
            rt_printf("Loaded the audio file '%s' with %d frames (%.1f seconds)\n", 
        			gFilename.c_str(), gPlayer.size(),
        			gPlayer.size() / context->audioSampleRate);
    
    	return true;
    }
    
    float gActualOffset = 0;
    enum {
    	NONE,
    	INCREMENT,
    	ONEPOLE,
    };
    unsigned int gTransitionMode = 0;
    void render(BelaContext *context, void *userData)
    {
    
    	float delay = controller.getSliderValue(0);	// delay (seconds) is first slider 
    	float feedback = controller.getSliderValue(1); // feedback is second slider
    	float transition = controller.getSliderValue(2);
    	if(transition < 1)
    		gTransitionMode = NONE;
    	else if (transition < 2)
    		gTransitionMode = INCREMENT;
    	else
    		gTransitionMode = ONEPOLE;
    
    	float alpha = controller.getSliderValue(3); // smoothing for one pole smoothing
    	gTargetOffset = (int)(delay * context->audioSampleRate); // convert delay seconds to samples
    
        for(unsigned int n = 0; n < context->audioFrames; n++) {
            
        	float in = gPlayer.process();
    
        	// circular buffer code goes here
            float out = gDelayBuffer[gReadPointer];
            
            gDelayBuffer[gWritePointer] = in + out * feedback;
            
            gWritePointer++;
            if (gWritePointer >= gDelayBuffer.size()) {
            	gWritePointer = 0;
            }
    
            if(NONE == gTransitionMode) {
    			gActualOffset = gTargetOffset;
    		} else if (INCREMENT == gTransitionMode) {
    		    if(gActualOffset < gTargetOffset)
    				gActualOffset++;
    			else if (gActualOffset > gTargetOffset)
    				gActualOffset--;
    		} else if (ONEPOLE == gTransitionMode) {
    			gActualOffset = gTargetOffset * (1 - alpha) + gActualOffset * alpha;
    		}
            gReadPointer = (gWritePointer - int(gActualOffset + 0.5) + gDelayBuffer.size()) % gDelayBuffer.size();
    		
            // Write the input and output to different channels
        	audioWrite(context, n, 0, in);
        	audioWrite(context, n, 1, out);
        }
    }
    
    void cleanup(BelaContext *context, void *userData)
    {
    }
    8 months later

    I was revisiting this lecture today after a lengthy delay, and only then did I notice your solution Guilio. Thank you for demonstrating the different methods!

    3 years later