음성 녹음 로직을 구현해보자(1)

부루베릐·3일 전
0

TIL

목록 보기
22/23

개요

실시간 통역 서비스를 개발하면서, 기기의 마이크를 통해 음성을 녹음하고 이 음성 데이터에 대한 STT 데이터를 서버로부터 실시간으로 받아와야 하는 경우가 많았다. 일반적인 상황에서 음성을 다루고 분석하는 경우가 거의 없었던지라, 이번 프로젝트를 기회로 삼아 어떤 식으로 브라우저에서 미디어 기기에 접근해 음성 녹음 기능을 구현할 수 있는지에 대해 살펴보았다.

미디어 관련 용어

우선 기본적인 미디어 관련 용어를 살펴보도록 하자.

샘플링(Sampling)

아날로그 파형 등의 신호를 일정한 작은 시간 단위로 쪼개는 것을 샘플링이라 한다. 샘플링을 거치면 파형이 PAM(Pulse Amplitude Modulation) 펄스로 변환된다.

PCM(Pulse Code Modulation)

아날로그 신호를 디지털 형식으로 변환하기 위해 사용하는 방법이다. 아날로그 신호를 샘플링하여 펄스열로 바꾼 후 부호화하여 디지털 신호로 변조(modulation)한다.

  1. 아날로그 신호를 샘플링
    • Sampling Rate: 1초에 몇 번 아날로그 신호를 쪼갤 것인가. 대부분 44.1khz(1초에 44100번) 혹은
  2. 양자화
    • 샘플링 결과인 아날로그 펄스 진폭을 디지털화(이산화)하는 과정이다. 쉽게 이야기하자면 숫자로 크기를 표현하는 것이다.
    • Bit Depth: 이 때 얼마나 정밀하게 펄스를 양자화할 것인지를 나타내는 척도이다. 16bit나 24bit 등등. 16bit면 총 2^16 = 65,536개의 단계로 펄스를 수치화할 수 있다는 뜻이다. Bit Depth가 높을수록 더 좋은 음질을 의미한다.
  3. 부호화
    • 양자화된 숫자값들을 전송할 수 있도록 바이너리 bit 시퀀스로 변환한다.

PCM를 통해 변환된 결과물은 다음과 같은 일련의 비트 시퀀스가 된다. 이 때 데이터가 스테레오 채널로 들어오는 경우 하나의 프레임에 좌채널 샘플과 우채널 샘플이 모두 담긴다.

WAV(WAVeform audio format)

무손실 무압축 오디오 파일 형식이다. 이와 다르게 MP3 파일은 유손실 유압축 오디오 파일이므로 음질이 손상될 우려가 커, 개발자 입장에서는 WAV를 사용하는 것이 안전하고 편하다.

크게 세 가지 청크로 이루어져 있다. RIFF, fmt, 데이터 청크가 그것이다. RIFF(Resource Interchange File Format, 리소스 교환 파일 형식) 청크는 WAV 파일 형식과 관련된 데이터를, fmt(format) 청크는 데이터 청크에 담긴 PCM 데이터를 해석하는 데 필요한 메타데이터를 담고 있다.

RIFF란 마이크로소프트에서 만든 일반적인 파일 컨테이너 포맷을 의미한다. RIFF 형식의 데이터는 리틀 엔디안 형식으로 저장되는 것이 특징이다(후에 다시 등장할 예정).

더 자세한 내용은 아티클 하단의 블로그를 참고하는 것이 좋을 거 같다.

MediaStream

어느 정도 기본적인 미디어 데이터에 대한 용어는 알았으니, 브라우저에서 음성 데이터를 다루는 법을 알아보자. 먼저 가장 중요한 것은 MediaStream API가 무엇인지 파악하는 것이다.

MediaStream API

Media Streams API 혹은 Media Capture and Streams API로도 불린다. 스트리밍되는 오디오와 비디오 데이터를 다룰 때 사용하는 API이다.

MediaStream 객체는 말 그대로 오디오 혹은 비디오로부터 받은 스트림 데이터를 이야기한다. 이 미디어 스트림은 사용자의 기기(마이크 등)로부터 직접 받을 수도 있고, <audio><video> 요소를 통해서도 받을 수 있다. 이렇듯 미디어 스트림은 어떻게 생성되었는지에 따라 크게 두 가지 종류로 나눌 수 있는데 다음과 같다.

  • 사용자의 미디어 장치로부터 직접 생성된 로컬local 미디어 스트림

    // getUserMedia를 통해 로컬 미디어 장치에서부터 미디어 스트림을 받아옴
    const mediaStream = await navigator.mediaDevices.getUserMedia()
  • 그 외 외부 소스로부터 받아오는 비 로컬non-local 미디어 스트림

    • HTMLMediaElement(<audio><video>) 혹은 HTMLCanvasElement의 captureStream() 메서드을 통해 생성
    • Web Audio API를 통해 생성
    • WebRTC API와 같은 네트워크를 통해 생성 등등
    // <audio> 요소에서부터 미디어 스트림을 받아옴
    const audioEl = document.getElementById("audio");
    const mediaStream = audio.captureStream();

주의해야 할 것은, 이렇게 받아오는 미디어 스트림 자체가 음성 혹은 비디오 데이터는 아니라는 것이다. 미디어 스트림은 말 그대로 모든 실시간 미디어 입출력을 다루는 인터페이스를 이야기한다.

구조도를 살펴보면 전체 미디어 입출력을 의미하는 미디어 스트림 내부에 미디어 스트림 트랙(MediaStreamTrack) 객체들이 존재한다. 이 미디어 스트림 트랙 각각이 입출력을 하나하나와 대응되는 인터페이스라 생각하면 된다. 미디어 스트림 트랙은 크게 비디오 트랙과 오디오 트랙으로 구분되고, 각 트랙의 소스(source)나 용도에 따라서 여러 값을 가질 수 있다.

미디어 스트림 내의 각 트랙을 확인하기 위해서는 MediaStream 객체의 getTracks() 메서드를 호출하면 된다.

mediaSream.getTracks();

정리하자면 미디어의 소스가 로컬 미디어 기기인지 원격인지에 따라 미디어 스트림이 나뉘고, 각각의 미디어 스트림 내부에서 미디어 입출력의 기기, 소스, 용도 등에 따라 각각의 미디어 스트림 트랙이 생겨난다고 이해할 수 있겠다. 그리고 이 미디어 스트림 내의 미디어 데이터를 추출해서 사용하면 된다.

오디오 권한 획득

getUserMedia

getUserMedia의 인자로 여러 제약 조건(contraints)들을 줄 수 있고, 이를 통해 내가 원하는 대상의 미디어 스트림만을 받을 수 있다.

예를 들어 오디오 데이터나 비디오 데이터 중 하나만 선택해서 받을 수도 있고,

// video는 제외, 오직 audio 스트림만
const audioStream = await navigator.mediaDevices.getUserMedia({
  audio: true,
  video: false,
});

혹은 내가 원하는 특정 디바이스에서 나오는 데이터만을 선별해서 받을 수 있다.

const deviceList = await navigator.mediaDevices.enumerateDevices();
console.log('deviceList', deviceList);
const mediaStream = await navigator.mediaDevices.getUserMedia({
  audio: { deviceId: { exact: deviceList[0].deviceId } },
});

이제 이렇게 마이크로 들어오는 미디어 스트림을 받았으니, 이 미디어 스트림을 잘 만져서 녹음 기능을 구현해 보도록 하자.

참고 자료

WAV 파일의 헤더 구조와 Raw Data(pcm data)

MDN - Taking still photos with getUserMedia()

post-custom-banner

0개의 댓글