Skip to content

fix(au): let the main input stream format select a layout - #5

Merged
unohee merged 2 commits into
masterfrom
feat/au-mono-input-negotiation
Aug 20, 2026
Merged

fix(au): let the main input stream format select a layout#5
unohee merged 2 commits into
masterfrom
feat/au-mono-input-negotiation

Conversation

@unohee

@unohee unohee commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

문제

AU 래퍼의 kAudioUnitProperty_StreamFormat 입력 스코프 처리가 요청 채널 수를 "지금 출력 채널 수가 함의하는 레이아웃"에만 대조했다:

let expected = bus_channel_count::<P>(this.n_channels(), Input, element)?;
if expected != req_ch { return kAudioUnitErr_FormatNotSupported; }

두 사실이 겹치면서 출력 스코프를 먼저 세팅하지 않는 호스트에게는 두 번째 레이아웃이 존재하지 않는 것이 된다:

  1. n_channels 는 생성 시 2 로 하드코딩된다 (wrapper.rs:542) — 레이아웃 배열의 index 0 이 아니다.
  2. layout_for_outputmain_output_channels 만 본다. 입력 쪽은 선택에 관여하지 못하고 검증만 당한다.

AU 는 채널 포맷을 스코프 하나씩 협상하고 호스트마다 순서가 다르다. auval 의 채널 테스트는 입력 포맷을 먼저 1ch 로 세팅한다 — 그 순간 레이아웃은 아직 {2,2}expected=2 != 1 → 거절, 이후 렌더 준비가 -10868 로 죽는다.

{2,2} + {1,1} 을 선언한 이펙트(= plugins/examples/gain 을 포함해 스톡 예제들이 쓰는 바로 그 모양)에서 실측:

Reported Channel Capabilities (explicit):
      [2, 2]  [1, 1]
Input/Output Channel Handling:
1-1  ...  2-2
X         X
...
1 Channel Test:
ERROR: :-10868 Render Preparation, 512 frames
* * FAIL

능력 광고와 채널 핸들링 매트릭스는 정상이다. 죽는 곳은 렌더 준비다.

수정

메인 입력 버스도 출력 버스와 똑같이 레이아웃을 선택하게 한다 (layout_for_main_input 추가). 출력 채널 수를 함께 옮긴다.

경계 조건 — "아무거나 수용"이 되지 않게 하는 것들:

  • 보조(사이드체인) element 는 기존 엄격 검증 유지. 그 채널 수는 다른 메인 레이아웃을 함의하지 않는다.
  • 레이아웃이 하나뿐인 플러그인은 영향 없음. 조회가 아무것도 못 찾아 그대로 거절한다.
  • 거절된 포맷은 선택 상태를 건드리지 않는다.
  • main_input_channels: None 인 인스트루먼트는 is_main_input 이 false 라 경로에 들어오지 않는다.
  • 프로퍼티 세팅은 uninitialized 일 때만 허용되므로(핸들러 상단 is_initialized() 가드) 렌더 스레드와 경합하지 않는다. n_channels 저장은 출력 분기가 이미 쓰던 것과 같은 atomic·같은 ordering 이다.

검증

  • 래퍼 테스트 3종 추가 ({2,2}+{1,1} 이펙트 대상): 입력 우선 협상이 레이아웃을 옮기는지, 미선언 채널 수는 여전히 거절하는지, 출력 스코프 선택이 그대로인지.
  • negative control: 수정만 되돌리면 main_input_stream_format_selects_a_matching_layoutleft: -10868 / right: 0 으로 실패한다 — auval 이 낸 것과 같은 오류 코드다.
  • cargo test -p nih_plug --features au --lib 125 passed.
  • 실제 auval (소비자 de-artifact, macOS 26, .app 래핑 + -oop): 수정 후 PASS1 Channel Test: Render Test at 512 frames → PASS. 스테레오 렌더 테스트도 그대로 통과.

남는 것 (이 PR 범위 밖)

auvalWARNING: Can Initialize Unit to un-supported num channels:InputChan:1, OutputChan:2 를 낸다. 섹션도 전체도 PASS 다. 래퍼가 마지막으로 세팅된 입력 채널 수를 따로 기억하지 않아 Initialize 에서 교차검증을 못 하기 때문인데, 고치려면 Initialize 단계 검증을 새로 넣어야 한다 — 포맷 세팅 순서가 제각각인 실제 호스트에서 "AU 초기화 실패"라는 새 실패 계열을 만들 위험이 WARNING 하나보다 크다고 보고 별도로 남긴다.

관련: de-artifact AUD-1278 (모노 레이아웃), AUD-1259 (포크 분기 정리)

unohee added 2 commits August 20, 2026 00:47
The input scope was only ever validated against the layout the *output*
channel count implied, so a layout was reachable only if the host
configured the output bus first. Hosts do not agree on that order:
auval's channel tests set the input format first, so a plugin declaring
both {2,2} and {1,1} was told FormatNotSupported for a mono input it
fully supports, and validation then died with -10868 in Render
Preparation — making the mono layout unusable in practice.

