Skip to content

ADR-318: Audio playback position reports - #324

Open
LautaroPetaccio wants to merge 10 commits into
mainfrom
feat/adr-318-audio-playback-position
Open

LautaroPetaccio wants to merge 10 commits into
mainfrom
feat/adr-318-audio-playback-position

Conversation

@LautaroPetaccio

@LautaroPetaccio LautaroPetaccio commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Proposes extending PBAudioEvent so renderers report the clip playback position while an AudioSource plays, in addition to the media state changes they already report. A scene asks for the reports with a new PBAudioSource.report_playback_position flag; state changes are reported whatever the flag says.

Why

Scenes that align gameplay or visuals with sound have no way to learn where a clip's playhead is: PBAudioSource.current_time is a write-only seek, and PBAudioEvent carries only a media state and a counter. Measured on the Unity explorer, audible playback starts 100 to 250 ms after the scene asks for it, with a different value on each start, which is as wide as a rhythm game's hit window. PBVideoEvent already reports current_offset and tick_number for video.

Contents

  • Problem statement with the measurements
  • Protocol fields, and why the reports are opt-in rather than always on
  • Why a report carries a tick rather than a timestamp, and why the per-tick clock history belongs in the client library rather than in every scene
  • What the reports do not remove: the renderer's sampling granularity, and the output latency between the decoder position a report carries and the moment a sample leaves the speaker
  • Renderer behaviour, including bounds on the reporting rate
  • Alternatives considered (scheduled playback, readable current_time, higher tick rates, onset detection), compatibility

Notes on two decisions

The reporting rate is bounded here rather than left to each renderer. A survey of the three explorers found they already disagree on video: one reports on every frame the position changes, one at most once per second of playback, one once the position has moved half a second. Scenes cannot write against that. The document now states at most one report per tick, since the tick is the resolution of the stamp and finer carries nothing a scene can place in time, and at least one per second of playback so a scene converges promptly. All three existing implementations sit inside those bounds.

The specification stays protocol-only. An earlier revision showed the SDK implementation and a scene usage example. Both are gone; what an SDK does with these fields is a client-library concern and the code lives in the js-sdk-toolchain pull request. What survives is the requirement that the tick-to-clock correlation belongs in the library rather than in each scene, and that a report must not be dropped when its tick is missing from the history. That requirement is also the protocol rationale for stamping a tick rather than a wall-clock time.

Status: Draft. Numbering: main reaches 316 and open PRs use 317, so 318 is the next free number; a few open PRs sit out of sequence at 323, 400 and 401.

Related

Related pull requests

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 15, 2026 •

Copy link
Copy Markdown

Deploying adr with  Cloudflare Pages  Cloudflare Pages

Latest commit: 8e98c96
Status: ✅  Deploy successful!
Preview URL: https://d92bb443.adr-cvq.pages.dev
Branch Preview URL: https://feat-adr-318-audio-playback.adr-cvq.pages.dev

View logs

@LautaroPetaccio
LautaroPetaccio force-pushed the feat/adr-318-audio-playback-position branch from 2f78c61 to 7282ff5 Compare September 15, 2026 23:38
@LautaroPetaccio
LautaroPetaccio marked this pull request as ready for review September 15, 2026 23:38
@LautaroPetaccio
LautaroPetaccio requested a review from a team as a code owner September 15, 2026 23:38
@pravusjif

Copy link
Copy Markdown
Member

The only potential problem with that is that both the scene and the engine could modify the property (current_time) "at the same time", and also if any other property of the component is modified from the scene, the engine receives the full component data through the CRDT command, so wouldn't know if the current_time was modified or not by the scene, since that property was designed to tell the engine to START at a specific time of the audio.

What we can do is follow the same approach we do for videos, constantly updating their videoEvent with the current time in that other component, WDYT?

@LautaroPetaccio

LautaroPetaccio commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor Author

You're right on both counts, and that is how it works now: the position goes in PBAudioEvent, not in PBAudioSource. The renderer never writes PBAudioSource, so current_time stays a write-only seek, neither side clobbers the other, and a full-component put can never be mistaken for a new seek. It is the VideoEventsSystem pattern applied to audio (decentraland/unity-explorer#10123).

The abstract names the PBVideoEvent precedent, and the rejected "readable current_time" alternative carries both of your reasons.

### Renderer behaviour

