결과물
사이트
github code
자매품: vite + threejs template
프로젝트 소개
ASMR room은 Threejs와 Audio WebAPI로 만든 공간음향 인터랙티브 웹 사이트이다.
나는 ASMR을 좋아한다. 한참 정신나갔을 때는 이런 것도 만들어서 올렸다. (킹받음 주의)
요새 블렌더로 3D 모델링 하는 것에 빠져서 이걸 어떻게 활용할까 하다가 인터랙티브 웹을 생각하게 되었다. 그리고 거기에 내가 좋아하는 ASMR을 더했다.
ASMR 룸은 빗소리, 키보드 타이핑, 책 넘기는 소리, 발자국 소리 4가지 소리가 있고 공간음향을 통해 사용자가 소리를 적절히 조절하여 자신만의 ASMR 룸을 만들 수 있게 해두었다.
ASMR room을 즐기는 법
1.
아이콘을 클릭해 소리가 나는 오브젝트를 씬에 추가한다.
2.
오브젝트를 움직이면서 소리를 조절한다. (오브젝트가 움직이면 소리도 같이 움직인다.)
3.
키보드의 경우 타이핑을 할 수 있고(실제 키보드 or 모달창에 입력), 발자국은 마우스가 움직이는 지점을 따라 찍힌다.
4.
모바일 지원도 된다!
Web Audio API로 공간음향 사운드 구현하기
Web Audio API란?
웹에서 오디오를 다양한 방면으로 다룰 수 있게 해주는 Web API이다. 공간음향, 음향의 시각화, 사운드 이펙트 추가 등 다양한 기능이 있다.
코드를 보면서 어떻게 사용하는지 확인해보자
전체 코드
AudioContext 및 AudioNode들 추가
const audioContext = new AudioContext();
this.audioContext = audioContext;
this.source = this.audioContext.createBufferSource();
this.panner = this.audioContext.createPanner();
this.panner.panningModel = 'HRTF';
this.panner.distanceModel = 'inverse';
this.panner.refDistance = 1;
this.panner.maxDistance = 10000;
this.panner.rolloffFactor = 1;
this.panner.coneInnerAngle = 360;
this.panner.coneOuterAngle = 0;
this.panner.coneOuterGain = 0;
TypeScript
복사
1.
AudioContext
Audio Web API를 사용하기 위해서는 AudioContext를 컨트롤해야 한다. AudioContext 안에서 데이터 소스를 입력 받고 이펙트를 내고(스테레오 패닝을 통한 공간음향 연출 등), 마지막 출력(소리를 내는 것)까지 할 수 있다.
AudioContext안에는 AudioNode들이 추가되고 입력과 출력을 통해 그래프처럼 서로 연결된다.
2.
AudioBufferSourceNode
this.source = this.audioContext.createBufferSource();
TypeScript
복사
오디오 버퍼 소스 노드는 오디오 재생을 위해 데이터를 담는 역할을 한다. 오디오 버퍼는 한번 재생하면 끝이기 때문에 만약 중간에 stop시키면 오디오 버퍼를 재할당해서 다시 play해야한다. 혹은 loop를 Infinity로 설정해놓으면 오디오를 루프 재생시킬 수 있다.
3.
PannerNode
this.panner = this.audioContext.createPanner();
this.panner.panningModel = 'HRTF';
this.panner.distanceModel = 'inverse';
this.panner.refDistance = 1;
this.panner.maxDistance = 10000;
this.panner.rolloffFactor = 1;
this.panner.coneInnerAngle = 360;
this.panner.coneOuterAngle = 0;
this.panner.coneOuterGain = 0;
TypeScript
복사
패너노드는 물리적 공간 안에서 오디오 소스의 위치, 방향, 동작 등을 처리한다.
3D 공간 안에서 음원과 청취자의 거리, 위치에 따라 소리가 달라지게 하려면 pannerNode의 위치를 바꾸거나 audioListner의 위치를 변경해야 한다. 나의 경우는 고양이가 청취자이기 때문에 오디오리스너는 가만히 두고 패너노드의 위치만 업데이트했다.
그리고 패너노드는 오디오 채널이 두 개(스테레오) 이상이어야 패닝 효과(소리의 위치를 이동시키는 효과)를 낼 수 있다.
패닝 효과는 소리가 청취자의 전 방향에서 들리게 한다. 이런 효과를 내기위해서 각 채널에 각기 다른 수준의 음량을 전달한다. 예를 들어, 왼쪽에서 들리는 것처럼 느끼게 하려면 왼쪽 이어폰의 오디오에서 더 큰 소리를 내고 오른쪽에서는 더 작은 볼륨을 낸다. 상대적인 음량의 차이가 소리의 위치를 나타낸다. 모노 오디오(채널이 하나인 경우)는 하나의 채널만 사용되어 두 귀에 동일한 신호가 전달된다. 그래서 음량의 차이나 시간 차이로 두 귀에 상대적인 차이를 느낄 수 없어 소리의 방향을 구별할 수 없다.
옵션 설명
•
panningModel: 소리의 방향을 결정하는 모델. 'HRTF'는 실제 귀의 수신 방식을 모델링해서 스테레오 패닝을 제공한다. (default는 equalpower 이다. equalpower는 왼쪽 오른쪽 오디오 간의 음량 차이는 만들어낼 수 있지만 3D 같은 공간 내에서의 방향, 위치 변경으로 인한 소리의 변화까지 복잡하게 계산해내지는 못한다. 한편 HRTF는 stereo only이다. 오디오 채널이 두 개 이상인 경우에만 사용 가능하다.)
•
distanceModel: 소리가 멀어질 때 볼륨이 어떻게 감소하는지를 결정하는 모델. 'inverse'는 거리에 반비례하여 볼륨이 감소한다. 즉 거리가 멀어지면(증가하면) 볼륨은 줄어든다. (linear, inverse, exponential 등등)
•
refDistance: 이 거리 안에서는 볼륨이 최대로 유지된다.
•
maxDistance: 이 거리 이상에서는 볼륨이 더 이상 감소하지 않는다.
•
rolloffFactor: 거리에 따른 볼륨 감소율을 결정한다.
•
coneInnerAngle과 coneOuterAngle: 소리가 발산되는 각도를 정의한다. 이 경우에는 360도와 0도로 설정되어 전 방향으로 소리가 동일하게 퍼져나간다.
•
coneOuterGain: 외부 원뿔 영역에서의 볼륨 감소를 정의한다.
cone? 갑자기 원뿔은 왜 나오는 걸까.
출처: mdn 패너노드 문서
MDN에서는 패너 노드에 대해 이렇게 설명한다.
This AudioNode uses right-hand Cartesian coordinates to describe the source's position as a vector and its orientation as a 3D directional cone.
산 넘어 산이다. 오른쪽 좌표계는 또 뭐란 말인가. 일반적으로 3D 그래픽 응용 프로그램에서 사용하는 좌표계 중 왼손, 오른손 좌표계가 있다. 그 중에 하나가 오른손 좌표계이다.
Right-Hand Cartesian Coordinates. 쉽게 말하면 오른손으로 3차원의 세 축을 표현하는 것이다. 엄지가 X축, 검지가 Y축, 중지가 Z축. 엄지가 오른쪽을, 검지가 하늘을, 중지가 나를 바라보게 손가락을 펴면 오른쪽 직교 좌표계가 된다.
패너노드는 이 좌표계를 이용해 소스의 위치를 정할 수 있다. 예를 들어 소스가 청취자의 정면에 위치하고 싶다면, 소스의 위치를 (0, 0, -1)과 같이 설정할 수 있고, 오른쪽에 위치하길 원한다면 (1, 0, 0)으로 설정할 수 있다. 청취자가 (0,0,0)에 위치하고 있다고 생각하면 된다.
그럼 원뿔은 뭘까? 원뿔은 소리가 방출되는 모양을 시각화한 모형이다. 원뿔의 중심선에 가까울 수록 크고 멀어질 수록 소리의 강도는 낮아진다. 원뿔의 내각은 소리가 가장 크게 들리는 범위, 외각은 감소하기 시작하는 범위를 지정한다. 즉 원뿔의 내각은 원뿔의 꼭지점에서 퍼져나가는 양쪽의 모서리 사이의 각도를 의미하고 그 각도 안에서는 소리가 크게 들린다는 말을 의미한다.
아래의 사진은 webaudio 공식문서에서 가져온 사진이다. 청취자와 패너 노드 각각은 방향 벡터를 가지고 있다. 여기서 패너 노드의 방향은 리스너의 뒤쪽을 향하고 있다. 여기서 만약 방향을 바꿔 coneInnerAngle 각도 안에 리스너가 들어오게 되면 소리가 크게 들리고(gain 값이 1이므로 이 안에서의 소리 강도는 모두 같다) coneOuterAngle쪽으로 가까워질 수록 소리는 coneOuterGain이라는 변수만큼 줄어들게 된다.
출처: webaudio 공식문서
나는 위에서 coneInnerAngle 을 360도, coneOuterAngle 을 0도라고 설정했다. 이것은 소리가 전 방향으로 퍼지는 것을 의미한다. 트여있는 공간 안에서 오브젝트가 움직여도 전 방향에서 들려야 해서 이렇게 설정했다.
Audio Source 로드
const response = await fetch(this.filePath);
const arrayBuffer = await response.arrayBuffer();
this.buffer = await this.audioContext.decodeAudioData(arrayBuffer);
this.init();
TypeScript
복사
filePath 경로에 있는 오디오 소스를 가져와 arrayBuffer에 담고 decodeAudioData로 arrayBuffer를 AudioBuffer로 디코드한다. 그 다음 init을 하게 되는데 이제 init 코드를 보자.
init코드 - 오디오 버퍼 소스 노드 초기화 및 audio graph 연결
this.source = this.audioContext.createBufferSource();
if (!this.buffer) {
throw new Error('audio data not exists');
}
this.source.buffer = this.buffer;
this.source.loop = this.loop;
this.source.connect(this.panner);
this.panner.connect(this.audioContext.destination);
TypeScript
복사
위에서 언급한 것처럼 오디오 버퍼 소스 노드는 한번 실행하면 버퍼소스 노드를 다시 생성해야 한다. 그래서 init 코드를 따로 만들었다.
위에서 언급한 것처럼 AudioContext는 오디오 노드들의 연결로 이루어진다고 했다. 그리고 그 순서는 입력 - 이펙트 - 출력이다.
여기서는 생성한 오디오 source 노드를 입력으로, 그다음 이펙트 노드는 패너노드, 그리고 패너 노드는 오디오콘텍스트의 destination으로 연결된다.
재생, 정지, 여러번 재생
play() {
this.source.start();
}
stop() {
this.source.stop();
this.init();
}
repeat() {
this.init();
this.play();
}
updatePosition(newPosition: Vector3) {
this.panner.positionX.value = newPosition.x;
this.panner.positionY.value = newPosition.y;
this.panner.positionZ.value = newPosition.z;
}
TypeScript
복사
•
play: 소스 노드를 start해서 오디오를 재생시킨다.
•
stop: 중간에 오디오를 정지시킬 수 있다. 마지막에 init하는 이유는 이렇다. 한번 stop하면 오디오버퍼소스노드를 다시 사용할 수 없기 때문에 소스 노드를 새로 생성하고 그래프에 다시 연결시켜야 한다.
•
repeat: 여러번 재생할 수 있게 해준다.
•
updatePosition: 패너 노드의 위치를 업데이트해서 공간음향 효과를 준다. 이 메서드는 오브젝트가 움직일 때 해당 오브젝트의 위치를 카피해서, 오브젝트의 움직임에 따라 소리도 함께 이동하는 효과를 주었다.
그 외 코드는 github 레파지토리에서 구경해주시길. 코드 기여나 리뷰, 제안 환영합니다!
threejs examples에 내 프로젝트 올리는 방법
1.
threejs 공식 사이트에 들어가서
2.
우측 하단에 submit project 클릭
3.
사이트 가입하고 ‘showcase’ 카테고리로 게시글을 올린다.
4.
게시글 올릴 때는 이미지와 함께 프로젝트에 대해 상세히 설명해주면 된다. 보통 2~3일 내로 메인테이너가 답변을 달아준다.
내 포스트
게시글 예시