Synchronization
Sample-accurate multi-track timeline synchronization. Namespace: OwnaudioNET.Synchronization
MasterClock
Every AudioMixer owns one MasterClock. Attach FileSource instances to it for drift-free multi-track playback. The clock advances automatically as the mixer processes audio frames.
Properties
| Property | Type | Description |
|---|---|---|
CurrentTimestamp | double | Current position in seconds. |
CurrentSamplePosition | long | Current position in samples (lock-free read). |
SampleRate | int | Sample rate in Hz. |
Channels | int | Channel count. |
Mode | ClockMode | Rendering mode: Realtime, Offline, NetworkServer, NetworkClient. |
IsNetworkControlled | bool | True when driven by a remote network server. |
Methods
// 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 to the Clock
FileSource implements both IMasterClockSource and ISynchronizable. Attach before calling Play().
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
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.
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:
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 Zones
When a source is attached to the master clock, its position is compared to the clock on every buffer. Correction is automatic:
| Zone | Drift Range | Action |
|---|---|---|
| Green | < SyncTolerance (20ms default) | No correction â within acceptable range. |
| Yellow | SyncTolerance âĻ SoftSyncTolerance (20â100ms) | Soft sync: gradual tempo adjustment up to Âą2%. |
| Red | > SoftSyncTolerance (100ms) | Hard sync: skip/fill buffer to realign immediately. |
The TrackDropout event on AudioMixer is raised when a Red Zone correction occurs, so you can log or react to sync issues.
Offline / File Rendering
Set ClockMode.Offline to render audio deterministically to a file without dropouts:
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();