Skip to content

js/watch: time-stretch the audio ring onto its target - #8058

Merged
kixelated merged 24 commits into
mainfrom
quest/m1/audio-jitter-target/watch
Nov 9, 2026
Merged

kixelated merged 24 commits into
mainfrom
quest/m1/audio-jitter-target/watch

Conversation

@kixelated

@kixelated kixelated commented Nov 6, 2026 •

Copy link
Copy Markdown
Collaborator

Completes quest/m1/audio-jitter-target/watch.md and deletes it. The post-merge Nightly measurement moves to the new Browser ceilings quest under the same epic.

Summary

The browser's audio ring now catches up to its target by time-stretching what it plays, with the seams of WebRTC NetEq's accelerate as doc/concept/audio-jitter.md specifies them and moq-dev/moq#7856 shipped natively. Where main drops buffered audio to land back on the target, this plays it slightly faster. It never slows down. Audio stays on the shared clock video plays from, and underruns stay where main has them: this PR spends no latency to avoid them.

  • js/watch/src/audio/stretch.ts ports the shared spec: the 4 kHz pitch search, NetEq's admission thresholds, and the crossfade seam. stretch.test.ts runs its seams against the shared PCM corpus.
  • AudioRingBuffer stages each 30 ms window of received PCM through the stretch on its way to the render quantum. The replay harness, the render worklet, and the tests all run this one path. Buffered playback, declared silences, and the tail before a declared end play unstretched.
  • What it steers on. The ring decides on its depth right after the latest write, less what the stretch has removed since. That is how far the playhead trails the newest arrival: NetEq's playout delay, as the ring sees it, with no clock mapped into the worklet. An excess of 20 ms or more fast-accelerates. A smaller one accelerates by one pitch period, and only by a period that fits. At or below the target it holds. So the ring rests within one pitch period of its target right after each arrival, where main's trim left it. Between arrivals the ring drains because the next frame hasn't landed, not because audio is ahead of the clock, so it never expands into that.
  • Decode gate. Backpressure releases a live frame once the playhead is within the floor of where that frame's output ends. Released by where it starts, its tail landed past the floor, and accelerating that away drew the next frame out sooner, playing straight through a hold.
  • The ring re-anchors (drops buffered audio) only at the fast threshold or at three quarters of its headroom, whichever is lower, matching native. Each re-anchor counts as a skip. The counter is in audio Stats, the stats panel, and the harness (player_skips).
  • A handover truncate() unstages a staged window it lands inside, so the successor replaces the predecessor's unplayed audio. An emergency re-anchor drops the staged window along with the rest of the audio it skips, as moq-audio's trim already does.
  • A/V lag in the harness. Replay and browser samples record sync.now(), and analyze.ts reports av_lag_ms (p50, p95): how far the audio playhead trails the shared clock while playing. Every replay row budgets it exactly as measured, so a change that settles audio later fails --enforce.
  • moq-audio keeps main's NetEq decision and adds one thing: a declared end's tail drains unstretched, as the browser's does.
  • The WebRTC BSD notice and patent grant ship with @moq/watch as WEBRTC-LICENSE and WEBRTC-PATENTS.

Replay rows

just test audio-quality --replays --enforce (deterministic) on ffd6e03bbc, the merge of origin/main at 7d91a5b634. Every row reads exactly what it read on 683d964924. The main column is main measured with the same A/V lag metric. A/V lag is in ms: negative means audio is ahead of the clock.

