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:
| Where | How | Affects |
|---|---|---|
| One track | SourceWithEffects โ AddEffect() | Only that source |
| The whole mix | mixer.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.
| Member | Type | Description |
|---|---|---|
Id | Guid | Unique identifier. |
Name | string | Effect name. |
Enabled | bool | Enable/disable without removing from chain. |
Mix | float | Wet/dry mix (0.0 = dry only, 1.0 = wet only). |
IsReady | bool | Whether initialized and ready to process. |
Initialize(AudioConfig) | method | Called automatically when added to mixer or chain. |
Process(Span<float>, int) | method | In-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() | method | Clear internal state (delay lines, filter history, etc.). |
ReverbEffect
Professional quality reverb based on an optimized extended Freeverb algorithm.
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.
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.
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.
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.
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.
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.
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.
| Class | Description |
|---|---|
ReverbEffect | Freeverb-based reverb with presets |
OwnReverbEffect | 16-line FDN reverb with diffusion, damping, modulation and a ducker |
DelayEffect | Stereo delay with ping-pong and damping |
OwnDelayEffect | Tape style delay: in-loop saturation, diffusion and filters, glide or crossfade, ducking, freeze |
EqualizerEffect | 10-band parametric EQ |
Equalizer30BandEffect | 30-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. |
CompressorEffect | Dynamic range compressor |
OwnCompressorEffect | Log-domain compressor with look-ahead, auto release, stereo link, mid/side and parallel mix |
LimiterEffect | Soft/hard peak limiter |
ChorusEffect | Chorus / spatial thickening |
FlangerEffect | Flanging / sweeping comb filter |
PhaserEffect | Phase modulation |
DistortionEffect | Distortion / hard clipping |
OverdriveEffect | Overdrive / soft saturation |
AutoGainEffect | Automatic gain control (AGC) |
EnhancerEffect | Loudness enhancement / exciter |
DynamicAmpEffect | Dynamic range amplification |
OwnDynamicAmpEffect | Loudness rider (BS.1770, LUFS) with gated programme estimate, tolerance window and a true-peak look-ahead limiter |
RotaryEffect | Rotating 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.
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 stopsSmartMasterConfig
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 โ
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
// 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
| Property | Type | Description |
|---|---|---|
Name | string | Plugin name. |
Vendor | string | Plugin vendor/manufacturer. |
Version | string? | Plugin version string. |
IsEffect | bool | true for audio effect plugins. |
IsInstrument | bool | true for instrument plugins (not supported). |
HasEditor | bool | Whether the plugin has a graphical editor. |
IsEditorOpen | bool | Whether the editor window is currently open. |
IsReady | bool | Whether initialized and ready to process audio. |
PluginPath | string | Filesystem 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 want | Call |
|---|---|
| One source's chain | mixer.CreateEffectTap(sourceId) |
| The master chain | mixer.CreateMasterEffectTap() |
For a spectrum comparison โ the usual reason to want this โ wrap the tap in an EffectSpectrumAnalyzer and poll it from a UI timer:
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:
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 countWhat the tap guarantees
| Member | Type | Description |
|---|---|---|
Read(Span, Span) | method | Drains a chunk into both spans, filled to the same length and lined up in time. Returns the sample count. |
Channels | int | Interleaved channel count of the tapped audio. |
SampleRate | int | Sample rate of the tapped audio. |
LatencySamples | int | Latency of the running effects, in frames. Already compensated for โ here so you can display it. |
IsActive | bool | false once disposed. |
Three things are handled for you, and they are the parts that are easy to get wrong by hand:
- The two sides stay paired. If the drain falls behind, whole pre/post pairs are dropped โ never one side alone, which would slide the streams apart and quietly corrupt every later comparison.
- Latency is compensated. A look-ahead limiter or a hosted VST3 answers late; the dry side is held back by exactly that much so
pre[i]andpost[i]really are the same instant. Bypassed effects are excluded, since a bypassed effect delays nothing. - The fader is not in the picture. The tap sits ahead of gain, pan and delay compensation, so moving a volume slider never shows up as something the effects did.
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 โ