• About
[JavaScript 오디오 이펙터 만들기] 소리의 흐름을 파악하자
프로그래밍 / 오디오

[JavaScript 오디오 이펙터 만들기] 소리의 흐름을 파악하자


이번 포스팅에서는 컴퓨터는 어떻게 소리를 들을까? 포스팅에서 진행했던 오디오 파형 그리기에 이어서 오디오에 여러가지 효과를 줄 수 있는 이펙터를 만드는 과정을 설명하려고 한다. HTML5의 Audio API는 오디오에 효과를 줄 수 있는 여러가지 노드를 제공하는데, 대부분의 이펙터는 이 노드들만 사용해도 구현할 수 있을 정도로 완성도있는 API를 제공한다.

또한 이 포스팅은 총 두편에 나눠서 작성될 예정이며, 이번 포스팅에서는 HTML5 Audio API의 개요와 오디오의 음량을 조절할 수 있는 GainNode를 사용하는 방법에 대해서, 다음 포스팅에서는 좀 더 복잡한 오디오 이펙터들에 대한 설명과 제작 과정을 설명할 예정이다.

지난 포스팅에서 이미 오디오에 관한 기본적인 이론을 설명했으니 이번에는 기본적인 이론이 아니라 실제로 녹음실에서 오디오를 어떤 방식으로 컨트롤하고 효과를 주는 지에 대한 방법에 대해 초점을 맞추고 설명을 진행하도록 하겠다.

오디오 신호는 흐르는 것이다

일반적인 녹음실에서 우리는 마이크를 통해서 오디오를 녹음하거나 혹은 이미 녹음된 오디오를 Logic ProCubase와 같은 DAW(Digital Audio Workstation)으로 불러와서 사용하게 된다. 이때 처음으로 받게되는 이 오디오를 소스(Source)라고 한다.

이 소스는 앰프, 컴프레서, 이퀄라이저 등 오디오에 특별한 느낌을 줄 수 있는 여러가지 이펙터들을 지나서 최종적으로 스피커나 헤드폰을 통해서 출력되게 된다. 이 흐름을 알고나면 HTML5의 Audio API가 제공하는 노드(Node)의 개념을 쉽게 이해할 수 있다. 일단 이해를 돕기 위해 필자가 예전에 사운드 엔지니어로 일할 때 사용했던 시스템을 예로 들겠다.

mixer 필자가 예전에 사용했던 장비들

사진의 중앙에 있는 커다란 장비는 아마 여러분도 TV에서 몇번 보았던 장비일 것이다. 이 장비는 여러 개의 채널로 나눠진 오디오 소스의 볼륨이나 패닝, 이퀄라이징까지 할 수 있는 일종의 컨트롤 타워 역할을 하는 믹싱 콘솔이다.

그리고 믹싱 콘솔의 오른쪽에 있는 것들이 바로 오디오에 효과를 줄 수 있는 이펙터들이다. 보통은 믹싱 콘솔 양쪽에 가득 채워놓고 쓰는데 저 사진은 아직 녹음실 셋업이 덜 끝났을 때라서 몇가지 장비만 들어가 있다. 그리고 사진에는 나오지 않았지만 따로 콘솔 랙(Console Rack)이라는 선반을 두고 거기에도 이펙터들이 가득 채워져 있다.

그리고 이펙터들의 위쪽을 보면 붉은색 선이 꽂혀있는 것을 볼 수 있는데, 저 장비가 오디오의 흐름을 컨트롤할 수 있는 패치 테이블(Patch Table)이라고 하는 장비이다.

보통 사운드 엔지니어들은 같은 역할을 하는 이펙터라고 하더라도 여러 종류의 장비를 사용하게 되는데, 이는 같은 역할을 하는 이펙터라고 하더라도 장비마다 조금씩 소리가 다를 수 있기 때문이다. 즉, 같은 리버브를 사용한다고 해도 최종적으로 만들고자하는 소리가 어떤 느낌인지에 따라 A 리버브를 사용할 수도 있고 B 리버브를 사용할 수도 있다는 것이다. 그래서 이런 소리의 질을 만드는 고유한 알고리즘은 이펙터 제조 회사들의 기업 비밀이다.