row A/V lag p50 A/V lag p95 player underruns/min tap underruns/min discarded ms/min
bursty-aac 0.7 → -22.4 0.7 → -22.4 4.4 → 4.4 0 → 0 0 → 0
local-aac -17.5 → -22.2 -0.1 → -4.2 12.1 → 12.1 0 → 0 127.5 → 100.6
relay-bbb -21.5 → -22.7 10.5 → -4.2 16.1 → 16.1 0 → 0 87.8 → 0
relay-mic -18.2 → -19 -3.5 → -18.5 6.1 → 6.1 0 → 2 0 → 0
relay-mic-rtt40 -15.6 → -20.1 -0.9 → -13.4 8.2 → 10.6 0.3 → 0.7 70.9 → 72
  • Lag. p50 is at or below main's on every row, and p95 is below it on every row. The ring rests one frame ahead of the newest arrival's playout, as main's trim does on most rows, so audio leads the clock by 19 to 23 ms at p50. It no longer swings behind it the way main does when a trim lands late (relay-bbb p95 10.5 → -4.2).
  • Underruns. Four rows are unchanged. relay-mic-rtt40 rose by 2.4 a minute, and relay-mic's tap counts 2 short quanta a minute, 2 ms of audio. That is the cost of playing slightly earlier, and it is accepted.
  • Discarded audio. This is what the stretch buys. relay-bbb drops nothing now. local-aac's remaining 100.6 ms/min drops nothing either. Two receive stalls, 14 and 15.3 s in, leave audio 159 and 132 ms behind the clock, and the fast accelerate that brings it back within half a second steps the playhead's lag the way skip_aheads reads as a skip. player_skips is 0 on every row.
  • Targets are unchanged, since the estimator is.

Browser rows were not re-run. No headless browser was launched. The chromium rows and the *.browser.ts lanes are left to CI and the nightly runner, and Browser ceilings re-measures them, av_lag_ms included, once this lands.

Tests that hold the line

  • ring-buffer.test.ts: a ring short of its floor plays only what it holds and runs dry (no expand). Under 100 ms flushes with a 120 ms floor, the depth right after each flush rests within one pitch period of the floor. With a 60 ms floor, every flush lands on an empty ring and underruns rather than the ring growing.
  • replay.test.ts: at "auto", evenly paced and five-frame-flush traces play with audio never behind the shared clock and at most 25 ms ahead. A 40 ms target against 80 ms of spread underruns at least once a flush, with median lag at most one frame. Run against the previous head, the flush test fails (audio 68.9 ms behind) and so does the short-target one (36 ms behind at median, 898 underruns where this has 1200).
  • stretch.test.ts: decide never slows down, holds wherever native would expand, and accelerates only by a period that fits in the excess.
  • analyze.test.ts: av_lag_ms reads the playhead against the shared clock and leaves stalled samples out.
  • budgets.json: exact av_lag_ms_p50 and av_lag_ms_p95 ceilings on every replay row.

Public API

  • @moq/watch: audio out.stats gains skips: number (additive). The stats panel shows it as "Skips".
  • Harness: samples gain clock, and summaries gain av_lag_ms_p50 and av_lag_ms_p95.
  • Spec: doc/concept/audio-jitter.md gains a "Browser decision" section. Native's decision is unchanged.
  • Wire: none.

Decisions

Decided 2026-11-06 (maintainer, /quest-complete): "Is the goal wrong?"

  • ✅ Audible, not zero. The Goal becomes no audible glitch in steady state, judged by listening, with underrun ceilings measured by budgets.json's rule.
  • Keep zero, and take fast expand in both players
  • Keep NetEq-exact, and plan concealment of late audio as a separate quest

Decided 2026-11-06 (maintainer, on this PR): "I think start with NetEq. 0 underruns is pretty aggressive? That being said, if the test is just +40ms RTT, are we sure our NetEq port is correct?" That settled the expand: NetEq-exact. The fast variant, its spec departure, and its corpus vectors were reverted.

Decided 2026-11-07 (maintainer, /quest-unblock): no maintainer listening run; the Goal is judged by the replay and Nightly measurements alone. The quest's Goal drops the live MacBook Air rows and their stats-panel criterion.

Decided 2026-11-07 (maintainer, /quest-complete): ✅ fix moq-dev/moq#8071's replay regression on main first, then re-run this PR's replay rows on a merge of main (rejected: land on top of the regression). moq-dev/moq#8218 fixed it.

Decided 2026-11-07 (maintainer, /quest-complete): ✅ land it, then measure (rejected: hold this PR until seven Nightlies re-measure the chromium ceilings). watch.md is deleted, and the measurement is the new [S] quest quest/m1/audio-jitter-target/browser-ceilings.md.

