Search guides and the APICtrl K
  1. API reference
  2. Plugin model

output_guard.h

The output guard: a block with NaN or infinity in it leaves as silence.

#include <sonore/output_guard.h>

Design notes

Why

A DSP bug -- a filter driven unstable, a division by zero, a logarithm of zero -- makes NaN or infinity, and both are sticky: every recursive filter, smoother and feedback path the value reaches keeps it for good. A host passes it on, and what happens next is the host's business and never good. A device converting it to an integer plays a full-scale burst or a click; a mix bus sums it into every other track, so one plugin takes the whole session with it -- the limiter on the master included, which is the plugin a user reaches for when something sounds wrong.

So every format checks the block it is about to hand back, on every output port the host sees. A block with one non-finite sample anywhere becomes silence on EVERY port: a channel with a NaN in it holds nothing worth keeping, and half a stereo image is a louder surprise than none. The DSP is not reset. Its state is the plugin's own, a reset is the plugin's decision to take, and a filter that recovers plays its next finite block as normal. The first time, the wrapper asks for the main thread and says so there, once -- in the host's log where the host has one, on stderr otherwise -- because a silent fix for a bug nobody hears about is a bug that ships.

The check reads the bits

The obvious test accumulates x * 0 over the block -- 0 for every finite x, NaN for a NaN and for either infinity -- and asks whether the sum is NaN. Under -ffast-math or /fp:fast that is no test at all: the compiler may fold x * 0 to 0 and s != s to false, and the guard compiles to nothing in exactly the builds an audio developer is most likely to ship. MSVC's /fp:fast does exactly that: the arithmetic form, fed a NaN and then an infinity, called both blocks clean. A float is non-finite exactly when its exponent bits are all ones, which is (bits & 0x7fffffff) > 0x7f7fffff, and no floating-point flag rewrites an integer compare. It is cheaper as well: an and, a compare and an or per four samples, single-cycle integer operations, no branch until the block ends. The arithmetic form is a chain of dependent float adds that a compiler may not reorder without fast-math, so it does not vectorise: 355 ns for 512 samples where this takes 38.

What it costs

Measured on the machine this was written on (Ryzen 7 4700U, MSVC /O2, SSE2; stereo, the check alone against the example's DSP alone and against the CLAP wrapper's whole process() call, the guard included):

frames   guard (64-bit)    trim DSP / process()    saturator DSP / process()
    32    12 ns  (19 ns)     153 ns /   495 ns        4.0 us /  4.3 us
    64    16 ns  (29 ns)     303 ns /   612 ns        9.2 us /  9.9 us
   512    83 ns (177 ns)    2.55 us /  3.14 us       55.4 us / 59.7 us

2.4-2.7% of process() for trim, a smoothed gain and about the least a plugin can do; 0.14-0.31% for the saturator, an oversampled waveshaper. A plugin that wants to see its own NaN -- a test rig, an analyser of broken signals -- sets PluginDescriptor::guardOutput = false and no process path runs the check.

Nothing public is declared here: the header supports the others in its area, and its notes above say how.

↑↓ to move↵ to openEvery public name in the headers, and every guide