(not) using OffsetOut

Recently, I've been working a bit on doing granular synthesis with tidal. Because I needed to add envelopes as effects, I noticed a subtle superdirt bug that I have introduced. I'll use this post to explain what it is.

The bad news: don't use OffsetOut in your synthdefs any more (I'll fix the ones in the repository).

The good news: using just normal Out will lead to a sample-accurately scheduled sound, because internally superdirt compensates the difference.

why?

In SuperCollider, every new synth is started on a block boundary. What is a block? For efficiency reasons, audio calculation is scheduled in blocks, each block is calculated at once. By default, this is 64 sample frames, so about 14 ms at 44100 Hz. This is also the interpolation time of a control rate ugen when it feeds into an audio rate ugen:

// This will nicely ramp the pulse wave so there are no clicks
SinOsc.ar(LFPulse.kr(0.1) * 400 + 700)

As mentioned, the block boundaries are also the moments in time where synths are started. This means that irrespective of the precise timestamp at which it is scheduled, there will be a little bit of offset to the block boundary. I therefore comes a little early. Usually this is just fine.

Sometimes, however, we want sample accurate starting points, in particular in granular synthesis and microsound, where the sound wave is shaped by the precise onsets. In order to compensate the difference, there is the OffsetOut ugen, which uses the information from the timestamp and delays the audio a bit. SuperDirt uses OffsetOut internally (in the dirt_gate synth defined in core-synths.scd) to route the audio and add an envelope to it.

Now there is one case where this breaks, and unfortunately this has happened a lot so far, though probably mostly unnoticed. If you play a synth that already compensates for the delay by using a OffsetOut, the dirt_gate synth adds another envelope on top of it. This is fine, because the synth has the same total duration (sustain), so the envelope is just for cutting off any effects that have been added on top of the synth (e.g. with # bandf).

But: the synth's own envelope is delayed by the OffsetOut, so the envelope that is added will cut short the inner envelope. Also it delays the sound a second time, which is also wrong. Unfortunately, there is no simple enough way to know if the audio is already delayed or not.

why does it matter?

This may result in clicks in the audio, that some have reported already.

how to fix it?

You can get rid of it by replacing OffsetOut with a simple Out UGen.

// gabor grain
(
SynthDef(\gabor, { |out, sustain = 0.03, freq = 440, pan = 0, width = 0.3|
	var env, sound;
	sound = SinOsc.ar(freq, 0.5pi);
	env = LFGauss.ar(sustain, width, loop: 0).range(0, 1);
	// this uses an Out UGen now, not an OffsetOut
	Out.ar(out,
		DirtPan.ar(env * sound, ~dirt.numChannels, pan)
	)
}).add;
);
10 Likes

Interesting thanks! I've had similar problems on the Tidal side, caused by processing events in blocks. In Tidal, an event can now change the cps (e.g. d1 $ sound "bd*8" # cps (range 1 1.5 rand)). The problem is that Tidal processes 1/20th of a second's worth of events at a time. If one of those events then slows the cps, the events will overlap into the next block, when they will get repeated. If one of the events speeds up the cps, then there will be a gap and events will get missed. The first problem was easy to fix by dropping events, the latter is a bit more annoying to fix and is still a bug..

The proper solution would be giving up on digital computers, and going back to analogue ones, which can process events continuously instead of having to separate them into blocks.

3 Likes

Maybe yes. On the other hand, think of the work to replace the yardsticks every day and refill the sand in the clocks every five seconds. And all the noise!

2 Likes