Step 5 of 8

Timeline & Synchronization

Why two files started together drift apart, and what to do about it. Namespace: OwnaudioNET.Synchronization

Start two files at "the same time" and they will separate — different decoders, different buffer boundaries, different rounding. Over a few minutes it becomes an audible flam. The master clock is the cure: one shared timeline that every attached source is continuously nudged back onto.

MasterClock

Every AudioMixer owns one, and it is the single answer to "where are we". It advances by itself as the mixer processes frames — you read it for your playhead, and seek it to move everything at once.

Properties

PropertyTypeDescription
CurrentTimestampdoubleCurrent position in seconds.
CurrentSamplePositionlongCurrent position in samples (lock-free read).
SampleRateintSample rate in Hz.
ChannelsintChannel count.
ModeClockModeRendering mode: Realtime, Offline, NetworkServer, NetworkClient.
IsNetworkControlledboolTrue when driven by a remote network server.

Methods

C#
// Seek the timeline
mixer.MasterClock.SeekTo(double timestamp);   // position in seconds
mixer.MasterClock.Reset();                    // seek to 0.0

// Advance manually (Offline mode only)
mixer.MasterClock.Advance(int frameCount);

// Convert between units
long   samples = mixer.MasterClock.TimestampToSamplePosition(5.0);  // 5 seconds → samples
double seconds = mixer.MasterClock.SamplePositionToTimestamp(240000L);

Attaching sources

FileSource implements both IMasterClockSource and ISynchronizable. Attach before calling Play() — attaching afterwards will not undo drift that already happened.

C#
var mixer   = new AudioMixer(OwnaudioNet.Engine!.UnderlyingEngine, 1024);
var vocals  = new FileSource("vocals.wav",  targetSampleRate: 48000, targetChannels: 2);
var backing = new FileSource("backing.mp3", targetSampleRate: 48000, targetChannels: 2);

// 1. Attach to clock (BEFORE Seek and Play)
vocals.AttachToClock(mixer.MasterClock);
backing.AttachToClock(mixer.MasterClock);

// 2. Optional: timeline offsets (backing starts 2.5 seconds into the session)
backing.StartOffset = 2.5;

// 3. Seek and play
mixer.MasterClock.SeekTo(0.0);
vocals.Seek(0);
backing.Seek(0);
vocals.Play();
backing.Play();

mixer.AddSource(vocals);
mixer.AddSource(backing);
mixer.Start();

Detach on Stop

C#
vocals.Stop();
vocals.DetachFromClock();   // allows independent playback or reuse

mixer.RemoveSource(vocals.Id);
mixer.MasterClock.SeekTo(0.0);

ISynchronizable Interface

Sources that support sample-accurate position tracking implement ISynchronizable. Use this for precise position display or external sync.

C#
if (source is ISynchronizable sync)
{
    long   samplePos = sync.SamplePosition;
    double posInSec  = samplePos / (double)OwnaudioNet.Engine!.Config.SampleRate;

    // Force hard resync (jumps buffer to match clock position)
    sync.ResyncTo(targetSamplePosition);
}

Accurate Position Display

Interpolate between timer ticks for smooth UI updates at 30+ FPS without polling the audio thread too often:

C#
private double _lastEnginePos;
private double _lastEnginePosAt;
private readonly Stopwatch _watch = Stopwatch.StartNew();

// Update at 30 Hz (33ms timer)
private void OnPositionTimer()
{
    double enginePos = mixer.MasterClock.CurrentTimestamp;
    double nowSec    = _watch.Elapsed.TotalSeconds;

    if (enginePos != _lastEnginePos)
    {
        _lastEnginePos   = enginePos;
        _lastEnginePosAt = nowSec;
    }

    // Interpolate between engine updates
    double displayPos = _lastEnginePos + (nowSec - _lastEnginePosAt);
    CurrentPositionSeconds = displayPos;
}

Drift correction, in three zones

On every buffer, an attached source's position is compared to the clock. What happens next depends on how far off it is — and all of it is automatic.

ZoneDrift RangeAction
Green< SyncTolerance (5ms default)No correction — within acceptable range.
YellowSyncTolerance â€Ļ SoftSyncTolerance (5–25ms)Soft sync: gradual tempo adjustment up to Âą2%.
Red> SoftSyncTolerance (25ms)Hard sync: skip/fill buffer to realign immediately.
â„šī¸

Red Zone corrections are handled inside the native chain and are not surfaced as an event. To see how hard the sync engine is working, read FileSource.SyncDiagnostics.

Adaptive Tolerance

The sync engine monitors red-zone hit frequency and automatically widens the tolerance thresholds on hardware that cannot sustain the strict defaults. No configuration is required — the system self-tunes at runtime.

AdaptiveScaleGreen ZoneYellow ZoneMeaning
1.05 ms25 msOptimal — strict defaults active.
2.010 ms50 msModerate load — tolerances doubled.
4.020 ms100 msHigh load — fully relaxed (original values).

The scale increases by 0.5 steps when â‰Ĩ 5 red-zone hits occur within a 3-second window, and decreases by 0.5 steps after 8 consecutive seconds in the green zone.

âš ī¸

A scale above 1.0 means the engine is compensating for processing pressure. Use SyncDiagnostics to detect this condition — it may indicate a bug or performance issue in a custom effect or processing pipeline.

C#
SyncDiagnosticsSnapshot diag = fileSource.SyncDiagnostics;

if (diag.IsRelaxed)
{
    Console.WriteLine($"Adaptive tolerance active — Scale: {diag.AdaptiveScale:F1}x");
    Console.WriteLine($"Green zone: {diag.EffectiveSyncToleranceMs:F1} ms (baseline: 5 ms)");
    Console.WriteLine($"Yellow zone: {diag.EffectiveSoftSyncToleranceMs:F1} ms (baseline: 25 ms)");
    Console.WriteLine($"Red zone hits in window: {diag.RedZoneHitsInWindow}");
}

SyncDiagnosticsSnapshot is a readonly struct — zero heap allocation, safe to call from any thread.

Offline rendering

Realtime mode drops audio it can't produce in time — right for playback, wrong for a bounce. Offline mode makes the mix thread wait for every source, so the file comes out identical no matter how busy the machine is.

C#
mixer.RenderingMode = ClockMode.Offline;
mixer.StartRecording("render.wav");
mixer.Start();
// In Offline mode the mix thread blocks until all sources provide data
// — no dropouts, deterministic output
mixer.Stop();
mixer.StopRecording();

Next