Step 4 of 8

Effects & VST3

Shaping the sound โ€” 19 built-ins, an adaptive mastering chain, any VST3 plugin, and a tap to see what they did. Namespace: OwnaudioNET.Effects

Every effect, built-in or plugin, is an IEffectProcessor, and they all go in one of two places:

WhereHowAffects
One trackSourceWithEffects โ†’ AddEffect()Only that source
The whole mixmixer.AddMasterEffect()Everything, after summing

Order matters: effects run in the order you add them.

IEffectProcessor

The shared surface. Enabled is the cheap way to bypass something without disturbing the chain, and Mix is the wet/dry blend on effects that have one.

MemberTypeDescription
IdGuidUnique identifier.
NamestringEffect name.
EnabledboolEnable/disable without removing from chain.
MixfloatWet/dry mix (0.0 = dry only, 1.0 = wet only).
IsReadyboolWhether initialized and ready to process.
Initialize(AudioConfig)methodCalled automatically when added to mixer or chain.
Process(Span<float>, int)methodIn-place processing through the native engine. The mixer does not call this โ€” it uses a separate native twin on the audio thread. Direct Process (Matchering, tests, ReadSamples) is the same DSP, own handle.
Reset()methodClear internal state (delay lines, filter history, etc.).

ReverbEffect

Professional quality reverb based on an optimized extended Freeverb algorithm.

C#
var reverb = new ReverbEffect
{
    RoomSize   = 0.7f,  // 0.0โ€“1.0 (larger = longer tail)
    Damping    = 0.5f,  // 0.0โ€“1.0 (higher = darker)
    Mix        = 0.3f,  // 0.0โ€“1.0 wet/dry
    Width      = 1.0f,  // 0.0โ€“1.0 stereo spread
    WetLevel   = 0.33f, // 0.0โ€“1.0 reverb tail level
    DryLevel   = 1.0f   // 0.0โ€“1.0 direct signal level
};

// Or use a preset
reverb.SetPreset(ReverbPreset.LargeHall);

Presets: Default, SmallRoom, LargeHall, Cathedral, Plate, Spring, AmbientPad, VocalBooth, DrumRoom, Gated, Subtle

DelayEffect

Stereo delay with ping-pong mode and feedback damping.

C#
var delay = new DelayEffect
{
    Time     = 375,    // ms (1โ€“5000)
    Repeat   = 0.4f,   // feedback 0.0โ€“1.0
    Damping  = 0.3f,   // 0.0โ€“1.0, and against the name higher = brighter repeats (0 = silent)
    Mix      = 0.3f,
    PingPong = true    // stereo ping-pong
};
delay.SetPreset(DelayPreset.PingPong);

Presets: Default, SlapBack, ClassicEcho, Ambient, Rhythmic, PingPong, TapeEcho, Dub, Thickening

EqualizerEffect

10-band parametric EQ. Bands: 31.25 Hz, 62.5 Hz, 125 Hz, 250 Hz, 500 Hz, 1 kHz, 2 kHz, 4 kHz, 8 kHz, 16 kHz.

C#
var eq = new EqualizerEffect();
eq.Band0Gain = +6.0f;       // boost 31.25Hz by 6dB
eq.Band9Gain = -3.0f;       // cut 16kHz by 3dB
float g = eq.Band4Gain;     // query 500Hz band

// Sets the gain of band 4. The frequency and Q arguments are ignored.
eq.SetBandGain(4, 500f, 1.0f, +2.5f);

eq.SetPreset(EqualizerPreset.Rock);

Presets: Default, Bass, Treble, Rock, Classical, Pop, Jazz, Voice

โš ๏ธ

The band centres and Q of this EQ are fixed: the native filter bank runs the ISO frequencies above at Q = 1.0 and takes gain only. SetBandGain keeps its frequency and q parameters for source compatibility but does nothing with them โ€” setting them is silent, not an error. Use Equalizer30BandEffect when you need to move a bell or change its width.

CompressorEffect

Professional dynamic range compressor with makeup gain.