하지만 다른 이펙터를 사용하고 싶을 때마다 장비에 꽂혀있는 케이블을 일일히 하나하나 빼서 다시 다른 장비에 연결하는 것은 비효율적이기도 하고 케이블을 계속 뺐다가 꼈다가 하면 장비에 손상이 갈수도 있기 때문에 모든 장비의 라인을 저 패치 테이블에 연결해놓고 사용하는 것이다. 게다가 케이블은 대부분 장비 뒤쪽에 위치하기 때문에 저 믹싱 콘솔을 앞으로 살짝 밀고 봐야하는데, 딱 봐도 저 큰 장비를 계속 밀었다 당겼다 하기에는 무거워 보이지 않는가? 허리 나간다.

patch table chart 패치 테이블은 대략 이런 느낌으로 정리된다

사운드 엔지니어는 이렇게 복잡한 여러 개의 장비 사이를 흘러다니는 오디오 신호의 흐름을 패치 테이블을 통해서 한번에 파악하고 컨트롤 할 수 있다. 소리를 컨트롤하는 사람에게 오디오 신호의 흐름이라는 개념은 굉장히 중요하다. 필자가 방금 예로 든 하드웨어 장비 뿐만 아니라 소프트웨어로 구현된 이펙터를 사용하려할때도 결국은 이 흐름을 프로그램 내부에서 그대로 구현해줘야하기 때문이다.

protools

위 사진은 전 세계 녹음실 중 90%가 사용하고 있는 Protools라는 DAW의 믹서 창이다. 사진에 강조된 부분에 Vocal Bus라고 적혀있는 곳을 보면 맨 오른쪽 채널은 위쪽에 위치하고 있고 나머지 채널은 아래쪽에 위치하고 있다. I/O 메뉴에서 위쪽은 In을 의미하고 아래쪽은 Out을 의미하기 때문에 이 그림에서 오디오의 흐름은 대략 다음과 같이 나타날 수 있다.

auxes

이때 저 네모 하나하나가 HTML5의 Audio API에서 제공해주는 노드와 정확히 같은 개념이다. 즉, 자바스크립트로 저 흐름을 완벽히 동일하게 구현할 수 있다는 뜻이다.

이해를 돕기 위해 저 노드들의 역할에 대해서 조금 더 부가설명을 하자면, 일단 Lead Vox는 말 그대로 보컬의 노래 소스를 가진 노드이고 LeadVxDbl은 노래를 풍부하게 들리게 하기 위해 같은 멜로디를 한번 더 녹음한 것, 즉 더블링 작업을 한 노드이다. 그리고 Vox Fill은 화음을 쌓은 코러스를 담은 노드이다.

그리고 보컬이 노래한 이 오디오 소스를 모두 Vocal Bus라는 노드로 모으고 있다. 이렇게 하는 이유는 여러 개의 오디오 소스에 이펙터를 각각 사용하면 노드마다 조금씩 소리의 느낌이 달라질 수 있기 때문에 Vocal Bus라는 하나의 노드로 오디오 신호를 모은 다음 해당 노드에만 이펙터를 걸어주는 것이다. 이렇게 하면 모든 노드에 이펙터를 사용하지않고 하나의 노드에만 이펙터를 사용해도 되기 때문에 메모리 비용도 아낄 수 있고, 보컬이라는 하나의 소스에 동일한 느낌을 부여할 수 있다.

그리고 최종적으로 신호가 들어가는 Sub Master 노드는 아마 최종 아웃풋으로 소리가 나가기 전에 한번 더 이펙터 처리를 하고 싶어서 생성한 것일 테고, Sub Master까지 도달한 오디오는 아웃풋, 즉 스피커를 통해 출력되어 우리의 귀로 들어오게 되는 것이다. 결국 in -> out -> in -> out의 계속된 반복이라고 보면 된다. 그래서 필자가 오디오의 흐름이라고 표현하는 것이다.

이제 오디오 소스의 흐름이라는 것이 대략 이해가 되었으면 한번 직접 HTML5의 Audio API를 사용해서 이 흐름을 구현해보도록 하자.

