Using buffers as audio rate data structures

In Max/MSP, the buffer~ object can be read/written to by peek~/poke~/index~ as an audio-rate data structure, usually in the global or patch scope. Using gen~ in particular, you can treat buffers (and its gen specific data structure ‘data’) as an array that can be used for audio rate control structures. For example, I’ve written scheduling and voice-allocation algorithms with these techniques:

E.g. here is a snippet from a granular synth I made in gen, which runs a for loop at every audio sample, reading and writing the relevant parameters to the voice params within the data array

etc…

In SC, I havent seen much discussion around using buffers as data structures on the server/within SynthDefs. Is it possible? if so what are the limitations around this technique?

Thanks, Asher

You can use BufRd / BufWr to write float data - and this can read an index into that memory as fast as audio rate.

Does that help?

Josh

1 Like

If the Intention is to just read and write data you could use dbufr / dbufwr. Using Bufrd / Bufwr There will always be one Controlblock Delay between Reading and writing, which is undesirable in the Context of Granulation. if you want to Read and Write ugens to a sample accurate circular buffer there is no Option available.

1 Like

If you guarantee that the write occurs before the read, then there is no block delay.

var writer = BufWr.ar(xxx...);
var reader = BufRd.ar(1, bufnum <! writer, ...);

Perhaps that’s a little known trick, but the firstArg operator is a good way to shoehorn a UGen into another UGen’s inputs (ensuring order) without actually using its value.

hjh

5 Likes

It is a great way to access large arrays of data:

a = Array.fill(100, {Array.fill(5, {rrand(0,100)})});

b = Buffer.loadCollection(s, a.flatten, 5);

(
{
	BufRd.ar(5, b, K2A.ar(MouseX.kr(0,99.99).floor.poll)).poll;
	nil
}.play;
)

Sam

2 Likes

This is all great info, going to do write some tests and come back to this thread :slight_smile:

this is very interesting, but both <! and firstArg are undocumented, so I am trying to piece this together here:

From the class files I am able to tell that <! is a shorthand for firstArg, (where x <! y is equivalent to x.firstArg(y), and that in turn gives a BinaryOpFunction.new(x, y) … i.e., composition of functions x and y, presumably y(x), if I haven’t accidentally switched my x’s and y’s…
But then the example seems off:

Here I get confused … not sure how composing the bufnum with the writer helps, so I’m assuming this is a typo; otoh, I’m also not quite sure off the bat how one would compose the read/write functions in that case, maybe you can elaborate?

Maybe this should go on some steadily growing list of documentation-worthy topics.

Thanks!

It is not a typo.

Without the <! writer, there is no relationship between the BufWr and BufRd UGens. As a result, when the SynthDef sorts the UGens into their final order, there is no inherent reason why the writer should come first – their order is arbitrary. There are cases where the writer flips to the end – meaning that old content gets read first, before the new content gets written into the buffer. This is the block delay that dietcv said was unavoidable.

By forcing the writer into the reader’s inputs, now there is a dependency. The inputs must evaluate first: bufnum, writer, <!, other inputs, and only after that can the BufRd be rendered into the SynthDef. This guarantees that new content gets written into the buffer first, before being read, eliminating the block delay.

Btw Jordan has done a lot of work on the SynthDef sorting algorithm (not only sort, but that’s the relevant bit for this question), where one of the requirements was to keep the order of independent branches. With the new algorithm, if BufWr is written first, it will remain first. This change isn’t in SC 3.13 as far as I know, though. After that change, then <! wouldn’t be needed.

hjh

4 Likes

Aah thanks, I’ve been overthinking this. The relevant place in the doc would be <! in “Operators”, and there it’s pretty clear (“return first argument”, i.e. bufnum in this case). Just the usual woes about finding that entry (I must have searched for firstArg instead earlier. I think I still haven’t found the ‘firstArg’ selector explained in the doc, but it’s clear now).

Here’s a concrete example:

(
SynthDef(\test, { |out = 0, modAmt = 0.1, modRate = 100, bufnum|
	var frames = BufFrames.kr(bufnum);
	var phase = Phasor.ar(0, 1, 0, frames);
	var mod = LFDNoise3.kr(modRate) * 0.5 + 0.5;
	var writer = BufWr.ar(WhiteNoise.ar, bufnum, phase);
	var reader = BufRd.ar(1, bufnum,
		phase - (mod * frames * modAmt),
		loop: 1
	);
	Out.ar(out, reader);
}).dumpUGens;
)

[ 0_Control, control, nil ]
[ 1_BufFrames, control, [ 0_Control[3] ] ]
[ 2_Phasor, audio, [ 0, 1, 0, 1_BufFrames, 0.0 ] ]
[ 3_LFDNoise3, control, [ 0_Control[2] ] ]
[ 4_MulAdd, control, [ 3_LFDNoise3, 0.5, 0.5 ] ]
[ 5_*, control, [ 4_MulAdd, 1_BufFrames ] ]
[ 6_*, control, [ 5_*, 0_Control[1] ] ]
[ 7_-, audio, [ 2_Phasor, 6_* ] ]
[ 8_BufRd, audio, [ 0_Control[3], 7_-, 1, 2 ] ]
[ 9_Out, audio, [ 0_Control[0], 8_BufRd[0] ] ]

// after this point, this is not what we want
[ 10_WhiteNoise, audio, [  ] ]
[ 11_BufWr, audio, [ 0_Control[3], 2_Phasor, 1.0, 10_WhiteNoise ] ]

The UGen sort first follows the mod chain all the way down to BufRd, then backtracks and fills in BufWr. BufWr was written earlier than BufRd, but processes later.

With the <! tweak:

(
SynthDef(\test, { |out = 0, modAmt = 0.1, modRate = 100, bufnum|
	var frames = BufFrames.kr(bufnum);
	var phase = Phasor.ar(0, 1, 0, frames);
	var mod = LFDNoise3.kr(modRate) * 0.5 + 0.5;
	var writer = BufWr.ar(WhiteNoise.ar, bufnum, phase);
	var reader = BufRd.ar(1,
		bufnum <! writer,    // <-- here's the currently available fix
		phase - (mod * frames * modAmt),
		loop: 1
	);
	Out.ar(out, reader);
}).dumpUGens;
)

[ 0_Control, control, nil ]
[ 1_BufFrames, control, [ 0_Control[3] ] ]
[ 2_Phasor, audio, [ 0, 1, 0, 1_BufFrames, 0.0 ] ]
[ 3_LFDNoise3, control, [ 0_Control[2] ] ]
[ 4_MulAdd, control, [ 3_LFDNoise3, 0.5, 0.5 ] ]
[ 5_*, control, [ 4_MulAdd, 1_BufFrames ] ]
[ 6_*, control, [ 5_*, 0_Control[1] ] ]
[ 7_-, audio, [ 2_Phasor, 6_* ] ]
[ 8_WhiteNoise, audio, [  ] ]     // now we are writing first, then reading
[ 9_BufWr, audio, [ 0_Control[3], 2_Phasor, 1.0, 8_WhiteNoise ] ]
[ 10_firstArg, audio, [ 0_Control[3], 9_BufWr ] ]
[ 11_BufRd, audio, [ 10_firstArg, 7_-, 1, 2 ] ]
[ 12_Out, audio, [ 0_Control[0], 11_BufRd[0] ] ]

hjh

2 Likes