Docs/Architecture/EffectsDesign.md
User-facing documentation (how to apply effects, build chains, write your own,
the full catalogue) lives in Docs/AudioEffects.md. This
document records the architectural decisions behind the NAudio.Effects
framework and the NAudio 2 → 3 delta — what got built, what got retired,
and what is deliberately deferred.
A high-quality, pure-C#, cross-platform effects suite as a first-class part of NAudio 3, serving two audiences with one toolkit:
The effects also have to be building blocks for the future NAudio sampler and synthesiser, so the DSP kernels must be usable standalone (process your own buffer) — not only as pull-model stream wrappers.
IAudioEffect,
configured with a WaveFormat, processing interleaved Span<float> in
place. No source reference. This is the layer the future synth/sampler talks
to.EffectSampleProvider : ISampleProvider hosts an IAudioEffect; an EffectChain runs an ordered
list. The kernels stay reusable; existing ISampleProvider pipelines keep
working unchanged.IAudioEffect contractpublic interface IAudioEffect
{
void Configure(WaveFormat format); // sample rate + channel count
void Process(Span<float> buffer); // in-place, interleaved
void Reset(); // clear delay lines / state
int LatencySamples { get; } // 0 for most; non-zero for look-ahead
} // limiter, convolution reverb, etc.
Configure(WaveFormat) rather than baking rate/channels into the
constructor — effects are reusable across format changes and inside chains.Span<float> matches ISampleProvider. Effects deinterleave
internally only if they must (e.g. spectral processing).Reset() is mandatory so any effect can be reused after a seek.LatencySamples is reported so a host can do delay compensation.A thin abstract AudioEffect base wraps Process to provide click-free
Bypass and dry/wet Mix for free — concrete effects only override
ProcessBlock.
A live effect is mutated on exactly one thread: the audio thread, at a block boundary.
IParameterized companion interface exposes
IReadOnlyList<EffectParameter> (Continuous / Toggle / Choice / read-only
Meter). It is additive — IAudioEffect itself is unchanged, and the
parameter list is built from the effect's normal typed properties via a
small helper.EffectParameter.Value setters post to a lock-free
single-producer/single-consumer ParameterDispatchQueue instead of running
inline. With no sink (offline render, chain hosted on a single thread, unit
tests) edits apply inline exactly as before.Drains the queue once at the top of each block, before
DSP. Coefficient recomputation, delay-buffer resizes and the
CrossfadingBiQuadFilter swap therefore all run where there is no
concurrent reader.Bypass/Mix stay direct writes — they are single bool/float fields
(atomic per ECMA, no torn read) that the base class already ramps click-free.Same model later drives a VST3-host generic UI, presets, serialisation, and automation.
PR #1259 made BiQuadFilter.SetCoefficients reset filter state on every
change — a deliberate NaN-recovery tradeoff. That means a running filter
cannot be retuned click-free in place, and smoothing the parameter doesn't
help (the discontinuity is in the state reset).
The framework therefore provides CrossfadingBiQuadFilter: two filter
instances, retune the idle one, equal-power crossfade over a few milliseconds.
Every filter-based / automatable effect (Equalizer, GraphicEqualizer, the
demo FilterEffect, future dynamic EQ / auto-wah / modulated filters) uses
this.
EffectChain.Add / Insert / RemoveAt / Move publish a new chain
atomically on the next block (copy-on-write — Read stays lock-free). Adding
or reordering effects mid-stream does not reset the other effects in the
chain. The realtime engine uses the same swap-by-reference pattern for its
ASIO callback.
NAudio.Effects namespace, inside NAudio.Core (cross-platform,
AOT-safe). NAudio.Dsp stays the low-level-primitive namespace.double); allocation-free steady state;
no reflection (NAudio.Core is IsAotCompatible). TensorPrimitives is
used where the algorithm allows (gain, mix, waveshaping) — matching the
existing VolumeSampleProvider precedent.DenormalGuard test asserts no subnormal floats leak from decaying feedback
paths.The framework (IAudioEffect, AudioEffect, EffectSampleProvider,
EffectChain), the optional parameter model (IParameterized,
EffectParameter, ParameterDispatchQueue), and ~27 effects across EQ /
filtering, level / pan / stereo, dynamics, saturation / lo-fi, delay /
modulation, reverb, pitch, and voice-comms. New DSP primitives underpin them:
EnvelopeFollower, ParameterSmoother, DelayLine, CrossfadingBiQuadFilter,
Lfo, Oversampler, LinkwitzRileyCrossover, PartitionedConvolver,
VoiceActivityDetector, plus BiQuadFilter.ResetState().
The full catalogue, usage and parameter list per effect are in
Docs/AudioEffects.md; the RELEASE_NOTES.md
"New features" section is the canonical changelog.
The WPF Realtime Effects demo is the subjective-quality evaluation tool
and the seed for a future VST3 host: ASIO duplex monitoring + file
playback/render through an editable chain, with auto-generated parameter
panels driven by IParameterized. A separate Convolution Reverb demo
handles the IR-as-input workflow that doesn't fit the generic panel.
| NAudio 2 type | Disposition | Replacement |
|---|---|---|
SimpleCompressorEffect (was SimpleCompressorStream) | Deleted (public, breaking) | CompressorEffect (soft knee, peak/RMS, channel-linked) |
SimpleCompressor, SimpleGate | Deleted (internal) | CompressorEffect, GateEffect |
EnvelopeDetector, AttRelEnvelope | Deleted (internal) | EnvelopeFollower (clean public float rewrite) |
ImpulseResponseConvolution | Deleted (public, breaking) | ConvolutionReverbEffect (partitioned FFT) |
NAudio.Extras.Equalizer, EqualizerBand | Deleted, replaced (public, breaking) | NAudio.Effects.Equalizer / EqualizerBand in NAudio.Core — per-channel, click-free retune, shelves/pass/notch/band-pass/all-pass added. Band API: Bandwidth/Gain → Q/GainDb (or ShelfSlope); equaliser is now an IAudioEffect (wrap with EffectSampleProvider instead of passing a source to the constructor). |
DMO effects (DmoCompressor, DmoEcho, DmoChorus, …) | Kept as a Windows convenience | Out of scope for the cross-platform suite by construction — they are COM/DirectSound objects. The managed suite is a parallel, cross-platform offering, not a reimplementation obligation. |
Kept and built on (no behavioural change required for the effects work):
BiQuadFilter (hardened by #1259), FastFourierTransform / FftProcessor,
WdlResampler, SmbPitchShifter, EnvelopeGenerator,
VolumeSampleProvider / FadeInOutSampleProvider / MeteringSampleProvider.
Default: build. ~70% of the suite (EQ, dynamics, saturation, delay, modulation, stereo tools, DC blocker, gate) is a few dozen to a few hundred lines of well-understood textbook DSP. Hand-writing idiomatic, allocation-free, AOT-safe C# is less work than porting, adapting, and re-validating C++, and the result doubles cleanly as a synth/sampler building block with no third-party attribution sprawl.
Port selectively, only where the algorithm is genuinely hard and a strong
permissively-licensed reference exists. In NAudio 3 this means:
algorithmic reverb (Freeverb / Dattorro as reference, not transliteration),
partitioned-convolution reverb (built on our existing FftProcessor), and the
voice-comms DSP. Even then, prefer "port as algorithm, reimplement
idiomatically" over line-by-line transliteration.
Licence policy. NAudio is MIT. Only MIT / BSD / Zlib / public-domain
sources may be ingested, with attribution preserved in-file — a header comment
naming the original author, project and licence, as SmbPitchShifter and
WdlResampler already do. That is the whole mechanism: an earlier draft of this
policy also called for an entry in a central THIRD-PARTY-NOTICES.txt, but no such
file was ever added and it is not required. In-file attribution keeps the notice
next to the code it applies to, which is where a reader looks and what survives a
file being moved between packages. Revisit only if a future dependency's licence
demands a distributed notices file. GPL / LGPL / proprietary code (Rubber Band,
SoundTouch, Surge, Vital, TAL, JUCE DSP) is out and must never be copied from.
NoiseSuppressionEffect STFT spectral suppressor. ~85 KB model, needs a
small in-process GRU/dense inference path (no external ML runtime).SmbPitchShifter / PitchShiftEffect.WaveStream vs non-positional ISampleProvider"
pain. Leading direction: a generic
PositionPreservingSampleProvider : WaveStream, ISampleProvider returned
by waveStream.AddEffect(...) — forwards Position/Length to the
seekable source while Read pulls through the effect chain, so the object
the player reads from is the object you seek. Single-source transforms
only (Skip/Take/channel remap/resample); fan-in stays plain
ISampleProvider. Known wrinkles (byte↔sample mapping, latency offset,
tails past EOF, seek-resets-state) are intrinsic to any seekable-effects
solution. Requires an EffectChain.Reset() and a small ownership/disposal
convention.IAudioEffect small. A
later concern, separable.