204 lines
5.8 KiB
Markdown
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초).
|