# Phase 6-1: Audio System Foundation — Design Spec ## Overview `voltex_audio` crate를 신규 생성한다. WAV 파서, WASAPI 백엔드(Windows), 채널 기반 오디오 스레드로 기본 사운드 재생을 구현한다. ## Scope - WAV 파서 (PCM 16-bit, mono/stereo) - AudioClip (파싱된 오디오 데이터, f32 샘플) - WASAPI 백엔드 (Windows, shared mode, FFI 직접 호출) - 오디오 스레드 + mpsc channel 명령 처리 - AudioSystem API (load_wav, play, stop, set_volume) - 믹싱 (다중 동시 재생, 볼륨, 루프) ## Out of Scope - macOS (CoreAudio), Linux (ALSA/PulseAudio) 백엔드 - 3D 오디오 (거리 감쇠, 패닝) - 믹서 (채널 그룹, 페이드) - OGG/Vorbis 디코더 - 비동기 로딩 - ECS 통합 (AudioSource 컴포넌트 등) ## Module Structure ``` crates/voltex_audio/ ├── Cargo.toml └── src/ ├── lib.rs — public exports ├── wav.rs — WAV 파서 ├── audio_clip.rs — AudioClip 타입 ├── backend_wasapi.rs — WASAPI FFI (Windows 전용, cfg(target_os)) ├── mixer_thread.rs — 오디오 스레드 + 믹싱 로직 └── audio_system.rs — AudioSystem (메인 스레드 API) ``` ## Dependencies - 없음 (외부 crate 없음) - Windows FFI: `windows-sys` 스타일이 아닌 직접 `extern "system"` 선언 - `std::sync::mpsc`, `std::thread`, `std::sync::Arc` ## Types ### AudioClip ```rust #[derive(Clone)] pub struct AudioClip { pub samples: Vec, // interleaved, normalized -1.0~1.0 pub sample_rate: u32, pub channels: u16, } ``` ### AudioCommand (내부) ```rust enum AudioCommand { Play { clip_index: usize, volume: f32, looping: bool }, Stop { clip_index: usize }, SetVolume { clip_index: usize, volume: f32 }, StopAll, Shutdown, } ``` ### PlayingSound (내부, 오디오 스레드) ```rust struct PlayingSound { clip_index: usize, position: usize, // 현재 샘플 위치 volume: f32, looping: bool, } ``` ### AudioSystem (public API) ```rust pub struct AudioSystem { sender: Sender, _thread: JoinHandle<()>, } ``` **Methods:** - `new(clips: Vec) -> Result` — WASAPI 초기화, 오디오 스레드 시작. clips를 Arc로 공유. - `play(clip_index: usize, volume: f32, looping: bool)` — 재생 명령 - `stop(clip_index: usize)` — 정지 - `set_volume(clip_index: usize, volume: f32)` — 볼륨 변경 - `stop_all()` — 전체 정지 - `Drop` — Shutdown + join ## WAV Parser ### 지원 포맷 - RIFF WAV, PCM (format_tag = 1) - 16-bit 샘플만 - Mono (1ch) 또는 Stereo (2ch) - 임의 sample rate ### 파싱 과정 1. RIFF 헤더 검증 ("RIFF", "WAVE") 2. "fmt " 청크: format_tag, channels, sample_rate, bits_per_sample 읽기 3. "data" 청크: raw PCM 데이터 4. i16 → f32 변환: `sample as f32 / 32768.0` ### 에러 - 파일 읽기 실패 - RIFF/WAVE 시그니처 불일치 - format_tag != 1 (non-PCM) - bits_per_sample != 16 - data 청크 미발견 ```rust pub fn parse_wav(data: &[u8]) -> Result ``` 바이트 슬라이스를 받아 파싱. 파일 I/O는 호출부에서 처리. ## WASAPI Backend ### 초기화 순서 1. `CoInitializeEx(null, COINIT_MULTITHREADED)` 2. `CoCreateInstance(CLSID_MMDeviceEnumerator)` → `IMMDeviceEnumerator` 3. `enumerator.GetDefaultAudioEndpoint(eRender, eConsole)` → `IMMDevice` 4. `device.Activate(IID_IAudioClient)` → `IAudioClient` 5. `client.GetMixFormat()` — 장치 기본 포맷 확인 6. `client.Initialize(AUDCLNT_SHAREMODE_SHARED, ...)` — shared mode 7. `client.GetService(IID_IAudioRenderClient)` → `IAudioRenderClient` 8. `client.Start()` — 재생 시작 ### 버퍼 쓰기 루프 1. `client.GetCurrentPadding()` → 사용 가능한 프레임 수 계산 2. `render_client.GetBuffer(frames)` → 버퍼 포인터 3. 믹싱된 샘플을 버퍼에 쓰기 4. `render_client.ReleaseBuffer(frames)` 5. `thread::sleep(Duration::from_millis(5))` — CPU 사용 조절 ### FFI 타입 COM 인터페이스를 vtable 기반 raw pointer로 직접 선언. `#[repr(C)]` 구조체 사용. ### 샘플 레이트 변환 클립의 sample_rate와 장치의 sample_rate가 다른 경우, 선형 보간으로 리샘플링. ### 채널 변환 - Mono 클립 → Stereo 장치: 양 채널에 동일 샘플 - Stereo 클립 → 장치 채널 수 매칭 ## Mixing 오디오 스레드에서 수행하는 순수 함수: ```rust fn mix_sounds( output: &mut [f32], playing: &mut Vec, clips: &[AudioClip], device_sample_rate: u32, device_channels: u16, ) ``` 1. output 버퍼를 0으로 초기화 2. 각 PlayingSound에 대해: - 클립에서 샘플 읽기 (리샘플링/채널 변환 적용) - volume 곱하기 - output에 합산 - position 전진. 끝에 도달하면 looping이면 0으로, 아니면 제거 3. 클리핑: -1.0~1.0으로 clamp ## Test Plan ### wav.rs (단위 테스트 가능) - 유효한 WAV 바이트 → AudioClip 파싱 성공 - 잘못된 RIFF 헤더 → 에러 - non-PCM format → 에러 - 24-bit → 에러 (16-bit만 지원) - 빈 data 청크 → 빈 samples - i16→f32 변환 정확도 (32767 → ~1.0, -32768 → -1.0) ### audio_clip.rs - 생성, 속성 확인 ### mixer_thread.rs (순수 함수 테스트) - 단일 사운드 믹싱 → 출력 = 클립 샘플 * 볼륨 - 두 사운드 합산 - 클리핑 (-1.0~1.0) - 루핑: 끝에 도달 후 처음부터 재개 - 비루핑: 끝에 도달 후 제거 ### WASAPI + AudioSystem - 실제 하드웨어 필요 → `examples/audio_demo`로 수동 테스트 - WAV 파일 로드 → play → 소리 확인 ## Example `examples/audio_demo` — WAV 파일을 로드하고 키 입력으로 재생/정지하는 데모. 테스트용 WAV 파일은 코드로 생성 (440Hz 사인파, 1초).