Digital Music Programming II: kola~




This lab demonstrates how to loop audio without clicks using a constant overlap-add windowing method with Kaiser-Bessel windows.

  • example 1: Best case looping for a 1000 Hz sinewave, with a 100 ms loop, using a 1012 sample half-window, and an alpha of 0.
  • example 2: Worst case looping for a 1000 Hz sinwewave, with a 100 ms loop, using a 988 sample half-window, and an alpha of 1.


Source Code Example Usage
kola.c


#include "ext.h"
#include "z_dsp.h"
#include <cmath>

typedef struct {
   t_pxobject msp_data;
   float  *window, *windowb, *buffer;
   long   winsize, bufsize, index, recordQ, loopQ, endQ, bufallocsize;
   float  alpha;
} MyObject;

void* object_data;

void   main              (void);
void*  create_object     (float maxsize);
void   destroy_object    (MyObject* mo);
void   InputBang         (MyObject* mo);
void   InputLoopTime     (MyObject* mo, float value);
void   InputWindowSize   (MyObject* mo, int value);
void   InputAlpha        (MyObject* mo, float value);
void   MessageDSP        (MyObject* mo, t_signal** signal, short* count);
void   MessageClear      (MyObject* mo);
void   MessageStart      (MyObject* mo);
void   MessageStop       (MyObject* mo);
t_int* Perform           (t_int *parameters);
double BesselI0          (double x);
void   MakeKaiserWindow  (float* window, int size, float alpha);

void main(void) {
   setup((t_messlist**)&object_data, (method)create_object,
         (method)destroy_object, (short)sizeof(MyObject),
         NULL, A_FLOAT, A_NOTHING);
   addftx ((method)InputAlpha,      1);
   addinx ((method)InputWindowSize, 2);
   addftx ((method)InputLoopTime,   3);
   addbang((method)InputBang);
   addmess((method)MessageDSP,   "dsp",   A_CANT, A_NOTHING);
   addmess((method)MessageClear, "clear", A_NOTHING);
   addmess((method)MessageStart, "start", A_NOTHING);
   addmess((method)MessageStop,  "stop",  A_NOTHING);
   dsp_initclass();
}

void* create_object(float maxsize) {
   long maxsamples;
   MyObject *mo = (MyObject*)newobject(object_data);
   dsp_setup((t_pxobject*)mo, 1);
   outlet_new((t_pxobject*)mo, "signal");
   floatin(mo, 1);   intin(mo, 2);   floatin(mo, 3);
   if (maxsize < 1) {
      post("Error: buffer size too small, setting it to 1000 milliseconds.");
      maxsize = 1000.0;
   }
   maxsamples = (long)(maxsize/1000.0 * 44100.0 + 0.5);
   mo->buffer       = (float*)malloc(maxsamples * sizeof(float));
   mo->bufallocsize = mo->bufsize = maxsamples;
   mo->alpha        = 1.25;
   mo->window       = NULL;
   InputWindowSize(mo, 1024);
   MessageClear(mo);
   return mo;
}

void destroy_object(MyObject* mo) {
   dsp_free(&(mo->msp_data));
   free(mo->buffer); free(mo->window);
   mo->buffer = mo->window = mo->windowb = NULL;
   mo->bufallocsize = mo->bufsize = 0;
}

void InputBang(MyObject* mo) {
   if (mo->loopQ)   MessageStop(mo);
   else                MessageStart(mo);
}

void MessageStart(MyObject* mo) {
   mo->loopQ = mo->recordQ = 1;
   mo->index = 0;
   post("kola~ start");
}

void MessageStop(MyObject* mo) {
   mo->endQ  = 1;
   post("kola~ stop");
}

void MessageClear(MyObject* mo) {
   int i;
   for (i=0; i<mo->bufallocsize; i++)   mo->buffer[i] = 0.0;
   mo->index = 0;
}

void InputAlpha(MyObject* mo, float value) {
   MakeKaiserWindow(mo->window, mo->winsize, value);
   mo->alpha = value;
}

void InputWindowSize (MyObject* mo, int value) {
   if (value < 2)   value = 2;
   value = value + (2 - value%2);   value = value * 2;
   if (mo->window != NULL)   free(mo->window);
   mo->window = (float*)malloc(value * sizeof(float));
   mo->winsize = value;
   mo->windowb = mo->window + value/2;
   InputAlpha(mo, mo->alpha);
}

void InputLoopTime(MyObject* mo, float value) {
   long samples = (long)(value / 1000.0 * 44100 + 0.5);
   if (samples >= mo->bufallocsize) {
      samples = mo->bufallocsize;
      post("kola~: cannot set loop buffer that big!");
   }
   if (samples < 4) {
      samples = 4;
      post("kola~: setting minimum audio delay buffer sample size to 4.");
   }
   mo->bufsize = samples;
   mo->index = 0;
}

void MessageDSP(MyObject* mo, t_signal** signal, short* count) {
   #pragma unused(count)
   int paramcount = 5;
   dsp_add(Perform, paramcount, paramcount, mo, signal[0]->s_vec,
         signal[1]->s_vec, signal[0]->s_n);
}

