Step 1 of 8

Engine & Configuration

Opening the sound card, picking a device, and knowing what the engine is doing. Namespace: OwnaudioNET

Everything starts here. OwnaudioNet is a static class, there is exactly one engine per application, and nothing else in the library works until it is initialized. Its whole job is three moments: initialize, start, shut down.

🧭

Just want sound? Quick Start covers it in fifteen lines. This page is what you read when the defaults aren't enough — a specific interface, lower latency, more headroom, or a log that tells you what went wrong.

What you can ask it

PropertyTypeDescription
IsInitializedboolWhether the audio system has been initialized.
IsRunningboolWhether the audio engine is actively processing.
EngineAudioEngineWrapper?The active engine wrapper. null before initialization. Access UnderlyingEngine for the IAudioEngine.
InputLatencyFramesintHardware capture latency in frames. 0 before capture starts. See Hardware Latency.
OutputLatencyFramesintHardware playback latency in frames. 0 before playback starts. See Hardware Latency.
VersionVersionLibrary version ().

Start & shut down

Two flavours of the same three calls. Use the synchronous ones in console apps and services, the async ones in anything with a window.

âš ī¸

On Linux, Initialize() can block up to 5 seconds waiting for PulseAudio. Always use InitializeAsync() in UI applications.

C# — Synchronous (console apps / services)
// Default config: 48kHz, stereo, 512 frames
OwnaudioNet.Initialize();

// Custom config
OwnaudioNet.Initialize(config);

// Unit testing — no hardware required
OwnaudioNet.Initialize(config, useMockEngine: true);

// Custom config
OwnaudioNet.Initialize(config);

// Start and stop
OwnaudioNet.Start();
OwnaudioNet.Stop();

// Full release of all resources
OwnaudioNet.Shutdown();
C# — Asynchronous (recommended for UI apps)
// Default config
await OwnaudioNet.InitializeAsync();
await OwnaudioNet.InitializeAsync(cancellationToken);

// Custom config
await OwnaudioNet.InitializeAsync(config);
await OwnaudioNet.InitializeAsync(config, useMockEngine: false, cancellationToken: cancellationToken);

// Pre-created engine (custom platform implementations)
await OwnaudioNet.InitializeAsync(engine, config, cancellationToken: cancellationToken);

// Stop / Shutdown
await OwnaudioNet.StopAsync(cancellationToken);
await OwnaudioNet.ShutdownAsync(cancellationToken);
â„šī¸

bufferMultiplier is still accepted, but it no longer changes anything. Playback headroom moved into the engine's native render ring in 4.0 — tune it with AudioConfig.OutputRingMilliseconds. For a heavy mix, raise that, AudioConfig.BufferSize, or the mixer's bufferSizeInFrames.

Logging — turn this on first when something is wrong

The library reports every initialization, teardown and parameter change, together with any error it would otherwise swallow. It is off by default — pass logLevel to Initialize whenever you want to see what is happening under the hood. The console is only the fallback destination; a sink or a file takes over when you set one.

C#
using Logger;

// Silent — this is the default
OwnaudioNet.Initialize(config);

// The full picture: lifecycle, device switches, errors and warnings
OwnaudioNet.Initialize(config, logLevel: Log.Level.Info);

// Only what actually went wrong
await OwnaudioNet.InitializeAsync(config, logLevel: Log.Level.Error);