C#
var comp = new CompressorEffect
{
    Threshold    = -12.0f, // dB, -60โ€“0
    Ratio        = 4.0f,   // 1.0โ€“100.0 (N:1)
    AttackTime   = 10f,    // 0.1โ€“1000ms
    ReleaseTime  = 100f,   // 1โ€“2000ms
    KneeWidth    = 6.0f,   // dB, 0 = hard knee
    MakeupGain   = 1.6f    // dB
};
comp.SetPreset(CompressorPreset.VocalGentle);

Presets: Default, VocalGentle, VocalAggressive, Drums, Bass, MasteringLimiter, Vintage

โš ๏ธ

The constructor and the properties do not use the same units. Threshold and MakeupGain are dB as properties, but the constructor arguments of the same name are linear โ€” new CompressorEffect(0.5f, makeupGain: 1.2f) is the default โˆ’6 dB threshold with +1.6 dB of makeup. Set them through the properties and you never have to think about it.

OwnCompressorEffect

Log-domain compressor with look-ahead, soft knee, programme dependent release, peak or RMS detection, stereo link, mid/side, a sidechain high-pass, range and parallel mix. Every setting takes the unit it says โ€” dB, ms, Hz โ€” in the properties and nowhere else.

C#
var comp = new OwnCompressorEffect
{
    Threshold         = -18f,  // dBFS, -60โ€“0
    Ratio             = 4f,    // 1โ€“100 (above 20 it acts as a limiter)
    Knee              = 6f,    // dB, 0 = hard knee โ€ฆ 24
    Attack            = 10f,   // ms, 0.01โ€“300 (10 % โ†’ 90 % of the step)
    Release           = 100f,  // ms, 5โ€“5000
    AutoRelease       = true,  // slower release after long, deep compression
    Lookahead         = 2f,    // ms, 0โ€“10 โ€” also the effect's latency
    Detector          = OwnCompressorDetector.Rms,          // or Peak
    Topology          = OwnCompressorTopology.FeedForward,  // or Feedback
    StereoLink        = 1f,    // 0 = independent channels โ€ฆ 1 = fully linked
    ChannelMode       = OwnCompressorChannelMode.LeftRight, // or MidSide
    SidechainHighPass = 100f,  // Hz, 0 = off or 20โ€“500 (detector only)
    Makeup            = 3f,    // dB, -24โ€“+24
    AutoMakeup        = false,
    Range             = 60f,   // dB, most gain reduction allowed
    Mix               = 1f     // parallel mix, 0 = dry โ€ฆ 1 = fully compressed
};

// Or start from a preset
var bus = new OwnCompressorEffect(OwnCompressorPreset.Bus);

// Meters, when the effect is processed directly
float grDb = comp.GainReductionDb;

Presets: Default, Vocal, Bus, Drums, Bass, Mastering, Parallel, Classic, Podcast

๐Ÿ’ก

Look-ahead is latency. LatencySamples reports it, and the mixer re-aligns every other track when it changes โ€” even after the effect is already in the chain. The jump itself is audible mid-signal, so set it up front. The dry half of Mix is delayed by the same amount, so parallel blending never comb filters.

OwnDelayEffect

Tape style stereo delay. Saturation, diffusion and the low/high cut all sit inside the feedback loop, so every repeat gets a little darker, softer and more smeared than the one before. Time changes either glide like tape (with the pitch bend) or crossfade to the new time cleanly.

C#
var delay = new OwnDelayEffect
{
    TimeLeft      = 375f,   // ms, 15โ€“4000
    TimeRight     = 500f,   // ms, 15โ€“4000
    Feedback      = 0.45f,  // 0.0โ€“1.0
    CrossFeedback = 0.1f,   // 0.0โ€“1.0, L โ†” R (ping-pong)
    TimeMode      = OwnDelayTimeMode.Glide,  // or Crossfade
    Glide         = 150f,   // ms, 5โ€“2000 (Glide mode only)
    Drive         = 6f,     // dB, 0โ€“24 tape saturation in the loop
    LowCut        = 150f,   // Hz, 20โ€“2000
    HighCut       = 6000f,  // Hz, 500โ€“20000
    Diffusion     = 0.3f,   // 0.0โ€“1.0, smears repeats towards a reverb
    ModRate       = 0.6f,   // Hz, 0.05โ€“10 wow
    ModDepth      = 0.5f,   // ms, 0โ€“5
    DuckAmount    = 0.5f,   // 0 = off โ€ฆ 1, the dry input pushes the wet down
    DuckThreshold = -30f,   // dBFS
    Width         = 1.2f,   // 0 = mono โ€ฆ 2
    Mix           = 0.3f
};

