FreeVerb outputting denormals?

Running SC 3.13 on Apple Silicon, I’m experiencing the following behaviour: after sending a signal into FreeVerb, then waiting around 30 seconds, I get a stream of denormals until I send another signal into the Bus or free the synth.

b = Bus.audio(s,2);

SynthDef(\verb,{
	var sig = In.ar(\in.kr(),2);
	sig = FreeVerb.ar(sig,1,0.9,0.99);
	Out.ar(\out.kr(),sig)
}).add;

x = Synth(\verb,[\in, b]);

(
p = Pbind(
	\dur,0.1,
	\legato,Pseq([0.2],20),
	\out,b
).play
)

// now wait 30 seconds or so...

...
CheckBadValues: normal found in Synth 1044, ID 0 (previous 355 values were denormal)
CheckBadValues: denormal found in Synth 1044, ID 0 (previous 1 values were normal)
CheckBadValues: normal found in Synth 1044, ID 0 (previous 92 values were denormal)
CheckBadValues: denormal found in Synth 1044, ID 0 (previous 1 values were normal)
...

In this example Synth 1044 was a synth from the SafetyNet quark; after using CheckBadValues before and after FreeVerb it seems to be the source of the junk. I’m going to posit this is a bug and not a feature? :smirk: can anyone else reproduce this behaviour?

Have you tried adding a very small noise signal to the input of the reverb? Something like PinkNoise.ar(0.0000001) or the like? Reverbs get nervous with “pure silence”.

2 Likes

Relatable…

That seemed to do the trick - weird that I’ve never experienced that before…encountered this behaviour with code that I’ve been running for several years now! But thanks for the fix! :slight_smile:

Can you explain this a bit more? Is it something to be aware when making our own reverb/delay effects? Is it something to do with feedback?

Thanks,
Paul

1 Like

Silent.ar should also do the trick…

Pure Silence isn’t the exact problem. It is Pure Silence AFTER something has already been fed into the system, and you start getting the tail levels of a reverb as they approach zero. Theoretically - a reverb should never reach zero ONCE a signal has been fed into it. So - silence after an input is where we get the denormals. Zero in to a reverb is fine as long as it stays zeroes. But then - that’s a worthless reverb :smiley:

  • Josh

I don’t think so… the point, to avoid denormals, is to add a very small non-zero signal. Silent.ar renders as DC.ar(0) – therefore sig + Silent.ar(1) will be equivalent to sig and the reverb result would be unchanged, denormals and all.

In any case… FreeVerb seems not to do anything about denormals.

At the server level, there’s sc_SetDenormalFlags() (“// Set denormal FTZ [‘flush-to-zero’] mode on CPUs that need/support it”). On my machine (Intel CPU), I don’t get denormals – so this is definitely working in some/many environments. Perhaps Apple Silicon needs different flags…?

hjh

Bumping this thread as I’ve noticed more Ugens spitting out denormals…This snippet spits out a bunch of denormals upon instantiation and then settles down after a second or so:

(
{
    SinOsc.ar(200).dup.cubed.cubed * 0.1;
}.play;
)

Removing the second .cubed method seems to fix it… which would suggest some more close-to-zero numbers being the issue, wouldn’t it? Perhaps someone with better knowledge of the server can help me out here?

I don’t get an denormals from:

(
{
    SinOsc.ar(200).dup.cubed.cubed * 0.1;
}.play;
)

But I get similar problems with JVerb.

(
	// Ndef with the JPverb
	Ndef(\rev, { arg
	t60 = 20,
	damp = 0.2,
	size = 1,
	earlyDiff = 0.707,
	modDepth = 100,
	modFreq = 1,
	low = 1,
	mid = 1,
	high = 1;

	JPverb.ar(SoundIn.ar([11]), t60, damp, size, earlyDiff, modDepth, modFreq, low, mid, high)}).play(vol: 1, out: 12);
)

I understand the solution is to add a little noise… but why does SC reverb get nervous from a little silence? Even an audacity reverb would not be that nervous… :slight_smile:

Anyways, also got recommended Sanitize.ar. Have you tried this Mike?

I checked it now, I still get denormals from the .dup.cubed.cubed! Maybe I should actually file a bug report…

I haven’t used Sanitize.ar, but I often use CheckBadValues.ar with SelectX.ar to replace nans, nils, infs…this way I get a printout when bad things happen and I can try to fix them. But Sanitize looks really good! :slight_smile:

I believe my comment from 2.5 years ago still holds.

When scsynth boots, it calls a function sc_SetDenormalFlags(). I’m not an expert on the details here, but it looks to me like various processors can set a mode per process (or per thread?) where the FPU will automatically replace denormals with zeros.

If that setting is taking effect, there would be no need for SC code to handle denormals explicitly, as the FPU would block denormals before they reach any user code.

And I still confirm that on my Intel CPU, I can’t reproduce the denormals that others are seeing. I think this is because this bit…

#if BOOST_HW_SIMD_X86 >= BOOST_HW_SIMD_X86_SSE_VERSION
    _MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);
    _mm_setcsr(_mm_getcsr() | 0x40); // DAZ

