Glitches when RecordBuf offset is between 0 and blockSize

Hi all,

I’ve been tinkering with RecordBuf/PlayBuf combinations, and encountered a situation I don’t fully understand. I’m writing to a buffer while simultaneously playing the contents of the buffer to speakers. If RecordBuf’s offset value is >= s.options.blockSize, I get expected results. If the offset is between 0 and the block size, the output is glitchy, and the waveform has the appearance of two interleaved waveforms (screenshot included). If offset is zero (the default), there is a delay equal to the buffer’s duration before sound is heard.

(offset = s.options.blockSize - 7)

I can accept the fact that trying to perform interrelated processes within the same control block are prone to these sorts of issues — perhaps not guaranteed to occur in the correct order. I would like to understand more deeply why these sub-blockSize results occur, if anyone is able and willing to explain.

Thanks,
Eli

s.boot;

z = Buffer.alloc(s, s.sampleRate/4);

(
SynthDef(\rec, { |buf=0, offset=0, out=0|
	var sig, delay;
	sig = BPF.ar(WhiteNoise.ar, 500, 0.01, 4);
	RecordBuf.ar(sig, buf, offset);
	delay = PlayBuf.ar(1, buf, loop:1);
	delay = delay + Impulse.ar(0, mul:0.5);
	Out.ar(out, delay!2);
}).add;
)

//fine:
x = Synth(\rec, [buf:z, offset:s.options.blockSize]);

(
x.free;
z.zero;
)

// > blocksize also fine
x = Synth(\rec, [buf:z, offset:s.options.blockSize * 50.19]);

(
x.free;
z.zero;
)

//(0 < offset < block size) produces glitchy sound
x = Synth(\rec, [buf:z, offset:s.options.blockSize - 7]);

(
x.free;
z.zero;
)

//0 offset produces buffer-length delay before sounding
x = Synth(\rec, [buf:z, offset:0]);

(
x.free;
z.zero;
)

Short answer: The topological sort is moving the PlayBuf earlier in the graph.

dumpUGens is very helpful to see the final structure of the SynthDef. Here, the final order is different from the order you wrote: RecordBuf, written earlier, becomes UGen index 5, after PlayBuf (written later but UGen index 1).

(
SynthDef(\rec, { |buf=0, offset=0, out=0|
	var sig, delay;
	sig = BPF.ar(WhiteNoise.ar, 500, 0.01, 4);
	RecordBuf.ar(sig, buf, offset);
	delay = PlayBuf.ar(1, buf, loop:1);
	delay = delay + Impulse.ar(0, mul:0.5);
	Out.ar(out, delay!2);
}).dumpUGens;
)

[ 0_Control, control, nil ]
[ 1_PlayBuf, audio, [ 0_Control[0], 1.0, 1.0, 0.0, 1, 0 ] ]
[ 2_WhiteNoise, audio, [  ] ]
[ 3_BPF, audio, [ 2_WhiteNoise, 500, 0.01 ] ]
[ 4_*, audio, [ 3_BPF, 4 ] ]
[ 5_RecordBuf, audio, [ 0_Control[0], 0_Control[1], 1.0, 0.0, 1.0, 1.0, 1.0, 0, 4_* ] ]
[ 6_Impulse, audio, [ 0, 0.0 ] ]
[ 7_MulAdd, audio, [ 6_Impulse, 0.5, 1_PlayBuf[0] ] ]
[ 8_Out, audio, [ 0_Control[2], 7_MulAdd, 7_MulAdd ] ]

Why PlayBuf moves earlier, I couldn’t tell you – I haven’t looked in that much detail at the sorting logic.

What I do know is that the only 100% certain way to force an order is to make the desired-earlier UGen an input of the desired-later UGen.

The <! “firstArg” operator may be useful. Below, PlayBuf is now indirectly dependent on the RecordBuf (even though <! simply discards RecordBuf’s nominal output). If that fails, you could also do “a + b - b”.

(
SynthDef(\rec, { |buf=0, offset=0, out=0|
	var sig, delay;
	var rec;
	sig = BPF.ar(WhiteNoise.ar, 500, 0.01, 4);
	rec = RecordBuf.ar(sig, buf, offset);
	delay = PlayBuf.ar(1, buf <! rec, loop:1);
	delay = delay + Impulse.ar(0, mul:0.5);
	Out.ar(out, delay!2);
}).dumpUGens;
)

[ 0_Control, control, nil ]
[ 1_WhiteNoise, audio, [  ] ]
[ 2_BPF, audio, [ 1_WhiteNoise, 500, 0.01 ] ]
[ 3_*, audio, [ 2_BPF, 4 ] ]
[ 4_RecordBuf, audio, [ 0_Control[0], 0_Control[1], 1.0, 0.0, 1.0, 1.0, 1.0, 0, 3_* ] ]
[ 5_firstArg, audio, [ 0_Control[0], 4_RecordBuf ] ]
[ 6_PlayBuf, audio, [ 5_firstArg, 1.0, 1.0, 0.0, 1, 0 ] ]
[ 7_Impulse, audio, [ 0, 0.0 ] ]
[ 8_MulAdd, audio, [ 7_Impulse, 0.5, 6_PlayBuf[0] ] ]
[ 9_Out, audio, [ 0_Control[2], 8_MulAdd, 8_MulAdd ] ]

So here, <! should make the problem go away: RecordBuf is now 4 and PlayBuf is now 6. PlayBuf.ar(1, buf + rec - rec, loop:1) would get basically the same result (with one extra unit).

Why did it interleave? Let’s assume an offset of 10.

  1. PlayBuf, being first, reads 0-63 – all stale data at this point.
  2. RecordBuf writes 10-73 because of the offset.
  3. Next control cycle: PlayBuf reads 64-127 but that includes 10 samples that had been written in the previous control cycle and 54 samples of stale data = interleaving.
  4. RecordBuf writes 74-137 etc.

FWIW you can run into the same problem in Pure Data, where a delay writer may be sorted into the graph after its corresponding delay reader = one-block delay, and it takes a bit of tap dancing involving subpatches to force the writer to come first. Delay writer → delay reader is an implicit dependency, but sorting can really look only at explicit dependencies.

hjh

I think I see it. “Available” UGens are those with no unresolved inputs (“unresolved” meaning the input UGens haven’t been added to the graph yet). These are considered in reverse order so that “early” UGens end up at the tail of the ‘available’ array – and then UGens are pulled from the tail first.

For fun, I had it print out the ‘available’ units at every step.

[ an Impulse, a WhiteNoise, a Control ]
[ an Impulse, a WhiteNoise, a PlayBuf ]
[ an Impulse, a WhiteNoise ]
[ an Impulse, a BPF ]
[ an Impulse, a BinaryOpUGen ]
[ an Impulse, a RecordBuf ]
[ an Impulse ]
[ a MulAdd ]
[ an Out ]

So PlayBuf ends up being earlier because it more quickly exhausts its input UGens (fixed number inputs don’t count). (So the <! works by disallowing PlayBuf to become available until after <!'s inputs have been dropped into the graph.)

hjh

Extremely interesting, as usual — thank you James. Your explanations about the interleaving and UGen sorting make sense. Out of curiosity, what does the asterisk signify in the dumpUGens post?

The <! operator is a cute trick. I remember seeing the firstArg method some time ago, and being totally unable to imagine a situation in which it would be useful. The a + b - b trick is also very clever.

Thanks for the help.
Eli

The BPF has a mul: of 4, which just becomes a simple * operator.

hjh