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.
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.
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 busTwo more that catch people out:
- Output went to a device nobody is listening to. Check
OwnaudioNet.GetOutputDevices()and which one is actually connected to speakers. - The file finished instantly. If
source.Durationis 0 the file didn't decode โ see the file won't load.
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.
| Step | Do this | Costs |
|---|---|---|
| 1 | Raise AudioConfig.BufferSize from 512 to 1024 | ~10 ms more latency |
| 2 | Raise the mixer's bufferSizeInFrames to 1024 for 20+ tracks | Slightly coarser position updates |
| 3 | Look at what your own code is doing on the audio path | โ |
// 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:
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 framesPeakLoad 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:
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.
// โ 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.
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.
// 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-runningIf 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:
if (mixer.SessionLoad is { } load && load.HasOverrun)
Console.WriteLine($"late block โ {load}"); // PeakLoad >= 1.0Treat 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.
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.
- You added effects to a source that is already in the mixer. A raw source has no chain โ the mixer has to be holding the
SourceWithEffectswrapper, not the source inside it. Enabledis false, orMixis at 0.- The effect isn't ready. VST3 processors must have
InitializeAudioAsync()completed before they are added;IsReadytells you.
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 inIf 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.
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.
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.
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.
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.
// 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 droppedStopRecording() is what finalizes and closes the WAV header. Kill the process without it and you get an unplayable file.
Exceptions you may see
| Type | What it means | What to do |
|---|---|---|
DeviceException | The requested device could not be opened โ wrong ID, in exclusive use, or gone. | Fall back to OutputDeviceId = null and re-enumerate. |
DecoderException | The file could not be decoded. | Check the format against the supported list; convert the file if it falls outside. |
StreamException | The native audio stream failed to open or died. | Try a larger buffer or a different host type. |
HostApiNotAvailableException | The HostType you asked for doesn't exist on this machine. | Use EngineHostType.None and let the engine choose. |
AsioDriverNotFoundException | No ASIO driver installed. | Install the interface's driver, or use WASAPI. |
AbiVersionMismatchException | The managed package and the native library are different versions. | Clear bin/ and obj/, restore, and make sure only one OwnAudioSharp version is referenced. |
AudioEngineException | A 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.