오디오의 음량을 조절하기

위에서 이야기했듯이 이번 포스팅에서는 본격적으로 이펙터를 구현해보기에 앞서 오디오의 흐름을 직접 구현해보고 체험해보는 것에 초점을 맞출 것이다. 그래서 이펙터라기에는 조금 애매한 단순한 구조의 흐름을 만들어보려고 한다. 바로 오디오의 음량을 조절하는 흐름이다. HTML5 Audio API의 GainNode를 사용하면 오디오 소스의 음량을 손쉽게 조절할 수 있다.

Gain이란 무엇인가요?

게인(Gain)이란 쉽게 말하면 입력 볼륨을 의미한다. 게인을 사용하여 마이크에서 오디오 믹서나 녹음기로 오디오 신호를 보낼 때 그 신호량을 컨트롤하는 것이다. 처음 오디오에 입문하시는 분들이 게인(Gain)볼륨(Volume)의 차이에 대해 헷갈려하시는데, 쉽게 말하면 게인은 “입력 신호를 조절하는 것”이고 볼륨은 “출력 신호를 조절하는 것”이다.

만약 100정도 세기의 신호를 처리할 수 있는 녹음기가 있다고 생각해보자. 이때 우리가 마이크에 대고 80 정도의 세기로 소리를 왁! 지르면 이 녹음기는 무리없이 이 신호를 받아들일 수 있지만, 150의 세기로 소리를 지르게 되면 이 녹음기는 50만큼의 소리를 받아들이지 못하고 그대로 유실시킨다.

clipping 회색으로 표시된 부분이 잘려나간 신호이다.

이 현상은 여러분도 살면서 몇번 경험한 현상일텐데, 스피커로 소리를 엄청 크게 틀면 지지직거리는 잡음이 발생하는 것을 들어본 적이 있을 것이다. 이렇게 장비가 처리할 수 있는 신호의 세기를 넘어가는 현상을 클리핑(Clipping)이라고 한다. 말 그래도 신호가 잘려나가는 것이다.

이렇게 잘려나간 신호는 위 그림에서 볼 수 있듯이 머리가 네모 반듯한 사각파의 형태를 가지게 되는데, 이 사각파는 우리가 EDM 등에서 멜로디를 표현할 때 많이 들을 수 있는 신디사이저 리드(Lead) 계열의 쭈와앙~하는 금속성 소리를 낸다. 말로는 잘 이해가 안될테니 한번 음악으로 들어보면서 잠시 쉬어가도록 하자. 아마 클럽 좀 다녀보신 분들은 아! 이 소리할 것이다.

해당 곡의 인트로가 끝나는 35초부터 메인 멜로디를 맡는 악기가 사각파를 사용한 리드이다.

아무래도 리드는 악기로써 파형이 어느 정도 정제된 상태이기 때문에 클리핑이 발생했을 때 나는 소리는 이것보다 더 거칠고 날카롭다. 참고로 이렇게 파형에 따라 소리가 달라진다는 개념은 왜곡계(Distortion) 이펙터를 만들때도 사용하는 개념이기 때문에 기억해두면 좋다.

어쨌든 이런 클리핑 문제 때문에 사운드 엔지니어들은 오디오 소스와 다음 장비 사이에 게인을 조절할 수 있는 장치를 두고, 장비가 받아들일 수 있는 신호의 세기에 맞춰서 알맞게 게인을 조절하여 소스 오디오의 신호가 커지더라도 모든 신호를 다 담을 수 있도록 한다.

반대로 볼륨은 소리를 내보낼 때 얼마나 증폭시킬 것이냐를 의미한다. 많은 분들이 게인과 볼륨을 헷갈려하는 이유가 둘 다 소리를 증폭시키거나 감소시키는 역할을 하기 때문인데, 볼륨은 이미 입력된 신호를 출력할 때 건드리는 것이기 때문에 클리핑이 발생하더라도 볼륨을 줄이면 신호가 돌아오지만 녹음할 때 게인을 잘못 설정하여 유실된 소리는 다시 돌아오지 않는다.

