Step 6 of 8
Network Synchronization
One timeline, several machines, under 5 ms apart. Namespace: OwnaudioNET
One device is the server, the others are clients. Every client plays its own copy of the audio on its own clock, like a standalone player, and follows the server's transport: when the server plays, the client goes in where the server is; when it stops, the client stops where it stands; when it jumps, the client jumps after it. What the network adds is a tiny tempo trim (at most Âą0.5%) that keeps the two heard positions together, and â rarely â one seek when they get more than 80 ms apart.
The network never writes the client's clock. Losing the server is just a trim of 1.0 â nothing jumps, and with allowOfflinePlayback the music simply plays on. Create and start the AudioMixer before starting the server or client; the timeline tracks it follows are the mixer's FileSource and GroupSource sources.
How it stays together
- Heard position, not rendered. Both sides compare the song position coming out of the speaker â the rendered position minus the device buffer and driver latency â so two machines with different buffer sizes don't sit a constant 20 ms apart.
- Clock offset, NTP style. The client pings twice a second; only the three fastest of the last 20 round trips count, since a slow trip was queued somewhere and queueing is never symmetric. All timestamps are monotonic, never the wall clock.
- Smoothed positions. The server sends its position 20 times a second while playing (once a second stopped, at once on a jump). A line through the last 1.5 s of readings hides the device-block steps on both sides.
- Trim, then seek. Nothing happens inside 3 ms. Above that the tempo is trimmed in proportion (a gap closes in about 4 s); only two readings in a row over 80 ms cause a seek, followed by 1.5 s of rest.
Server Mode
The server answers the clients' pings, sends them its heard position, and announces itself once a second on each network card's broadcast address (port + 1) so clients can find it. Control playback on the server as usual â the clients follow.
// Prerequisites: Initialize + Start + Create AudioMixer
await OwnaudioNet.InitializeAsync();
OwnaudioNet.Start();
var mixer = new AudioMixer(OwnaudioNet.Engine!.UnderlyingEngine);
mixer.Start();
var track = new FileSource("backing.mp3");
mixer.AddSource(track);
await OwnaudioNet.StartNetworkSyncServerAsync(port: 9876);
// From this point, control playback normally â clients follow play, pause, stop and seek:
track.Play();
mixer.Seek(42.0);
OwnaudioNet.StopNetworkSync();Client Mode
The client loads the same song on its side and leaves the transport to the server. It does not need to call Play().
await OwnaudioNet.InitializeAsync();
OwnaudioNet.Start();
var mixer = new AudioMixer(OwnaudioNet.Engine!.UnderlyingEngine);
mixer.Start();
var clientTrack = new FileSource("backing.mp3");
mixer.AddSource(clientTrack);
await OwnaudioNet.StartNetworkSyncClientAsync(
serverAddress: null, // null = find the server by its announcement, or "192.168.1.100"
port: 9876,
allowOfflinePlayback: true); // keep playing if the server is lostThe trim needs the Rust-native chain (the default): it rides on the native tracks' tempo, on top of whatever Tempo you set. source.Tempo itself never shows it.
Stop Network Sync
OwnaudioNet.StopNetworkSync();Events & commands
Transport needs no messages. Anything else the clients should do â a tempo change, a song change â the server sends with BroadcastCommand; each command is sent three times and arrives once.
// Client
OwnaudioNet.NetworkSyncConnectionChanged += (_, e) =>
Console.WriteLine($"Network sync state: {e.OldState} â {e.NewState}");
OwnaudioNet.NetworkSyncCommandReceived += (_, e) =>
{
if (e.Command.Type == NetworkSyncProtocol.CommandType.Tempo)
clientTrack.Tempo = e.Command.TempoValue; // raised on a sync thread
};
// Server
var tempo = NetworkSyncProtocol.CreateTempoCommand(0, 1.05f, useSmooth: false);
OwnaudioNet.BroadcastCommand(ref tempo);
// Either side
var status = OwnaudioNet.GetNetworkSyncStatus();
Console.WriteLine($"{status.ConnectionState}, one-way {status.AverageLatency:F1} ms, off by {status.SyncDrift:F1} ms");A following client also writes one [NetSync] log line a minute â average and worst gap, the share within 5 ms, trims, seeks, jitter and round trip.
Use Cases
API Parameters
StartNetworkSyncServerAsync
| Parameter | Type | Default | Description |
|---|---|---|---|
port | int | 9876 | UDP port the clients ping; announcements go to port + 1. |
useLocalTimeOnly | bool | true | Kept for compatibility, no effect â the sync never needs the internet. |
StartNetworkSyncClientAsync
| Parameter | Type | Default | Description |
|---|---|---|---|
serverAddress | string? | null | Server host or IP address. null = find it by its announcements. |
port | int | 9876 | The server's UDP port. The client itself binds a free port. |
allowOfflinePlayback | bool | true | Keep playing when the server is lost; false pauses. |