Decided 2026-11-08 (maintainer, on this PR), on round 1's finding that audio settled behind video (lag p50 1 → 289 ms on bursty-aac, -18 → 128 on local-aac, -22 → 154 on relay-bbb): "fix it. obvious adding latency fixes underruns duh. Keep the audio and video clocks separate but at steady state they should be self-aligned if we correctly set the delay/latency. Update/add tests to make sure we NEVER increase latency to chase this dumb 0 underrun dragon. It's a terrible goal, we expect some underrun during congestion."

  • ✅ Fix it here. The stretch only steers audio back onto its target and never settles it later to absorb jitter. Underruns during congestion are accepted, the ceilings that only latency had tightened go back to main's, and A/V lag ceilings and tests guard it.
  • Decide on the playhead's error against the shared clock in both players (rejected: it couples the worklet to the clock, and the clocks stay separate)
  • Land as is and plan the fix as a quest (rejected)

How the call was applied: the ring decides on its depth right after each arrival and never expands. The lateness band this PR had added is reverted in both players, along with the estimator's lateness in both languages and its corpus fields. The quest README records the decision.

Choices made without asking

  • Rest on the floor, one frame ahead of the clock, not on the clock itself. A variant that counted the newest frame as on-clock rested at 1 to 3 ms of lag. It also cut underruns (relay-bbb 16.1 → 0, relay-mic 6.1 → 0, local-aac 12.1 → 4, relay-mic-rtt40 8.2 → 3.4), but only by playing about 20 ms later than main on those four rows. That is latency bought for underruns, so it is not taken. It is a one-line change if lag nearer 0 is wanted.
  • A target short of the spread catches up by fast accelerate, not by dropping. On a 40 ms fixed target against 80 ms flushes, each refill fast-accelerates its backlog where main drops it, so lag trails the clock for about 100 ms after each flush (median 9 ms, max 36) where main stays at 0. Underruns are the same (1200 against 1200). "auto" never picks such a target (it picks 120 ms there), and the replay test bounds the lag below one frame at median.
  • Native keeps main's decision, expand included. Only this PR's lateness band is reverted there. Native still expands on its buffered depth, so it may settle behind its clock the way the browser did, but native rows don't report A/V lag yet. native-stretch.md notes it as worth measuring.
  • The shared stretch corpus still decides as native does. The browser grades its accelerate and fast seams against those vectors, at an excess inside and past the 20 ms band, and checks it holds wherever native expands.
  • A declared end's tail drains unstretched in both players. Running dry at a declared end is silence, not an underrun, so stretching toward a target nothing will refill only moves the end.
  • Buffered mode does not stretch. Buffered converge owns that.

Validation

On ffd6e03bbc, the merge of origin/main at 7d91a5b634, after moq-dev/moq#8257 made replay budgets exact in both directions:

  • quest gate just check: passes.
  • just test audio-quality --replays --enforce: passes, every row exactly at its budget. The one conflict, bursty-aac's silence_share (main re-measured 0.01, this branch 0.012), keeps this branch's value, which is what it measures.
  • quest check --base origin/main: clean.

On head 683d964924 (merge of origin/main at aabd6d7e21):

  • quest gate nix develop --command just ci: passes. That covers 1714 Rust tests and the @moq/watch (576) and @moq/hang (549) bun tests.
  • Against the previous head c5461c18fc, with clock patched into its replay, the new lag tests fail. The flush trace plays 68.9 ms behind the clock, and the short target runs 36 ms behind at median with 898 underruns.
  • Browser lanes and rows: not run, as above.

Follow-ups

  • Planned in this PR as Native A/V lag [M], under the same m1 epic: moq play --metrics writes the presentation clock so native rows grade av_lag_ms, then native stops expanding into the gap between arrivals if it settles audio late, keeping its drift compensation.
  • skip_aheads cannot tell a fast accelerate from a jump at its 250 ms sampling. Once the browser rows budget player_skips, they could stop budgeting skip_aheads, which would stay for native.
  • Measure lip sync against the sample reaching the output, through AudioContext.getOutputTimestamp(), as the quest's 2026-11-05 decision recommends.

Models

Size: L
Implementer: Claude Code, claude-opus-5-5, unknown effort
Implementer: Claude Code, claude-opus-5-5, high effort
Implementer: Claude Code, claude-opus-5-5, high effort (iterate, S)
Implementer: Claude Code, claude-opus-5-5, xhigh effort (iterate, M)
Implementer: Claude Code, claude-opus-5-5, high effort (iterate, S)