// Tempo sync
delay.SyncToTempo(120, OwnDelayNoteValue.DottedEighth, OwnDelayNoteValue.Quarter);

// Hold whatever is in the loop forever
delay.Freeze = true;

delay.SetPreset(OwnDelayPreset.TapeEcho);

Presets: Default, Slapback, PingPong, TapeEcho, DubEcho, Ambient, DuckedVocal, LoFi, Doubler

๐Ÿ’ก

Feedback + CrossFeedback is capped at 1, so the loop cannot run away however hard you push both. Freeze mutes the input, pins the loop gain at exactly 1 and bypasses the filters, so the held sound does not decay or darken.

OwnDynamicAmpEffect

The professional twin of DynamicAmpEffect. It measures perceived loudness (ITU-R BS.1770 K-weighting, LUFS) and rides the gain toward a long-term, gated estimate of the programme loudness. Passages far below that estimate and anything under the freeze threshold do not move the gain, material inside the tolerance window is left untouched, and every gain movement is rate limited and smoothed, so the level lands on the target while the dynamics of the music stay intact. A true-peak (4x oversampled) look-ahead limiter keeps the output under the ceiling.

C#
var rider = new OwnDynamicAmpEffect
{
    TargetLoudness  = -14f,   // LUFS, -40 to -5 (-14 streaming, -23 EBU R128)
    Window          = 8f,     // s, 0.4 to 60, memory of the loudness estimate
    MaxBoost        = 12f,    // dB, 0 to 30
    MaxCut          = 12f,    // dB, 0 to 30
    RiseRate        = 1.5f,   // dB/s, 0.1 to 20
    FallRate        = 3f,     // dB/s, 0.1 to 40
    Tolerance       = 1f,     // dB, 0 to 6, dead band around the target
    Smoothing       = 300f,   // ms, 10 to 5000, rounds the gain curve
    RelativeGate    = -10f,   // LU below the programme, quieter passages keep the gain
    FreezeThreshold = -55f,   // LUFS, silence and noise never get pumped up
    Ceiling         = -1f,    // dBTP, -20 to 0
    Lookahead       = 5f      // ms, 1 to 10, also the latency
};

// Or start from a preset
var broadcast = new OwnDynamicAmpEffect(OwnDynamicAmpPreset.Broadcast);

// Meters, when the effect is processed directly
float programLufs = rider.ProgramLoudness;
float gainDb      = rider.CurrentGainDb;

Presets: Default, Speech, Music, Broadcast, Mastering, Live, Transparent

๐Ÿ’ก

The gain is computed per sample, so the result does not depend on the block size. The look-ahead is latency: LatencySamples reports it and the mixer re-aligns the other tracks. DynamicAmpEffect stays unchanged for backward compatibility.

The whole list

Every one of these processes in place, on the native engine, and allocates nothing on the managed side. The seven above have presets and detailed parameters; the rest follow the same pattern.

ClassDescription
ReverbEffectFreeverb-based reverb with presets
OwnReverbEffect16-line FDN reverb with diffusion, damping, modulation and a ducker
DelayEffectStereo delay with ping-pong and damping
OwnDelayEffectTape style delay: in-loop saturation, diffusion and filters, glide or crossfade, ducking, freeze
EqualizerEffect10-band parametric EQ
Equalizer30BandEffect30-band graphic EQ on 1/3-octave ISO centres. Unlike the 10-band one, its SetBandGain really does carry the centre frequency and Q through to the engine.
CompressorEffectDynamic range compressor
OwnCompressorEffectLog-domain compressor with look-ahead, auto release, stereo link, mid/side and parallel mix
LimiterEffectSoft/hard peak limiter
ChorusEffectChorus / spatial thickening
FlangerEffectFlanging / sweeping comb filter
PhaserEffectPhase modulation
DistortionEffectDistortion / hard clipping
OverdriveEffectOverdrive / soft saturation
AutoGainEffectAutomatic gain control (AGC)
EnhancerEffectLoudness enhancement / exciter
DynamicAmpEffectDynamic range amplification
OwnDynamicAmpEffectLoudness rider (BS.1770, LUFS) with gated programme estimate, tolerance window and a true-peak look-ahead limiter
RotaryEffectRotating speaker simulation (Leslie)

SmartMaster

A whole mastering chain in one effect for the master bus: 30-band EQ, compressor, subharmonic synth and a brick-wall limiter. Its party trick is measuring the room through a microphone and calibrating the EQ to what the speakers actually do.

๐Ÿ’ก

The measurement judges every band against the midrange rather than against an absolute level, so the same speakers give the same verdict whether you ran the sweep loud or quiet. It also decides for itself whether the subharmonic synth would help: a speaker that carries 40โ€“80 Hz but runs out under it gets it, one that simply cannot do bass does not โ€” the synth is an octave divider, so there it would only write energy further down.

C#
var smartMaster = new SmartMasterEffect();
mixer.AddMasterEffect(smartMaster);

// Factory preset by speaker type
smartMaster.LoadSpeakerPreset(SpeakerType.Studio);
// Other types: Default, HiFi, Headphone, Club, Concert

// Save / load user presets
smartMaster.Save("my-studio");
smartMaster.Load("my-studio");

// Reset to defaults
smartMaster.ResetToDefaults();

// Auto room measurement - needs AudioConfig.EnableInput = true and a running mixer,
// the noise plays and the mic records through the mixer the effect sits on
smartMaster.StartMicMonitoring();          // optional: live level via GetLastMicLevel()
await smartMaster.StartMeasurementAsync(); // plays pink noise, measures the room
smartMaster.CancelMeasurement();
MeasurementStatusInfo status = smartMaster.GetMeasurementStatus();
// Result is saved to a "measured" preset, never applied on its own

// Edit the live config, then rebuild the chain from it
SmartMasterConfig cfg = smartMaster.GetConfiguration();
cfg.GraphicEQGains[5] = 2.5f;                   // 63 Hz
smartMaster.ApplyConfiguration();

// Lifecycle
smartMaster.OnPlaybackStopped(); // call when transport stops

SmartMasterConfig

C#
var config = new SmartMasterConfig
{
    GraphicEQGains       = new float[SmartMasterConfig.EqBands], // 30 bands, dB (0 = flat)
    CompressorEnabled    = true,
    CompressorThreshold  = 0.5f,           // 0.0โ€“1.0 linear
    CompressorRatio      = 4.0f,           // 4:1
    CompressorAttack     = 10f,            // ms
    CompressorRelease    = 100f,           // ms
    SubharmonicEnabled   = false,
    SubharmonicMix       = 0.0f,           // 0.0โ€“1.0, parallel level
    SubharmonicLowLevel  = 1.0f,           // 24โ€“36 Hz band
    SubharmonicHighLevel = 1.0f,           // 36โ€“56 Hz band
    LimiterThreshold     = -0.1f,          // dBFS
    LimiterCeiling       = -0.1f,          // dBFS
    LimiterRelease       = 50f             // ms
};

// Arrays are fitted to the length the chain expects, so an older or hand
// edited preset can't silently disable a stage.
smartMaster.ApplyConfiguration(config);

VST3 plugins

A loaded plugin hands you an IEffectProcessor, so from the mixer's point of view it is no different from a built-in effect โ€” including its own editor window. Effects only; instruments are not supported.

๐Ÿ’ก

The order is always the same: create โ†’ check IsEffect โ†’ InitializeAudioAsync() โ†’ GetProcessor(). Adding a processor before its audio is initialized is the usual reason a plugin does nothing. Full example with error handling โ†’

C#
using OwnaudioNET.Effects;

// Discover plugins
List<string>         paths = VST3PluginHost.FindPlugins();
List<VST3PluginInfo> info  = VST3PluginHost.ScanPluginsQuick();

// Load plugin
VST3PluginHost host = await VST3PluginHost.CreateAsync("/path/plugin.vst3");

if (!host.IsEffect) { host.Dispose(); return; } // reject instruments

// Initialize audio processing
int sampleRate = OwnaudioNet.Engine!.Config.SampleRate;
bool ready     = await host.InitializeAudioAsync(sampleRate, maxBlockSize: 1024);

// Add to mixer or track effect chain
mixer.AddMasterEffect(host.GetProcessor());
// or: trackFx.AddEffect(host.GetProcessor());

