This issue comes out of a cross-SDK conformance audit comparing every Sendspin client/server implementation (aiosendspin, sendspin-cli, sendspin-cpp, sendspin-dotnet, sendspin-go, sendspin-js, sendspin-jvm, sendspin-rs, SendspinKit) against the spec. sendspin-cpp is treated as the reference implementation throughout — it most closely matches the spec's language and is the only SDK that runs on the constrained-embedded target the spec was originally written for.
The spec's Playback Synchronization section names three rules
but is loose about how to implement them. Across nine SDKs we found wide
divergence — 200× variation in late-chunk drop thresholds, two distinct
"drift correction" clusters, and zero implementations of the spec's
underrun handshake. Recommendations below are anchored on sendspin-cpp,
which is the only SDK that stays inside the spec's "sample insertion /
deletion with interpolation" vocabulary and has hard-coded thresholds we
can lift wholesale.
1. Make rule #1 (drift correction) prescriptive
Add to Playback Synchronization:
Clients SHOULD use a two-tier correction strategy: a soft tier that
inserts or drops a single sample/frame using neighbour-averaging
interpolation, and a hard tier that inserts silence (when ahead of
the timeline) or skips frames (when behind).
Clients SHOULD engage the soft tier when |sync error| ≥ 100 µs and
the hard tier when |sync error| ≥ 5 ms. Clients SHOULD reanchor
(clear buffers and restart from the next chunk's timestamp) when
|sync error| ≥ 500 ms.
Clients MAY substitute a resampler or playback-rate adjustment for the
soft tier provided the resulting end-to-end sync error stays within
the same envelope.
These are the constants from sendspin-cpp/src/sync_task.cpp:36-39:
SOFT_SYNC_THRESHOLD_US = 100, HARD_SYNC_THRESHOLD_US = 5000,
HARD_SYNC_SETTLE_THRESHOLD_US = 500.
2. Make rule #3 (late chunks) prescriptive
Add:
Clients SHOULD drop any audio chunk whose scheduled play time is more
than 5 ms before the end of currently buffered/playing audio.
Clients MAY trim the stale prefix of a chunk whose tail is still in
the future.
This matches sendspin-cpp (5 ms) and sendspin-cli (expected-next-timestamp
trim). Today thresholds range from 5 ms (cpp, cli) to 50 ms (go, SendspinKit)
to 1 s (jvm).
3. Decide on rule #2 (underrun handshake)
This is the most material non-conformance finding from the audit: no
implementation sends the spec's state: 'error' → mute → buffer →
state: 'synchronized' handshake on buffer underrun. Several SDKs
(sendspin-js, sendspin-rs, SendspinKit) even define the error
state in their types but never emit it.
The practical consequence: a Sendspin server today has no way to learn
that a player has lost sync. Every client looks permanently synchronized
even when playing silence or stale audio.
Two options, pick one:
- (a) Keep the rule. Mandate the handshake, and we'll need every
SDK to fix it (issues already filed against each SDK).
- (b) Drop the rule. Acknowledge that the de-facto SDK pattern is
"silence on underrun, recover silently" and remove the
error/synchronized requirement from the spec.
Source audit doc: docs/stream-sync-correction.md
Full audit branch: claude/stream-sync-correction-sdks-AWoNC
Per-SDK digest: sdk-issues digest
This issue comes out of a cross-SDK conformance audit comparing every Sendspin client/server implementation (
aiosendspin,sendspin-cli,sendspin-cpp,sendspin-dotnet,sendspin-go,sendspin-js,sendspin-jvm,sendspin-rs,SendspinKit) against the spec.sendspin-cppis treated as the reference implementation throughout — it most closely matches the spec's language and is the only SDK that runs on the constrained-embedded target the spec was originally written for.The spec's Playback Synchronization section names three rules
but is loose about how to implement them. Across nine SDKs we found wide
divergence — 200× variation in late-chunk drop thresholds, two distinct
"drift correction" clusters, and zero implementations of the spec's
underrun handshake. Recommendations below are anchored on
sendspin-cpp,which is the only SDK that stays inside the spec's "sample insertion /
deletion with interpolation" vocabulary and has hard-coded thresholds we
can lift wholesale.
1. Make rule #1 (drift correction) prescriptive
Add to Playback Synchronization:
These are the constants from
sendspin-cpp/src/sync_task.cpp:36-39:SOFT_SYNC_THRESHOLD_US = 100,HARD_SYNC_THRESHOLD_US = 5000,HARD_SYNC_SETTLE_THRESHOLD_US = 500.2. Make rule #3 (late chunks) prescriptive
Add:
This matches
sendspin-cpp(5 ms) andsendspin-cli(expected-next-timestamptrim). Today thresholds range from 5 ms (cpp, cli) to 50 ms (go, SendspinKit)
to 1 s (jvm).
3. Decide on rule #2 (underrun handshake)
This is the most material non-conformance finding from the audit: no
implementation sends the spec's
state: 'error'→ mute → buffer →state: 'synchronized'handshake on buffer underrun. Several SDKs(
sendspin-js,sendspin-rs,SendspinKit) even define theerrorstate in their types but never emit it.
The practical consequence: a Sendspin server today has no way to learn
that a player has lost sync. Every client looks permanently
synchronizedeven when playing silence or stale audio.
Two options, pick one:
SDK to fix it (issues already filed against each SDK).
"silence on underrun, recover silently" and remove the
error/synchronized requirement from the spec.
Source audit doc:
docs/stream-sync-correction.mdFull audit branch:
claude/stream-sync-correction-sdks-AWoNCPer-SDK digest: sdk-issues digest