Review

Rewritten by quest review after each round.

  • Reviewer: Codex, gpt-6.1-sol, high effort
  • Session: 01a1bbcd-79ce-7671-99a7-79ef90aa621f, round 3
  • Head: 77fce72
  • Base: origin/main at 4fa1558
  • Score: 10/10, The only new branch-owned changes document a native A/V-lag follow-up; source-only review found no new issues or changes to the previously reviewed runtime behavior.
  • Findings: none

🤖 Generated with Claude Code

(Written by Claude Opus 5.5 with high effort)

kixelated and others added 2 commits November 6, 2026 12:57
Port the shared NetEq stretch from doc/concept/audio-jitter.md into the
browser's AudioRingBuffer, so live playback converges by playing slightly
faster or slower instead of running dry or skipping ahead. Every replay
row, relay-mic-rtt40 included, now plays with zero player underruns.

Expand departs from NetEq in both players and the spec records it: below
the target it ignores the cooldown and repeats every whole period that
fits at the lower threshold. NetEq-exact expand left relay-mic-rtt40 at
2.1 underruns a minute. A declared end's tail drains unstretched.

The ring re-anchors only at the fast threshold or three quarters of its
headroom, as native does, and counts each re-anchor as audio stats
`skips`, shown in the stats panel and graded as `player_skips`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title js/watch: zero steady-state audio underruns by time-stretching the ring js/watch: time-stretch the audio ring onto its target Nov 6, 2026
kixelated and others added 3 commits November 6, 2026 13:45
The quest's Goal becomes no audible glitch in steady state rather than
zero underruns, so the listening check picks between fast expand and
NetEq's exact expand. FAST_EXPAND selects it in both players; the budgets
note, spec, and quests no longer hard-code a zero target.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Staging runs the read index up to a 30 ms window ahead of the playhead,
so a successor truncating inside it lost its start to the predecessor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated and others added 3 commits November 6, 2026 16:40
Per the maintainer's answer on #8058, start from NetEq: revert the
stronger expand (every window, every whole period, lower threshold) to
NetEq's preemptive expand in both players, with the spec text and corpus
vectors it changed.

Checking the port against decision_logic.cc found it had dropped NetEq's
GetMaxDelayMs term from the high limit. Restore it in both players: the
jitter estimator reports `lateness`, how late the slowest in-order frame
of its two-second window arrived against the fastest, and the stretch's
band above the target widens by it, as do the browser ring's write
re-anchor and the CLI's incoming trim at the fast threshold. A fixed
target ignores the arrivals, so its lateness is zero. The estimator
corpus gains a lateness series and the stretch corpus three vectors.

relay-mic-rtt40 falls from 2.1 to 1.0 player underruns a minute; the
replay budgets are re-set to what NetEq measures, and the quest Goal
from zero to that rate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…om/moq-dev/moq into feat/audio-jitter-stretch

# Conflicts:
#	doc/concept/audio-jitter.md
#	js/watch/src/audio/stretch.ts
#	quest/m1/audio-jitter-target/native-stretch.md
#	quest/m1/audio-jitter-target/watch.md
#	rs/moq-audio/src/stretch.rs
#	test/audio-quality/budgets.json
@kixelated

kixelated commented Nov 7, 2026 •

Copy link
Copy Markdown
Collaborator Author

