Rhythmically skipping through a recording, audio probing

Hello everyone!

I have an hour long stereo file. I want to skip through it in fixed intervals and play a few seconds. I want to record this and end up with a crude compression of the recording (including hard cuts).

I asked around for help since my understanding of SuperCollider is so small at this point that I wouldn’t solve this task myself - I have absolutely no coding experience and slowly try to get a feeling for SC.
A code (do you call it code? Or pattern?) was suggested to me. It somehow works for me and I was able to work out most of it but two questions remain and your input would help me to better understand on how to work with audio files in Supercollider:

// define your buffer
b = Buffer.read(s,Platform.resourceDir +/+"sounds/a11wlk01.wav");

// this will be our playback for our buffer
(
SynthDef(\test,{|out=0, bufNum=0, start=0, end=1000|
[Out.ar](http://Out.ar)(out,
// instead of PlayBuf we use BufRd b/c we can 
[BufRd.ar](http://BufRd.ar)(1, bufNum, [Phasor.ar](http://Phasor.ar)(0, [BufRateScale.kr](http://BufRateScale.kr)(bufNum), start, end), loop: 0)
);
}).add; 
)

// test synth
s = Synth(\test)
s.free;

(
var numSkips, durationWait;
numSkips = 500;
durationWait = 0.02;

s = Synth(\test, [
\buf, b.bufnum,
]);
{
numSkips.do({|i|
// we now set the skips skip
s.set(\start, i/numSkips * b.numFrames, \end, (i/numSkips * b.numFrames) + 10000);
durationWait.wait;
// lets free the synth after the last step
if(i==(numSkips-1), {s.free});
});
// fork which will create a routine which we will need b/c we "wait"
}.fork;
)  
  1. “test synth” - executing this line just gives a buzzing noise on the left channel: I expect this to be a tiny section of the loaded buffer being played? Or what do I test here?

  2. To understand the concept here: I create a SynthDef holding my buffer where the recording lives in. This gets played later on and “s.set” plus the added 10000 make up for the different sections…
    What exactly is Phasor.ar for? It has nothing to do with the phase of the buffer right? The help file says it’s ramping up after a trigger so is this the thing that “goes” through the buffer?

  3. I’m not sure I get the variable numSkips… What exaclty are the values here? I see that it gets multiplied with numFrames and I know that the “sample” is made up of a finite number of frames. But where exactly do I read the code for the movement? At the added 10000? But why is it added to end, not to start. And what should I manipulate to modify the lenght of the crops? durationWait is obvious, but changing the numSkips yields somewhat unpredictable results: At some point they give me loops which play a few times before going to the next section…
    Maybe someone could break this line down for me, I’m afraid there is some super obvious math going on that I just don’t see…:

s.set(\start, i/numSkips * b.numFrames, \end, (i/numSkips * b.numFrames) + 10000);

Thank you!

Hi! And welcome to SuperCollider!

You are right! Let’s take the line where you are actually reading the buffer:

BufRd.ar(1, bufNum, Phasor.ar(0, BufRateScale.kr(bufNum), start, end), loop: 0)

You are telling to BufRd to read 1 channel of audio from bufNum. Its reading position (as it if was a mechanical reading head from a tape machine) is controlled by Phasor.
Phasor.ar generates a line from a start value to an end value, incrementing its value by one every sample. It is basically telling to BufRd which sample to read, all the time. As the value generated by Phasor ramps up, BufRd reads the corresponding sample. To be extra clear:

  • when you create the synth, start is set to 0 and end to 1000
  • after around 0.023 seconds (if your Buffer is recorded at 44.1k: 1000 samples / 44.1k = ~0.023 seconds) Phasor reaches its end value, 1000, and loops back to its start value, 0.
    This 0.023 seconds long loop is the buzz you hear when testing.

Exactly, Phasor is controlling the reading position of your BufRd. When you set your start and end controls, Phasor creates the line between them. And actually it loops: if you let it reach its end value, Phasor will jump back to its start value, and start again. This is why your are getting loops later on :slight_smile:
OT, but by the way, the second argument phasor takes, the one where you have BufRateScale, is it’s speed. Try to multiply it or divide it by some numbers to get tape-like pitch modulations.

Now let’s see the block where you control your cuts through the sample. I removed original comments and put numbers instead, to break it down in the following list:

{
// 1.
numSkips.do({|i|
    s.set(
      // 2.
      \start, i/numSkips * b.numFrames, 
      // 3.
      \end, (i/numSkips * b.numFrames) + 10000
   );
   // 4.
   durationWait.wait;
   // 5. lets free the synth after the last step
   if(i==(numSkips-1), {s.free});
});
}.fork;
  1. numSkips.do means “execute this function for numSkips times”. The function is what begins right after do, within curly brackets, including your s.set, your wait and your if. Those instructions will be executed numSkips times, that is 500 times. At every iteration, you get the iteration number into your i variable.
  2. this sets the start position for your Phasor: i/numSkips *b.numFrames. b.numFrames is the total number of samples in your buffer. ì is your iteration number: it is 0 at your first iteration, and will get all the way up to numSkips. i/numSkips is then a number that goes from 0 to 1 in 500 equal steps.
  3. this sets the end position of your Phasor, to the same sample as its start + 10000 samples (that is around 1/4 seconds). This doesn’t really matter if you change Phasor’s start point before it reaches it. But in case you wait longer than these 0.23 seconds, you will get a loop.
  4. Here you wait for durationTime before moving on to the the next iteration. durationWait is 0.02, which is 1/500. If every iteration waits for 0.02 seconds, the whole process will last exactly one second ( since it’s 500 iterations: 500*0.02 = 500 * 1 / 500 = 1 :slight_smile: )

That said, I’ll try to rewrite your routine, maybe it gets clearer to you?

var numSkips, finalDuration, durationWait;
// how long you want your piece to last
finalDuration = 1;
// how many skips during this duration
numSkips = 500;
// time to wait between every skip
durationWait = finalDuration / numSkips;

s = Synth(\test, [\buf, b.bufnum]);

{
numSkips.do({|i|
    var progress =  i / numSkips;
    var loopStartSample =  progress * b.numFrames;
    var loopEndSample = loopStartSample + 10000;
    // you could set this end value to be precisely the start of next segment:
    // loopEndSample = ( i + 1 ) / numSkips * b.numFrames;
    s.set(\start, loopStartSample, \end, loopEndSample);

    durationWait.wait;

    if(i==(numSkips-1), {s.free});
});
}.fork;

Hope it helps!

As a side note: watch out for hyperlink when posting code on this forum :slight_smile: it happened that Out.ar became a link pointing to http://Out.ar, which actually is a supercool domain name and could launch a new trend in buying domains for people using SuperColliders :alien:

1 Like

Hey Elgiano!

Thanks so much for thorough explanation! I try to get my head around it :slight_smile:

after around 0.023 seconds (if your Buffer is recorded at 44.1k: 1000 samples / 44.1k = ~0.023 seconds) Phasor reaches its end value, 1000, and loops back to its start value, 0.
This 0.023 seconds long loop is the buzz you hear when testing.

Ah wow, see; for me it wasn’t even obvious that the line
BufRd.ar(1, bufNum, Phasor.ar(0, BufRateScale.kr(bufNum), start, end), loop: 0)
was part of the SynthDef. It’s really hard for me to read and get the encapsulation with all the brackets.

Allow me to ask: Are these
|out=0, bufNum=0, start=0, end=1000|
variables that are used in the BufRd.ar… line you quoted?

Should I also change the number of channels to 2, since I use a stereo file in the buffer?

These are arguments to your SynthDef function, or in other words its control inputs. A SynthDef basically associates a unique name (\test in this case, where the \ is what is making it into a unique name, called Symbol in SC) with a function ( all the instructions between {} that follow the name).
The function will be sent to the server (by the .add call you have after the function), because in SC the actual sound synthesis is carried on by a separate program, called the server, or scsynth. There the arguments to the SynthDef function are turned into controls that you can adjust in real-time while the Synth is running.
So, your arguments are out=0, bufNum=0, start=0, end=1000, and this means that you can do things like:

// you can set controls when you create a synth
x = Synth(\test, [bufNum: b, start: 0, end: 29000]);
// and afterwards
x.set(\start, 0);

// for example you can change your buffer
c = Buffer.read(s, "path/to/another/sound/file.wav");
x.set(\bufNum, c);

By the way, it’s not a good idea to overwrite the s variable, because it contains a reference to the Server. Better to change your code to store your synth in another variable, e.g. x.

Yes! Go for it :slight_smile:

EDIT: extra tip about readability: in SuperCollider, try to select a block of code (or all your code) and press Tab. You should get automatic indentation, which makes your code way more readable!

I suppose numSkips isn’t a command or anything like that but a variable that I assigned a value to: So I suppose do has an instruction in it’s arguments (that’s what the things in curly brackets, divided by a comma a called, right?) that defines how many times something is done? One problem here is that I can’t find something in the help files, because searching for do gives me an “Alphabetical index of all methods” and I really don’t know which category is the right one. I guess the more you know the easier things like this are but it’s super hard that you have to know the program in order to fully use it’s helpfiles :slight_smile:

So this variable should be changed to another letter right? I already got some error messages after playing with the code a bit and I thought that it might have to do with the s.erver but I’m rather cautious in not messing with the code to much to change this.

Good to know - again thank you very much for all your efforts, highly appreciated!

Searching the help: put your cursor on a do in your code and press Ctrl (or Cmd if you are on a mac) + d.
You will get a list of all classes that implement the do method. numSkips is exactly a variable, and it contains an Integer number, so when you get to the list scroll down until you find Integer. Also have a look around and see how many classes have the do method!

There is something called the Robinson paradox. Deleuze made the long story short saying that “you can’t make sense of a language before you make sense of all its elements, but you can’t make sense of the elements until you make sense of the whole language”. Terrible, and fascinating, but we do speak, we do learn languages. Keep exposing yourself to it, read other people’s code, write and share your own, make pieces, go out and perform with it (well, this pretty much means stream it at the very present moment).
Also, if you have time, check the Getting Started Tutorial series (you find it in the help browser), @Bruno’s e-book, or @elifieldsteel Youtube Tutorial Series

2 Likes

Aside: I recommend using VDiskIn for hours-long material. I had crashes (at least in the 32-bit version) with just loading hour-long material into (large) buffers. VDiskIn streams the file by loading only a small portion into its buffer.

This would be my next question: Thanks to elgianos help I was able to realize my idea exactly as described. However shrinking an 1 hour long stereo file into a 3 min cutup also introduces some bitreduction-effect. I can’t tell wether it is only downsampling or also bitreduction but i guess it has something to do with

WARNING: The phase argument only offers precision for addressing 2**24 samples (about 6.3 minutes at 44100Hz).

Frankly for the file that I worked on this time based degration fits perfectly but for further use I would like to understand what exactly is happening? Also @RFluff would you mind helping me to insert VDiskIn into the code above? Reading the helpfile I wonder if I simply replace the BufRd object by VDiskIn or is there anything else to mind? I will check by myself tonight/tomorrow when I’m back at my computer but am curious for now…

VDiskIn has no playback position control though… so you won’t have that precision problem, but you also won’t be able to run your process, for which changing playback position is everything. You could try to do super fast, precisely-timed skips though: like, set a playback rate of 1000 for 0.01 seconds to move forward by 10 seconds almost instantly. But there VDisk could give trouble as well, because of a too high playback rate.

I haven’t been working with more than 5 hours of 44.1k (mono) material, but below that I haven’t had problems allocating and reading the buffers. I’m on the 64-bit version though, and 5 hours is more or less everything my RAM can load :slight_smile:

A single precision floating-point number has 24 binary digits of precision, which is multiplied by a power of two to extend the range. 10 is 1.01 x 2^3 (1.01 in binary = 1 1/4, times 8 = 10).

Binary fractions aren’t very intuitive when we’re used to counting in tens, but you can look at the same problem using limited-precision decimal floating-point.

If you have 5 digits of precision, then you can count:

1 = 1.0 * 10^0
2 = 2.0 * 10^0

9 = 9.0 * 10^0
10 = 1.0 * 10^1
11 = 1.1 * 10^1

99999 = 9.9999 * 10^4
100000 = 1.0 * 10^5

… now… what is 100,001?

It doesn’t exist. You need 6 digits of precision to represent this number, but you have only 5.

The next possible number, with five digits of precision, after 100000 is:

100010 = 1.0001 * 10^5

Once you go past the limit of precision, then decimal floating-point starts to count in tens.

That’s what’s happening with the buffer-read phase. Once you go past the 24 bits, then you have access only to every even sample (it’s binary, so it counts in twos).

2 ** 24 = 16777216 samples. 16777216 / 44100 = 380.43573696145 seconds, up to about 6 minutes and 20 seconds.

32-bit processes have a maximum 4GB addressing space. (In non-server versions of Windows, it’s actually 2GB.)

Assuming stereo, 4 GB * 1024 MB/GB * 1024 KB/MB * 1024 B/KB / (44100 samples/sec * 4 bytes/sample * 2 channels) = 4.0 * 1024 * 1024 * 1024 / (44100 * 4 * 2) = 12173.943582766 sec.

12173 / 60 / 60 = 3.38138… hours (but you also have to subtract some memory for the executable code and other data structures)

If you’re on Windows, 2 GB means half that, roughly an hour and a half. If they are very large buffers and scsynth addressing space is fragmented, then there might just not be a big enough block of memory = crash.

hjh

2 Likes

So, what exactly is the correct procedure in order to use longer files?
Interestingly, in SuperCollider the location of the “playhead” and the operation of the read file are intertwined. While every other DAW or software can easily play long files, there is this limit.
So, do you guys split the long files into smaller ones and write a routine where several buffers are used and used one after the other? I guess you could write something use an if statement which doesn’t let go of the synth but tells scsynth to jump to the next buffer / variable?

Sorry one last question: Could you help me understand why the iteration number equals numSkips-1 leads to freeing the server? Ah, it’s more rather the variable that’s assigned to the synth that gets freed, so I gotta change that variable, but still: If my numSkip is set to 500, why does it stop at 499?

because it starts at 0, so the 500th iteration is at number 499.
When i == 500, you would make your start position jump to the end of the buffer (nothing to play there, the buffer is finished).
So it makes sense to free the synth after the 500th iteration has finished, that is when i==499, after the .wait.
By the way, your numSkips.do loop will never reach i==500. The idea is that the do loop executes the function numSkips times, so i will go from 0 to 499. After that, the .do() is finished and it will exit its loop.
So, we could remove the check all together and just free the synth after we exit the loop:

numSkips.do({|i|
    var progress =  i / numSkips;
    var loopStartSample =  progress * b.numFrames;
    var loopEndSample = loopStartSample + 10000;
    // you could set this end value to be precisely the start of next segment:
    // loopEndSample = ( i + 1 ) / numSkips * b.numFrames;
    s.set(\start, loopStartSample, \end, loopEndSample);

    durationWait.wait;
});
// after the loop exits, code will continue to be executed from here
s.free;

tangential recommendation:
don’t use the s variable.

It’s traditionally used to refer to the server, and using it for something else, as has happened here, often leads to confusion (as it has here?).

cheers,
eddi

https://soundcloud.com/all-n4tural/sets/spotlight

https://alln4tural.bandcamp.com

1 Like

I wish I knew as well! I have three options as of now:

  1. Accept degradation of files which are longer than about 6 minutes (or less if they are played at very slow rate). This has been my main :slight_smile:
  2. Split long buffers in shorter buffers
  3. I’m trying to contribute to super-bufrd because I would really like to have a simpler way with no degradation. It looks very very promising IMO

This topic is important to me… but maybe is it a bit OT here? Maybe let’s move it to the other thread :slight_smile:

sigh
Would you give an example of a routine where several 6 min. buffers are sequentially used…? Each buffer get’s it’s own variable and after freeing the buffer, the same sequence is repeated for the following buffer?

Actually I forgot to mention one important thing: the “precision loss” problem happens in this case when we communicate the phase between Phasor and BufRd.
You could use PlayBuf to minimize the problem, since it keeps its phase internally!
So try rewriting your SynthDef using PlayBuf instead of BufRd and Phasor. It would be the simplest workaround

If you need to have start and end point (a loop) then take a look at LoopBuf from sc3-plugins

1 Like

Do you mean, playing the long file continuously (but split into buffers)? If so, elgiano is right that PlayBuf and maintain accuracy for longer (because its internal phase is double precision = 54 bits rather than 24).

Asking because the thread started with the idea of random access (“rhythmically skipping”). That would be a different solution.

hjh

Hey; I imagined a long file being cut into smaller files before moving to SuperCollider.
It takes me some longer to figure stuff out, still working on implementing PlayBuf…

By rhythmically skipping I meant a fixed rhythm of a given rate. Not a random access (agogic skipping?)