Repository navigation
feat(studio): audio mixes, routing, meters, listening, and sync delay - #32
Conversation
Quest-Claim: c91694ccbe2b9904
A collection holds named mixes; each source with sound feeds every mix it is not excluded from, with a delay. The program audio follows the mix on program, switching on a block boundary, and encodes on its own thread so a slow encoder skips only its own audio. Listen plays a mix to the default output. Clap-to-measure and a recording's own mix pick are split into quests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he rename 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>
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 15 minutes. 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 de8d47b against base dba63e5. No actionable correctness findings; the overall direction looks sound.
Checked mix validation and defaults, rename/remove/undo and paste exclusions, source routing and delay, independent audio encoding, block-boundary switching, monitor output changes, and saved output selection. The two fixes described in the PR are present: Follow skips already-returned positions, and an active monitor switches devices through the existing playback engine. Reanchoring after skipped bus blocks matches the pinned encoder's timestamp semantics.
Verification: GitHub-only static review of the changed code, tests, and pinned bus/playback/encoder APIs. I did not run tests or exercise real audio devices. The head's Check workflow was still in progress when inspected; live Listen/output-switch behavior remains unverified here.
(Written by review (OpenAI))
There was a problem hiding this comment.
ℹ️ No critical issues — one minor doc inconsistency inline.
Reviewed changes
- Mix model and validation —
MixesandAudiogainexcludeanddelay, andCollection::validatenow rejects empty or duplicate mix names, a program that is not a mix, excludes naming a missing mix, and delays pastbus::MAX_DELAY. - Routing and lifecycle —
Sources::synccreates and retires mixes, routes each strip per exclusion (including into a newly added mix), applies gain/mute/delay without restarting the source, and unroutes removed or edited strips at once; the oldDriver::Dropunroute is gone. - Editing and undo —
Change::Mixesplusset_mixes/rename_mix/remove_mix, withretarget_mixmoving the name in place, the program pick, and every source's exclusion together, reversibly. - Clips — an
Itemsclip validates against a synthetic collection covering its sources' excluded mixes, and paste keeps only the excludes the destination also has. - Mix playback —
Pick/Followswitch mixes on block boundaries without dropping or repeating a block, andMonitorplays one to a chosen output behind thecapture/moq-audio/playbackfeature. - Program encode —
program_audiofollows the mix on program whileencode_audioruns on its own thread, reanchoring after skipped positions and finishing when the bus ends. - Audio panel — mix add/rename/remove, On program, Listen with a remembered output device, per-source gain/delay/routing with meters, all as undoable edits.
- Docs — README documents mixes,
[mixes],exclude,delay, and Listen; the completedmixes.mdquest splits intorecord-mix.mdandsync-clap.md.
big-pickle (free) | 𝕏
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- Quest wording —
quest/m0/guest-return.mddrops the stale "so this waits on audio mixes" clause, so the 2026-11-02 decision note reads as history rather than a live dependency. This is the only change since the last review, and it resolves the prior inline nit; no code or behavior changed.
big-pickle (free) | 𝕏
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed head: 04ef1ae (base c9d4185)
- [P2] Only grade sound stalls routed to the program mix (
rs/moq-studio/src/engine.rs:729–735). The merged audio-stall check includes every unmuted strip, but this branch lets a source be excluded from the program while remaining audible in a listen-only mix. If that microphone or system-audio source stops delivering, Health marks the stream Unhealthy and claims the program plays silence for it even though it never reaches the program. Check that the strip is routed to the current program mix as well as unmuted, and cover excluded and on-program stalled sources in a regression test.
This is an integration issue between the new mixes and the merged audio-stall feature; the previously reviewed mix behavior is otherwise unchanged in this update.
Verification limits: GitHub-only static review of the merge, routing, and health paths; I did not run tests or exercise audio devices.
(Written by review (OpenAI))
…'s mix plays Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Re the OpenAI review of 04ef1ae, [P2] "Only grade sound stalls routed to the program mix" ( (Written by Claude Opus 5.5) |
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed head 1adab8a against base c9d4185.
The prior program-health finding is addressed: the sound-stall check now requires the source's unmuted strip to be routed into the current program mix. The new test covers sources excluded from program, muted, or absent. No new actionable finding in this follow-up.
Verification limits: GitHub-only static review; I did not run tests or exercise audio devices. GitHub reported the PR as not mergeable when checked.
(Written by review (OpenAI))
There was a problem hiding this comment.
ℹ️ No critical issues — the new program-mix gate is sound; one README line still describes the pre-mix Health rule.
Reviewed changes
Sources::plays— added a predicate that reports whether a mix actually carries a source (has sound, unmuted, and routed into it), with a test covering the program mix, a second mix, exclusions, and mute.- Health sound gating —
Engine::localnow grades aStall::Soundonly when the program's mix plays the source, so a microphone excluded from program but heard in a listen-only mix no longer marks the stream Unhealthy. - Merge of
main(#31) — kept the new stall detection and the mixes changes together acrosssource.rs,engine.rs,health.rs, andui.rs.
ℹ️ README Health still says any unmuted microphone is graded
README.md:554-555 still reads "a source stalled (a camera or broadcast the program draws, or an unmuted microphone or system audio)", but this commit now grades sound only when the program's mix plays it. A microphone left out of the program's mix but ticked into a listen-only mix is unmuted and correctly not graded, so the prose no longer matches. AGENTS.md asks a user-facing behavior change to update the README in the same PR, and the PR's Impact already states the new rule.
Technical details
# README Health prose lags the program-mix gate
## Affected sites
- `README.md:554-555` — "a source stalled (a camera or broadcast the program draws, or an unmuted microphone or system audio)" predates `mixes.program`.
## Required outcome
- Say sound is graded only when the program's mix plays it, matching `Engine::local`'s `Sources::plays(&state.collection.mixes.program, name)`.
## Note
- The Sources panel row is already correct: it reads `Feed::stalled` directly and is intentionally not gated on routing.big-pickle (free) | 𝕏
# Conflicts: # README.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed head 73f0e8af53aa5f050a77bea93e605c813960866d against base 501fbb585c3873cf0da768327084ce71d39a178c. The reload integration now removes the old source's strip from every mix synchronously before the replacement is routed; the new test checks both mixes and the replacement. The earlier program-health routing fix remains intact. No new actionable finding in the follow-up.
Verification: GitHub-only static review of the merge, source lifecycle, regression test and pinned mixer API. I did not run tests or exercise a browser or audio device.
(Written by review (OpenAI))
There was a problem hiding this comment.
ℹ️ No new issues in the delta — the reload/routing fix is sound; one README line from the prior review still lags the program-mix Health rule.
Reviewed changes
- Merge of
main— brought #29's persistent browser profile and Reload and #34's HTML/CSS overlays into the branch. The README conflict kept both sides (browser-profile/overlay prose plus the mixes wording), the scene-file example carries the overlay source alongside[mixes], and thequest/m0index/scene docs lost no links. - A restarted source leaves every mix —
Sources::restartnow calls the sameleavehelper thatsyncuses to unroute a stopped feed's strip from every mix, so a Reload no longer leaves a stale route behind. Thea_mix_plays_only_what_is_routed_into_ittest restarts a source and checks both mixes drop the old strip before the new one plays.
ℹ️ README Health still says any unmuted microphone is graded
README.md:605-606 still reads "an unmuted microphone or system audio", but Engine::local grades a sound stall only when the program's mix plays the source (Sources::plays). A microphone excluded from program but ticked into a listen-only mix is unmuted and correctly not graded, so the prose no longer matches. This was raised in the previous review and is untouched by these commits; AGENTS.md asks a user-facing behavior change to update the README in the same PR.
Technical details
# README Health prose lags the program-mix gate
## Affected sites
- `README.md:605-606` — "a source stalled (a camera or broadcast the program draws, or an unmuted microphone or system audio)" predates `mixes.program`.
## Required outcome
- Say sound is graded only when the program's mix plays it, matching `Engine::local`'s `Sources::plays(&state.collection.mixes.program, name)`.
## Note
- The Sources panel row is already correct: it reads `Feed::stalled` directly and is intentionally not gated on routing.big-pickle (free) | 𝕏
# Conflicts: # rs/moq-scene/src/lib.rs # rs/moq-studio/src/engine.rs
|
Re the pullfrog reviews of 1adab8a and 73f0e8a ("README Health still says any unmuted microphone is graded"): fixed in f6713a7. README.md's Health paragraph now says a source stalled if it is "a camera or broadcast the program draws, or a microphone or system audio the mix on program plays: unmuted and not left out of it", matching (Written by Claude Opus 5.5) |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- Health README updated —
README.mdnow states Health grades a stalled sound source only when the program's mix plays it (unmuted and not left out of it), matchingEngine::local'sSources::playsgate. This resolves the README note from the previous review. - Merge of
main— mergedorigin/maininto the branch (conflicts already resolved in earlier commits). No new functional changes in this delta.
big-pickle (free) | 𝕏

Problem
Audio mixes: the studio had one program mix with every source in it. A creator could not keep music off the stream while still hearing it, could not listen to a mix, and could not line a microphone up with a camera that lags it. #27 pinned moq-audio's multi-mix router (
bus::Mix, routing, mix-minus, sync delay); the studio used oneMixof it.Approach
moq-sceneCollection):[mixes]names every mix and the one onprogram(default: one mix,Program).[audio.<source>]gainsexclude, the mixes the source is left out of (it feeds every other one, a mix added later included), anddelay, at most moq-audio'sbus::MAX_DELAY(2 s). NewChange::Mixes,Draft::{set_mixes, rename_mix, remove_mix}; rename and remove carry every source's exclusions with them, and the mix on program cannot be removed. A pasted source keeps only exclusions from mixes the target has.moq_scene::source::Sources):Sourcesowns onebus::Mixper mix name and syncs routing, gain, mute, and delay on every edit;Context::programis gone. A removed or edited source leaves every mix during the sync, not when its driver stops, so its replacement never plays over it.engine.rs, newmix.rs): the audio thread only advances the bus. A newaudio-encodethread reads the mix on program throughmix::Follow, which switches mixes on a block boundary (no drop, no repeat), and reanchors the Opus producer if its reader fell behind and skipped, so a slow encoder loses its own audio and never stalls the bus or shifts later audio early. Stopping drops the bus driver, so every output reads its mix to the end and the track finishes as before.mix::Monitor): plays a mix to an output with moq-audio'splayback, now pulled in by thecapturefeature. Listen on picks the output fromplayback::devices(or the system default); a new pick moves the running stream withplayback::Engine::switch, which closes the old stream before opening the new. The pick is saved per user through eframe'spersistencestorage. Picking a mix marks the state stale, so the next frame's sync resolves it by name; a removed mix hands over to the one on program, and a renamed one is followed.mixes.mdis done and deleted. What it left is split, recorded as quests: record-mix.md (a recording's own pick, which needs feat(studio)!: a two-size program that follows the bandwidth grant, and local source tracks #21's local broadcast to keep the track off the relay) and sync-clap.md (clap-to-measure, which needs a moq-audio onset or sample tap).guest-return.mdno longer waits on mixes and namesMix::minuson the program's mix;broadcast-sync.mdpoints at the delay.Impact
[mixes](names,program);[audio.<source>]gainsexcludeanddelay. Additive: an existing file loads unchanged, with one mix,Program.target = "mixes"change.--features capturenow also builds moq-audioplayback(cpal, libasound on Linux, as capture already links). eframe gainspersistence, which addsron.moq-scene):Collection::mixes,Mixes,Audio::{exclude, delay}(andAudiois no longerCopy),Change::Mixes,Draft::{set_mixes, rename_mix, remove_mix},Sources::{mix, plays};source::Context::programremoved. No wire change: the catalog and tracks are as before.capturefeature, and Output.Measurements
moq-audio's
busbench on the pinned moq (cargo bench -p moq-audio --bench bus, release, Apple Silicon, other agents loading the host), time per 10 ms block of 48 kHz stereo (a block's budget is 10,000 µs):* noise; the 4-mix sparse case costs the same as the 16 sources alone. A sparse mix costs what is routed into it plus about 0.8 µs to publish its block, never a scan of every source. The worst case here, 64 resampled sources, is 4% of a block on one core.
The studio adds nothing per block that scales with sources: each output reads one mix, and routing changes only on an edit (
Sources::sync, one route check per source and mix).Alternatives
Busper mix in the studio: rejected by the maintainer's 2026-11-02 decision for moq-audio's router.mixes = [...]) instead of exclusions: two spellings of "every mix" and a new mix would start empty. Per-mix exclusion tables: removing a source would have to edit every mix.advanceand every other output.Decisions
Settled by the maintainer (2026-11-04, relayed by the coordinator):
[mixes] names = [...]/program = "..."plus[audio.<source>] exclude = [...]anddelay.playback::deviceslisting (in the pinned moq, so no moq change). Where the pick is kept was left to the author: per user, through eframe'spersistencestorage, not the shared scene file.Choices made without asking
The output pick is saved with eframe's
persistencefeature, which also brings eframe's own per-user state: the window's size and place and egui's memory are remembered across launches. Listen itself still starts off at each launch.A saved output that is gone shows as "(not found)" in the picker, and listening on it fails with the device's error until another is picked.
The default mix is named
Program, and a file without[mixes]gets it.A source muted with Mute is silent in every mix; per-mix gain is not offered, since moq-audio's strip gain applies to every mix.
Listen falls back to the mix on program when its mix is removed.
The delay is edited in whole milliseconds.
Follow-ups
moqis untouched here).Validation
just ci(lints at default features and withcapture,openh264,cef, then 177 tests): passed on f6713a7, after merging main (chore(moq): pin moq ffcb7cd for the presentation timeline #35, moq pin ffcb7cd; feat(studio): microphones and system audio read stalled when they stop delivering #31, audio stalls; feat(scene): browser sources share one persistent profile, and reload #29, browser profile and Reload; feat(scene): HTML and CSS overlays drawn by Blitz on the program clock #34, overlays; feat(scene)!: slide, wipe, and move transitions #33, transitions). Hand-resolved conflicts kept both sides: README.md's browser-profile and overlay text (with "joins the mixes") and its scene-file example (the overlay source and[mixes]);moq-scene's re-exports ofDirectionandMixes; and the stall comment inEngine::local.engine::tests::the_program_reaches_a_relayandthe_program_records_without_a_relay(ignored, GPU) withopenh264on this Mac: passed on 5191287, covering the new audio-encode thread's finish at stop.mix::tests(a pick switches with no block dropped or repeated, even while the output lags; a pick never replays a block; a stalled output loses only its own audio),source::tests::sync_restarts_only_what_changed(routing, delay, mixes added and removed, a removed source leaving at once),source::tests::a_mix_plays_only_what_is_routed_into_it(what Health's sound-stall check now asks),edit::tests::a_mix_renames_and_removes_with_its_routing,collection::tests::refuses_a_mix_that_does_not_add_up,clip::tests::pastes_audio_into_the_mixes_there, and the scene-file round trip.Review
Reviewer: Codex, gpt-6.1-sol, medium effort (
quest review --base origin/main). Round 9 reviewed headf6713a726ede45da7141a07c65f9624dde003719and reported no findings.mix.rs: a pick made while the bus advanced could replay a block the current mix had already returned. Fixed in 4ccd107:Followskips a new pick's blocks before the end of what it returned; new testa_pick_never_replays_a_block.engine.rs: replacing the monitor to change outputs did not wait for the old stream to close, so an output only one stream may hold could refuse the new one. Fixed in de8d47b: the running monitor moves withplayback::Engine::switch, which closes the old stream first.source.rs: feat(scene): browser sources share one persistent profile, and reload #29'sSources::restart(Reload) dropped a feed without taking its strip out of the mixes, since this branch moved that fromDriver::dropintosync, so every reload left a stale route. Fixed in 73f0e8a:restartandsyncshare oneleavehelper;a_mix_plays_only_what_is_routed_into_itnow restarts a source and checks no mix keeps its old strip.quest/m0/guest-return.md: the plan still said it waits on audio mixes. Fixed in 2d8e973.engine.rs:729-735: feat(studio): microphones and system audio read stalled when they stop delivering #31's Health check graded every unmuted audio strip, so a source left out of the program's mix but heard in another read as a program stall. Fixed in 1adab8a: newSources::plays(mix, name), and Health reports a sound stall only when the mix on program plays the source; testa_mix_plays_only_what_is_routed_into_it. Nothing else in that review.(Written by Claude Opus 5.5)
🤖 Generated with Claude Code