게임을 위한 Web Audio API
좋은 오디오는 게임을 완전히 바꿔놓습니다. 소리 없는 게임은 생기가 없게 느껴지죠. Web Audio API는 브라우저 안에서 저지연 음향 효과, 음악 재생, 심지어 3D 공간 음향까지 제공합니다.
직접 해보기 (버튼을 눌러 합성된 소리를 들어보세요):
자동 재생 문제
다른 것을 하기 전에 자동 재생 제한부터 이해해야 합니다. 브라우저는 사용자가 페이지와 상호작용하기 전까지 오디오를 막아둡니다. 이건 좋은 일이지만(웹사이트가 로드되자마자 소리를 쏟아내는 걸 원하는 사람은 없으니까요), 클릭, 탭, 키 입력에 반응해서 오디오를 초기화해야 한다는 뜻이기도 합니다.
let audioCtx = null
function initAudio() {
if (!audioCtx) {
audioCtx = new AudioContext()
}
if (audioCtx.state === 'suspended') {
audioCtx.resume()
}
return audioCtx
}
// 첫 상호작용에서 초기화
document.addEventListener('click', () => initAudio(), { once: true })
document.addEventListener('keydown', () => initAudio(), { once: true })이 전에 오디오를 재생하려고 하면 조용히 실패하거나 AudioContext가 suspended 상태로 머뭅니다. 많은 개발자가 여기서 당황합니다.
사운드 불러오기
오디오 파일은 재생 전에 디코딩해야 합니다. 파일을 가져오고, 디코딩하고, 버퍼를 보관해서 재사용하세요.
async function loadSound(url) {
const response = await fetch(url)
const arrayBuffer = await response.arrayBuffer()
const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer)
return audioBuffer
}디코딩에는 시간이 걸리니, 재생이 필요한 순간이 아니라 로딩 화면에서 모든 사운드를 미리 불러두세요.
사운드 재생하기
사운드를 재생하려면 버퍼 소스를 만들고, 볼륨 조절을 위해 게인 노드를 거쳐 연결한 다음 시작합니다.
function playSound(buffer, volume = 1) {
const source = audioCtx.createBufferSource()
source.buffer = buffer
const gainNode = audioCtx.createGain()
gainNode.gain.value = volume
source.connect(gainNode)
gainNode.connect(audioCtx.destination)
source.start(0)
return source
}버퍼 소스는 일회성입니다. 소스가 재생을 마치면 다시 시작할 수 없습니다. 재생할 때마다 새로 만드세요. 낭비처럼 느껴지지만 API가 원래 이렇게 동작하고, 브라우저도 이 방식에 맞춰 최적화되어 있습니다.
사운드 매니저
실제 게임에서는 모든 것을 매니저 클래스로 감싸세요.
class SoundManager {
constructor() {
this.ctx = null
this.sounds = new Map()
this.masterGain = null
}
init() {
this.ctx = new AudioContext()
this.masterGain = this.ctx.createGain()
this.masterGain.connect(this.ctx.destination)
}
async load(name, url) {
const response = await fetch(url)
const buffer = await this.ctx.decodeAudioData(await response.arrayBuffer())
this.sounds.set(name, buffer)
}
play(name, volume = 1) {
const buffer = this.sounds.get(name)
if (!buffer) return
const source = this.ctx.createBufferSource()
source.buffer = buffer
const gain = this.ctx.createGain()
gain.gain.value = volume
source.connect(gain)
gain.connect(this.masterGain)
source.start(0)
return source
}
setMasterVolume(v) {
this.masterGain.gain.value = v
}
}마스터 게인 노드가 있으면 전역 볼륨 슬라이더를 만들 수 있습니다. 모든 사운드가 이 노드를 거쳐 라우팅됩니다.
배경 음악
음악은 반복 재생이 필요하고, 멈추거나 페이드 아웃할 수 있어야 합니다.
class MusicPlayer {
constructor(ctx, masterGain) {
this.ctx = ctx
this.masterGain = masterGain
this.currentTrack = null
this.gain = ctx.createGain()
this.gain.connect(masterGain)
}
play(buffer, volume = 0.5) {
this.stop()
this.currentTrack = this.ctx.createBufferSource()
this.currentTrack.buffer = buffer
this.currentTrack.loop = true
this.currentTrack.connect(this.gain)
this.gain.gain.value = volume
this.currentTrack.start(0)
}
stop() {
if (this.currentTrack) {
this.currentTrack.stop()
this.currentTrack = null
}
}
fadeOut(duration = 1) {
this.gain.gain.linearRampToValueAtTime(0, this.ctx.currentTime + duration)
}
}linearRampToValueAtTime 메서드는 갑작스럽게 끊는 대신 부드러운 페이드를 만들어줍니다.
공간 음향
3D 게임에서는 소리를 공간에 배치할 수 있습니다. 소리는 청자에게서 멀어질수록 작아집니다.
function playSpatialSound(buffer, x, y, z) {
const source = audioCtx.createBufferSource()
source.buffer = buffer
const panner = audioCtx.createPanner()
panner.panningModel = 'HRTF'
panner.distanceModel = 'inverse'
panner.refDistance = 1
panner.maxDistance = 100
panner.positionX.value = x
panner.positionY.value = y
panner.positionZ.value = z
source.connect(panner)
panner.connect(audioCtx.destination)
source.start(0)
}
function updateListener(x, y, z, fx, fy, fz) {
const listener = audioCtx.listener
listener.positionX.value = x
listener.positionY.value = y
listener.positionZ.value = z
listener.forwardX.value = fx
listener.forwardY.value = fy
listener.forwardZ.value = fz
listener.upX.value = 0
listener.upY.value = 1
listener.upZ.value = 0
}매 프레임마다 카메라나 플레이어 위치로 updateListener를 호출하세요. panner가 거리와 방향에 따라 음량과 스테레오 패닝을 자동으로 조정합니다.
빠르게 연발되는 사운드
초당 여러 번 재생되는 소리(총소리, 발소리)는 겹치는 인스턴스가 쌓이는 걸 원하지 않을 겁니다. 간단한 풀 패턴이 도움이 됩니다.
class SoundPool {
constructor(ctx, buffer, size = 8) {
this.sources = []
this.index = 0
this.ctx = ctx
this.buffer = buffer
this.size = size
}
play(volume = 1) {
const source = this.ctx.createBufferSource()
source.buffer = this.buffer
const gain = this.ctx.createGain()
gain.gain.value = volume
source.connect(gain)
gain.connect(this.ctx.destination)
source.start(0)
this.index = (this.index + 1) % this.size
}
}풀은 같은 사운드를 동시에 몇 개까지 재생할 수 있는지를 제한합니다. 한계에 도달하면 가장 오래된 것이 교체됩니다.
오디오 포맷
상황마다 더 잘 맞는 포맷이 다릅니다.
MP3는 어디서나 동작하고 음악에 적합합니다. OGG Vorbis는 같은 파일 크기에서 더 좋은 품질을 냅니다. Safari가 오랫동안 버텼지만 Safari 18.4에서 네이티브 Ogg Vorbis 재생을 추가했으니, 이제는 현재의 모든 주요 브라우저가 지원합니다. 구버전 Safari까지 신경 쓴다면 여전히 MP3 폴백을 두는 게 좋습니다. AAC/M4A는 Apple 기기에서 잘 동작합니다. WAV는 압축되지 않아 크기가 크지만 즉시 디코딩되는데, 파일 크기를 감당할 수 있는 짧은 음향 효과에 좋습니다. WebM/Opus는 품질 대비 크기가 가장 좋지만 최신 브라우저에서만 동작합니다.
최대한의 호환성을 위해 폴백을 제공하세요.
const audioUrl = canPlayOgg() ? 'sound.ogg' : 'sound.mp3'유용한 팁
음높이 변화는 반복되는 소리가 덜 기계적으로 느껴지게 합니다.
function playWithPitchVariation(buffer, variance = 0.1) {
const source = audioCtx.createBufferSource()
source.buffer = buffer
source.playbackRate.value = 1 + (Math.random() - 0.5) * variance
source.connect(audioCtx.destination)
source.start(0)
}총소리나 발소리가 매번 조금씩 다르게 들립니다.
**더킹(Ducking)**은 대사나 중요한 소리가 나오는 동안 음악 볼륨을 낮춥니다.
function duckMusic(musicGain, duration = 0.3) {
musicGain.gain.linearRampToValueAtTime(0.2, audioCtx.currentTime + duration)
}
function unduckMusic(musicGain, duration = 0.3) {
musicGain.gain.linearRampToValueAtTime(1, audioCtx.currentTime + duration)
}iOS의 함정
iOS는 다른 플랫폼보다 오디오 제한이 더 엄격합니다. AudioContext는 사용자 제스처 중에 생성하고 resume해야 합니다. 일부 iOS 버전은 컨텍스트를 만드는 것만으로는 부족하고, 제스처 중에 소리(무음이라도)를 실제로 재생할 것을 요구합니다. 데스크톱에서는 오디오가 되는데 iOS에서는 안 된다면 아마 이게 원인입니다.
더 많은 자료
빠르게 로드되는 웹 게임 출시하기에서 오디오를 포함한 에셋 압축을 다룹니다.
모바일 친화적인 웹 게임에 iOS 관련 이슈가 더 나옵니다.
오프라인 게임을 위한 PWA에서 오프라인 재생을 위해 오디오 파일을 캐싱하는 방법을 보여줍니다.
게임 오디오를 위한 Tone.js에서 더 높은 수준의 라이브러리로 절차적 사운드 생성과 음악 시스템을 다룹니다.
무료 게임 에셋을 찾을 수 있는 곳에서 Freesound, Poly Haven 같은 무료 음향 효과와 음악 소스를 나열합니다.
외부 자료
- MDN: Web Audio API — 전체 API 레퍼런스
- MDN: AudioContext — 오디오 컨텍스트 생성과 관리
- Web Audio API spec — W3C 명세