t_int* Perform(t_int *parameters) {
   long      paramcount = (long)     (parameters[1]);
   MyObject *mo         = (MyObject*)(parameters[2]);
   t_float  *input      = (t_float*) (parameters[3]);
   t_float  *output     = (t_float*) (parameters[4]);
   long      count      = (long)     (parameters[5]);
   long i;
   long tindex;

   for (i=0; i<count; i++) {
      if (mo->recordQ) {
         mo->buffer[mo->index] = input[i];
         if (mo->index < mo->winsize/2) {
            output[i] = mo->buffer[mo->index] * mo->window[mo->index];
         } else if (mo->index >= mo->bufsize - mo->winsize/2) {
            tindex = mo->winsize/2 - (mo->bufsize - mo->index);
            output[i] = mo->buffer[mo->index] * mo->windowb[tindex]
                        + mo->buffer[tindex] * mo->window[tindex];
         } else {
            output[i] = mo->buffer[mo->index];
         }
      } else if (mo->loopQ) {
         if (mo->index < mo->winsize/2) {
            tindex = mo->bufsize - mo->winsize/2 + mo->index;
            output[i] = mo->buffer[mo->index] *  mo->window[mo->index] +
                        mo->buffer[tindex] * mo->windowb[mo->index];
         } else if (mo->index >= mo->bufsize - mo->winsize/2) {
            if (mo->index == mo->bufsize - mo->winsize/2) {
               post("kola~ ending");
            }
            tindex = mo->winsize/2 - (mo->bufsize - mo->index);
            output[i] = mo->buffer[mo->index] * mo->windowb[tindex];
         } else {
            output[i] = mo->buffer[mo->index];
         }
      } else {
         output[i] = 0.0;
      }

      mo->index++;
      if (mo->recordQ && (mo->index >= mo->bufsize)) {
         mo->index   = mo->winsize/2;
         mo->recordQ = 0;
      } else if ((!mo->recordQ) && (!mo->endQ) && mo->loopQ &&
            (mo->index >= mo->bufsize - mo->winsize/2)) {
         mo->index   = 0;
      } else if (mo->index >= mo->bufsize) {
         mo->endQ = mo->loopQ = mo->recordQ = mo->index = 0;
      }
   }

   return parameters+paramcount+1;
}

#ifndef PI
   #define PI 3.14159265358979323846264338328
#endif

void MakeKaiserWindow(float* window, int size, float alpha) {
   double sumvalue = 0.0;
   int i;
   for (i=0; i<size/2; i++) {
      sumvalue += BesselI0(PI * alpha * sqrt(1.0 - pow(4.0*i/size - 1.0, 2)));
      window[i] = sumvalue;
   }
   sumvalue += BesselI0(PI * alpha * sqrt(1.0 - pow(4.0*(size/2)/size-1.0, 2)));
   for (i=0; i<size/2; i++) {
      window[i] = window[i]/sumvalue;
      window[size-1-i] = window[i];
   }
}

double BesselI0(double x) {
   double denominator, numerator, z;
   if (x == 0.0)   return 1.0;
   else {
      z = x * x;
      numerator = (z* (z* (z* (z* (z* (z* (z* (z* (z* (z* (z* (z* (z*
                     (z* 0.210580722890567e-22  + 0.380715242345326e-19 ) +
                         0.479440257548300e-16) + 0.435125971262668e-13 ) +
                         0.300931127112960e-10) + 0.160224679395361e-7  ) +
                         0.654858370096785e-5)  + 0.202591084143397e-2  ) +
                         0.463076284721000e0)   + 0.754337328948189e2   ) +
                         0.830792541809429e4)   + 0.571661130563785e6   ) +
                         0.216415572361227e8)   + 0.356644482244025e9   ) +
                         0.144048298227235e10);
      denominator = (z*(z*(z-0.307646912682801e4)+
                       0.347626332405882e7)-0.144048298227235e10);
   }
   return -numerator/denominator;
}

Windowing Audio Buffer

To prevent clicks, the circular audio buffer is windowed at its endpoints as shown in the diagram below. The windowed endpoints are overlapped to generate a constant overlap-add envelope for the circular buffer of audio data.

The window should be designed so that the sum of the first half plus the reverse of the last half of the window sum to a constant value of 1.0. For the kola~ object, a Kaiser-Bessel window was chosen (arbitrarily).

The window size is determined by user input, but it will be less than or equal to the total size of the audio buffer. When generating output audio, the first half of the window buffer is multiplied by the first part of the audio buffer. The last half of the window buffer is multiplied by the last part of the audio buffer.

When the audio is looped back to the beginning of the sound buffer, there is a region (shaded in pink, magenta and blue) which is added from the start and the end of the audio buffer. The looping is done by going from the end of the blue section directly to the magenta section. During the blue section the initial pink section is add to the output audio. During the magenta section the final pink section is added into the output audio. During the magenta section the final pink section is added into the output audio.

Window Parameter

The alpha parameter for the windowing controls the alpha parameter for a Kaiser-Bessel window. When alpha = 0, the window is a triangle window. When alpha is about 1.25, the window is nearly a raised-cosine window. For large values of alpha, the window is approximately a square window. Here are pictures of the Kaiser-Bessel window using various alpha values:

Exercises

  1. Compile and test the kola~ object.

  2. Compare the behavior of the kola~ object to other sound looping objects. Try looping a sinewave and noise to test the different objects.
  3. Extra Credit: Have the looping monitor the pitch of the loop points and adjust the windows size to minimize beats during looping.