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

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

파싱 과정

  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 청크 미발견
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

오디오 스레드에서 수행하는 순수 함수:

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초).