Synced audio
Use Plot.toolkit.syncedAudio()
to keep a local sound aligned with Plot.time.
Give every game copy the same audio assets and timestamp; the player loads the
clips, skips elapsed content when late, and corrects its position as the shared
clock changes. Plot.toolkit contains optional conveniences built from the
public SDK and browser APIs.
Scheduling timestamps are game-time milliseconds, as returned by
Plot.time.now(). Audio offsets and durations are seconds, matching browser
audio conventions. play(5_000) means game time five seconds, not five seconds
after the call and not a Unix timestamp. If that clip finishes loading at game
time ten seconds, playback begins approximately five seconds into it.
These controls affect this object on this device. They do not broadcast commands or audio. Store a shared cue in Game State when everyone should hear the same sound, including late joiners and recovering players.
Choose a playback mode
Loop transitions use the decoded clips' exact lengths without waiting for a JavaScript callback. Late playback skips an elapsed intro and starts at the current loop position. The toolkit adds no silence between clips or repetitions; the assets must still have matching boundaries to sound seamless. Trim silence and encoder padding, and audition the intro transition and the loop wrap.
Sources and mode are immutable. Create another object to change them. Relative URLs resolve against the game page's base URL. Use public game assets or URLs that allow the game to fetch them; cross-origin assets need CORS access. Asset requests do not include browser credentials. Supported URLs use HTTP or HTTPS and cannot contain embedded usernames or passwords. Ordinary browser restrictions still apply, including HTTPS pages blocking insecure assets.
Pause, resume, and seek
pause() takes no arguments and freezes this object's intended position. It
cancels loading-related playback and future starts, while allowing useful asset
loading to finish. Repeating it does not advance the checkpoint. Pausing an
object with no playback request, including an object only being preloaded, does
nothing. It does not stop the shared clock or other audio objects.
resume(offsetSeconds, resumeAtGameTimeMs) works with or without a previous
pause. It replaces the current playback request immediately, then remains silent
until its timestamp. A client receiving resume(12, 20_000) at game time 23
seconds starts at position 15 seconds. An offset beyond a standard clip's end
finishes successfully without playing. In loop mode, offsets can span any
number of repetitions.
play(T) is equivalent to resume(0, T). Reapplying the same offset/timestamp
pair while pending, active, or already ended does not restart it. A different
pair replaces it. After pause, stop, disconnection, or failure, the same pair is
a new attempt and catches up to the current clock.
stop() clears the playback anchor and resets the position to zero. Both stop
and pause retain loaded assets. muted = true silences playback while its
timeline keeps advancing; unmuting catches the current sound, without restarting.
Volume is a local linear gain from 0 to 1.
For a multiplayer pause, the game's host stores one common paused offset.
Each client calls pause() when it sees that cue, so local stop positions may
differ slightly with message arrival time. Resume everyone using the stored
offset and a single new shared timestamp. Do not independently resume from each
client's local elapsedTime value.
Enable sound after a player interaction
The SDK automatically listens for a real click, tap, or qualifying keypress
inside the game and attempts to enable its shared audio engine. It starts
listening when the SDK loads, so an interaction before Plot.ready() or before
you create a sound can prepare audio for later playback. Loading the SDK alone
does not open browser audio or start sound.
For ordinary playback, no manual unlock handler is needed. Browser settings and autoplay rules still apply: interaction with the surrounding Plot page does not guarantee that the game can play sound. Script-generated events do not count as player interaction. If the browser later suspends audio, the SDK listens for another interaction and tries again.
Keep an Enable sound action as a fallback. Call
Plot.toolkit.unlockAudio()
directly in that handler, before awaiting loading or other work:
Automatic and explicit unlocking work before Plot.ready() and never start
readiness. Preloading also works before readiness.
play() and resume() require connected, ready game state. They can be called
inside the initial connected lifecycle callback and can also unlock sound when
called directly from a gesture.
When a new attempt rejects with PlotErrorCode.UserActivationRequired, show an
Enable sound action, unlock, then explicitly retry the stored cue. Automatic
unlocking and unlockAudio() alone do not play a previously rejected request.
Neither restarts stopped or explicitly paused sounds. If a standard cue has already
elapsed, it ends successfully without requiring sound to be enabled.
An accepted request interrupted by the browser retains its timeline and catches up when audio becomes available again. A paused object stays paused. A Plot disconnection instead stops playback and clears the anchor; reapply current Game State in the connected lifecycle callback after recovery.
Share a recoverable soundtrack
This example assumes the game page includes an Enable sound button:
Register lifecycle and state-change listeners before readiness. The lifecycle callback handles startup and recovery; the focused listener handles live changes.
Handle rejected Game State writes using the error guide. Only the host computes the future timestamp; receivers use the stored value. One second of lead after preloading can help everyone hear the opening, but is not a loading guarantee. A player joining while paused stays silent and uses the shared paused offset for UI; its local object need not have a matching checkpoint.
For multiple tracks, store an asset key and cue identity with the anchor, map the key to your game's URLs, and dispose the old object when assets change. A reliable message can signal a transient sound, but cannot alone tell a late joiner which soundtrack is current. Audio completion is local presentation, not proof that the game should advance for everyone.
Observe playback
The full interface is SyncedAudio.
Position getters describe the intended timeline rather than a measured sample at
the speaker. They hold the requested offset before a future boundary and return
zero without a request or checkpoint. While loading, a position that needs
unknown clip lengths maps to NaN in currentTime. currentTime is read-only;
use resume(offset, timestamp) to seek with a shared anchor. There is no variable
playback rate.
subscribe(listener) immediately delivers an immutable snapshot, then reports
state, anchor, metadata, gain, mute, and error changes. It is not a progress timer;
read the position getters when rendering a progress display.
onEnded(listener) reports { anchor, reason } once per accepted standard
request. completed means playback reached its end; missed means no samples
could be scheduled before it expired. A missed cue is a successful no-op.
Pause, stop, replacement, and loop boundaries do not emit ended events.
onError(listener) reports operational failures, including failures after the
playback promise has resolved. Neither focused listener replays past events.
Every listener registration returns an idempotent unsubscribe function.
play() and resume() resolve when scheduled or already elapsed, not when the
sound starts or finishes. Expected cancellation rejects pending promises with
operation-cancelled without setting error or emitting onError. Other initial
operational failures both reject and notify onError; choose one path for
player-facing messages to avoid duplicates. Invalid arguments reject with
TypeError and leave existing playback intact. Invalid factory options or
property assignments throw synchronously.
Asset and browser limits
This player fully loads and decodes compact cues and music or ambience loops. It is not a streaming player for long soundtracks. Assets decode to a common 48 kHz timeline and support mono or stereo. Keep the same asset versions on every device; browser differences in compressed formats can still affect trimming. PCM WAV is a useful reference for checking authored boundaries.
The toolkit permits up to 16 MiB compressed per clip and 128 MiB of retained decoded audio across objects, with two concurrent loads and a 30-second load deadline. Intro and loop clips share decoded buffers without an extra combined copy. Stereo audio at 48 kHz uses about 22 MiB per minute; browser decoding also needs working memory beyond the retained limit.
audio-resource-limit means to use smaller clips or dispose
unused objects. Fetch/CORS failures use audio-load-failed; invalid or unsupported
audio uses audio-decode-failed; audio-engine failures use audio-unavailable.
operation-timeout can describe an asset load deadline, and unsupported-browser
means the required browser audio features are unavailable. See
error codes and recovery.
Disposal releases this object's resources and listeners. It never stops other
objects. Getters remain readable; repeated stop() and dispose() are safe.
Other valid method calls and gain/mute assignments after disposal fail with
object-disposed.
Playback uses the shared clock with best-effort output-latency estimates. It cannot promise simultaneous sound at different speakers: devices, Bluetooth, browser suspension, and clock estimates affect that result. Corrections after a clock change may be audible. Timers maintain synchronization rather than drive loop boundaries; a suspended browser can delay correction until execution returns. Test your game on the browsers and output devices your players use.