Order of Execution: In.ar() versus ~bus.asMap

Hi all,

Today I encountered some code that behaved in a way I wasn’t able to explain, related to order of execution of server nodes.

Below, the first chunk of code behaves in a way that seems predictable and normal to me. If the reverb node is after the source node, all is well. If \addAfter is replaced with \addBefore, the nodes are in the wrong order, and signal is not passed to hardware.

In the second chunk of code, I am declaring an audio rate argument named in instead of using In.ar, and I’m using .asMap so that the value of ‘in’ is read from a private audio bus. I had always assumed this is just a different way of doing the same thing. But, interestingly, the second chunk of code works, regardless of node order.

Can anyone explain why node order doesn’t seem to be significant in the second case?

Eli

(
s.newBusAllocators;
~bus = Bus.audio(s, 1);

SynthDef(\sin, {
	var sig = SinOsc.ar(\freq.kr(1000), mul: 0.2);
	sig = sig * Env.perc(0.001, 0.3).ar(0, Impulse.kr(0.5));
	Out.ar(\out.kr(~bus), sig);
}).add;

SynthDef(\rev, {
	var sig = In.ar(\in.kr(~bus), 1);
	sig = FreeVerb.ar(sig, 0.4, 0.999, 0.7);
	Out.ar(\out.kr(0), sig);
}).add;
)

(
s.bind({
	~src = Synth(\sin, [out: ~bus]);
	~fx = Synth(\rev, [in: ~bus, out: 0], ~src, \addAfter);
	// ^^^ change to \addBefore and signal flow fails
});
)

/*------------------------*/

(
s.newBusAllocators;
~bus = Bus.audio(s, 1);

SynthDef(\sin, {
	var sig = SinOsc.ar(\freq.kr(1000), mul: 0.2);
	sig = sig * Env.perc(0.001, 0.3).ar(0, Impulse.kr(0.5));
	Out.ar(\out.kr(~bus), sig);
}).add;

SynthDef(\rev, {
	var sig = FreeVerb.ar(\in.ar(0), 0.4, 0.999, 0.7);
	Out.ar(\out.kr(0), sig);
}).add;
)

(
s.bind({
	~src = Synth(\sin, [out: ~bus]);
	~fx = Synth(\rev, [in: ~bus.asMap, out: 0], ~src, \addAfter);
	// ^^^ change to \addBefore and signal flow is fine
});
)

It is significant, actually, though you don’t notice until you switch the node order.

(
s.newBusAllocators;
~bus = Bus.audio(s, 1);

SynthDef(\sin, {
	var sig = SinOsc.ar(\freq.kr(1000), mul: 0.2);
	sig = sig * Env.perc(0.001, 0.3).ar(0, Impulse.kr(0.5));
	Out.ar(\out.kr(~bus), sig);
}).add;

SynthDef(\rev, {
	var sig = FreeVerb.ar(\in.ar(0), 0.4, 0.999, 0.7);
	Out.ar(\out.kr(0), sig.dup);
}).add;
)

(
s.bind({
	~src = Synth(\sin, [out: ~bus]);
	~fx = Synth(\rev, [in: ~bus.asMap, out: 0], ~src, \addAfter);
	// ^^^ change to \addBefore and signal flow is fine
});
)

// clicky glitchy
(
r = fork {
	loop {
		~src.moveBefore(~fx);
		0.1.wait;
		~src.moveAfter(~fx);
		0.1.wait;
	}
};
)

r.stop; ~src.free; ~fx.free;

When ~src is before ~fx, the AudioControl behaves like In.ar. When ~src is after ~fx, it behaves like InFeedback.ar (and, because the source comes after the effect, the AudioControl can get the audio only after a blockSize delay).

If you need to control the blockSize delay (i.e., if, in a certain use case, it’s very important not to have a block delay between synths), then In.ar will guarantee that, and \in.ar(0) cannot. So I think In.ar is safer in that sense. But in many use cases, it may not matter.

hjh

1 Like

that is a pretty fascinating bit of arcana - I wonder if that is documented somewhere!

