Troubleshooting

What happened, why it happened, and the line of code that fixes it.

๐Ÿ”ฆ

Before anything else, turn the log on. It is silent by default and it will usually tell you the answer outright.

C#
using Logger;

await OwnaudioNet.InitializeAsync(config, logLevel: Log.Level.Info);

// or flip it at any time while running
Log.LoggerLevel = Log.Level.Info;

// No console (desktop, Android, iOS)? Write it to a rotating file, or take it yourself
string? path = Log.ToFile();
Log.Sink += (level, line) => _logger.Log(Map(level), line);
โ„น๏ธ

Log.ToFile() caps itself โ€” around 2 MB per file, three rotated generations, older ones pruned when it opens โ€” so leaving it on in a released app will not fill anyone's drive. See log destinations.

Nothing plays

Why: almost always one of four things โ€” the engine never started, the mixer never started, the source was never added, or something is at zero volume.

C# โ€” walk down the chain
Console.WriteLine(OwnaudioNet.IsInitialized);   // false โ†’ InitializeAsync never completed
Console.WriteLine(OwnaudioNet.IsRunning);       // false โ†’ you forgot Start()
Console.WriteLine(mixer.IsRunning);             // false โ†’ mixer.Start()
Console.WriteLine(mixer.SourceCount);           // 0     โ†’ AddSource()
Console.WriteLine(mixer.MasterVolume);          // 0     โ†’ there it is
Console.WriteLine(source.State);                // Stopped โ†’ Play()
Console.WriteLine(mixer.LeftPeak);              // 0 while playing โ†’ nothing reaching the bus

Two more that catch people out:

Crackling, clicks or stuttering

Why: the engine asked for the next block of audio and it wasn't ready. The render callback is not keeping up with the buffer size you gave it.

Fix it in this order โ€” each step costs a little more latency than the last.

StepDo thisCosts
1Raise AudioConfig.BufferSize from 512 to 1024~10 ms more latency
2Raise the mixer's bufferSizeInFrames to 1024 for 20+ tracksSlightly coarser position updates
3Look at what your own code is doing on the audio pathโ€”
C# โ€” the usual cure
// 8+ sources, or 2+ VST plugins on the master bus
config.BufferSize = 1024;
await OwnaudioNet.InitializeAsync(config);
OwnaudioNet.Start();

var mixer = new AudioMixer(OwnaudioNet.Engine!.UnderlyingEngine, bufferSizeInFrames: 1024);
mixer.Start();

To find out whether the machine is really running out of time, measure the callback. With an AudioMixer the mix is rendered inside the device callback, so no ring ever drains and every underrun counter legitimately reads zero โ€” the load is the only signal there is:

C# โ€” with a mixer
mixer.ResetSessionLoad();                    // start counting from a settled playback
// ... let it run ...

if (mixer.SessionLoad is { } load)
    Console.WriteLine(load);
    // load avg 31.4% peak 96.2%, block avg 3.35ms peak 10.26ms over 900 blocks, 0 underrun frames

PeakLoad approaching 1.0 is the crackle you are hearing: that block used up the whole period it had. The average being comfortable says nothing โ€” one late block is one click. If instead you push samples in yourself with OwnaudioNet.Send, the ring is what starves, and the engine counts that:

C# โ€” with Send
OwnaudioNet.Engine!.BufferUnderrun += (_, e) =>
    Console.WriteLine($"Underrun: {e.MissedFrames} frames at {e.Position}");

Console.WriteLine($"Underruns so far: {OwnaudioNet.Engine!.TotalUnderruns}");
โš ๏ธ

If you wrote a StreamingSource generator, check it for allocations first. A new, a LINQ query or an interpolated string inside the render callback is the most common cause of clicks that survive every buffer increase. If you also lowered LookAheadSeconds, put it back to the default before blaming anything else โ€” a callback that cannot render faster than real time starves a short feed first.

The app freezes at startup

Why: a blocking engine call on the UI thread. Opening a device is not instant โ€” on Linux, PulseAudio enumeration alone can take five seconds.

C#
// โŒ freezes the window
OwnaudioNet.Initialize(config);
OwnaudioNet.Shutdown();

// โœ…
await OwnaudioNet.InitializeAsync(config);
await OwnaudioNet.ShutdownAsync();

The same applies to Stop() / StopAsync(), which waits for the audio thread to join. Everything else โ€” Play, Seek, Volume, AddSource โ€” is safe to call from anywhere.

The audio interface was unplugged

