Rhythmically skipping through a recording, audio probing

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?)

Check PlayBuf’s Help File. In case you don’t know it, you can open an Help page (Documentation) for every class in SuperCollider by putting your cursor on it and pressing Ctrl+D (or Cmd+D on a mac).
It explains all the arguments PlayBuf takes and also gives some code examples you can execute right away :slight_smile:

The only catch about PlayBuf compared to BufRd is that when you change its start position you need to retrigger it. It won’t jump to the new position by itself. In your case you can do it easily like this: automatically trigger PlayBuf when the start position changes.

SynthDef(\player){|out=0, bufNum=0, start=0|
    Out.ar(out,
        PlayBuf.ar(1, bufNum, 
            rate: BufRateScale.ir(bufNum), 
            trigger: Changed.kr(start), 
            start: start, 
            loop:0
        )
    )
}.add;

x = Synth(\player,[bufnum:b]);
// now when you set a new start, PlayBuf will jump to it (popping)
x.set(\start, 44100);
x.set(\start, 44100 * 2);
x.set(\start, 0);
// ...

And PlayBuf doesn’t take an end argument. We are not anymore controlling a “playhead” directly, there is no Phasor (since the precise phase is handled internally by PlayBuf), but we can still make it jump to a new position by changing the start parameter and sending a trigger to its ‘trigger’ parameter (which updates the internal phase to the new start).
So, you won’t get those small loops anymore. Instead, the player will just continue playing. (Want the loops again? One way with PlayBuf is to “retrigger” it rhytmically with an Impulse.kr(1/loopDur): trig: Changed.kr(start) + Impulse.kr(1/loopDur) )

The reason why I’m not sending you yet some code for loading a soundfile into separate shorter buffer is that loading them is quite straightforward, but then we would have to find a clever system to handle the situation when a “skip” happens across the boundaries between two different buffers. I can’t find a way to do it without adding a lot of complexity to your code, which can be avoided by using PlayBuf with only one “long” buffer.

I would avoid that by loading buffers per played segment, rather than a fixed arrangement. Can’t post an example now – maybe tomorrow.

hjh

Here’s a way to do “rhythmically skipping” through a file, without a large buffer and without phase precision issues.

At any time, you need three buffers:

  • One of them is playing.
  • Another may have just released (still fading out) – so it needs to remain stable.
  • So the third one can be reading from disk.

These should be allocated with the longest duration of a segment you plan to play – not the entire sound file.

The Prout is the key. It “rotates” the buffers into their new roles for the next event to play, loads a segment for the following event, and then yields the buffer that should play this time.

Buffer.read (at least in 3.11) passes the starting frame as an integer, so you get at least 31-bit precision (I forget if it’s treated as signed or unsigned – if signed, then you lose the sign bit). That’s the 6.xyz minutes we were talking about before * (2 ** 7) = over 768 minutes = over 12 hours, that you can access with sample accuracy.

This example, again, isn’t playing the sound file continuously. For that, I think elgiano’s suggestion is easiest (PlayBuf). (The thing that would be tricky about playing segmented buffers continuously is that language-server communication is not sample accurate. So the “hand-off” from one buffer to the next would have to be triggered within SynthDefs – it wouldn’t be completely smooth to do it with a pattern. It’s certainly possible, but a bit more than I have bandwidth to try right now.)

EDIT: This example is doing random access. To repeat the same segment rhythmically, it’s easier: no need to load a buffer on every iteration.

hjh

~path = "/media/dlm/7D53-8744/Docs/ardour/19-0808-quietcode-01/19-0808-quietcode-01-edit.wav";
f = SoundFile.openRead(~path);
~frames = f.numFrames;
~channels = f.numChannels;
f.close;

s.boot;

b = Array.fill(3, { Buffer.alloc(s, 88200, ~channels) });

(
SynthDef(\segment, { |out, bufnum, rate = 1, time = 1, amp = 0.1|
	var eg = EnvGen.kr(Env.linen(0.01, time, 0.01), doneAction: 2),
	sig = PlayBuf.ar(~channels, bufnum,
		rate * BufRateScale.kr(bufnum));
	Out.ar(out, sig * (eg * amp));
}).add;
)

