5.8 KiB
5.8 KiB
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
#[derive(Clone)]
pub struct AudioClip {
pub samples: Vec<f32>, // interleaved, normalized -1.0~1.0
pub sample_rate: u32,
pub channels: u16,
}
AudioCommand (내부)
enum AudioCommand {
Play { clip_index: usize, volume: f32, looping: bool },
Stop { clip_index: usize },
SetVolume { clip_index: usize, volume: f32 },
StopAll,
Shutdown,
}
PlayingSound (내부, 오디오 스레드)
struct PlayingSound {
clip_index: usize,
position: usize, // 현재 샘플 위치
volume: f32,
looping: bool,
}
AudioSystem (public API)
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
파싱 과정
- RIFF 헤더 검증 ("RIFF", "WAVE")
- "fmt " 청크: format_tag, channels, sample_rate, bits_per_sample 읽기
- "data" 청크: raw PCM 데이터
- i16 → f32 변환:
sample as f32 / 32768.0
에러
- 파일 읽기 실패
- RIFF/WAVE 시그니처 불일치
- format_tag != 1 (non-PCM)
- bits_per_sample != 16
- data 청크 미발견
pub fn parse_wav(data: &[u8]) -> Result<AudioClip, String>
바이트 슬라이스를 받아 파싱. 파일 I/O는 호출부에서 처리.
WASAPI Backend
초기화 순서
CoInitializeEx(null, COINIT_MULTITHREADED)CoCreateInstance(CLSID_MMDeviceEnumerator)→IMMDeviceEnumeratorenumerator.GetDefaultAudioEndpoint(eRender, eConsole)→IMMDevicedevice.Activate(IID_IAudioClient)→IAudioClientclient.GetMixFormat()— 장치 기본 포맷 확인client.Initialize(AUDCLNT_SHAREMODE_SHARED, ...)— shared modeclient.GetService(IID_IAudioRenderClient)→IAudioRenderClientclient.Start()— 재생 시작
버퍼 쓰기 루프
client.GetCurrentPadding()→ 사용 가능한 프레임 수 계산render_client.GetBuffer(frames)→ 버퍼 포인터- 믹싱된 샘플을 버퍼에 쓰기
render_client.ReleaseBuffer(frames)thread::sleep(Duration::from_millis(5))— CPU 사용 조절
FFI 타입
COM 인터페이스를 vtable 기반 raw pointer로 직접 선언. #[repr(C)] 구조체 사용.
샘플 레이트 변환
클립의 sample_rate와 장치의 sample_rate가 다른 경우, 선형 보간으로 리샘플링.
채널 변환
- Mono 클립 → Stereo 장치: 양 채널에 동일 샘플
- Stereo 클립 → 장치 채널 수 매칭
Mixing
오디오 스레드에서 수행하는 순수 함수:
fn mix_sounds(
output: &mut [f32],
playing: &mut Vec<PlayingSound>,
clips: &[AudioClip],
device_sample_rate: u32,
device_channels: u16,
)
- output 버퍼를 0으로 초기화
- 각 PlayingSound에 대해:
- 클립에서 샘플 읽기 (리샘플링/채널 변환 적용)
- volume 곱하기
- output에 합산
- position 전진. 끝에 도달하면 looping이면 0으로, 아니면 제거
- 클리핑: -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초).