// Flip it any time while the API is running
Log.LoggerLevel = Log.Level.Info;      // on
Log.LoggerLevel = Log.Level.Disabled;  // off
LevelWhat you get
DisabledNothing at all. The default.
FatalErrorUnrecoverable faults only — device lost, recording aborted, a background loop giving up.
ErrorPlus every failure that would otherwise stay hidden.
WarningPlus recoverable fallbacks — a dropped sync client, a clock tier downgrade, a rejected device.
InfoEverything, including the whole lifecycle: engine start/stop, device switches, source and effect changes.
Sample output — Log.Level.Info
[21:04:12] [INFO] [EngineFactory] Creating RustAudioEngine (cpal): 48000Hz 2ch, buffer 512
[21:04:12] [INFO] [RustEngine] Initialized on CoreAudio: 48000Hz 2ch requested -> 4ch opened out / off in, out 'Scarlett 4i4' in '(none)', buffer 512 frames requested, ring 4800 frames (100.0ms, 100ms requested), latency out/in 512/0 frames
[21:04:12] [INFO] [OwnaudioNet] Initialized
[21:04:13] [INFO] [Mixer] Source 'a3f1c8e2-...' added (1 total)
[21:04:13] [INFO] [Mixer] Started (rust-native), 1 sources
[21:04:31] [ERROR] [RustEngine] No output device named 'Focusrite Scarlett 2i2'
💡

Audio-rate code never logs per buffer. A failing effect or a broken socket reports its first occurrence and then a periodic summary, so the log stays readable while audio keeps flowing.

Getting the log out of a GUI app

A desktop, Android or iOS app has no console, so the lines above go nowhere by default. Point them at your own logging with Log.Sink, or let the library write a self-rotating file.

C#
using Logger;

Log.LoggerLevel = Log.Level.Info;   // the filter still applies to both

// Into your own logging: file, ILogger, a diagnostics panel, a bug report
Log.Sink += (level, line) => _logger.Log(Map(level), line);

// Or straight to disk: ~2 MB per file, 3 rotated generations kept (~8 MB total)
string? path = Log.ToFile();

// Your own path and budget
Log.ToFile(@"C:\logs\myapp-audio.log", maxFileSizeKb: 512, keepFiles: 2, maxAgeDays: 14);

Log.CloseFile();
â„šī¸

The file can never fill the drive: it rotates at maxFileSizeKb into name.1.log, name.2.log â€Ļ, keeps keepFiles generations, and prunes anything past that — or past maxAgeDays — when it is opened, so a crashed process still gets cleaned up on the next start. Total disk use stays at roughly maxFileSizeKb × (keepFiles + 1). Subscribing a sink turns the console fallback off; a handler that throws is ignored.

Three configs you don't have to write

Start from one of these and change what you need — they cover most cases.

C#
// 48kHz ¡ stereo ¡ 512 frames (~10.6ms)
AudioConfig config = OwnaudioNet.CreateDefaultConfig();

// 48kHz ¡ stereo ¡ 128 frames (~2.7ms)
AudioConfig config = OwnaudioNet.CreateLowLatencyConfig();

// 48kHz ¡ stereo ¡ 2048 frames (~42.7ms)
AudioConfig config = OwnaudioNet.CreateHighLatencyConfig();

Choosing a device

Leave OutputDeviceId at null and you get the system default, which is the right answer for most apps. Enumerate when you want to offer the user a choice — see the device picker recipe for the full flow.

C#
// Enumerate devices
List<AudioDeviceInfo> outputs = OwnaudioNet.GetOutputDevices();
List<AudioDeviceInfo> inputs  = OwnaudioNet.GetInputDevices();

// Async variants
List<AudioDeviceInfo> outputs = await OwnaudioNet.GetOutputDevicesAsync();
List<AudioDeviceInfo> inputs  = await OwnaudioNet.GetInputDevicesAsync();

// Use a specific device
var device = outputs.First(d => d.Name.Contains("Focusrite"));
config.OutputDeviceId = device.DeviceId;

// Hot-plug monitoring — pause when opening VST editors
OwnaudioNet.PauseDeviceMonitoring();
// ... open editor ...
OwnaudioNet.ResumeDeviceMonitoring();

AudioDeviceInfo Properties

PropertyTypeDescription
DeviceIdstringUnique identifier — pass to AudioConfig.OutputDeviceId.
NamestringHuman-readable device name.
EngineNamestringBackend: Wasapi, CoreAudio, PulseAudio, etc.
IsInput / IsOutputboolDevice direction.
IsDefaultboolSystem default device for its type.
MaxInputChannels / MaxOutputChannelsintHardware channel limits.

Hardware Latency

