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