Why: the device vanished mid-stream. By default the engine hops to the system default and hops back when the device returns; if you turned that off it parks in DeviceDisconnected and waits.

C#
config.FallbackToDefaultOnDisconnect = true;   // the default โ€” keep playing on built-in output

// Be told when the stream itself dies, instead of just going quiet
mixer.StreamFaulted += (_, e) =>
{
    if (e.Kind == AudioStreamFaultKind.DeviceNotAvailable)
        ShowToast(e.Direction == AudioStreamDirection.Input
            ? "Microphone disconnected โ€” every input source is silent"
            : "Audio device disconnected");
};

// Be told when the engine moves to a different device
OwnaudioNet.Engine!.OutputDeviceChanged += (_, e) =>
    Console.WriteLine($"Now playing on {e.NewDeviceInfo.Name}");

Tracks drift apart

Why: the sources are free-running on their own decoders instead of riding the master clock. They start together and slowly separate. AddSource and AddSourcePrepared attach every source they register, so this shows up when a track was started before it reached the mixer โ€” attaching afterwards does not retro-fix the drift.

C#
// Register first, start second
foreach (var src in sources)
    mixer.AddSourcePrepared(src);   // attaches to mixer.MasterClock

mixer.StartPreparedSources(0.0);

// Not sure a track made it onto the clock? Ask it.
Console.WriteLine(sources[0].IsAttachedToClock);   // false = free-running

If they are attached and still drift, ask the mixer what the callback is costing โ€” a machine that cannot render a block inside its period is the usual culprit:

C#
if (mixer.SessionLoad is { } load && load.HasOverrun)
    Console.WriteLine($"late block โ€” {load}");   // PeakLoad >= 1.0

Treat an overrun as the same symptom as crackling and give the mix a larger buffer. Don't reach for FileSource.SyncDiagnostics here: in 4.x it only echoes the configured thresholds back, its AdaptiveScale is hardwired to 1.0 and IsRelaxed is never true. See Callback load.

A file won't load

Why: either the format isn't in the built-in set, or the file is damaged, or the path is wrong. An Ownaudio.Core.Common.AudioException means the decoder saw the file and gave up on it โ€” the real cause is in its InnerException.

Handled natively, on every platform, with no extra install: MP3, FLAC, WAV (PCM/ADPCM), AAC, ALAC, MP4/M4A, OGG/Vorbis, AIFF.

C#
try
{
    var source = new FileSource(path, targetSampleRate: sr, targetChannels: ch);
    if (source.Duration <= 0)
        Console.WriteLine("Decoded, but empty โ€” the file is probably truncated.");
}
catch (Ownaudio.Core.Common.AudioException ex)
{
    Console.WriteLine($"Cannot decode {path}: {ex.InnerException?.Message ?? ex.Message}");
}

Anything outside that list โ€” Opus, WMA, AC3 โ€” will not open, and there is no fallback decoder to install. Convert the file ahead of time instead. See Decoding.

An effect does nothing

Why: three candidates, in order of likelihood.

C# โ€” the wrapper swap
var chain = new SourceWithEffects(source);
chain.AddEffect(new ReverbEffect { Mix = 0.3f });

mixer.RemoveSource(source.Id);   // the raw one comes out
mixer.AddSource(chain);          // the wrapper goes in

If you'd rather see it than guess: an effect tap hands you the signal on both sides of the chain while it plays. Identical spectra means the chain really is doing nothing to the audio, which rules the first two causes in or out in one look.

C#
using var analyzer = new EffectSpectrumAnalyzer(mixer.CreateEffectTap(source.Id));

if (analyzer.Update())
{
    // dry == wet โ‡’ nothing in the chain is touching the audio
    Compare(analyzer.PreMagnitudesDb, analyzer.PostMagnitudesDb);
}

A VST3 plugin won't load

Why: it's an instrument, its architecture doesn't match, or its audio setup failed for the current sample rate.

C#
VST3PluginHost host;
try { host = await VST3PluginHost.CreateAsync(path); }
catch (Exception ex) { ShowError($"Could not load the plugin: {ex.Message}"); return; }

if (!host.IsEffect)
{
    host.Dispose();
    ShowError(host.IsInstrument
        ? "That's an instrument โ€” only effect plugins can go in a slot."
        : "Unknown plugin type.");
    return;
}

if (!await host.InitializeAudioAsync(OwnaudioNet.Engine!.Config.SampleRate, maxBlockSize: 4096))
{
    host.Dispose();
    ShowError("The plugin could not run at this sample rate.");
    return;
}
โš ๏ธ