// Plugin UI
host.OpenEditor();
await host.OpenEditorAsync();
host.CloseEditor();
var size = await host.GetEditorSizeAsync();

Parameters & State

C#
// Read parameters
VST3ParameterInfo[] params = await host.GetParametersAsync();
int    count = await host.GetParameterCountAsync();
double value = await host.GetParameterAsync(paramId);

// Set parameters
host.SetParameter(paramId, 0.5);
await host.SetParametersAsync(new Dictionary<int, double> { { paramId, 0.75 } });

// Preset state (for project save/load)
byte[]? state = await host.GetStateAsync();
await host.SetStateAsync(state);

// Cleanup
await host.DisposeAsync();
// or:
host.Dispose();

VST3PluginHost Properties

PropertyTypeDescription
NamestringPlugin name.
VendorstringPlugin vendor/manufacturer.
Versionstring?Plugin version string.
IsEffectbooltrue for audio effect plugins.
IsInstrumentbooltrue for instrument plugins (not supported).
HasEditorboolWhether the plugin has a graphical editor.
IsEditorOpenboolWhether the editor window is currently open.
IsReadyboolWhether initialized and ready to process audio.
PluginPathstringFilesystem path to the .vst3 bundle.
โš ๏ธ

Call PauseDeviceMonitoring() before opening the VST3 editor window and ResumeDeviceMonitoring() after closing to prevent device enumeration interference.

๐Ÿšซ

Take an effect out of its chain before disposing it, and leave a moment in between โ€” the audio thread may still be inside the native twin. Safe removal โ†’

Seeing what an effect actually does

Effects process in place, so by the time you can reach the buffer the original signal is gone. An effect tap asks the engine to keep a copy: it mirrors every rendered block on both sides of the chain, and hands you the two back paired up. Namespace: OwnaudioNET.Monitoring.

What you wantCall
One source's chainmixer.CreateEffectTap(sourceId)
The master chainmixer.CreateMasterEffectTap()

For a spectrum comparison โ€” the usual reason to want this โ€” wrap the tap in an EffectSpectrumAnalyzer and poll it from a UI timer:

C#
using var analyzer = new EffectSpectrumAnalyzer(_mixer.CreateEffectTap(vocals.Id));

// from a 30โ€“60 ms timer
if (analyzer.Update())
{
    ReadOnlySpan<float> dry = analyzer.PreMagnitudesDb;    // going into the chain
    ReadOnlySpan<float> wet = analyzer.PostMagnitudesDb;   // coming out of it
    ReadOnlySpan<float> hz  = analyzer.Frequencies;
    Redraw(hz, dry, wet);
}

Update() returns false when not enough new audio has arrived yet, and leaves the previous spectra untouched โ€” so a timer that runs faster than the audio simply redraws the same picture. Magnitudes are dBFS, calibrated against a sine: a full-scale tone reads 0 dBFS, and anything below EffectSpectrumAnalyzer.FloorDb (โˆ’120) reads as silence.

Raw samples

Skip the analyzer if you want the audio itself โ€” a scope, a correlation meter, your own DSP:

C#
using var tap = _mixer.CreateMasterEffectTap();

var pre  = new float[4096];
var post = new float[4096];

int read = tap.Read(pre, post);   // 0 = nothing new yet; both spans get the same count

What the tap guarantees

MemberTypeDescription
Read(Span, Span)methodDrains a chunk into both spans, filled to the same length and lined up in time. Returns the sample count.
ChannelsintInterleaved channel count of the tapped audio.
SampleRateintSample rate of the tapped audio.
LatencySamplesintLatency of the running effects, in frames. Already compensated for โ€” here so you can display it.
IsActiveboolfalse once disposed.

Three things are handled for you, and they are the parts that are easy to get wrong by hand:

โš ๏ธ

Taps need the Rust-native chain and a source with a native track behind it. CreateEffectTap throws InvalidOperationException for a source that has none, and CreateMasterEffectTap throws until the session exists โ€” add a source first.

๐Ÿ’ก

Dispose the tap (or the analyzer, which owns it) when the window closes. Until then the engine keeps mirroring every block, which is cheap but not free. Full analyzer recipe โ†’

Next