<<home  <<previous  next>>

Slicing a Circular Buffer






The previous page described a technique to detect attacks in an audio signal. I use this technique for the instantaneous slicing of an audio stream from the soundcard. While the audio is refreshed, the newest slices are immediately and automatically available for playback, with speed and direction of choice. With a sequencer, the sliced acoustic notes are transformed to rhythm section on the fly. It is quite rewarding to hear your sounds turn into music so fast. On this page, I want to illustrate some practical details of the concept.


[slicerec~]

The audio is recorded into a small circular buffer with the [slicerec~] object for Pure Data. I found that a buffer of about 6 seconds is sufficient for my purpose, though it could be longer if required. Here you see [slicerec~] with it's connections:



slicerec~




The buffer in which [slicerec~] writes has length 'power-of-two-plus-one-sample'. The power of two is for efficient operation like bitmasking. The one sample extra is for linear interpolation, and it holds the same value as the sample at index 0.

The [slicerec~] object is constantly analysing the input signal to find attacks or identify silence. When an attack is detected, recording starts or continues, and a cuepoint indicating the attack start location is issued from the left outlet. When the signal decreases below a user-definable level, recording is paused and a cuepoint of that location is issued from the middle outlet. When a new attack is found, recording resumes. It's cuepoint is identical to the silence cuepoint, there is nothing inbetween. There is only useful audio in the buffer.

[slicerec~] can receive messages to alter it's analysis parameter settings. Details of the analysis are on the previous page. Although these settings can be tuned to input type and conditions of performance, they do not require a lot of attention, and for many cases the object defaults are valid.



slicerec~2




Slicing a circular buffer, where recording goes on continuously, is not so straightforward. There is always one slice which wraps around the buffer end, and there is always one slice being overwritten by a new one. Moreover, the amount of slices in the buffer is not fixed, because it depends on their length. Therefore, apart from a stale slice, you can also have a bunch of stale cuepoints. Here is an illustration with a wrapped- and a stale slice:



circbuf




By far the simplest way of avoiding stale cuepoints is to oversize the audio buffer, and use a fixed amount of cuepoints, the most recent ones everytime. Playing a wrapped-around slice is no problem, although it requires some adjustments which will be discussed further down.

By definition, the start point of one slice terminates the preceding slice:



cycbuf2




Still it is useful to store both start and end point for slices, because the end point of the most recent slice is not the start point of the oldest slice in the records. So it is best to wait for the end of a slice, and then store start and end point together. The end can be induced by silence or by a new attack, and in both cases it comes in due time. A table with cuepoints can have this structure:





cuetable




This table can also be handled as a circular buffer, where the newest pair of cuepoints overwrites the oldest pair in the table. Provided the audio buffer is large enough to hold the slices, you end up with a table which has fresh legitimate cuelists only, all of which can be referenced for playback,


[sliceplay~]

Now comes the question of how to read data from the audio buffer at different speeds, forward and backward, and eventually across the wrap-around point. To start with, the number of samples in the slice must be calculated. In most cases, subtracting the start index value from the end index value would simply give the answer. But for a wrapped-around slice, the start index is larger than the end index, and the result would be a negative number. Eating the sign of that result will not lead to the correct number of samples. Let me do a hypothetic example to illustrate how the number of samples can be calculated, no matter where the start and end point are:




buffer1



The loop in the sketch above has length 8, and it's indexes run conventionally from 0 till 7. The hypothetic slice has three samples at index 7, 0 and 1. The start point is at 7, and the end point is at index 2, because that is the start point of the next slice. The computation method goes like this:

length = (end point - start point + loop length) modulo loop length

the example: (2 - 7 + 8) modulo 8 = (-5 + 8) modulo 8 = 3 modulo 8 = 3

Three samples, this is correct, but what is the modulo good for? It does not seem to make any difference here. But look at the case where start- and end points are swapped respective to the previous case:




buffer2




again: length = (end point - start point + loop length) modulo loop length

the example: (7 - 2 + 8) modulo 8 = (5 + 8) modulo 8 = 13 modulo 8 = 5

The result is 5 samples indeed. One calculation method applies for both cases. That is convenient. For loop lengths which are a power of two, modulo computation can be done by bitwise-and masking, that is extra convenient.

Now we know the number of samples which should be played, if at the original speed. At different speeds, the playback time is computed with:

playback time in number of samples = original number of samples / abs(speed)

The playback time is calculated by [sliceplay~] from the desired playback speed given in it's left inlet and the start/end points given as a pair in the right inlet. A countdown timer is set for the playback time and playback will stop when the countdown reaches zero.



sliceplay~




During playback, another counter runs upward from zero, and the playback pointer index is calculated by multiplying this upward counter with the playback speed. The playback speed is obviously a floating point number. For reasons of precision, I do not use floating point increments, but the multiplication instead. The result must be cast back to an integer index. The difference between floating point value and it's rounded value is computed (typecasting again), and used as a fraction for linear interpolation. This relieves aliasing effects. With speeds faster than 1 or negative speeds, the indexes could run beyond the buffer bound. This is avoided by applying exactly the same trick as used before: adding the loop length and wrapping the result modulo loop length. For negative speeds, playback starts at the end point minus one sample. Well, these things are all handled inside the object.


instantaneous slicing

Although [slicerec~] and [sliceplay~] are specialised for realtime audio processing, their use is not preconceived otherwise. Because [slicerec~] records into a named buffer, this buffer can be accessed by other objects, and it can be graphed for test or illustration purposes. More than one [sliceplay~] object can read from the same buffer simultaneously, reading different slices at different speeds. This is how I assemble a complete gamelan orchestra from the sounds of my tiny kalimba.

Each [sliceplay~] instance is monophonic, and it will play it's assigned slice from start till end uninterrupted, immediately after cuepoints are given. Cuepoints arriving at the inlet during playback are simply discarded. No windowing is done at playback, and when complete slices as recorded and cue'd by [slicerec~] are played, this should not be necessary anyway. If you want to play back incomplete slices, it is of course possible by altering cuepoints accordingly. You will need to organise fade-in and/or fade-out externally, to eliminate the ugly clicks which arise from arbitrary cuts.

The Pure Data abstraction slicerec~-help introduces the objects and their possible configuration. There is one recorder, four players, and a simple sequencer in a subpatch, triggering the players.





helpfile




The fact that slices are played uninterrupted, means that short slices can be triggered at a higher tempo. It also means that some triggers are ignored, specially with low playback speeds and long slices. A sequence may not play with the exact rhythm which was programmed. On the other hand, this offers an extra opportunity to steer a sequence with the acoustic input, without touching a digital controller. It is an interaction with the machine which I had not anticipated or programmed on purpose.

Around the slice objects, I configured an instantaneous beatslicer module for Pure Data. It has one recorder, four players and a sequencer. Below, two such modules are synched in one Pure Data patch. The left one is set to 'decent' tunings, with playback speeds being multiples of 0.25, and the one on the right does 'false' notes. Both have their charm.



recyc~



Still on my wishlist is a method which produces irregular flocks or swarms of triggers for the slice players. This would extend their use into a different realm of musical expression.

One more convenient aspect of the [slicerec~] and [sliceplay~] objects is their CPU friendlyness. The patch above, with two recorders and eight players in total, is responsable for about one percent CPU load on my 2GHz MacIntel computer. This implies that instantaneous beatslicing may be done on quite modest systems as well.

Source code of [slicerec~] and [sliceplay~] can be downloaded from the previous page (bottom). The help file, demonstrating the objects with a sequencer, is included.