… is working in Intel-land.

The only other part of this function is an #elif defined(__VFP_FP__), which a comment identifies as being for “the Cortex A8, along with the SIMD Neon unit” = basically, Bela. This was added 4 years ago.

The Intel bits of this function are 13 years old.

Missing from the function is any mention of Apple Silicon chips. So it’s quite possible that we just aren’t doing anything about denormals on recent Macs (unless the SIMD directive applies to Silicon chips too…? I’m well outside my expertise here but I’d guess that the X86 in the identifier rules that out). Hence, I think at minimum this function needs to be reviewed for applicability to Silicon chips.

Edit: This may be of interest – How to flush denormal numbers to zero for apple silicon? – but the page isn’t displaying well on my phone, so I can’t guess how good the info is.

hjh

For reference and completeness, the most common strategies are:

1 FTZ/DAZ

A simple flag to the C compiler will allow you to enable or disable flushing denormal floating-point numbers from your program. For example, with gcc and clang, the compiler flag allows this feature.

You can test compiling this program with and without the flag:

#include <float.h>
#include <pmmintrin.h> // _MM_SET_DENORMALS_ZERO_MODE
#include <stdio.h>
#include <xmmintrin.h> //  _MM_SET_FLUSH_ZERO_MODE

int main() {
  // Enable
  _MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);
  _MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_ON);

  double tiny = DBL_MIN / 2.0; // denormalized floating-point number
  double result = tiny + 0.0;  // force runtime operation

  printf("Denormalized: %e\n", result);

  // Disable
  _MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_OFF);
  _MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_OFF);

  result = tiny + 0.0;
  printf("With denormals enabled: %e\n", result);

  return 0;
}

2 Branchless checks

There are other alternatives, this one is particularly ingenious, and great for dsp because it is branchless:

#include <limits>
#include <cmath>

state *= fabs(state) > std::numeric_limits<float>::min();

Why is the branchless alternative particularly well-suited for real-time audio code?

Well, technical reasons:

a) The code becomes more predictable, and CPUs can struggle with unpredictable branches
b) No conditional jumps
c) SIMD-friendly: Can be vectorized more easily in C++
d) Consistent timing

Consequence: when operating with many filter states to check, you will prefer to avoid branches in your inner loop

3 SIMD-specific situations,

In this case, vector operations already have built-in denormal handling.

NOTE Sometimes a program can choose different alternatives.

Since each situation is different, if you are serious about it and optimization, you may see yourself using more than one option:

a) FTZ/DAZ is fast (the first alternative), but affects the entire thread, not just your dsp function (it may not be the best option)
b) DC offset is super simple, but remember that it will add coloration to your filter (listen if you like it or not)
c) Noise injection: this is not bad for filters, can be used, and even makes it sound a bit better hehe
d) State clamping has precise control, but with lots of conditional checks (opposite of the branchless alternative)
e) There are also a few algorithms, which may bring some benefits, but will also require maintenance compared to the other options

EDIT: The fact that we are still discussing this in 2025 means it will likely remain a topic for some time.

We can summarize the points made here. This problem is:

  • Architecture-dependent
  • Algorithm-dependent (reverbs with high feedback are particularly vulnerable)
  • Time-dependent (takes time for decay to reach denormal range)
  • Still a pain in the ass

I have a particular preference for noise insertion.

Why? Well, it

  • Decorrelate stereo channels slightly
  • Mask quantization artifacts
  • Prevent DC buildup

I imagine that those are positive qualities in audio dsp filters

2 Likes

@jamshark70 You’re correct - sc_SetDenormalFlags() is missing Apple Silicon support.

Your link is good. I also checked others. While Apple provides FE_DFL_DISABLE_DENORMS_ENV in their fenv.h, this constant is Apple-specific and doesn’t exist on Linux ARM64 or other platforms. So, I believe it would not be the ideal solution.

Minimal patch:

@@ -159,6 +159,18 @@ void sc_SetDenormalFlags() {
 #if BOOST_HW_SIMD_X86 >= BOOST_HW_SIMD_X86_SSE_VERSION
     _MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);
     _mm_setcsr(_mm_getcsr() | 0x40); // DAZ
+
+#elif defined(__aarch64__) || defined(__arm64__)
+    // ARM64: Set FPCR FZ (bit 24) and FZ16 (bit 19)
+    uint64_t fpcr;
+    __asm__ __volatile__(
+        "mrs %0, fpcr           \n\t"
+        "orr %0, %0, %1         \n\t"
+        "msr fpcr, %0           \n\t"
+        : "=r"(fpcr)
+        : "r"((1ULL << 24) | (1ULL << 19))
+    );
+
 #elif defined(__VFP_FP__)
     // the Cortex A8, along with the SIMD Neon unit.

Would appreciate if anyone with an M1/M2/M3 Mac could test this

References: