Rhythmically skipping through a recording, audio probing

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!