Repository navigation
feat(audio): native playback fills declared silence with comfort noise - #8010
Conversation
Quest-Claim: 0205c726842d5f28
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rt noise Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eclared end Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 54 seconds. View limit details
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. |
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed head 47f63f8 against base 0672ad5.
Findings: No actionable regression found in this review.
Coverage: Reviewed all 16 changed files and the relevant surrounding code. This included:
- Ring end/next state, publication and clearing of pending resumes, separate physical-position and stretch-adjusted consumption counters, virtual cushion playback/discard, and buffered-depth accounting.
- Resampler flushing across successive utterances, the padding-accounting fix, emergency-refill release at a declared end, sink finish/drain, and device-rebuild carry.
- Mixer gap/stall accounting and the allocation-free comfort generator against the documented level rule and shared Rust/JavaScript corpus.
- CLI End/Gap/Resume handling, deferred ends, terminal codec frames, retained-sink completion, and the mute/lost-group/quick-resume tests.
Direction: Keeping the sink across a declared silence puts the noise and received-audio level measurement in the same playback state. The migration notes and shared corpus describe the added behavior clearly. The documented delayed resampler tail and coalescing of multiple pending resumes were treated as intentional choices. No code change is requested from this pass.
Validation limits: GitHub-only static review. I did not execute quest context, just check, Cargo/Bun tests, browser/native playback, or concurrency testing. The author's reported checks were not independently reproduced; the full just check result in the PR predates the final ring fixes. The exact-head Check workflow is still in progress. The native push-to-talk harness coverage remains the documented follow-up. This is a comment review, not merge approval.
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
moq-audioring (rs/moq-audio/src/ring.rs): addsend()/next()/carry(), theends/resumestate, andRead::Comfort. Running dry at a declared end now fills with comfort noise and counts as supplied; a resume publishes a cushion so the next utterance starts one latency after its write. The trim and refill paths account for the pending cushion, and the resampler flush now counts its padding as admitted input.- Comfort generator (
rs/moq-audio/src/comfort.rs): a no-allocation port ofjs/watch/src/audio/comfort.tsthat measures the quietest 20 ms block played since the last resume and caps the level at -60 dBFS. - Playback sink (
rs/moq-audio/src/playback/sink.rs,mixer.rs): exposesSink::end()/Sink::next(), carries a pending declaration across a device rebuild, and treats a comfort read as supplied so the observer reports no gap or stall. moq play(rs/moq-cli/src/play/media.rs,fake.rs,output.rs): callssink.end()at an end marker andsink.next()at the resume instead of draining and replacing the sink; a declared end followed by a lost group starts the next utterance and resets the timeline.- Shared conformance: a new "Comfort noise" section in
doc/concept/audio-jitter.mdanddoc/concept/audio-jitter/comfort/corpus.json, consumed by both the Rust and JS tests;ComfortNoise.levelexposes the measured level to the JS test.
The declared-silence state machine is covered by the new ring, sink, and moq play tests, including the resampler-flush regression and the emergency-refill release. I found no behavioral regressions in this review.
deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏

Finishes quest/m1/push-to-talk/native-comfort-noise.md, the last child of the push-to-talk epic.
Summary
moq playnow treats a publisher's declared silence the way@moq/watchhas since moq-dev/moq#7942. When playback runs dry at a declared end, it plays comfort noise at the level of the quietest audio heard before it, at most -60 dBFS. It counts no gap or stall, and it keeps the noise going through the refill before the next utterance.moq-audioring (rs/moq-audio/src/ring.rs): learns the browser ring'send/nextpair.end()counts a declared end. Running dry while one is outstanding fills the read with comfort noise and returns a newRead::Comfort, which the mixer counts as supplied and playing.next()makes the next push start a new utterance one latency out. It queues a virtual comfort-noise span that tops the queue up to the target, placed behind whatever tail is still queued. The span is counted inwritten, sobuffered()matches what a fresh sink reports. When the reader reaches the span, the ends it covers are cleared.rs/moq-audio/src/comfort.rs): a port ofjs/watch/src/audio/comfort.ts, written for the audio thread with no allocation and an xorshift PRNG. It measures only received audio, never comfort noise or the startup cushion.playback::Sink::end()/Sink::next(): new public methods. A device rebuild carries a pending declaration over to the new ring.moq play(rs/moq-cli/src/play/media.rs):sink.end().sink.next(), instead of draining the old sink and opening a new one. The old tail, then comfort noise, play whilerole.resumewaits for the old media to drain.sink.next()plus a timeline reset). Before, the whole mute counted as a hole: either filled with silence or, pastfill_max, a new sink and a stall.doc/concept/audio-jitter.md, plusdoc/concept/audio-jitter/comfort/corpus.json. Bothcomfort.rsand a newjs/watch/src/audio/comfort.test.tscheck against the corpus.ComfortNoisegains alevelgetter for that test.Public API and wire
moq-audio: addsplayback::Sink::end()andplayback::Sink::next(). Not breaking. There is a note inMIGRATE.md.@moq/watch:ComfortNoise.levelgetter. The class is internal to the package.Choices made without asking
end()is the silence whatever timestamp the decoded audio stopped at, and the pre-skip question does not arise. The stopped encoder's final frames, which follow the marker, still play before the silence.next(), not atend(). Flushing atend()would put zero padding between the last speech and the codec drain frames that follow the marker. This keeps today's behaviour: up to 20 ms of tail plays just before the next utterance. It applies only when the device rate differs from the stream rate.just test push-to-talkdrives only Chromium today, and the native audio-quality rows never mute.Observer: comfort RMS matches the background, the next utterance starts exactly one latency after its first write, and there is no gap or stall.moq playtests: a 2 s mute keeps the sink and the next press plays exactly one delay after it arrives, and the same holds after a lost group.Validation
just rs play(moq-cli withplay): 263 passed.cargo nextest run -p moq-audio --features playback,bus,capture --lib: 339 passed, including the new comfort, ring, and sink tests.just rs captureandjust rs media-defaults(clippy with the playback features): clean.bun test js/watch/src/audio/comfort.test.ts: 6 passed.quest check: ok.just check: passed at 6b569f1. The review fixes after it only touchrs/moq-audio/src/ring.rs; the moq-audio,just rs play,just rs captureandcargo fmt --checkruns above are at the head.Review
Codex (gpt-6.1-sol, medium), round 2 on head 47f63f8: no findings.
Round 1 on 6b569f1 raised two findings, both fixed:
ring.rsresampler flush counted its zero padding as output but not as input, so a second flush could leave the next utterance's tail pending. Fixed: the padding counts as input. Regression test:every_utterance_through_the_resampler_is_heard.a_declared_end_releases_an_emergency_refill.Both tests fail without their fix.
Suggested follow-ups
just test push-to-talkdrive a nativemoq play --metricslistener next to the browser one, so the epic's acceptance proof covers native playback too.(Written by Claude Opus 5.5)