Levels and volume safety

I’ve seen a few variants of a question that’s occasionally asked in the SuperCollider community. Here is my phrasing of it:

When I write SynthDefs in SC, I’ve found that the levels can vary widely. Most of the classic analog oscillators (SinOsc, Saw, Pulse) range from -1 to +1 or -0.5 to +0.5 and aren’t a huge issue, but certain UGens such as Klank or highly resonant BPFs can produce levels that need to be amplified or attenuated by multiple orders of magnitude, like 1000 or 0.001. These amplification factors can be very unwieldy to work with when typing in amplitudes to mix multiple instruments in a piece, not to mention the danger of suddenly loud sounds.

Is there a way to automatically balance or normalize SynthDefs? I’ve tried Normalizer, Compander, and rolling something myself with Amplitude, but these seem to produce artifacts and unsubtle alterations to the envelope of the synths.

It’s an interesting question, and deserves an extensive treatment to answer in full. In this post, I will share some techniques I’ve developed over time to address this issue. But first, and closely related to this, is…

Digression: volume safety when working with SC

Fun fact: “SuperCollider” is an anagram for “Louder Splicer.” SuperCollider is notorious for a few things, and a big one is its tendency to create very loud sounds that can endanger your hearing or your playback equipment. There are two distinct causes of volume dangers in SC, and it’s critical to disentangle them because they entail different solutions.

Failure to clip on macOS: This only affects macOS users on CoreAudio before 3.12. If SuperCollider’s audio API outputs produce a signal outside the range [-1, +1], the audio is not clipped. The nastiest part is that the macOS volume control merely multiplies this loud signal by an attentuation factor, and turning down the system volume does not save you. Even if the slider’s at 1%, SC can produce a signal of amplitude +40 dBFS out of SC and you’ll still get a full-volume signal right in the ears.

The solution to this issue? Upgrade to SuperCollider 3.12. Using any previous version on macOS is an accident waiting to happen! (Note that SC on macOS clips to [-2, +2] for backward compability reasons that I personally disagree with. For consistency with Windows and Linux, you can set this to [-1, +1] by running s.options.safetyClipThreshold = 1 in your startup.scd.)

Excessive dynamic range: This affects all SC users. You’re working on a SynthDef that produces quiet output to the audio API at -40 dBFS. You can’t hear it, so you turn up your machine’s volume until you can hear it at a nice comfortable 60 dB SPL in your ears. Then you modulate RLPF a little too fast and it blows up. Your quiet signal goes loud and clips to 0 dBFS (or, worst of all, doesn’t clip until reaching the DAC). You perceive a sudden jump to 100 dB SPL in your headphones, the comparable to that of a jackhammer one meter away. Ouch.

This is the same idea as the prank used in a certain class of juvenile YouTube videos – start with a quiet sound to coax the listener into turning up the volume to hear it, then blast them with a sudden full-volume signal. It isn’t unique to SC, and affects all synthesis environments with sufficient flexibility and unpredictability. The solutions:

  1. Ironically, you want to work at higher levels in SC, which lowers the headroom. If the SC signal is peaking at -6 dBFS, and you’ve set your headphones to a comfortable level, a synthesis accident only has 6 dB of headroom to blow up in your ears. It can be startling, but unlikely to be truly dangerous (but I’m not an audiologist).
  2. If the signal that SC produces is quiet, don’t automatically reach for your hardware or system volume control. Instead, turn it up in SC.
  3. Make use of the server meter to monitor the volume, and stay in the yellow. This is why you don’t hear much about volume accidents in DAWs – levels are all visualized, encouraging you to turn up the fader on quiet signals or normalize waveforms rather than adjust a downstream volume.
  4. If you’re concerned about clipping, use a Limiter (or SafetyLimiter) on the master bus. If you make GUIs in SuperCollider, consider building a GUI visualization of the signal level and the amount of attenuation of the limiter.

How to balance levels in SynthDefs

Back to the question. Is there a way to automatically balance levels in SC? My answer is “sort of.” In signal processing, this problem is known as automatic gain control or AGC, a device that originated in radio communications.