The main input bus now selects a matching layout the same way the output
bus already did, moving the output channel count with it. Auxiliary
sidechain elements keep the strict check: their counts never imply a
different main layout, and single-layout plugins are unaffected because
the lookup simply finds nothing and still rejects.

Covered by three wrapper tests over a stereo+mono effect; the layout
selection test fails with exactly -10868 when the change is reverted.
The previous commit let the main input stream format select a layout, but
persisted only its output channel count. Everything downstream re-derives
the layout from that count through `layout_for_output`, which takes the
first match — so for a plugin whose layouts share an output count but
differ on the input side, e.g. [{in:2,out:2}, {in:1,out:2}], setting the
input to 1 channel returned noErr while the derivation still landed on
{2,2}. The wrapper reported success for a format it would not honor;
before the relaxation this case was rejected.

Accept a selection only when `layout_for_output` resolves back to the same
layout. Symmetric layouts — the shape every stock example plugin and the
AU-supported configuration use — are unaffected.

Also correcting an overclaim in the previous commit message: single-layout
plugins are unaffected only when their output count is 2. A mono-only
plugin still cannot negotiate input-first, because `n_channels` starts at
a hardcoded 2 and `current_layout(2)` finds nothing, so the input branch
returns InvalidElement before the new lookup is reachable. That is
pre-existing, not a regression — but the fix makes declared layouts
reachable from a resolvable starting state, not in general.

The new test returns noErr instead of -10868 when the round-trip check is
removed.
@unohee

unohee commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator Author

후속 커밋 5faea281 — 첫 커밋의 회귀 하나를 되돌린다

독립 리뷰가 잡았다. 첫 커밋은 입력 채널 수로 레이아웃을 고르되 저장은 그 레이아웃의 출력 채널 수만 했는데, 이후 파생은 전부 layout_for_output(출력 채널 수로 첫 매치)을 탄다. 그래서 출력이 같고 입력만 다른 레이아웃 쌍에서:

[{in:2,out:2}, {in:1,out:2}] — 호스트가 입력을 1ch 로 세팅 → layout_for_main_input(1) = {1,2}n_channels 는 2 그대로 → noErr. 호스트가 되읽으면 2ch 이고 initialize{2,2} 를 만든다. 지키지 않을 포맷에 성공을 답한 것이고, 완화 전 코드는 이 경우를 거절하고 있었다 — 순수한 회귀다.

이제 고른 레이아웃이 layout_for_output 으로 되짚어 같은 것으로 돌아올 때만 수용한다. 대칭 레이아웃(스톡 예제와 AU 가 지원하는 구성)은 영향 없다.

negative control: 라운드트립 검사를 빼면 새 테스트가 left: 0 / right: -10868 로 실패한다 — 거절해야 할 것을 수용한다는 뜻이다.

첫 커밋 메시지의 과장 정정

"single-layout plugins are unaffected" 는 출력 채널 수가 2 일 때만 참이다. 모노만 선언한 플러그인([{1,1}])은 여전히 입력 우선 협상이 안 된다 — n_channels 가 하드코딩 2 로 시작하는데 출력 2 짜리 레이아웃이 없어 current_layout(2) 가 None 이고, 새 조회에 닿기 전에 InvalidElement 로 빠진다 (wrapper.rs:1608-1615).

기존부터 그랬던 것이지 회귀는 아니다. 다만 이 PR 은 선언한 레이아웃을 일반적으로 도달 가능하게 만들지 않는다 — 해결 가능한 시작 상태에서 도달 가능하게 만들 뿐이다.

cargo test -p nih_plug --features au --lib 126 passed.

@unohee

unohee commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator Author

🔍 Fresh review of 5faea28 (openswarm pr review --fresh)

✓ Decision: APPROVE
The relaxed main-input negotiation is correctly scoped to element 0 of a layout with a main input, and the new ptr::eq round-trip check against layout_for_output correctly prevents accepting a layout the wrapper's output-keyed derivation can't reproduce. Atomic ordering (Release/Acquire on n_channels) is unchanged from the pre-existing pattern. Added tests cover the positive case, an undeclared-channel-count rejection, the unreproducible-layout rejection, and an output-scope regression check.
Suggestions:
- Could not execute cargo test/cargo check in this sandboxed session (Bash approval unavailable) to directly confirm the new tests compile and pass — worth confirming via CI/local build if not already done.
- Minor theoretical edge case: is_main_input gates on the currently selected layout's main_input_channels being Some; a plugin whose layouts mix presence/absence of a main input at element 0 across variants could fail to negotiate from a no-main-input layout into a has-main-input one via the input scope. Unlikely for real nih-plug plugins (effects vs instruments are consistent), not a regression, not blocking.

@unohee
unohee merged commit 16e3166 into master Aug 20, 2026
11 of 12 checks passed
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