Step 4 of 8

Effects & VST3

Shaping the sound โ€” 15 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)methodZero-allocation in-place processing.
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,   // high-freq damping 0.0โ€“1.0
    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

// Retune a band completely: index, centre frequency, Q, gain in dB
eq.SetBandGain(4, 500f, 1.0f, +2.5f);

eq.SetPreset(EqualizerPreset.Rock);

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

CompressorEffect

Professional dynamic range compressor with makeup gain.

C#
var comp = new CompressorEffect
{
    Threshold    = 0.7f,   // 0.0โ€“1.0 linear amplitude
    Ratio        = 4.0f,   // 1.0โ€“100.0 (N:1)
    AttackTime   = 10f,    // 0.1โ€“1000ms
    ReleaseTime  = 100f,   // 1โ€“2000ms
    MakeupGain   = 1.2f    // linear amplitude multiplier
};
comp.SetPreset(CompressorPreset.VocalGentle);

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

The whole list

Every one of these is allocation-free and processes in place. The four above have presets and detailed parameters; the rest follow the same pattern.

ClassDescription
ReverbEffectFreeverb-based reverb with presets
DelayEffectStereo delay with ping-pong and damping
EqualizerEffect10-band parametric EQ
Equalizer30BandEffect30-band graphic EQ
CompressorEffectDynamic range compressor
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
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
await smartMaster.StartMeasurementAsync(); // plays test signal, auto-calibrates EQ
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 Process(). 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