- Renderers keep appending an `AudioEvent` on every media state change, as today.
- While an `AudioSource` is in `MS_PLAYING`, renderers also append a report at least every 15 scene ticks (about twice a second at the reference tick rate), carrying `tick_number`, `current_offset` and `clip_length`. This is the same mechanism the Unity explorer uses for `PBVideoEvent` in [`VideoEventsSystem`](https://github.com/decentraland/unity-explorer/blob/fe6974465b0d2e3a70eeb1ba2da3cb87df27e654/Explorer/Assets/DCL/SDKComponents/MediaStream/Systems/VideoEventsSystem.cs#L63), applied to audio sources.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renderers also append a report at least every 15 scene ticks (about twice a second at the reference tick rate)

It'd be a very strange approach leading to heavy desync.
VideoEventsSystem doesn't do it - it writes the component off every frame if the video has progressed

@LautaroPetaccio LautaroPetaccio Sep 17, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. There is no cadence now: a report goes out whenever the state or the clip position differs from the last propagated value, exactly as VideoEventsSystem does. A playing clip reports every frame, and a paused or stopped one emits nothing.

Comment thread content/ADR-318-audio-playback-position-reports.md Outdated
Comment thread content/ADR-318-audio-playback-position-reports.md Outdated
Comment thread content/ADR-318-audio-playback-position-reports.md Outdated
Address review: reports go out whenever the clip position changes, as
VideoEventsSystem does, not on a fixed cadence; the scene-clock history that
resolves a report's tick is part of the SDK, shown in full, and no round-trip
estimation is involved because renderer and scene key on the same tick.
The text claimed the transport delay cancels out by construction and that the
remaining error is one tick. The first half is what the tick stamp does; the
second was wrong. Two terms survive it: the sampling granularity of the
playhead, and the renderer's output latency, which is the gap between the
decoder position a report carries and the moment a sample leaves the speaker.
The second is tens of milliseconds and is precisely what a scene aligning to
audible sound wants removed, so it is named rather than implied away.

Also correct the SDK bullets: delivery is once per scene frame with the newest
report, not one callback per appended report.
Players rewind the playhead on stop and on reaching the end, so the last report
of a finished clip carries zero, not the clip length. A scene driving a
progress bar from current_offset would see it snap back instead of complete.
@LautaroPetaccio

LautaroPetaccio commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor Author

Status across the five pull requests.

Protocol (decentraland/protocol#488): three optional fields on PBAudioEvent, tick_number, current_offset and clip_length.

Renderer (decentraland/unity-explorer#10123): a report goes out whenever the media state changed or the clip position moved, which is the rule VideoEventsSystem applies to video. The emit rule is ShouldReport, beside IsNaturalFinish, covered by tests that need no audio device. Protocol bindings are regenerated, so the branch compiles as it stands.

SDK (decentraland/js-sdk-toolchain#1624): registerAudioPlaybackEntity delivers the newest position report of the frame, already resolved against the scene clock at the tick it was sampled in. getSceneTimeAtTick exposes the same lookup for scenes handling raw reports and for video.

Test scene (decentraland/sdk7-test-scenes#99): three beat cubes contrasting the scene's own clock, the resolved reading, and the same reading timed on arrival, against a clip that beeps on every whole second.

Documentation and skills: decentraland/documentation#609 and decentraland/sdk-skills#97.

One behaviour worth flagging: a finished clip reports a zero offset rather than clip_length, because players rewind the playhead on stop and at the end of a clip. A scene tracking completion reads the state transition out of playing.

The SDK and renderer pull requests both hold a branch pin on @dcl/protocol, so their dependency checks stay red until #488 merges.

The proposal listed a raw-report callback beside a resolved one. They were the
same delivery; the resolved payload carries the raw report as a field, so the
raw entry point added no reachable information and was the shorter name a scene
author would pick, leading back to per-scene histories of clock snapshots.

Record the choice and why the raw form was dropped: reports carrying no position
are media-state changes, which registerAudioEventsEntity already delivers.
Adds PBAudioSource.report_playback_position. A position report is written
whenever the playhead moves, far more often than a media state changes, and a
scene can hold many more audio sources than video players, which are capped by
a prioritisation mechanism. AudioEvent is also a grow-only set bounded per
entity, so an unconditional reporter evicts its own state history within
seconds. State changes are reported whatever the flag says.

State a reporting rate rather than leaving it to each renderer: at most one
report per tick, since the tick is the resolution of the stamp, and at least one
per second of playback so a scene converges promptly. Existing video
implementations already differ from one another inside that band, which is the
reason to bound it here.

Drop the SDK implementation and the scene usage example. What the SDK does with
these fields is a client-library concern, and the code duplicated what the
js-sdk-toolchain pull request already carries. What survives is the requirement:
the tick-to-clock correlation belongs in the library, not in every scene, and a
report must not be dropped when its tick is missing from the history. That
requirement is also the protocol rationale for stamping a tick rather than a
wall-clock time, so it belongs here.

Move the accuracy limits out of the SDK section. Sampling granularity and output
latency bound what any renderer can deliver through these fields, so they are
protocol-level, not a property of one client library.
@LautaroPetaccio

Copy link
Copy Markdown
Contributor Author

You're right, and the spec is protocol-only now. The SDK implementation and the scene usage example are gone.

What I kept is the requirement rather than the code: the tick-to-clock correlation belongs in the client library instead of each scene, and a report must not be dropped when its tick is missing from the history. That one is really protocol rationale, since it is why a report carries a tick instead of a wall-clock timestamp, so removing it would leave a future reader asking why the field is shaped that way. The implementation itself is in decentraland/js-sdk-toolchain#1624.

I also moved the accuracy limits out of that section. Sampling granularity and output latency bound what any renderer can deliver through these fields, so they belong next to the protocol rather than under one client library.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants