Files
game_engine/docs/superpowers/specs/2026-03-25-phase6-1-audio.md
2026-03-25 11:37:16 +09:00

204 lines
5.8 KiB
Markdown

# 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<f32>, // 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<AudioCommand>,
_thread: JoinHandle<()>,
}
```
**Methods:**
- `new(clips: Vec<AudioClip>) -> Result<Self, String>` — 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<AudioClip, String>
```
바이트 슬라이스를 받아 파싱. 파일 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<PlayingSound>,
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초).