신호 입력 단계에서 이미 유실된 것이기 때문에 영원한 이별인 것이다. 게다가 녹음이라는 특성 상 그 원본 소스는 사람인 경우가 많다. 결국 이 유실된 신호를 다시 살려낼 수 없기 때문에 게인을 잘못 설정하면 다시 녹음을 해야하는 슬픈 상황이 벌어질 수 있는 것이다.

그래서 사운드 엔지니어들은 소리를 녹음할 때 게인을 잘 다루는 것을 엄청 중요하게 생각한다. 사실 게인만 해도 좀 더 깊이 들어가면 하고 싶은 이야기가 많지만, 이 포스팅은 오디오 전문 포스팅이 아니므로 그냥 비슷한 거라고 생각하고 넘어가도 상관없다.

이제 게인이 무엇인지 이해했다면 GainNode와 함께 오디오 소스의 신호 세기를 조절해서 소리의 크기를 변형시켜보자!

Gain Node를 사용하여 음량을 조절해보자

일단 게인을 사용해보려면 오디오 소스가 필요하다. 오디오 소스는 HTML5의 <audio> 태그를 사용하거나 사용자가 직접 업로드한 파일에서 추출하는 두가지 방법이 있는데, 필자는 이 중 후자의 방법을 사용하였다.

이것도 엄밀히 말하자면 <audio>태그를 사용하여 소스를 추출했을 때와 파일 버퍼에서 직접 추출했을 때는 다른 소스 노드 객체가 생성되긴 하지만 기능상 큰 차이 없으므로 그냥 개인의 취향대로 하면 된다.

const audioContext = new (Audiocontext || webkitAudioContext)();

document.getElementById('audio-uploader').onchange = evt => {
  const file = evt.currentTarget.files[0];
  if (!file) {
    return;
  }

  const reader = new FileReader();
  reader.onload = async evt => {
    const buffer = await audioContext.decodeAudioData(file);
    const sourceNode = audioContext.createBufferSource();
    sourceNode.buffer = buffer;
    console.log(sourceNode);
  }
};
AudioBufferSourceNode {buffer: AudioBuffer, playbackRate: AudioParam, detune: AudioParam, loop: false, loopStart: 0,}

우선 간단한 설명을 하자면 buffer 변수에 담긴 오디오 데이터는 raw한 오디오 데이터일 뿐 아직 하나의 노드가 아니기 때문에 사용할 수 없는 상태이다. 그렇기 때문에 createBufferSource 메소드를 사용하여 소스 노드를 생성한 후 해당 소스 노드에 오디오 데이터를 입력해줘야만 비로소 오디오 데이터를 사용할 수 있는 상태가 되는 것이다.

이때 필자는 사용자가 업로드한 파일에서 직접 오디오 버퍼 데이터를 뽑아와서 노드를 만든 것이기 때문에 createBufferSource 메소드를 사용하여 소스 노드를 생성했지만, 만약 <audio> 태그에서 추출한 오디오 데이터를 사용하여 소스 노드를 생성하고 싶다면 createMediaElementSource 메소드를 사용해야 한다.

그럼 이제 GainNode를 생성하고 소스 노드에 연결만 시켜주면 바로 이 오디오 소스의 음량을 조절할 수 있게 된다.

const gainNode = audioContext.createGain();
sourceNode.connect(gainNode);
gainNode.connect(audioContext.destination);

이 코드에서 소스 -> 게인 -> 데스티네이션으로 설정된 오디오의 흐름이 보인다면 사실상 Audio API에 대한 이해는 거의 끝났다고 봐도 무방하다. 위에서도 말했듯이 오디오를 컨트롤할 때는 이 개념을 이해하는 게 제일 중요하기 때문이다.

또한 gainNode가 연결된 audioContext.destination은 최종 아웃풋, 즉 스피커로 향하는 정보를 가지고 있다. 그럼 이제 여기서 오디오의 소리를 증폭시키거나 감소시키려면 어떻게 해야할까?

gainNode.gain.value = 1.2;
// 또는
gainNode.gain.setValueAtTime(1.2, audioContext.currentTime);

