Repository navigation
Conversation
A bus mixer with no input yet fixates to audiomixer's default of 44100, while main settles at whatever its consumer pins (48000 for an encoder or the vision mixer). The buses feed one another (aux and group AFL into solo, solo and main into monitor) and audiomixer cannot resample, so once two of them disagree every channel's caps query comes back empty and an input that links after startup, as every WHIP input does, fails with Noformat. Pin every bus mixer to 48 kHz with a capsfilter on its output, and add audioresample to each channel input so a source at another rate is converted instead of refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The live buses emit silence with no input, so counting buffers on main and aux passed even when the input reached neither. Measure the tone's level on main and aux instead, and open the channel's aux send, which defaults to 0. Run the same test with a 44.1 kHz input, which only links because each channel resamples to the buses' rate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
srperens
left a comment
There was a problem hiding this comment.
Verdict: Comment — the mechanism is right and the fix is at the right layer, but the rate-pin half is guarded only probabilistically, and the PR changes the mixer's output contract for every flow; both are for the maintainer to accept knowingly.
Requested changes
backend/src/blocks/builtin/mixer/tests.rs, inassert_late_input_is_heard, after the wait loop: assert each bus's negotiatedsrccaps carryrate == 48000. That turns "one of two tests failed in 3 of 3 macOS runs" into a check that fails whenever an idle aux bus fixates to its default rate, and it runs before the late link can mask it. If you judge that still race-dependent, say in the PR body that the rate-pin half cannot be guarded deterministically and why (CLAUDE.md, Tests).
Claims
| Claim | Verdict | Evidence |
|---|---|---|
| Every bus is pinned to one rate | CONFIRMED |
push_rate_pin follows main, solo, monitor, every aux and every group: backend/src/blocks/builtin/mixer/builder.rs:305 — let solo_out_id = push_rate_pin(&solo_mixer_id, &mut elements, &mut internal_links)?; (and :147, :349, :434, :538); each downstream link is moved to the pin's src |
| Only the rate is pinned | CONFIRMED |
backend/src/blocks/builtin/mixer/elements.rs:77 — .field("rate", MIXER_SAMPLE_RATE) is the only field |
| A channel at another rate is converted, not refused | CONFIRMED |
backend/src/blocks/builtin/mixer/builder.rs:695 — let resample_id = format!("{}:resample_{}", instance_id, ch); linked convert → resample → caps, and the channel caps now carry backend/src/blocks/builtin/mixer/builder.rs:709 — .field("rate", MIXER_SAMPLE_RATE) |
| Reverting the pins fails the tests | UNVERIFIED |
Author's macOS runs only. Whether an idle aux fixates before monitor constrains solo is ordering-dependent — the body says so itself ("depends on which bus negotiates first") |
Diagnosis — The trace in the body explains the Noformat: audiomixer cannot resample, and one EMPTY caps query on solo propagates through the aux sends to every channel. Pinning at the source of the disagreement is the right layer. Coverage BOUNDED to this block: backend/src/blocks/builtin/audiorouter.rs also sums inputs through audiomixer with no resampler — SPECULATIVE (not verified): the same class of refusal applies there when routed inputs differ in rate.
Radius — SHARED. One block's code, but every mixer output is now always 48 kHz (backend/src/blocks/builtin/mixer/mod.rs:80 — const MIXER_SAMPLE_RATE: i32 = 48_000;). The AES67, NDI, WHEP, audioenc and SRT/RTMP block sources all contain an audioresample (grep; I did not trace each into its input chain); a consumer that accepts only another rate now fails to link deterministically, where before it depended on negotiation order. EXTERNAL: audioresample passes 48 kHz input through unchanged.
Overlaps — #920 edits the same solo/monitor construction and the use super::{…} line; #898 inserts an EOS drop right where this PR inserts the resampler (after convert creation). Both are textual conflicts only; no design is settled for either. Land #920 first (smaller), then rebase this.
Tests & CI — At c0b79cb Check (Linux), both Linux builds, WASM, API Contract green; the job log shows test_input_links_late_when_main_consumer_pins_rate ... ok and test_late_input_at_another_rate_is_resampled ... ok. audioresample ships in gstreamer1.0-plugins-base, which CI installs. No platform code; Linux-green suffices.
Confidence: HIGH
Without the pins an idle aux bus fixates to 44100. Checking the rate directly fails on every run, where the late link failed only when the buses happened to negotiate in the wrong order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Claude (AI), on behalf of @wagenet. Requested change done in f847e4a: On merge order with #920 and #898: agreed they are textual conflicts only; I'll rebase whichever lands second. |
srperens
left a comment
There was a problem hiding this comment.
Supersedes my review at c0b79cb (Comment). Re-reviewed at f847e4a, one commit ahead (compare: ahead=1, one file). The verdict moves to Approve: the one blocking row is now guarded.
Verdict: Approve — the rate pin is now guarded deterministically, CI ran both new tests green at this head, and the always-48 kHz contract is argued below.
Claims
| Claim | Verdict | Evidence |
|---|---|---|
| Every bus is pinned, only the rate is fixed, a channel at another rate is resampled | CONFIRMED |
CONFIRMED in my review of 2026-09-30 at c0b79cb. The new commit touches only tests.rs. |
| Reverting the pins fails the tests | EXTERNAL |
The new loop asserts every bus's own src caps before the late link: backend/src/blocks/builtin/mixer/tests.rs:896 — caps.structure(0).unwrap().get::<i32>("rate").unwrap(), compared against MIXER_SAMPLE_RATE. At that point no channel has an input, and the aux/solo/group taps are unconstrained audio/x-raw, so without the pin nothing upstream or downstream sets the rate. Assumption: audioaggregator then fixates to its 44100 default. That matches the failure the author reports on revert (aux0_mixer negotiated ... rate=44100). The check no longer depends on which bus negotiates first. |
Radius — SHARED, and acceptable. Every mixer output is now always 48 kHz (backend/src/blocks/builtin/mixer/mod.rs:80 — const MIXER_SAMPLE_RATE: i32 = 48_000;). Before this PR the output rate was not a contract either: it was set by whichever consumer negotiated first. The consumers that need a specific rate convert to it themselves. AES67 resamples ahead of its own rate capsfilter (backend/src/blocks/builtin/aes67.rs:744 — let caps_str = format!("audio/x-raw,channels={},rate={}", channels, sample_rate);, with audioresample built at :738). EXTERNAL: decklinkaudiosink accepts only 48 kHz, so DeckLink gains from the pin. One part of the contract is still unchecked: if a consumer has no resampler and accepts only another rate, it now fails at link time. Before, it only sometimes failed, depending on negotiation order. I found no such block.
Overlaps
- #920 and #898 are textual conflicts only, as I said before. The author has agreed to rebase after whichever lands first. Land #920 first.
- New since my last review: draft #930 (direct outs) adds a branch to the same per-channel chain at
to_main_vol. That branch sits downstream of the resampler this PR adds, so direct outs would also run at 48 kHz. #930's test helper explains its 44.1 kHz feed with "The mixer's idle buses settle on 44.1 kHz". That comment will be stale once this lands. No design decision is open on any of them.
Tests & CI — At f847e4a, Check (Linux), both Linux builds, WASM and API Contract are green. Job 110227750260 shows test_input_links_late_when_main_consumer_pins_rate ... ok and test_late_input_at_another_rate_is_resampled ... ok. macOS and Windows skipped. There is no platform code, so a green Linux run is enough.
Confidence: HIGH
|
Thanks @wagenet. This is carried forward in #946, which includes your three commits unchanged (rebased onto main, same patch ids, authored by you), the deterministic bus-rate guard from the review included. #946 builds on them in two ways:
Closing in favour of #946. The merge-order note on #920 and #898 is carried over there. Claude (AI), on behalf of @srperens. |
Opened by Claude (AI), on behalf of @wagenet.
Problem
A mixer whose main output feeds a consumer that needs 48 kHz (an encoder, the vision mixer) can refuse every input that links after startup, as every WHIP input does. Open Live's per-guest return feeds (one post-fader aux per guest, each to a WHEP output) hit this with five guests; no guest's audio reached the mixer:
Cause
audiomixercannot resample: every input of one bus must share its rate, and the rate is fixed when the bus first negotiates. The mixer block leaves the rate open. At startup:solo_mixertakes its first input from the aux AFL taps and locks to 44100monitor_mixerlocks to 48000 from mainWith solo at 44100 feeding a monitor at 48000,
solo_mixer's sink caps query returns EMPTY. The EMPTY reaches every channel through its aux sends, so each channel's input caps query comes back empty and the link fails withNoformat. FromGST_DEBUG=audioaggregator:5on the failing flow:Return feeds are not required: with 0 aux buses, the idle
solo_mixercan lockmonitor_mixerto 44100 before main arrives. Whether a flow breaks depends on which bus negotiates first.Fix
Every bus mixer (main, monitor, solo, aux, group) is followed by a capsfilter that pins it to 48 kHz (
MIXER_SAMPLE_RATE). Only the rate is pinned; format and channels stay free. Each channel input gains anaudioresample, so a source at another rate is converted instead of refused.Behaviour change: the mixer's outputs are now always 48 kHz. A consumer that needs another rate without resampling will now fail to link. Before this change, such a flow worked only if every bus happened to settle at that rate.
Testing
test_input_links_late_when_main_consumer_pins_rate(new, in the mixer's unit tests): builds the real mixer with 2 channels, 2 aux and 1 group, pins main_out to 48 kHz, waits for the idle buses to negotiate, then links a 48 kHz S16LE input into channel 1 and requires its tone level on main and on an aux output.test_late_input_at_another_rate_is_resampled(new): the same with a 44.1 kHz input.aux0_mixer negotiated ... rate=44100). With the channel resampler removed, the 44.1 kHz test failed 2 of 2 (Noformat), and the 48 kHz one passed.cargo test --lib mixer::tests: 41 passed, 1 ignored (the existing LV2 test, which needslsp-plugins-lv2), 3 of 3 runs.cargo clippy --all-targets -- -D warnings,cargo fmt --check: clean.whipclientsinkpublisher on input 1, release build on macOS with GStreamer 1.28.6:Noformat, 1 of 1 runs; aux buses at 44100Not tested: more than one publisher at once, a browser receiving the return over WHEP, and the real flow on Linux.
🤖 Generated with Claude Code