Transparent AGC doesn’t really exist. The design of AGC always suffers from unavoidable artifacts due to the need to operate in real-time. Neither can SuperCollider predict how loud a SynthDef is without running it.

The good news is that you don’t need automatic mixing to get reasonable levels. It is possible to do relatively painless balancing of SynthDefs in SuperCollider. Just like the volume safety issue, the key is to develop good habits. My recommendations:

  1. Try to type in dB for gain factors. For example, * -30.dbamp instead of * 0.03. I’ve found these much more natural to work with and less prone to typos, especially at 30 dB or more.
  2. Compensate volume early and often. When synthesizing a tone or applying a filter that gets you to a wacky volume, immediately multiply it by the necessary gain to approximately compensate for it.
  3. Use gain factors embedded in SynthDefs rather than in the Patterns/Routines/etc. that invoke the Synths.
  4. Make use of compressors and limiters to tame unpredictable volumes.

An example would help here. Let’s start with an LFO-modulated pulse wave:

(
SynthDef(\alien, {
    var snd;
    snd = Pulse.ar(LFNoise2.kr(2).exprange(100, 800), LFNoise2.kr(3).range(0.3, 0.7));
    snd = snd ! 2;
    snd = snd * \amp.kr(-6.dbamp);
    Out.ar(\out.kr(0), snd);
}).play;
)

Now let’s add a modulated comb filter:

(
SynthDef(\alien, {
    var snd;
    snd = Pulse.ar(LFNoise2.kr(2).exprange(100, 800), LFNoise2.kr(3).range(0.3, 0.7));
    snd = CombC.ar(LeakDC.ar(snd), 0.01, LFNoise2.kr(3).range(0, 0.01), 1);
    snd = snd ! 2;
    snd = snd * \amp.kr(-6.dbamp);
    Out.ar(\out.kr(0), snd);
}).play;
)

The signal seems to get a good amount louder. Let’s crank the comb filter down a bit:

(
SynthDef(\alien, {
    var snd;
    snd = Pulse.ar(LFNoise2.kr(2).exprange(100, 800), LFNoise2.kr(3).range(0.3, 0.7));
    snd = CombC.ar(LeakDC.ar(snd), 0.01, LFNoise2.kr(3).range(0, 0.01), 1) * -10.dbamp;
    snd = snd ! 2;
    snd = snd * \amp.kr(-6.dbamp);
    Out.ar(\out.kr(0), snd);
}).play;
)

I know this clips a bit, but it’s fine. In practice, I’d have this running through a master limiter anyway. Now let’s run this through two parallel BPFs and modulate crossfading between them. This makes the signal significantly quieter, so I’m multiplying by 10 dB to compensate:

(
SynthDef(\alien, {
    var snd;
    snd = Pulse.ar(LFNoise2.kr(2).exprange(100, 800), LFNoise2.kr(3).range(0.3, 0.7));
    snd = CombC.ar(LeakDC.ar(snd), 0.01, LFNoise2.kr(3).range(0, 0.01), 1) * -10.dbamp;
    snd = BPF.ar(snd, 460, 0.4).blend(BPF.ar(snd, 260, 0.3), LFNoise2.kr(4).range(0, 1)) * 10.dbamp;
    snd = snd ! 2;
    snd = snd * \amp.kr(-6.dbamp);
    Out.ar(\out.kr(0), snd);
}).play;
)

Let’s put this through four series pitch shifters with really strong time and pitch dispersion. The granulation makes things pretty quiet, so I had to boost it a bit:

(
SynthDef(\alien, {
    var snd;
    snd = Pulse.ar(LFNoise2.kr(2).exprange(100, 800), LFNoise2.kr(3).range(0.3, 0.7));
    snd = CombC.ar(LeakDC.ar(snd), 0.01, LFNoise2.kr(4).range(0, 0.01), 1) * -20.dbamp;
    snd = BPF.ar(snd, 360, 0.4).blend(BPF.ar(snd, 460, 0.3), LFNoise2.kr(4).range(0, 1)) * 10.dbamp;
    4.do {
        snd = PitchShift.ar(snd, Rand(0.05, 0.3), ExpRand(1, 1.2), 1e-2, 1e-2);
    };
    snd = snd * 15.dbamp;
    snd = snd * \amp.kr(0.5);
    snd = snd ! 2;
    Out.ar(\out.kr(0), snd);
}).play;
)