(
p = Pbind(
	\instrument, \segment,
	\startFrame, Pwhite(0, ~frames - 100000, inf),
	\bufnum, Prout { |ev|
		var playing, releasing, loading, temp;
		#playing, releasing, loading = b;
		
		// first time through the loop, haven't loaded anything
		// load something, and rest (because 'playing' isn't ready yet)
		loading.read(~path, ev[\startFrame]);
		ev = Rest(playing).yield;
		
		loop {
			// playing is now the *previous* buffer that was playing
			// playing --> releasing etc.
			temp = playing;
			playing = loading;  // was loading, should play next
			loading = releasing;  // released last time, should load next
			releasing = temp;
			
			loading.read(~path, ev[\startFrame]);  // to be ready next time
			
			ev = playing.yield;  // use this buffer as 'bufnum'
		}
	},
	\dur, Pwhite(1, 4, inf) * 0.25,  // rhythmically
	\time, Pfunc { |ev| ev[\dur] / thisThread.clock.tempo },
	\amp, 1
).trace.play;
)

p.stop;
1 Like

Good day to you!

Thanks a lot for your effort.
While your code is working (that is without the need for me to readjust many parameters I don’t unterstand :slight_smile: ) it seems to be skipping in a more random way than I intended: To be clear: By rhythmically I ment; moving sequentially, with a fixed rhythm, through a file from start to finish. Like cutting a loaf of bread and throw away most of it to shrink it to 10 % of it’s size (for whatever reason you should do this).

I notice that in the Pbind object both the \startFrame and the \dur both hold the Pwhite object, which generates random values. Also observing the ‘startFrame’ value in the post windows indicates this.
However trying to embed something like phasor instead of the Pwhite for the start point just generates a loop at the same position: Maybe each buffer is starting anew again without ever advancing? Is there a more linear movement implementable in your code?

Two side-questions: b = Array.fill(3, { Buffer.alloc(s, 88200, ~channels) });
Is there a certain reason why you chose 88200? Do you suppose a sampling rate of 44100 and a stereo file, or do you deliberately oversample the buffer?

And, in general: Is there a way to reveal which argument is used? SuperCollider can skip through the arguments with tab and can skip arguments, so how can I know which argument listed in a helpfile is used in a code?

Chris

You can replace the Pwhite for startFrame with an arithmetic series (Pseries), then – just advance the number of frames corresponding to the time you want to advance. (And, change \dur to a fixed value as well.)

Nope, Phasor is server side. Patterns are language side. You need a pattern that increases by a step size every time: Pseries.

The maximum duration of a segment, plus some extra. The pattern as I wrote it plays at most 1 second in a segment, and I added a totally arbitrary 1 second to that.

The size is completely up to you. Choose the size that fits your needs.

Assume they are in the exact sequence given in the method definition, unless you see a keyword like argname: in the list. A keyword argument is the only way to skip over arguments.

There should be a keyboard shortcut to reopen argument list auto-completion. It’s different on different platforms. Check the full list of keyboard shortcuts in Preferences.

hjh

So I could also write something like s.sampleRate * 2 to define lenghts in SuperCollider, right?
Even though it’s obvious, thinking in samples (hertz) is still unfamiliar.

Also, @jamshark70 how can I insert a way to stop the loop once it reaches the “end”: In your code the loop repeates it’s last buffer steps over and over again while the startFrame in the post windows keeps adding up. I thought about making an if statement when a certain frame is reached by reading the numFrames of the recordin loaded to the buffer? Do you think this is a fruitful idea? I’ll try to implement it tomorrow.

Reminds me of those high school physics classes about converting units. If you want to think in seconds, but Buffer.alloc expects samples, then the conversion factor is samples/seconds (which happens to be sample rate). 2 seconds * 44100 samples/seconds, seconds cancel out leaving 2*44100 samples. That’s a general principle, applicable to all conversions.

how can I insert a way to stop the loop once it reaches the “end”

I think that would be the job of the \startFrame pattern. If you want 10 slices, just Pseries(0, mySoundFile.numFrames / 10, 10) – Pseries runs out of values after 10, so Pbind runs out of values too, and stops.

hjh

1 Like

Ah well, obiously the length argument is the way to go: Sorry, that was too easy!