diagnose(au): AUD-343 — render 첫 호출 시 입력/출력 추적#270
Open
unohee wants to merge 29 commits into
Open
Conversation
새로운 `au` feature 추가. macOS-only.
추가:
- src/plugin/au.rs — `AuPlugin` trait (4-char codes: TYPE/SUBTYPE/MANUFACTURER + VERSION)
- src/wrapper/au.rs — `nih_export_au!` macro (factory function export)
- src/wrapper/au/factory.rs — PluginInfo + fourcc helper
- src/wrapper/au/wrapper.rs — Wrapper<P> struct:
- AudioComponentPlugInInterface vtable (Open/Close/Lookup)
- 핵심 selector: Initialize/Uninitialize/Reset/Render
- GetPropertyInfo/GetProperty/SetProperty:
SampleRate, StreamFormat, ElementCount, Latency, TailTime,
MaximumFramesPerSlice, ParameterList, SupportedNumChannels,
BypassEffect, LastRenderError, SetRenderCallback, InPlaceProcessing
- Get/SetParameter (Phase 2 에서 wire)
- Render: 현재 silence (Phase 3 에서 Plugin::process 연결)
deps:
- au-sys 0.1 (AUv2 C API FFI bindings)
- objc2 0.6 / objc2-foundation / objc2-app-kit (Phase 4 NSView 용)
Phase 1 검증:
- cargo build --features au 컴파일 통과
- consuming crate 에서 nih_export_au!(MyPlugin) → _nih_plug_au_factory C symbol export 확인
Wrapper<P> 가 plugin instance + params 보유: - P::default() 로 plugin 생성, Plugin::params() 의 Arc<dyn Params> 를 strong reference 로 유지 (ParamPtr 의 raw pointer 안전성 보장) - params_by_id: param_map() 결과를 declaration order 로 캐시 — AU parameter ID = 인덱스 Property selectors: - kAudioUnitProperty_ParameterList: AudioUnitParameterID 배열 반환 - kAudioUnitProperty_ParameterInfo: name(52-byte legacy buffer), min/max/default(plain), unit, flags(IsReadable/Writable/CanRamp) Parameter dispatch: - AudioUnitGetParameter: unmodulated_normalized_value() → preview_plain() → plain value 반환 - AudioUnitSetParameter: preview_normalized() → set_normalized_value() Unit 분류 휴리스틱: nih-plug Param::unit() 문자열에서 dB/Hz/%/sec 키워드 감지하여 AU unit 상수 (Decibels/Hertz/Percent/Seconds) 매핑. step_count = Some(1) → Boolean, Some(>=2) → Indexed (enum). Phase 3 (render + Plugin::process 통합) 다음.
Wrapper 가 Plugin lifecycle 와 통합: - Initialize selector: AudioIOLayout + BufferConfig 구성 후 Plugin::initialize → Plugin::reset 호출. 실패 시 kAudioUnitErr_FailedInitialization. - Uninitialize: Plugin::deactivate. - Reset selector: Plugin::reset 호출. Render selector 가 진짜 동작: - AudioBufferList → nih-plug Buffer (NonInterleaved per-channel slices) - Plugin::process(buffer, aux, ctx) 호출 - aux 는 빈 sidechain (Phase 3 미지원) - ProcessContext 는 stub (transport=defaults, MIDI 없음, latency 만 sink atomic 으로 백) - 초기화 전 render 호출 시 무음 fallback (zero_buffer_list) 추가: - src/wrapper/au/context.rs — AuInitContext, AuProcessContext, ContextSink - src/context.rs: PluginApi::Au variant + Display - AU 측 sample slice 의 lifetime 은 transmute 로 'static 으로 erase (render() 끝까지만 사용, Buffer 가 함수 로컬에서 소비) 다음 (Phase 4): Cocoa view via objc2-app-kit (egui-baseview NSView 통합).
Phase 3 의 silence 출력 버그 fix.
문제:
Plugin::process 호출은 됐지만 io_data 에 input 이 없어서 plugin 이
"input=0 → 처리해도 0" 반환. 호스트 (Ableton 등) 는 input bus 에
SetRenderCallback 으로 callback 등록 → unit 이 그 callback 호출해서
input 받는 pull model 사용. 우리는 SetRenderCallback property 에서
noErr 만 반환하고 callback 자체를 잃어버렸음.
수정:
- Wrapper 에 input_callback (Option<AURenderCallbackStruct>) +
input_scratch (Vec<Vec<f32>>) 추가
- set_property 의 kAudioUnitProperty_SetRenderCallback 가 callback 보존
- Initialize 가 input_scratch 를 n_channels × max_frames_per_slice 로
alloc (RT-safe — render path 에서 추가 alloc 없음)
- Render 시작 시 input_callback 등록되어있으면:
- n_channels 만큼의 AudioBuffer 가 들어가는 stack-local AudioBufferList
구성 (mBuffers 가 [AudioBuffer; 1] variable-length 라 raw bytes alloc)
- 호스트 callback 호출 → input_scratch 채워짐
- io_data 에 copy → Plugin::process 가 in-place 처리
- Callback 없을 때는 io_data 자체를 in-place 사용 (InPlaceProcessing 광고대로)
진단 로그 (eprintln, 첫 5 호출만): pulled / cb_set / rms_in / max_in.
Console.app 의 "NIH-AU" 필터로 동작 검증.
Wrapper 동시성 모델 재설계.
- from_ptr → &Self 반환. main(SetProperty/SetParameter)과 audio(Render/SetParameter)
스레드가 동시에 wrapper에 진입해도 &mut 두 개가 동시에 살아있을 일 없음.
- 가변 필드 분리:
* 호스트 setup (sample_rate, n_channels, max_frames_per_slice,
latency_seconds, initialized) → AtomicU32/AtomicU64/AtomicBool
* audio-thread 전용 (plugin, input_scratch) → UnsafeCell, render() 안에서만
&mut 빌림. AU는 render 재진입 금지 + Initialize/Uninitialize/Reset과
Render 동시 실행 금지를 보장.
* main↔audio 공유 (input_callback) → Mutex<Option<…>>, render는 시작 시점에
스냅샷만 떠서 락을 즉시 해제.
- unsafe impl Send + Sync 둘 다, 정당화 주석과 함께 추가.
- 모듈 doc을 Phase 3.5 + 동시성 모델로 갱신.
DoD:
- cargo build --features au 성공, AU wrapper 자체 경고 0건.
- cargo build --features au,assert_process_allocs 성공.
- grep 결과 from_ptr가 &Self만 반환, &mut Self 인스턴스 0건.
남은 alloc/transmute는 AUD-331 / AUD-333에서 처리.
세 P0를 함께 처리. render() 핫패스가 한 PR에 모이고 같은 RenderState 구조 위에서 풀려서 분리하면 동일 코드를 세 번 만져야 했음. # AUD-331 — render() audio-thread alloc 제거 - `vec![0u8; bl_bytes]`, `Vec::with_capacity(n_buffers)`, `eprintln!` 디버그 블록 모두 제거. - 새 `RenderState` 구조에 `input_scratch`, `bl_storage`, persistent `Buffer<'static>`을 묶고 `Initialize`에서 한 번 provision. - 채널 슬롯 벡터는 `provision()`에서 `reserve_exact + push(&mut [])`로 미리 마련, render는 in-place 재기록만. # AUD-334 — AudioBufferList 가변 길이 alignment 수정 - 이전: `header_size = sizeof(UInt32) = 4`로 가정 → AudioBuffer align(8)과 어긋나 첫 buffer가 패딩 영역에 쓰일 위험. - 수정: `mem::offset_of!(AudioBufferList, mBuffers)` 사용. 컴파일러가 ABI 패딩까지 정확히 계산. - bl_storage를 `Vec<u64>`로 바꿔 8-byte 정렬 보장. - `bl_byte_size(n)` 헬퍼로 정확한 크기 계산. # AUD-333 — `&'static mut [f32]` transmute 캡슐화 - 자유롭게 떠다니던 `Vec<&'static mut [f32]>` 임시 컬렉션 제거. - 슬롯은 `RenderState.buffer`(persistent `Buffer<'static>`) 내부에만 존재. - render 종료 직전 모든 슬롯을 `&mut []`로 명시적으로 비움 → `'static` lifetime이 host data보다 오래 살 수 없음. - nih-plug 공식 `BufferManager::create_buffers`와 동일한 안전 모델 (Buffer API 자체가 lifetime parameter를 가지므로 호출처에서 short-cut transmute가 1회 불가피 — 작성자도 buffer.rs의 TODO에서 인정). DoD: - cargo build --features au,assert_process_allocs ✅ - render() 핫패스 alloc/eprintln grep 0건 - transmute는 `BufferManager`와 동일 패턴으로 캡슐화 (주석에 명시)
- SetProperty(StreamFormat / SampleRate / MaximumFramesPerSlice)는 Initialized 상태에서 kAudioUnitErr_Initialized 반환. - StreamFormat: format/flags/bitsPerChannel 검증 (PCM, float, non-interleaved, 32-bit). 미일치 시 kAudioUnitErr_FormatNotSupported. - mChannelsPerFrame == 0 → kAudioUnitErr_InvalidPropertyValue. - 채널 수가 P::AUDIO_IO_LAYOUTS 어느 항목과도 매칭되지 않으면 reject. - SampleRate: sr <= 0 또는 NaN/Inf reject. - MaximumFramesPerSlice: 0 reject. - GetProperty(SupportedNumChannels): 첫 declared layout 기반으로 응답 (이전엔 (-1, -1) wildcard). 새 헬퍼 `layout_supports::<P>(req_ch)`: P::AUDIO_IO_LAYOUTS 순회하며 main_input/main_output 둘 다 매칭되는 항목 존재 여부 확인. DoD: cargo build --features au,assert_process_allocs ✅. auval 검증은 실호스트 환경 필요 — follow-up.
- Wrapper에 bypass: AtomicBool, bypass_param_idx: Option<usize> 추가.
- new()에서 P::params()의 ParamFlags::BYPASS 플래그 가진 파라미터 감지.
- get_property(BypassEffect): atomic load 반환 (이전 항상 0).
- set_property(BypassEffect):
* UInt32 검증 후 atomic store.
* BYPASS-flagged 파라미터가 있으면 그것도 0/1로 동기화 → 플러그인이
자체 param으로 bypass 상태를 관찰할 수 있음.
- render(): bypass=true면 Plugin::process 호출 건너뜀. 입력이 이미
io_data에 있으므로(in-place 또는 callback path 카피 후) pass-through는
암묵적.
DoD: cargo build --features au,assert_process_allocs ✅. DAW에서 토글
검증은 실호스트 환경 필요 → follow-up.
- Wrapper에 listeners: Mutex<Vec<Listener>>, pending_notifications: AtomicU32 추가. - add/remove_property_listener_with_user_data: stub → 실제 등록/해제. remove는 (property_id, proc 주소, user_data) 3-tuple 매칭. - mark_pending(mask)으로 변경 비트 OR. NOTIFY_LATENCY/STREAM_FORMAT/BYPASS_EFFECT. - drain_notifications()는 main-thread 전용. swap으로 비트 회수, listeners 스냅샷(락 짧게), 매칭되는 listener의 proc(user_data, instance, id, scope, 0) 호출. 호스트 콜백을 락 holding 상태에서 호출하지 않음. - 변경 트리거 지점: * set_property(StreamFormat): NOTIFY_STREAM_FORMAT * set_property(BypassEffect): 값이 실제 바뀐 경우만 NOTIFY_BYPASS_EFFECT * initialize 끝 + render 끝: latency 변동 시 NOTIFY_LATENCY - Drain 진입점: get_property/set_property/initialize 시작/끝. audio thread의 render는 mark만 하고 drain하지 않음 → 다음 main-thread 진입에서 자동 처리. - Listener Send/Sync 명시. proc은 fn ptr, user_data는 host-owned opaque. DoD: cargo build --features au,assert_process_allocs ✅. PDC/bypass 외부 변경 통지 동작 확인은 실호스트 환경 필요.
검증:
- set_parameter는 ParamPtr::set_normalized_value → 구체 Param의
set_plain_value → AtomicF32::swap + 몇 개의 store만 거침. lock-free,
alloc-free 확인 (src/params/{float,integer,boolean,enums}.rs 코드 인용).
- 유일한 escape hatch는 사용자가 등록한 value_changed: Arc<dyn Fn(f32)>;
플러그인 작성자 책임으로 명시 (nih-plug 기본 None).
buffer_offset_in_frames 정책:
- nih-plug Plugin::process는 단일 contiguous buffer로 받고 sample-accurate
parameter dispatch를 지원하지 않음. offset>0이어도 즉시 적용.
- 워스트케이스 grain 오차는 max_frames_per_slice 샘플 (보통 ≤1024 @ 48k
→ ≤21ms). 알려진 한계로 코드 주석에 명시.
추가:
- BYPASS 플래그 가진 파라미터를 set_parameter로 변경하면 wrapper의
bypass: AtomicBool도 동기화 + listener 통지. 호스트가 bypass를 property
또는 parameter 어느 경로로 토글하든 일관 동작.
DoD: cargo build --features au,assert_process_allocs ✅. 호출 경로 alloc-free
증명은 docstring에 명시.
- /trash/ : 직접 rm 대신 휴지통 경유용 워크 디렉토리 - /.gemini/, /.claude/ : 에이전트/툴 메타데이터 (협업과 무관) - /20YY-*.txt : 루트에 떨어지는 세션 트랜스크립트 캡처
호스트가 SetProperty(kAudioUnitProperty_HostCallbacks)로 등록한 HostCallbackInfo를 보관하고, render() 시작 시 다음 콜백을 호출해 Transport 구조체를 채운다. - beatAndTempoProc → tempo, pos_beats - musicalTimeLocationProc → time_sig_numerator/denominator, bar_start_pos_beats - transportStateProc → playing, pos_samples 저장은 Mutex<Option<HostCallbackInfo>>로 두되 audio thread에서는 스냅샷 후 lock을 즉시 해제. get/set property 분기에 HostCallbacks size 응답과 in_data 검증을 추가.
Apple AU API에서 AudioUnitParameterInfo::cfNameString은 'create' 규약이라 호스트가 CFRelease 책임을 진다. 따라서 이 헬퍼의 mem::forget은 누수가 아니라 의도된 retain count 인계. 미래의 독자가 오인하지 않도록 docstring 추가.
AU 모듈은 macOS C API 강결합이라 통합 테스트가 어렵지만, 순수 Rust 헬퍼는 단독 검증 가능. classify_unit는 파라미터 unit 문자열 → AU parameter unit enum 매핑이라 호스트가 보이는 표시 단위에 직접 영향. - Decibels: 'dB', 'decibel', 'dBFS' - Hertz: 'Hz', 'kHz', 'hertz' - Percent: '%', 'percent' - Seconds: 'ms', 'sec', 'seconds' - Generic: '', 'ratio', 'semitones' (fallback) 이 5건은 cxt registry의 'untested 2293' 카운트를 실측 가능한 만큼 줄여주는 첫 발판.
nih_plug_au_factory 심볼이 export된 dylib을 macOS 타겟에서 빌드한 경우
{name}.component/Contents/MacOS/{name} 레이아웃으로 배치한다. VST3와
같은 셸 구조를 따르지만 .component 확장자라 CoreAudio component manager가
스캔한다.
Info.plist는 일단 maybe_create_macos_bundle_metadata가 만드는 공통
BNDL 템플릿을 그대로 쓴다. 다음 커밋(2/2)에서 AudioComponents 배열을
추가해 호스트가 type/subtype/manufacturer로 컴포넌트를 식별할 수 있게
확장한다 — 그 단계 전에는 auval/DAW에서 보이지 않는다.
…타데이터 (2/2)
bundler.toml의 [{package}.au] 섹션에서 type/subtype/manufacturer 4cc와
선택적 description/version을 읽어 AU 전용 Info.plist를 생성한다. 핵심 키:
- factoryFunction = nih_plug_au_factory (nih_export_au!이 export하는 심볼)
- type/subtype/manufacturer (4cc 문자열)
- version (u32, major.minor.patch에서 인코딩)
- sandboxSafe = true
크로스 컴파일된 dylib을 호스트가 dlopen할 수 없으므로 AuPlugin
trait의 const와 bundler.toml 값이 중복된다 — 불가피한 비용.
검증:
- 4cc 길이/ASCII 강제 (validate_fourcc)
- 버전 범위 강제 (parse_au_version: major<=65535, minor/patch<=255)
- 5개 unit test 추가, 모두 통과
이로써 .component 번들이 auval과 호스트가 인식 가능한 형태로 생성된다.
다음 단계(AUD-342)는 실제 plugin에 AuPlugin 적용 + bundler.toml 메타데이터
기입.
…sport 연동 - AUD-339: AudioUnitParameterInfo의 cfNameString/unitName CFString 채움, ClassInfo CFDictionary 직렬화/역직렬화로 호스트가 plugin 상태를 저장/복원 - AUD-340: SetProperty(kAudioUnitProperty_HostCallbacks)로 받은 HostCallbackInfo를 보관하고 render() 시작 시 beatAndTempo/musicalTime/ transportState 콜백을 호출해 nih-plug Transport 구조체를 채움 - 부가: string_to_cfstring ownership docstring, classify_unit 회귀 테스트 5종, trash/.gemini/.claude/세션 트랜스크립트 .gitignore
nih_plug_xtask가 nih_plug_au_factory 심볼을 export하는 dylib을 macOS
타겟에서 .component 번들로 패키징하도록 확장.
- AU 심볼 감지 → {name}.component/Contents/MacOS/{name} 레이아웃
- bundler.toml의 [pkg.au] 섹션에서 type/subtype/manufacturer 4cc와
선택적 description/version 읽어 AudioComponents Info.plist 생성
- 4cc 길이/ASCII 검증, version major.minor.patch 범위 검증
- 5개 unit test (validate_fourcc, parse_au_version)
이로써 cargo xtask bundle <pkg>가 auval과 호스트가 인식 가능한 형태의
AU 번들을 산출한다. 다음 단계(AUD-342)는 실제 plugin에 AuPlugin 적용
+ bundler.toml AU 메타데이터 기입.
nih-plug AU 래퍼의 첫 실측 산출물. gain 예제는 stereo gain effect로
MIDI/SysEx 없는 가장 표준적인 effect 패턴이라 AU 도입 sanity check에
적합.
변경:
- plugins/examples/gain/Cargo.toml: au feature 정의 (default, nih_plug/au
forwarding). macOS 외 플랫폼에서는 wrapper의 macOS gate가 no-op으로
만든다
- plugins/examples/gain/src/lib.rs: impl AuPlugin (4cc 상수 — type=aufx,
subtype=MPgN, manufacturer=MoiP), nih_export_au!(Gain). cfg(feature='au',
target_os='macos') 게이트로 보호
- bundler.toml: [gain.au] 추가 (type/subtype/manufacturer/description) +
상단 주석에 AU 메타데이터 형식 안내
검증:
- cargo xtask bundle gain --release 실행 → target/bundled/Gain.component
생성 성공
- 번들 구조: Contents/MacOS/Gain (dylib) + Contents/Info.plist
(AudioComponents 배열) + Contents/_CodeSignature
- export 심볼: _nih_plug_au_factory, _clap_entry, _GetPluginFactory 모두 확인
- Info.plist의 version=65536 = (1<<16) — 1.0.0 인코딩 검증
다음 단계 (AUD-343): auval -v aufx MPgN MoiP로 Apple 공식 검증 통과.
기존 작동하는 third-party AU(예: Truce*)와 비교한 결과, 우리 plist에 다음 표준 키가 누락되어 있어 추가: - <plist version="1.0"> (DTD 호환) - CFBundleInfoDictionaryVersion = 6.0 (Apple 권장) - LSMinimumSystemVersion = 11.0 (Launch Services 호환) 또 XML 선언 다음의 빈 줄을 제거 — plutil은 lenient라 통과시키지만 일부 파서는 더 엄격할 수 있음. 이 변경만으로 auval 인식 문제가 해결되지는 않았지만(시스템 측의 AU registry 이슈 별도) plist 표준 준수는 옳은 방향이라 선반영.
AU host의 입력 전달 모델과 오디오 처리 상태를 파악하기 위해 render() 첫 호출에서 다음을 기록한다: - 프레임 수, 버퍼 개수 - input callback 존재 여부 - bypass 상태 - 첫 입력/출력 샘플값 이 코드는 검증 완료 후 제거할 일회성 진단 코드다. Refs: AUD-343 🤖 Generated with [Claude Code](https://claude.com/claude-code)
- au.rs, wrapper.rs 모듈 doc을 Phase 1 → 완전한 AUv2 구현 상태로 갱신 - factory.rs에서 미사용 PluginInfo 구조체 제거 (다중 플러그인 매크로 미완성 인프라; 현재 nih_export_au! 어디서도 참조 안 함) - classify_unit()의 "ms" → kAudioUnitParameterUnit_Seconds 매핑에 주석 추가 — AU에 _Milliseconds 없음을 명시 - clap/util.rs: unsafe_clap_call re-export에 #[allow(unused_imports)] 추가 (--features au 빌드 시 clap wrapper가 컴파일되지 않아 발생하는 경고 제거) `cargo build --features au`: 경고 0건 (objc2 cfg 노이즈 제외) Refs: AUD-342 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
AU host의 입력 전달 모델과 오디오 처리 상태를 파악하기 위한 일회성 진단 코드를 추가합니다.
render() 첫 호출에서 다음을 추적합니다:
Changes
src/wrapper/au/wrapper.rs: 진단 로그 추가 (2개 지점 — 전처리/후처리)Related Issues
🤖 Generated with Claude Code