The samples you capture were grabbed a bit before the callback saw them, and what you send out won't hit the speaker until a bit after. That gap is the driver's hardware latency. The engine reads it back from the backend once audio is flowing and hands it to you in frames — divide by the sample rate for seconds. It's 0 until the first buffer runs, or when the backend doesn't report one.

C#
// Frames, live once the engine is running
int inFrames  = OwnaudioNet.InputLatencyFrames;
int outFrames = OwnaudioNet.OutputLatencyFrames;

double inMs = inFrames * 1000.0 / config.SampleRate;

// Line a recording up with the real timeline: the buffer you just
// got was actually captured inFrames earlier, so shift it back.
long trueCapturePosition = playbackPosition - inFrames;
💡

Mainly useful when you line captured audio up against a timeline or a backing track — recording and overdub work. Pure playback doesn't need it. On ASIO the driver reports real figures; some WASAPI/CoreAudio setups report 0 when the number isn't available.

Render ring depth

On top of the hardware latency the engine keeps a render ring queued ahead of the DAC, sized by AudioConfig.OutputRingMilliseconds (default 100 ms). Since the producer keeps it topped up, that depth is paid on every buffer, and for live monitoring it is usually the largest single term. A request shallower than three device buffers is pulled back up, so read back what the engine settled on:

â„šī¸

The ring belongs to the push path — OwnaudioNet.Send and the buffered output stream, where managed code hands the engine samples. An AudioMixer renders inside the device callback and stacks no ring on top of it, which is the lower latency shape; there BufferSize is the whole knob, and the dropout signal is SessionLoad rather than an underrun count.

C#
var config = new AudioConfig { BufferSize = 128, OutputRingMilliseconds = 10 };
OwnaudioNet.Initialize(config);

// What the engine actually opened the ring with, after clamping
int ringFrames = OwnaudioNet.Engine!.OutputRingFrames;
double ringMs  = ringFrames * 1000.0 / config.SampleRate;

// How full it is right now — how far ahead of the DAC you've pushed
int queued = OwnaudioNet.Engine!.OutputBufferAvailable / config.Channels;
💡

Going shorter trades headroom for latency. Watch AudioEngineWrapper.TotalUnderruns after a change — if it starts climbing, the ring is too shallow for your producer. AudioConfig.OutputRingFrames gives the same request in frames before the engine is anywhere near it, and reads 0 when the value is outside 1â€ĻAudioConfig.MaxOutputRingMilliseconds, which every engine takes as "use your own default".

Granted buffer size

FramesPerBuffer is the size the engine was configured with — the request. Drivers are free to round it, ASIO especially, and the backend has no way to report the granted size at init: the only ground truth is what the callback is handed. So the engine records it as audio runs:

C#
int asked   = OwnaudioNet.Engine!.FramesPerBuffer;         // known from init
int granted = OwnaudioNet.Engine!.OutputCallbackFrames;    // 0 until audio runs

if (granted != 0 && granted != asked)
    Console.WriteLine($"Driver rounded {asked} to {granted} frames");

int capture = OwnaudioNet.Engine!.InputCallbackFrames;     // capture side
â„šī¸

These read the most recent callback, not a fixed contract. ASIO holds one size; WASAPI shared and CoreAudio may vary the block from callback to callback. Input and output need not match outside ASIO — that's why they're two properties.

Direct Audio I/O

These methods bypass the mixer. Prefer AudioMixer + sources for most use cases.

C#
// Send interleaved samples to output
OwnaudioNet.Send(ReadOnlySpan<float> samples);

// Receive from input — ALWAYS return the buffer!
float[]? buffer = OwnaudioNet.Receive(out int sampleCount);
if (buffer != null)
{
    // process buffer[0..sampleCount-1]
    OwnaudioNet.ReturnInputBuffer(buffer); // returns buffer to pool (zero-allocation)
}
đŸšĢ

Never discard a buffer returned by Receive() without calling ReturnInputBuffer(). The buffer comes from a pool and leaking it causes memory growth.

AudioMixer Registry

The last created AudioMixer is automatically registered for NetworkSync. You can override this:

