output_guard.h
The output guard: a block with NaN or infinity in it leaves as silence.
#include <sonore/output_guard.h>- Guides: Getting started, Parameters and state, Presets
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.