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:
| 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 | Zero-allocation in-place processing. |
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, // 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.
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.
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.
| Class | Description |
|---|---|
ReverbEffect | Freeverb-based reverb with presets |
DelayEffect | Stereo delay with ping-pong and damping |
EqualizerEffect | 10-band parametric EQ |
Equalizer30BandEffect | 30-band graphic EQ |
CompressorEffect | Dynamic range compressor |
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 |
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
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 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 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 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 โ