C#
// Set the primary mixer explicitly (e.g. if you have multiple mixers)
OwnaudioNet.SetPrimaryAudioMixer(mixer);

// Get the currently registered mixer
AudioMixer? active = OwnaudioNet.GetRegisteredAudioMixer();

AudioConfig

The format and the hardware, in one object. You hand it to Initialize and it decides everything downstream — the sample rate every source resamples to, the latency you get, and which device makes noise. Namespace: Ownaudio.Core

PropertyTypeDefaultDescription
SampleRateint48000Sample rate in Hz. Common values: 44100, 48000, 96000.
Channelsint21 = mono, 2 = stereo. The default for both directions.
OutputChannelsint?nullPlayback width, when it differs from Channels. null = use Channels.
InputChannelsint?nullCapture width, same deal. A 2-in / 8-out interface needs the two described separately.
BufferSizeint512Desired frames per buffer. Actual size negotiated with driver.
OutputRingMillisecondsint100Playback headroom the engine keeps queued ahead of the DAC. The producer keeps it topped up, so this is the bulk of the output latency, on top of BufferSize. Live monitoring wants 10–20; anything shallower than three device buffers gets pulled back up. Read the effective depth from AudioEngineWrapper.OutputRingFrames.
EnableOutputbooltrueEnable audio output (playback).
EnableInputboolfalseEnable audio input (recording / microphone).
OutputDeviceIdstring?nullOutput device ID from AudioDeviceInfo.DeviceId. null = system default.
InputDeviceIdstring?nullInput device ID. null = system default.
HostTypeEngineHostTypeNoneAudio backend: WASAPI, ASIO, WDMKS, COREAUDIO, ALSA, JACK, AAUDIO, OPENSL, WEBAUDIO. None lets the engine pick — leave it there unless you need ASIO.
FallbackToDefaultOnDisconnectbooltrueDevice unplugged mid-stream: true hops to the system default and hops back when it returns, false parks in DeviceDisconnected and waits.
C# — Common configurations
// Standard stereo output
var config = new AudioConfig { SampleRate = 48000, Channels = 2, BufferSize = 512 };

// Windows WASAPI, specific device, input disabled
var config = new AudioConfig
{
    SampleRate     = 48000,
    Channels       = 2,
    BufferSize     = 512,
    HostType       = EngineHostType.WASAPI,
    OutputDeviceId = myDevice.DeviceId,
    EnableInput    = false
};

// 8-channel multi-output device
var config = new AudioConfig
{
    Channels = 8
};

// A 2-in / 8-out interface — the two directions are not the same width
var config = new AudioConfig
{
    OutputChannels = 8,
    InputChannels  = 2,
    EnableInput    = true
};
âš ī¸

What you ask for is a request. A device that cannot serve the width gets adapted to the nearest one it supports, and nothing in AudioConfig tells you which that was. Anything drawing physical sockets — or deciding how far a per-source route may reach — must read the real numbers off the engine:

C# — what actually opened
int outs = OwnaudioNet.Engine!.ActualOutputChannels;  // physical playback channels
int ins  = OwnaudioNet.Engine!.ActualInputChannels;   // physical capture channels

// Before Start these fall back to the requested width; after it they are the truth.
â„šī¸

At most 16 channels are routable per source — see Channel routing. AudioConfig.MaxRouteChannels is that limit, and Validate() rejects a channel selector past it.

Checking a config before the engine sees it

A configuration is rejected before a single device is touched, and "invalid audio configuration" on its own leaves you guessing between nine settings. The overload that takes an out says which field it was — this is what the engine logs, too.

C#
if (!config.Validate(out string? why))
    throw new InvalidOperationException(why);
    // e.g. "OutputRingMilliseconds 5000 is outside 1..2000"
    //      "OutputChannelSelectors must hold 4 distinct channels below 16"

The bounds behind those messages are on AudioConfig as constants, so a settings dialog can clamp instead of guessing: MaxOutputRingMilliseconds, MinRingFrames, MaxRingFrames and MaxRouteChannels.

Next