Alright, not winning any sound design awards here, but you get the idea. In practice, I don’t multiply by a dB value on every single line. I might add these compensating factors in only every few lines, or at the SynthDef level. In any case, the habit worth developing is to deal with volume issues as soon as they come up, rather than making a SynthDef that requires a 60 dB boost or cut to fit into a mix. Your SynthDefs don’t need to be all perfectly balanced out of the box, just balanced enough to avoid absurd and error-prone amplification factors when incorporating into a sequence or pattern.

The technique of fixing levels early and often also helps somewhat with the safety issues mentioned above. By paying attention to levels while synthesizing, you’ll probably notice high dynamic range issues and compensate for them at the source, rather than letting them lie in wait for a future accident. .dbamp is also far, far less typo-prone – it’s much easier to visually distinguish -60.dbamp from -80.dbamp than 0.001 from 0.0001.

Conclusions

Thanks for sticking through and reading this! I hope it gave you some ideas to protect your ears and speakers, and use levels effectively in SC.

Let me know what you thought of this article. Do you have additional tips and advice in this direction? Did I get anything wrong?

15 Likes

Excellent and thorough. I absolutely second the advice to use compressors more often – when used judiciously, they don’t destroy dynamic range, but they do help layers to blend better.

I would only add that I’ve found it useful to include safety checks for each channel, not only at the end of the chain. Some years ago, I was performing and one of the processes spat out a roughly +700 dB signal (amplitude 1e+35) – only for a moment – but it entered a long ambient-style reverb, where even a 3 second 60dB decay time would take at least 30 seconds to get rid of the deafening noisy distortion. Recompile class lib, reload the set and try to continue, with the audience wondering what happened.

So after that, I put a CheckBadValues into my MixerChannel synths, and added a level check on top of that. When a bad value or out of range signal is detected, it immediately replaces the signal with DC.ar(0) – not multiplying, because nan * 0 = nan – I use Select instead. in is the signal; clip is a control for large amplitudes to suppress.

bad = CheckBadValues.ar(in, post: 0) + (8 * (in.abs > clip));
SendReply.ar(bad, '/mixerChBadValue', [bad, in].flat, 0);
badEG = EnvGen.ar(Env(#[1, 0, 1], #[0, 0.05], releaseNode: 1), bad);
in = Select.ar(bad > 0, [in * badEG, DC.ar(0)]);

(The out-of-range logic is a bit dodgy, but I think I can replace it with Nathan’s new limiter.)

Fast forward to summer 2010 2020, playing in a different venue. One of my instruments stopped making sound. A little investigation (while onstage!) found that a NaN had entered a delay line. But, because MixerChannel was suppressing the bad value, it didn’t escape into any reverbs or mixdown channels where it would have silenced everything. So I could simply delete and re-create that instrument, and no one else knew anything had gone wrong.

hjh

Your story, on the edge of our seats here, unfolds more fluidly if that’s 2020, yes?

Cheers, especially to your tireless efforts & MixerChannel,
eddi
https://alln4tural.bandcamp.com

Yes, it was 2020… no idea why I didn’t spot that before posting.

hjh

Thanks for taking the time to write this, it’s a great example of how quickly gain related problems accumulate.

I find getting a good healthy rms signal one of the more slippery aspects of working with SC. And I often just tap out and pipe it into a DAW to manage it there, which is not a good solution in a lot of cases. This is also partly from habit. after having a couple of serious accidents on the older mac version mentioned, I started using the DAW with a saftey limiter there.

I’ll try to get into the habit of choosing dbamp over floats. It’s also nice and simple how you’ve just rebalanced near the end of the signal path. I often find myself falling into the trap of tweaking values everywhere - EnvGen level, mul params, multiplying all over the place haha.

I’m now wondering about measuring crest factor in SC and a hard clipper would probably be handy too. Will do some research as I’m sure someone would’ve looked into it already.

Interesting also re AGC, I always thought of it as a fairly transparent process for some reason. but that of course makes sense when you consider the oversampling required.

Excellent resource, really instructive! Thanks a lot!