Variable delay lines and granular processing in Pure Data

This is a short post about the implementation of granulators using variable delay lines. There are several examples of delay line granulators in Pure Data so I am not trying to reinvent the wheel, but since I have investigated them a bit I thought that some could still be interested in the mechanism behind this approach. After a quick introduction to variable delay lines, we will see why they are particularly useful for granulation. This design is just an extension of Miller Puckette’s pitch shifter which can be found in the G09 Pure Data help patch.

Variable delay lines allow for the delay time to be modulated at audio rates and also make it possible to have fractional delays by interpolating samples. In Pure Data, the interpolating algorithm is a four-point cubic function, the same used for its wavetable lookup object ([tabread4~]) used for the implementation of samplers.

Wavetables and variable delay lines share the same properties. (See Miller Puckette’s book for a discussion of the topic: http://msp.ucsd.edu/techniques/latest/book-html/.) The main difference is that the content of a delay line is constantly updated, which makes it ideal for a live situation, especially if you, like me, avoid mixing audio signal and message domain in Pure Data as much as possible.

You can think of a variable delay line as a system with, virtually, a rotating tape loop to which a fixed writing head and a moving reading head are attached. The input of the writing head is the signal which is being written on the tape; the input of the reading head is the delay after which that signal will be output. The length of the tape (D) is the maximum possible delay time and is the case where the reading head is just behind the writing head. On the other hand, if the reading head is immediately next to the writing head we will have a zero-delay output. All other positions, determined by the input signal of the reading head, are the possible delays between 0 and D.

Intuitively, if the reading head moves towards the opposite direction of the tape we will have an increase in speed. Conversely, if it moves towards the same direction of the tape we will have a decrease in speed. The first case is a pitch transposition up, the second case is a transposition down.

Generally speaking, if d[n] is the input of the reading head, setting the delay in samples, the pitch transposition factor t[n] for delay lines is given by the following formula:

t[n] = 1 - (d[n] - d[n - 1]).

Using this formula, we can easily calculate the slope of a line – a [phasor~] object, in this case – which represents the delay variation necessary to perform the desired transposition, although this variation can only take place for a limited period given the finite length of the delay line.

In order to have a continuous transposition, two overlapping reading heads are commonly used. These heads are 180º out of phase and their cycles are smoothed out using windowing functions to avoid audible discontinuities. In fact, this is the reason why such a design can be used for granular processing: each cycle of a reading head is a portion of sound which is being read, and, as we will see, this portion can be of any length. 

There are a number of windowing functions with different characteristics; here we are using the positive sine window to compensate for the statistical phase cancellation taking place when summing two signals together. (Check out one of the awesome posts by Katja Vetter to find out more: http://www.katjaas.nl/pitchshift/pitchshift.html.)

Now that it is possible to transpose a signal for an arbitrary amount of time, the remaining parts of the granulator are the grain rate – which, in this design, is linked to the grain duration – and the time transposition or time stretching.

Once the slope for a given pitch transposition has been calculated, keeping the same transposition for different grain rates is only a matter of scaling down the delay variation size to have a constant slope.

Time transposition, in this case, can be implemented by consistently offsetting the grains with regard to the movement of the tape (metaphorically speaking), and we can still use the transposition formula above to calculate the slope of the offset. For example, if the grains are offset at the same rate as the motion of the tape, the reading heads will always be in the same region of the buffer and we will have a ‘freeze’ effect (or zero-factor time transposition).

You can also note that this design is consistent with negative transposition factors of both time and pitch, and those will result, respectively, in exploring the buffer backwards and playing the grains in reverse.

You can save the text below into a plain text file with the .pd extention to try the patch. The object takes an argument which is the delay line length in milliseconds.

#N canvas 104 0 956 778 10;
#X obj 116 200 inlet~;
#X obj 115 372 samphold~;
#X obj 249 438 *~;
#X obj 115 585 vd~ \$0-grain;
#X obj 670 144 phasor~;
#X obj 731 176 +~ 0.5;
#X obj 731 198 wrap~;
#X obj 731 227 s~ \$0-ph2;
#X obj 670 227 s~ \$0-ph1;
#X obj 166 348 r~ \$0-ph1;
#X obj 300 346 r~ \$0-ph1;
#X obj 227 486 r~ \$0-ph1;
#X obj 115 635 *~;
#X obj 435 372 samphold~;
#X obj 649 438 *~;
#X obj 435 585 vd~ \$0-grain;
#X obj 435 695 *~;
#X obj 486 348 r~ \$0-ph2;
#X obj 700 346 r~ \$0-ph2;
#X obj 567 557 r~ \$0-ph2;
#X obj 291 732 outlet~;
#X obj 249 373 samphold~;
#X obj 649 373 samphold~;
#X obj 510 171 /~;
#X obj 525 100 inlet~;
#X obj 510 198 *~ 1000;
#X obj 670 70 inlet~;
#X obj 359 100 inlet~;
#X obj 329 227 s~ \$0-position;
#X obj 115 318 r~ \$0-position;
#X obj 329 152 *~;
#X obj 329 177 phasor~;
#X text 713 70 grain rate;
#X text 568 100 pitch factor;
#X obj 329 201 *~ \$1;
#X obj 116 227 delwrite~ \$0-grain \$1;
#X obj 115 522 wrap~;
#X obj 435 522 wrap~;
#X obj 264 26 sig~ 1;
#X obj 264 108 /~;
#X obj 279 54 sig~ \$1;
#X obj 279 76 *~ 0.001;
#X obj 344 76 sig~ 1;
#X obj 344 129 -~;
#X obj 510 76 sig~ 1;
#X obj 510 138 -~;
#X obj 670 107 max~ 0;
#X obj 510 227 s~ \$0-size;
#X obj 249 318 r~ \$0-size;
#X obj 649 318 r~ \$0-size;
#X obj 435 318 r~ \$0-position;
#X obj 664 409 r~ \$0-ph2;
#X obj 264 409 r~ \$0-ph1;
#X obj 115 493 /~ \$1;
#X obj 115 553 *~ \$1;
#X obj 435 493 /~ \$1;
#X obj 435 553 *~ \$1;
#X text 528 318 buffer position;
#X text 718 318 delay variation size;
#X text 478 522 wrap around when margins are exceeded;
#X text 401 100 time factor;
#X obj 227 585 cos~;
#X obj 227 518 -~ 0.5;
#X obj 227 552 *~ 0.5;
#X obj 567 655 cos~;
#X obj 567 588 -~ 0.5;
#X obj 567 622 *~ 0.5;
#X connect 0 0 35 0;
#X connect 1 0 53 0;
#X connect 2 0 53 0;
#X connect 3 0 12 0;
#X connect 4 0 8 0;
#X connect 4 0 5 0;
#X connect 5 0 6 0;
#X connect 6 0 7 0;
#X connect 9 0 1 1;
#X connect 10 0 21 1;
#X connect 11 0 62 0;
#X connect 12 0 20 0;
#X connect 13 0 55 0;
#X connect 14 0 55 0;
#X connect 15 0 16 0;
#X connect 16 0 20 0;
#X connect 17 0 13 1;
#X connect 18 0 22 1;
#X connect 19 0 65 0;
#X connect 21 0 2 0;
#X connect 22 0 14 0;
#X connect 23 0 25 0;
#X connect 24 0 45 1;
#X connect 25 0 47 0;
#X connect 26 0 46 0;
#X connect 27 0 43 1;
#X connect 29 0 1 0;
#X connect 30 0 31 0;
#X connect 31 0 34 0;
#X connect 34 0 28 0;
#X connect 36 0 54 0;
#X connect 37 0 56 0;
#X connect 38 0 39 0;
#X connect 39 0 30 0;
#X connect 40 0 41 0;
#X connect 41 0 39 1;
#X connect 42 0 43 0;
#X connect 43 0 30 1;
#X connect 44 0 45 0;
#X connect 45 0 23 0;
#X connect 46 0 4 0;
#X connect 46 0 23 1;
#X connect 48 0 21 0;
#X connect 49 0 22 0;
#X connect 50 0 13 0;
#X connect 51 0 14 1;
#X connect 52 0 2 1;
#X connect 53 0 36 0;
#X connect 54 0 3 0;
#X connect 55 0 37 0;
#X connect 56 0 15 0;
#X connect 61 0 12 1;
#X connect 62 0 63 0;
#X connect 63 0 61 0;
#X connect 64 0 16 1;
#X connect 65 0 66 0;
#X connect 66 0 64 0;