소개
오디오는 멀티미디어 환경을 매력적으로 만드는 데 큰 역할을 합니다. 소리를 끄고 영화를 시청해 본 적이 있다면 이 점을 눈치챘을 것입니다.
게임도 예외는 아닙니다. 가장 기억에 남는 비디오 게임 추억은 음악과 음향 효과입니다. 지금도 좋아하는 게임을 플레이한 지 거의 20년이 지났지만 코지마 코도의 젤다 작곡과 맷 울먼의 분위기 있는 디아블로 사운드트랙이 머릿속에서 떠나지 않습니다. Warcraft의 단번에 알아볼 수 있는 유닛 클릭 응답이나 Nintendo의 명작 게임의 샘플과 같은 음향 효과에도 똑같이 적용됩니다.
게임 오디오는 몇 가지 흥미로운 과제를 안겨줍니다. 설득력 있는 게임 음악을 만들려면 디자이너는 플레이어가 처한 예측할 수 없는 게임 상태에 맞게 조정해야 합니다. 실제로 게임의 일부는 알 수 없는 시간 동안 진행될 수 있으며, 사운드는 환경과 상호작용하고 방 효과, 상대적 사운드 위치 지정과 같은 복잡한 방식으로 믹스될 수 있습니다. 마지막으로, 한 번에 많은 수의 사운드가 재생될 수 있으며, 이러한 사운드는 모두 성능 저하 없이 함께 제대로 사운드를 내고 렌더링해야 합니다.
웹의 게임 오디오
간단한 게임의 경우 <audio>
태그만으로도 충분할 수 있습니다. 그러나 많은 브라우저가 제대로 구현하지 않아 오디오 글리치와 높은 지연 시간이 발생합니다. 공급업체가 각 구현을 개선하기 위해 노력하고 있으므로 일시적인 문제일 수 있습니다. <audio>
태그의 상태를 간단히 살펴보려면 areweplayingyet.org에서 유용한 테스트 모음을 확인할 수 있습니다.
그러나 <audio>
태그 사양을 자세히 살펴보면 미디어 재생용으로 설계되었기 때문에 그만으로는 할 수 없는 일도 많다는 것을 알 수 있습니다. 당연한 일입니다. 몇 가지 제한사항은 다음과 같습니다.
- 소리 신호에 필터를 적용하는 기능 없음
- 원시 PCM 데이터에 액세스할 방법이 없음
- 소스 및 리스너의 위치와 방향에 대한 개념이 없음
- 세분화된 타이밍은 없습니다.
이 문서의 나머지 부분에서는 Web Audio API로 작성된 게임 오디오와 관련하여 이러한 주제 중 몇 가지를 자세히 살펴보겠습니다. 이 API에 관한 간단한 소개는 시작하기 튜토리얼을 참고하세요.
배경음악
게임에는 배경 음악이 반복 재생되는 경우가 많습니다.
루프가 짧고 예측 가능한 경우 매우 성가실 수 있습니다. 플레이어가 특정 영역이나 레벨에 머물러 있고 동일한 샘플이 백그라운드에서 계속 재생되는 경우 트랙을 점진적으로 페이드아웃하여 추가적인 불만을 방지할 수 있습니다. 또 다른 전략은 게임의 맥락에 따라 점진적으로 서로 교차 페이드되는 다양한 강도의 사운드를 믹스하는 것입니다.
예를 들어 플레이어가 장대한 보스 전투가 벌어지는 영역에 있다면 분위기 있는부터 예보, 강렬한 감정까지 다양한 감정을 혼합할 수 있습니다. 음악 합성 소프트웨어를 사용하면 내보내기에 사용할 트랙 세트를 선택하여 곡을 기준으로 동일한 길이의 여러 믹스를 내보낼 수 있습니다. 이렇게 하면 내부 일관성이 유지되고 한 트랙에서 다른 트랙으로 크로스페이드할 때 불편한 전환이 방지됩니다.
그런 다음 Web Audio API를 사용하여 XHR을 통해 BufferLoader 클래스와 같은 것을 사용하여 이러한 샘플을 모두 가져올 수 있습니다 (Web Audio API 소개 문서에서 자세히 설명함). 사운드를 로드하는 데 시간이 걸리므로 게임에서 사용되는 애셋은 페이지 로드 시, 레벨 시작 시 또는 플레이어가 플레이하는 동안 점진적으로 로드되어야 합니다.
그런 다음 각 노드의 소스와 각 소스의 이득 노드를 만들고 그래프를 연결합니다.
이렇게 하면 이러한 소스를 모두 동시에 루프로 재생할 수 있으며, 소스가 모두 동일한 길이이므로 Web Audio API는 소스가 정렬된 상태로 유지되도록 보장합니다. 캐릭터가 최종 보스전에서 멀어지거나 가까워짐에 따라 게임은 다음과 같은 이득 금액 알고리즘을 사용하여 체인의 각 노드에 대한 이득 값을 변경할 수 있습니다.
// Assume gains is an array of AudioGainNode, normVal is the intensity
// between 0 and 1.
var value = normVal - (gains.length - 1);
// First reset gains on all nodes.
for (var i = 0; i < gains.length; i++) {
gains[i].gain.value = 0;
}
// Decide which two nodes we are currently between, and do an equal
// power crossfade between them.
var leftNode = Math.floor(value);
// Normalize the value between 0 and 1.
var x = value - leftNode;
var gain1 = Math.cos(x - 0.5*Math.PI);
var gain2 = Math.cos((1.0 - x) - 0.5*Math.PI);
// Set the two gains accordingly.
gains[leftNode].gain.value = gain1;
// Check to make sure that there's a right node.
if (leftNode < gains.length - 1) {
// If there is, adjust its gain.
gains[leftNode + 1].gain.value = gain2;
}
위 접근 방식에서는 두 소스가 동시에 재생되며 소개에 설명된 대로 동일한 전력 곡선을 사용하여 두 소스 간에 크로스페이드를 실행합니다.
누락된 링크: 웹 오디오에 대한 오디오 태그
오늘날 많은 게임 개발자는 콘텐츠 스트리밍에 적합하기 때문에 배경음악에 <audio>
태그를 사용합니다. 이제 <audio>
태그의 콘텐츠를 웹 오디오 컨텍스트로 가져올 수 있습니다.
이 기법은 <audio>
태그가 스트리밍 콘텐츠와 호환되므로 유용할 수 있습니다. 따라서 모든 콘텐츠가 다운로드될 때까지 기다리지 않고도 백그라운드 음악을 즉시 재생할 수 있습니다. 스트림을 Web Audio API로 가져와 스트림을 조작하거나 분석할 수 있습니다. 다음 예에서는 <audio>
태그를 통해 재생되는 음악에 로우 패스 필터를 적용합니다.
var audioElement = document.querySelector('audio');
var mediaSourceNode = context.createMediaElementSource(audioElement);
// Create the filter
var filter = context.createBiquadFilter();
// Create the audio graph.
mediaSourceNode.connect(filter);
filter.connect(context.destination);
<audio>
태그를 Web Audio API와 통합하는 방법에 관한 자세한 내용은 이 짧은 도움말을 참고하세요.
음향 효과
게임은 사용자 입력이나 게임 상태 변경에 대한 응답으로 음향 효과를 재생하는 경우가 많습니다. 하지만 배경 음악과 마찬가지로 음향 효과도 금방 지루해질 수 있습니다. 이를 방지하려면 비슷하지만 서로 다른 사운드 풀을 재생하는 것이 좋습니다. 이는 단순한 발자국 샘플의 변화에서부터 워크래프트 시리즈에서 유닛을 클릭할 때와 같이 급격한 변화까지 다양합니다.
게임에서 음향 효과의 또 다른 중요한 특징은 여러 개의 음향 효과가 동시에 재생될 수 있다는 점입니다. 여러 명의 배우가 기관총을 발사하며 총격전을 벌이고 있다고 가정해 보겠습니다. 각 기관총은 초당 여러 번 발사되므로 수십 개의 음향 효과가 동시에 재생됩니다. 정확한 타이밍으로 여러 소스의 사운드를 동시에 재생하는 기능은 Web Audio API의 강점이라 할 수 있습니다.
다음 예에서는 재생 시간이 엇갈리는 여러 음원 소스를 만들어 여러 개의 개별 총알 샘플로 기관총 탄환을 만듭니다.
var time = context.currentTime;
for (var i = 0; i < rounds; i++) {
var source = this.makeSource(this.buffers[M4A1]);
source.noteOn(time + i - interval);
}
하지만 게임의 모든 기관총이 이와 똑같은 소리를 낸다면 지루할 것입니다. 물론 타겟과의 거리와 상대적 위치에 따라 소리에 따라 달라집니다 (자세한 내용은 나중에 설명). 하지만 그것만으로는 충분하지 않을 수 있습니다. 다행히 웹 오디오 API는 다음 두 가지 방법으로 위의 예를 쉽게 조정할 수 있는 방법을 제공합니다.
- 총알 발사 간 은근한 시간 변화
- 각 샘플의 playbackRate를 변경하여 (피치도 변경) 실제 세계의 무작위성을 더 잘 시뮬레이션합니다.
이러한 기법이 실제로 사용되는 실용적인 예를 보려면 풀 테이블 데모를 살펴보세요. 이 데모에서는 무작위 샘플링을 사용하고 재생 속도를 변경하여 더 흥미로운 볼 충돌 소리를 만듭니다.
3D 위치 사운드
게임은 종종 2D 또는 3D의 기하학적 속성이 있는 세계에 설정됩니다. 이 경우 스테레오 위치 지정 오디오를 사용하면 몰입감을 크게 높일 수 있습니다. 다행히 Web Audio API에는 사용하기 매우 간단한 하드웨어 가속 위치 오디오 기능이 내장되어 있습니다. 다음 예를 이해하려면 스테레오 스피커 (헤드폰이 좋음)가 있어야 합니다.
위 예시에서는 캔버스 중앙에 리스너(사람 아이콘)가 있고 마우스가 소스(스피커 아이콘)의 위치에 영향을 미칩니다. 위 예는 AudioPannerNode를 사용하여 이러한 효과를 얻는 간단한 예입니다. 위 샘플의 기본 아이디어는 다음과 같이 오디오 소스의 위치를 설정하여 마우스 움직임에 응답하는 것입니다.
PositionSample.prototype.changePosition = function(position) {
// Position coordinates are in normalized canvas coordinates
// with -0.5 < x, y < 0.5
if (position) {
if (!this.isPlaying) {
this.play();
}
var mul = 2;
var x = position.x / this.size.width;
var y = -position.y / this.size.height;
this.panner.setPosition(x - mul, y - mul, -0.5);
} else {
this.stop();
}
};
웹 오디오의 공간화 처리에 대해 알아야 할 사항은 다음과 같습니다.
- 리스너는 기본적으로 원점(0, 0, 0)에 있습니다.
- 웹 오디오 위치 API는 단위가 없으므로 데모 사운드를 개선하기 위해 배수를 도입했습니다.
- 웹 오디오는 y가 위인 데카르트 좌표를 사용합니다(대부분의 컴퓨터 그래픽 시스템과는 반대). 그래서 위 스니펫에서 y축을 전환합니다.
고급: 소리 원뿔
위치 모델은 매우 강력하고 고급적이며 주로 OpenAL을 기반으로 합니다. 자세한 내용은 위에 링크된 사양의 3번과 4번 섹션을 참고하세요.
Web Audio API 컨텍스트에 연결된 단일 AudioListener가 있으며, 이 리스너는 위치와 방향을 통해 공간에서 구성할 수 있습니다. 각 소스는 입력 오디오를 공간화하는 AudioPannerNode를 통해 전달될 수 있습니다. 패너 노드에는 위치 및 방향뿐만 아니라 거리 및 방향 모델이 있습니다.
거리 모델은 소스와의 근접성에 따라 게인 양을 지정하는 반면, 방향 모델은 리스너가 내부 원뿔 내부에 있는 경우 (일반적으로 음수) 게인의 양을 결정하는 내부 및 외부 원뿔을 지정하여 구성할 수 있습니다.
var panner = context.createPanner();
panner.coneOuterGain = 0.5;
panner.coneOuterAngle = 180;
panner.coneInnerAngle = 0;
이 예는 2D이지만 이 모델은 세 번째 차원으로 쉽게 일반화할 수 있습니다. 3D로 스페이셜라이즈된 사운드의 예는 이 위치 샘플을 참고하세요. 웹 오디오 사운드 모델은 위치 외에도 원하는 경우 도플러 이동의 속도도 포함합니다. 이 예에서는 도플러 효과를 자세히 보여줍니다.
이 주제에 관한 자세한 내용은 [위치 오디오와 WebGL 믹싱][webgl]에 관한 자세한 튜토리얼을 참고하세요.
회의실 효과 및 필터
실제로 소리가 인지되는 방식은 소리가 들리는 방에 따라 크게 달라집니다. 지하실에서 똑같은 삐걱거리는 문은 큰 열린 홀과는 매우 다르게 울립니다 제작 가치가 높은 게임에서는 이러한 효과를 모방하고자 할 것입니다. 각 환경에 대해 별도의 샘플 세트를 만드는 것은 비용이 너무 많이 들고 더 많은 애셋과 더 많은 게임 데이터가 생성되기 때문입니다.
대략적으로 원시 사운드와 실제로 들리는 사운드의 차이를 나타내는 오디오 용어는 임펄스 응답입니다. 이러한 임펄스 응답은 수고스럽게 녹음할 수 있으며, 실제로 편의를 위해 이러한 사전 녹음된 임펄스 응답 파일 (오디오로 저장됨)을 많이 호스팅하는 사이트가 있습니다.
특정 환경에서 임펄스 응답을 만드는 방법에 관한 자세한 내용은 Web Audio API 사양의 컨볼루션 부분에 있는 '녹음 설정' 섹션을 참고하세요.
더 중요한 점은 Web Audio API는 ConvolverNode를 사용하여 이러한 임펄스 응답을 사운드에 쉽게 적용할 수 있는 방법을 제공한다는 것입니다.
// Make a source node for the sample.
var source = context.createBufferSource();
source.buffer = this.buffer;
// Make a convolver node for the impulse response.
var convolver = context.createConvolver();
convolver.buffer = this.impulseResponseBuffer;
// Connect the graph.
source.connect(convolver);
convolver.connect(context.destination);
Web Audio API 사양 페이지의 룸 효과 데모와 훌륭한 재즈 표준의 드라이(원시) 믹싱과 습식 (컨볼버를 통해 처리됨) 믹싱을 제어할 수 있는 이 예도 참고하세요.
마지막 카운트다운
지금까지 게임을 빌드하고 위치별 오디오를 구성했고 이제 그래프에 다수의 AudioNode가 동시에 재생되도록 했습니다. 좋습니다. 하지만 여전히 한 가지 고려해야 할 사항이 있습니다.
여러 사운드가 정규화 없이 서로 겹쳐서 쌓이기 때문에 스피커 성능의 기준점을 초과하는 상황이 발생할 수 있습니다. 이미지가 캔버스 경계를 초과하는 것처럼, 음향도 최대 임곗값을 초과하면 클립되어 뚜렷한 왜곡이 발생할 수 있습니다. 파형은 다음과 같습니다.
이것은 실제로 클립을 사용하는 실제 예입니다. 웨이브폼이 좋지 않습니다.
위와 같은 거친 왜곡이나 반대로 과도하게 조용하여 청취자가 볼륨을 올려야 하는 믹스를 들어보는 것이 중요합니다. 이 경우 문제를 해결해야 합니다.
클립 감지
기술적 관점에서 클리핑은 채널의 신호 값이 유효한 범위, 즉 -1과 1 사이를 초과할 때 발생합니다. 이러한 상황이 감지되면 이러한 상황이 발생하고 있음을 시각적으로 알려주는 것이 좋습니다. 이를 안정적으로 수행하려면 그래프에 JavaScriptAudioNode를 배치합니다. 오디오 그래프는 다음과 같이 설정됩니다.
// Assume entire sound output is being piped through the mix node.
var meter = context.createJavaScriptNode(2048, 1, 1);
meter.onaudioprocess = processAudio;
mix.connect(meter);
meter.connect(context.destination);
다음 processAudio
핸들러에서 클리핑이 감지될 수 있습니다.
function processAudio(e) {
var buffer = e.inputBuffer.getChannelData(0);
var isClipping = false;
// Iterate through buffer to check if any of the |values| exceeds 1.
for (var i = 0; i < buffer.length; i++) {
var absValue = Math.abs(buffer[i]);
if (absValue >= 1) {
isClipping = true;
break;
}
}
}
일반적으로 성능상의 이유로 JavaScriptAudioNode
를 과도하게 사용하지 않도록 주의하세요. 이 경우 측정의 대체 구현은 requestAnimationFrame
에 의해 결정된 대로 렌더링 시간에 오디오 그래프에서 getByteFrequencyData
의 RealtimeAnalyserNode
를 폴링할 수 있습니다. 이 접근 방식은 더 효율적이지만 렌더링은 초당 최대 60회 발생하는 반면 오디오 신호는 훨씬 더 빠르게 변경되므로 클립이 발생할 수 있는 위치를 포함하여 대부분의 신호를 놓칩니다.
클립 감지는 매우 중요하므로 향후 기본 제공되는 MeterNode
Web Audio API 노드가 표시될 수 있습니다.
클립 방지
마스터 AudioGainNode의 게인을 조정하여 클리핑을 방지하는 수준으로 믹스를 조절할 수 있습니다. 그러나 실제로 게임에서 재생되는 사운드는 다양한 요인에 따라 달라질 수 있으므로 모든 상태에서 클리핑을 방지하는 마스터 게인 값을 결정하기 어려울 수 있습니다. 일반적으로 최악의 경우를 예측하기 위해 이득을 조정해야 하지만 이는 과학이라기보다는 예술에 가깝습니다.
설탕을 약간 넣습니다.
컴프레서는 음악 및 게임 제작에서 일반적으로 신호를 부드럽게 하고 전체 신호의 스파이크를 제어하는 데 사용됩니다. 이 기능은 DynamicsCompressorNode
를 통해 웹 오디오 환경에서 사용할 수 있습니다. DynamicsCompressorNode
는 오디오 그래프에 삽입하여 더 크고 풍부하며 가득 찬 사운드를 제공하고 클리핑에도 도움이 됩니다.
사양을 직접 인용하는 이 노드는
일반적으로 다이내믹 압축을 사용하는 것이 좋습니다. 특히 이전에 설명한 대로 어떤 사운드가 재생되고 언제 재생되는지 정확히 알 수 없는 게임 환경에서는 더욱 그렇습니다. DinahMoe 실습의 Plink가 좋은 예입니다. 재생되는 사운드는 전적으로 사용자와 다른 참여자의 손에 달려 있기 때문입니다. 컴프레서는 이미 '딱 맞게' 소리가 나도록 조정된 세심하게 마스터링된 트랙을 처리하는 드문 경우를 제외하고 대부분의 경우에 유용합니다.
이를 구현하려면 오디오 그래프에 DynamicsCompressorNode를 포함하기만 하면 됩니다(일반적으로 대상 앞에 있는 마지막 노드로 포함).
// Assume the output is all going through the mix node.
var compressor = context.createDynamicsCompressor();
mix.connect(compressor);
compressor.connect(context.destination);
다이내믹 압축에 관한 자세한 내용은 이 Wikipedia 도움말을 참고하세요.
요약하면, 클리핑을 주의 깊게 수신 대기하고 마스터 게인 노드를 삽입하여 클리핑을 방지하세요. 그런 다음 다이내믹 컴프레서 노드를 사용하여 전체 믹스를 조입니다. 오디오 그래프는 다음과 같이 표시될 수 있습니다.
결론
여기에서 Web Audio API를 사용한 게임 오디오 개발에서 가장 중요한 측면을 다룹니다. 이러한 기법을 사용하면 브라우저에서 바로 매력적인 오디오 환경을 구축할 수 있습니다. 채팅을 종료하기 전에 브라우저 관련 도움말을 하나 알려드리겠습니다. 페이지 가시성 API를 사용하여 탭이 백그라운드로 전환되면 소리를 일시중지해야 합니다. 그러지 않으면 사용자에게 불편을 줄 수 있습니다.
웹 오디오에 관한 자세한 내용은 소개 시작하기 도움말을 참고하세요. 궁금한 점이 있으면 웹 오디오 FAQ에서 이미 답변이 제공되었는지 확인하세요. 마지막으로 추가 질문이 있는 경우 web-audio 태그를 사용하여 StackOverflow에서 질문하세요.
채팅을 종료하기 전에 현재 실제 게임에서 Web Audio API를 사용하는 몇 가지 멋진 예를 소개해 드리겠습니다.
- 필드 실행기, 일부 기술적 세부정보에 대한 글
- Angry Birds: 최근 Web Audio API로 전환했습니다. 자세한 내용은 이 문서를 참고하세요.
- Skid Racer: 서라운드 오디오를 효과적으로 활용합니다.