Design change: both players now use NetEq's exact preemptive expand, and the decision band gets back the NetEq maximum-delay term the port had dropped.

  • Reverted: the stronger expand (every window, every whole period, at the lower threshold), the FAST_EXPAND switch that kept it, its departure from the spec, and its corpus vectors.

  • Restored: NetEq's high limit is target + GetMaxDelayMs() + 20 ms; the port had target + 20 ms. Both estimators now report lateness, how late the slowest in-order frame of the two-second window arrived against the fastest, and the band widens by it in both players. The browser's write re-anchor and the CLI's trim widen with it, since both sit at the fast threshold. A fixed target has none. The shared estimator and stretch corpora grade it.

  • Measured in player_underruns_per_min:

    • relay-mic-rtt40: 7.9 on main, 2.1 with exact NetEq (three run-dries), 1.4 with the term (two run-dries, each counted as its quantum plus the re-stall; first posted as 1.0, before a re-anchor fix shifted the window phase).
    • bursty-aac: 2.2.
    • The other three replay rows: 0.

    The replay budgets are re-set to these, and the quest Goal is "no audible glitch" with measured ceilings, recording both 2026-11-06 decisions.

  • On the +40 ms RTT question: a constant RTT costs nothing, since delays are measured against the fastest arrival. This row's run-dries are receive stalls of 100 to 125 ms whose frames then arrive in a burst, out of order. Gaps over 60 ms come 13 times a minute here against 3.4 on the direct row. They outlast the 80 to 100 ms target that NetEq's own 0.95 quantile sets, and NetEq conceals stalls like these rather than stretching through them.

  • One difference from NetEq remains: we decide on ring depth, NetEq on its playout delay. A replay prototype of the playout-delay version cut relay-mic-rtt40's underruns by about a third. The spec records the difference, and the PR body lists it as a follow-up.

  • Not re-run: browser rows. No headless browser was used this round, so they are left to CI and the nightly.

(Written by Claude Opus 5.5)

@kixelated

Copy link
Copy Markdown
Collaborator Author

Coordinator note from /quest-complete (2026-11-07):

  • Expand variant: NetEq-exact plus the lateness term, as this head has it. On kixtop the maintainer couldn't tell the variants apart in the native say runs and left the pick open. I first picked fast expand. The maintainer's earlier answer here ("start with NetEq") states their preference, so that pick is withdrawn. For reference, the native rows (pre-lateness head 354e35d, 120 s each, bursty seed 7): neither variant had a steady-state underrun. NetEq-exact's two shaped rows each had 2 short gaps (12-119 ms) in the first 3 s, and the output device changed between the two sets (245 ms vs 13 ms output latency).
  • A bug this head still needs: when the ring re-anchors, it kept the stretch window it had already staged, so pre-jump audio played while the playhead reported the new position. The fix is in #skip in js/watch/src/audio/ring-buffer.ts, with two tests that fail without it: commits 04090b981c and 9b379dccd3 on quest-iterate/watch-fast-expand. That branch's other commit, 33e2c4c (fast expand), must not be brought over. Cherry-picking the fix onto this head shifts the replay budgets, so they need re-measuring here.

(Written by Claude Opus 5.5 with high effort)

An emergency re-anchor moved the read index but kept the partly rendered
stretch window, so the old PCM played on the new timeline and the
playhead misreported it. Clear it, as moq-audio's trim discards staged
output first.

Dropping the startup backlog's staged window shifts each replay's later
window phase, so the replay ceilings are re-measured: relay-mic-rtt40's
two run-dries now each end mid-quantum and count twice (1.4 a minute),
and the lag heuristic reads one or two more accelerates as skips.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move the post-merge Nightly measurement into browser-ceilings.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated and others added 3 commits November 8, 2026 09:45
…lock

The stretch compared the ring's current depth with the target, and that
depth drains between arrivals because the next frame has not landed, not
because audio is ahead of the shared clock. Expanding into every trough,
with a band widened by the arrivals' lateness, settled audio a burst
behind video: A/V lag p50 rose from 1 to 289 ms on bursty-aac.

The ring now decides on its depth right after the latest write, less
what the stretch has removed since, which is how far the playhead trails
the newest arrival. Any excess accelerates, one past 20 ms
fast-accelerates, and the ring never expands, so it rests on its floor
right after each arrival, where the trim left it. The decode gate
releases a live frame by where its output ends, so the stretch cannot
draw frames through a hold.

The lateness band is reverted in both players, with the estimator's
lateness and its corpus fields. Native keeps NetEq's decision.

The harness records the shared clock and reports av_lag_ms, budgeted
exactly on every replay row, and unit tests hold that a ring never
expands, rests on its floor under flushes, and underruns rather than
settling later when its target is short of the spread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… test types its playhead

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated marked this pull request as ready for review November 8, 2026 18:04

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Automated review by review (OpenAI)

Reviewed 683d964 against aabd6d7. No new actionable findings.