A 64-bit process cannot load a 32-bit plugin, and vice versa. On Apple Silicon an Intel-only plugin will not load either.

Memory keeps growing

Why: something that owns native memory is never being released. Two usual suspects.

C# โ€” always return input buffers
float[]? buffer = OwnaudioNet.Receive(out int sampleCount);
if (buffer != null)
{
    try     { /* use buffer[0..sampleCount-1] */ }
    finally { OwnaudioNet.ReturnInputBuffer(buffer); }   // never skip this
}

And dispose sources you replace. Loading a new file into a track without disposing the old FileSource leaks its decoder and ring buffer every time.

C#
mixer.RemoveSource(oldSource.Id);
oldSource.Dispose();

The recording is empty or out of time

Why: recording captures what leaves the mixer. If nothing was on the bus, the file is silence. If a live take lands late, that's the hardware capture delay.

C#
// Trim the capture delay off the front so the take lines up with the click
mixer.StartRecording("take.wav", compensateInputLatency: true);
// ...
mixer.StopRecording();

int trimmed = mixer.LastRecordingLatencyOffsetFrames;   // what was dropped
โš ๏ธ

StopRecording() is what finalizes and closes the WAV header. Kill the process without it and you get an unplayable file.

Exceptions you may see

TypeWhat it meansWhat to do
DeviceExceptionThe requested device could not be opened โ€” wrong ID, in exclusive use, or gone.Fall back to OutputDeviceId = null and re-enumerate.
DecoderExceptionThe file could not be decoded.Check the format against the supported list; convert the file if it falls outside.
StreamExceptionThe native audio stream failed to open or died.Try a larger buffer or a different host type.
HostApiNotAvailableExceptionThe HostType you asked for doesn't exist on this machine.Use EngineHostType.None and let the engine choose.
AsioDriverNotFoundExceptionNo ASIO driver installed.Install the interface's driver, or use WASAPI.
AbiVersionMismatchExceptionThe managed package and the native library are different versions.Clear bin/ and obj/, restore, and make sure only one OwnAudioSharp version is referenced.
AudioEngineExceptionA general engine-level failure.Turn on Log.Level.Error โ€” the message names the stage that failed.

FAQ

Which package do I need?

OwnAudioSharp for desktop with everything. OwnAudioSharp.Mobile for Android and iOS. OwnAudioSharp.Basic when you only want audio in and out and care about size. MIDI is its own package, and MT3 transcription is an optional add-on.

Do I need FFmpeg?

No โ€” and you cannot use it either. The Rust decoder handles the common formats out of the box and is the only decoder there is; the FFmpeg fallback of earlier versions was removed in 4.0.

How many sources can one mixer hold?

30 โ€” AudioConstants.MaxAudioSources. Past about eight, give the mixer a larger bufferSizeInFrames.

What latency can I expect?

BufferSize รท SampleRate. 512 frames at 48 kHz is ~10.6 ms; the low-latency preset's 128 frames is ~2.7 ms. If you push samples in with OwnaudioNet.Send, add the render ring โ€” AudioConfig.OutputRingMilliseconds, 100 ms by default, and the largest term of the three unless you lower it. An AudioMixer renders in the device callback and has no ring at all, so there the buffer is the whole story. A StreamingSource adds its own queue on top โ€” 0.12 s by default, and LookAheadSeconds if the generator is being played rather than scheduled. Then add the driver's own hardware latency. All three are readable once audio is flowing: OwnaudioNet.Engine!.OutputRingFrames for the ring the engine settled on, OwnaudioNet.InputLatencyFrames and OutputLatencyFrames for the driver. And check OutputCallbackFrames against FramesPerBuffer โ€” a driver that rounded your requested buffer size shows up nowhere else.

Can I run two mixers at once?

Yes, but one is almost always the right answer. If you do, register the one network sync should follow with OwnaudioNet.SetPrimaryAudioMixer().

Does it work with AOT and trimming?

Yes โ€” the managed side is AOT-compatible and trimmable, and the engine is native anyway.

Can I test without a sound card?

Yes. OwnaudioNet.Initialize(config, useMockEngine: true) gives you the whole API against a mock engine, which is how the library's own ~290 tests run in CI.

Why is my tempo change ignored?

Tempo and PitchShift are wired on FileSource and GroupSource. The other source types accept the value for compatibility but don't apply it. Tempo is also clamped to 0.8โ€“1.2.

Still stuck

Turn on Log.Level.Info, reproduce it, and open an issue with the log, your AudioConfig, and the platform. That is usually enough to spot it from the outside.