// 그 후 소스를 재생해보자
sourceNode.start();

[Warinig] 너무 값을 크게 올리면 재생했을 때 고막 터집니다.

간단하다. 그냥 GainNode.gain.value에 접근해서 값을 변경해주면 된다. 게인 같은 경우는 값에 직접 접근하여 변경하는 것이 가능하지만 다른 노드의 경우 자신의 값을 직접 변경하는 것이 허용되지 않는 경우가 있는데, 이럴 때는 setValueAtTime 메소드를 사용하면 된다.

setValueAtTime 메소드는 일종의 스케줄러같은 개념인데, 두번째 인자로 넘긴 시간이 지난 후에 값을 적용하는 기능을 가지고 있다. 이때 인자로 넘기는 시간의 단위는 이다. audioContext.currentTime을 인자로 사용하면 곧바로 값의 변경이 적용된다.

필자는 처음에 이런 노드들의 값을 변경할 때 헷갈렸던 것이 하나 있는데, 바로 minmax이다. 즉, 이 노드가 가지는 값의 범위를 알 수가 없었다. 물론 공식 문서에 다 나와있긴 하지만 그걸 어느 세월에 하나하나 검색해서 보겠는가?

그래서 문서를 조금 더 뒤져보니 이 노드들이 가지는 값은 공통적으로 AudioParam 타입이라는 것을 알 수 있었다. 이 타입은 min, max, defaultValue, value 속성을 가지고 있었고, 이 값들은 input[type="range"]를 사용하여 오디오를 컨트롤할때 유용하게 사용할 수 있다.

console.log(gainNode.gain);
AudioParam {value: 1, automationRate: "a-rate", defaultValue: 1, minValue: -3.4028234663852886e+38, maxValue: 3.4028234663852886e+38}

이 값을 잘 확인하고 게인의 값을 설정하면 적어도 고막과 이어폰이 터져나가는 불상사는 방지할 수 있을 것이다. 위에서 말했듯이 컴퓨터가 처리할 수 있는 신호의 세기를 넘어가게 되면 클리핑 현상이 발생하면서 찢어지는 듯한 소리가 나기 때문에 만약 이어폰을 끼고 있었다면 농담이 아니고 진짜 귀에 무리가 갈 수도 있다.

자, 이렇게 간단하게 오디오 소스의 게인을 조절해보았다. 나머지 이펙터들도 대부분 이런 느낌으로 구현된다. 간혹 조금 더 복잡한 연결이 필요한 이펙터들이 있긴 하지만 대부분의 경우 간단한 몇개의 노드를 연결하는 것만으로 구현할 수 있기 때문에 그렇게 어렵지 않다.

다음 포스팅에서는 이번에 알아본 개념을 바탕으로 소리를 압축하거나 공간감을 부여하고, 특정 주파수를 잘라내어 소리에 특별한 느낌을 줄 수 있는 다른 이펙터들을 만들어보도록 하겠다. 또한 기회가 된다면 이미 존재하는 오디오 소스를 변형하는 이펙터가 아니라 진짜로 오디오 신호 자체를 만들어 낼 수 있는 오실레이터(Oscillator)를 사용하여 나만의 악기를 만들어볼 수 있는 포스팅도 진행할 예정이다.

이상으로 JavaScript로 오디오 이펙터를 만들어보자 - 소리의 흐름을 파악하자 포스팅을 마친다.

관련 포스팅 보러가기

2019-08-21

[JavaScript 오디오 이펙터 만들기] 오디오 이펙터로 나만의 소리 만들기

프로그래밍/오디오
2019-07-10

컴퓨터는 어떻게 소리를 들을까?

프로그래밍/오디오
2019-10-27

[JS 프로토타입] 프로토타입을 사용하여 상속하기

프로그래밍/자바스크립트
2019-10-23

[JS 프로토타입] 자바스크립트의 프로토타입 훑어보기

프로그래밍/자바스크립트
2019-10-12

최소 값과 최대 값을 빠르게 찾을 수 있게 도와주는 힙(Heap)

프로그래밍/알고리즘