InFeedback help touches on it, but it could be organized more tidily. AudioControl and InFeedback (AFAICS) behave the same:

  • New signal on the bus earlier in this cycle: Pass the audio through directly (no block delay), and forget feedback from the previous cycle.
  • No new signal this cycle, but new data from previous cycle: Pass through the previous cycle as feedback (with block delay).
  • No new signal this time or last time: Pass silence.

hjh

This is very helpful, thanks. The glitchiness and control block delay make sense to me. But, I’m still a little confused on how and when audio bus data is zeroed.

I’ve been operating on the assumption that all audio busses are automatically zeroed at the start of every control cycle. I think this assumption originates from a semester of learning Csound and using the zak patch system. In this system, I recall the zak audio busses needed to be manually cleared at the end of every control cycle (otherwise audio from subsequent cycles is mixed and the sound spirals out of control almost immediately).

But it seems like this is not the case in SC, because if it were, InFeedback would not be able to work correctly. But since Out.ar mixes with existing bus content, the audio data from the previous cycle must get zeroed at some point during each control cycle…right? If so, what process is actually responsible for doing so?

But, I’m still a little confused on how and when audio bus data is zeroed.

On the server there are two arrays mAudioBusTouched and mControlBusTouched; for each bus they contain the number of the last DSP cycle where the bus has been written to. This info is used by most I/O Ugens. Some examples:

  • Out.ar: if the mAudioBusTouched entry for the given bus matches the current DSP cycle, it means someone has already written to that bus, so it has to accumulate. Otherwise it can just overwrite the data. Always updates the mAudioBusTouched entry.

  • ReplaceOut.ar: always overwrites the data and updates the mAudioBusTouched entry.

  • In.ar: if the mAudioBusTouched entry for the given bus matches the current DSP cycle, it means someone has already written to that bus, so it can copy the data; otherwise outputs silence.

  • FeedbackIn.ar: takes the difference between the mAudioBusTouched entry and the current DSP cycle and handles one of three cases:

    1. diff == 0 → someone wrote to the bus earlier in this cycle → copy the data
    2. diff == 1 → nobody has yet written to the bus in this cycle, but in the previous cycle → copy the data (feedback)
    3. diff > 1 → nobody has written to the bus neither in this cycle nor in the previous cycle → output silence

(InFeedback and AudioControl now also correctly handle the case where a UGen stops writing to a bus. Previously it would wrongly duplicate the last buffer. See https://github.com/supercollider/supercollider/commit/60b5c93bae16c62cb118afe0f546e25a5bbcba2f.)

As you can see, there is no need to explicitly zero the busses. You can find the relevant code here: https://github.com/supercollider/supercollider/blob/develop/server/plugins/IOUGens.cpp

1 Like

Thanks for the clarification. I wasn’t aware of the mAudioBusTouched container, but all of this makes sense to me as a general concept.

So, then, just a simple question. Suppose we have some signal generator writing to bus 0:

x = { Out.ar(0, PinkNoise.ar(0.1)) }.play;

and then we either free it or press command-period:

x.free;

Does the sample data from the most recent control cycle (right before the synth was freed) still technically exist on bus 0, until some output UGen overwrites it in the future?

Yes, it does! In.ar (and InFeedback.ar) just pretend that it doesn’t :slight_smile:

It might be interesting to compare this with Pd:

[send~] and [receive~] act like a bus with a single writer and multiple readers. [send~] owns the buffer and always overwrites it. [receive~] reads the buffer; depending on the position in the DSP graph, the values may come from this control cycle – or from the previous cycle (just like InFeedback.ar).

[throw~] and [catch~] act like a summing bus with multiple writers and a single reader. Since [catch~] owns the buffer and is the only reader, it can zero the buffer after reading, so that [throw~] can always accumulate.

In SuperCollider, busses can have multiple writers and multiple readers – which may also change dynamically! – that’s why we need the mAudioBusTouched trick.

Thanks again — sorry for stepping away from this thread for a week. Makes sense, and I appreciate you taking the time to reply!