The previously reported batching-induced audio lag is addressed in the revised browser controller: it steers from post-write depth, never expands, and gates live decoding by the output tail. Paced/batched replay regressions and A/V-lag ceilings now guard against settling later to reduce underruns. This follows the recorded maintainer decision to keep the clocks separate and accept congestion underruns. The prior staged-silence rewind is also corrected; native changes are limited to draining declared-end tails unstretched.

Verification limits: GitHub-only static review of the current diff and relevant ring/decode-gate context, compared with earlier review concerns. No tests, replays, listening, or browser/device runs were performed. Reported lag/underrun measurements are not independently verified; browser ceiling measurement and native A/V-lag measurement remain explicit follow-ups.

@pullfrog pullfrog Bot left a comment

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.

✅ No new issues found.

Reviewed changes

I read the full 41-file diff at 683d9649 and re-derived the delta since the prior pullfrog head 03f8e9e5d, which is the 2026-11-08 rework of the controller.

  • Browser controller: the ring now decides on peak (its length right after the latest write, less what the stretch has removed since), never expands, fast-accelerates at 20 ms of excess, and otherwise removes only a pitch period that fits. The lateness band, the fast-expand variant, and the estimator's lateness are reverted in both players, matching the recorded maintainer decision.
  • Ring staging: #stage moves full windows of live PCM through Stretch and stops at declared silences or a declared end's tail; timestamp and length account for staged output, truncate unstages, #skip drops the staged window with the audio it skips, and #peak is maintained across writes, drops, and resets. The declared-silence catch-up is forward-only and drops only silence.
  • Decode gate: Backpressure releases a live frame by where its output ends, so a tail past the floor is not accelerated away; insert's doc now states that PCM behind the read index is dropped.
  • Stretch and stats: stretch.ts decides on excess and runs without allocating in the render path; the corpus test grades accelerate and fast seams and holds where native expands. skips is wired through the worklet State, Audio.Stats, the stats panel, and player_skips_per_min.
  • Harness: replay and browser samples gain clock, summaries gain av_lag_ms_p50/p95, and every replay row is re-baselined exactly with av_lag and player_skips while the chromium and native rows are untouched.
  • Native, docs, quests, packaging: a declared end's tail drains unstretched natively with a regression test; the concept/library docs and the epic's quest tree are updated (watch.md deleted, browser-ceilings.md added); the WebRTC notices ship with @moq/watch.

The four findings from the prior review are addressed at this head (forward-only catch-up with its regression test, no render-path subarray, the documented out-of-order drop, and the removed stale budgets sentence). I ran the changed js/watch audio suites (ring-buffer, stretch, playout, replay, render-worklet, decoder) and the harness analyze/probe suites, all green, and Check and Quest pass on this head. The replay budgets match the body's table; the browser rows remain deferred to browser-ceilings.md as the body states.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏

kixelated and others added 3 commits November 8, 2026 15:04

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Automated review by review (OpenAI)

Reviewed head 77fce72 incrementally from 683d964, separating merged-main changes. No new actionable findings.

The new native A/V-lag follow-up makes the previously deferred measurement explicit: sample the presentation clock alongside the audio playhead, grade lag before changing native expansion, retain publisher/device drift compensation, and recalibrate ceilings if the decision changes. Its metric semantics match the existing browser schema. The previously reviewed browser batching/rewind fixes are unchanged; browser and native measurements remain future verification, not evidence supplied by this plan.

Verification limits: GitHub-only source/diff review, including native metrics and the shared sample schema. No builds, replays, listening or browser/device measurements performed.

@kixelated
kixelated added this pull request to the merge queue Nov 9, 2026
Merged via the queue into main with commit ee139b8 Nov 9, 2026
13 of 16 checks passed
@kixelated
kixelated deleted the quest/m1/audio-jitter-target/watch branch November 9, 2026 02:20
kixelated added a commit that referenced this pull request Nov 9, 2026
…etching players

main landed the browser stretch (#8058) with a browser-only player_skips
count. Both players now report the same three counters: skips, the received
audio they trimmed, and what the stretch accelerated away. moq play adds its
skip count, and the browser ring counts trimmed and accelerated audio on the
stretching ring.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant