개발일지/🙂 Boarlog

Javascript로 마이크 볼륨 시각화하기

무딘붓 2024. 1. 26. 13:46
기록으로 남기는 실시간 화이트보드 강의 서비스, 🙂 Boarlog 를 개발하며 구현한 내용을 담고 있습니다.
게시글의 기능을 이용한 최종 서비스는 GitHub 링크 에서 확인하실 수 있습니다. 

 

 

웹 페이지에서 마이크를 통해 음성을 입력받고, 볼륨을 시각화 하는 과정을 정리합니다.

 

바닐라 Javascript로 개발한 예제코드는 아래에서 확인할 수 있습니다.

https://codepen.io/vchgekmq-the-flexboxer/pen/MWLVJeO?editors=1111

🎤 1. 마이크를 통해 사용자의 음성을 입력 받기

navigator.mediaDevices
    .getUserMedia({ audio: true }) // 마이크 권한을 요청
    .then((stream) => {            // 마이크 권한 요청 성공
      isReording = true;
      audioStream = stream;
    })

    //
    // (볼륨 시각화 코드)
    //

    .catch((error) => {
        console.error("마이크 권한 획득 실패", error);
    });
};

 

우선 navigator.mediaDevices.getUserMedia({ audio: true }) 는 사용자에게 마이크 권한을 요청하는 부분입니다.

  • navigator.mediaDevices.getUserMedia({ audio: true })
    • 사용자에게 마이크 권한을 요청하는 부분입니다.
    • Web API인 MediaDevices의 getUserMedia 메서드를 호출하는 방식으로 작동합니다.
    • 각 객체와 프로퍼티 설명은 다음과 같습니다.
      • navigator 객체 : 브라우저의 정보와 상태에 액세스할 수 있는 객체
      • mediaDevices : 미디어 장치(예: 카메라, 마이크)에 액세스하기 위한 API 제공
      • getUserMedia()메서드 : 사용자의 미디어 디바이스(카메라, 오디오..)에 액세스할 수 있도록 권한을 요청하는 메서드입니다. { audio: true }를 전달하여 오디오 권한을 요청합니다 장치의 입력 데이터를 비디오/오디오 트랙으로 포함한 ``을 반환합니다.
  • .then((stream) => {})
    • 사용자가 오디오 권한을 수락하면 실행됩니다.
    • getUserMedia() 의 반환값으로 받은 MediaStream을 가지고 작업을 시작합니다.
    • MediaStream에 대해 간단히 설명하면,
      • 오디오나 비디오와 같은 미디어 데이터를 표현하는 객체입니다
      • 실시간으로 생성되고 내용이 지속적으로 업데이트되므로, 실시간 오디오 및 비디오를 다룰 때 사용합니다.
      • 비디오/오디오 두가지 트랙으로 구성되어 있습니다.
      • https://developer.mozilla.org/en-US/docs/Web/API/MediaStream

📈 2. 입력된 오디오 볼륨을 정규화 하기

const context = new AudioContext();
const analyser = context.createAnalyser();
const mediaStreamAudioSourceNode = context.createMediaStreamSource(stream);
mediaStreamAudioSourceNode.connect(analyser, 0);
const pcmData = new Float32Array(analyser.fftSize);

const onFrame = () => {
  analyser.getFloatTimeDomainData(pcmData);
  let sum = 0.0;
  for (const amplitude of pcmData) {
    sum += amplitude * amplitude;
  }
  const rms = Math.sqrt(sum / pcmData.length);
  const normalizedVolume = Math.min(1, rms / 0.5);
  colorVolumeMeter(normalizedVolume);
  onFrameId = window.requestAnimationFrame(onFrame);
};

onFrameId = window.requestAnimationFrame(onFrame);

 

사용자가 입력하는 오디오의 볼륨을 0~1 사이의 값으로 정규화 하는 과정입니다.

이번 코드에서 가장 어려운 내용이니까 가볍게 읽고 넘어가셔도 좋습니다.

 

여기서부터는 본격적으로 Web Audio API를 사용합니다.

Web Audio API 는 웹에서 오디오에 (이펙트를 추가, 파형 시각화 등) 다양한 기능을 구현할 수 있도록 도와주는 API라고 생각하시면 됩니다.

자세한 내용은 MDN 문서(링크)를 확인하세요.

 

코드를 한 줄씩 살펴보겠습니다.

 

1) AudioContext 생성

  • const context = new AudioContext();
    • AudioContext는 Web Audio API에서 오디오를 다루기 위한 기본 객체입니다. Web Audio API 는 모든 작업을 AudioContext 내에서 처리한다고 생각하면 됩니다.

 

2) AnalyserNode 생성

  • const analyser = context.createAnalyser();
    • createAnalyser() 를 이용해서 생성한 AnalyserNode 객체를 analyser에 할당합니다.
    • AnalyserNode 객체는 오디오 신호를 분석하여 주파수 영역 분석, 시간 영역 분석 등을 수행하는 데 사용합니다.
    • https://developer.mozilla.org/ko/docs/Web/API/AnalyserNode

 

3) MediaStreamAudioSourceNode 생성 및 연결

  • 간단히 요약하면 MediaStream을 AnalyserNode 객체 analyser에 연결해주는 과정입니다.
    • 이제 소스코드를 살펴보겠습니다.
  • const mediaStreamAudioSourceNode = context.createMediaStreamSource(stream);
    • createMediaStreamSource 메서드를 사용하여 오디오 스트림을 처리할 수 있는 MediaStreamAudioSourceNode를 생성합니다.
    • 앞서 얻은 미디어 스트림을 AudioContext 내에서 사용할 수 있는 형식으로 변환 해주는 역할을 합니다.
  • mediaStreamAudioSourceNode.connect(analyser, 0);
    • connect 메서드를 사용하여 mediaStreamAudioSourceNode를 analyser에 연결합니다.
    • AudioContext 내에서 사용할 수 있는 형식으로 변환된 미디어 스트림을 오디오 분석을 해주는 AnalyserNode 객체에 연결해준다고 생각하면 됩니다.
    • 두 번째 매개변수 0은 analyser의 입력 채널을 나타냅니다. 지금은 단일 채널을 사용하고 있으므로 0을 입력하면 됩니다.

 

4) PCM 데이터 버퍼 생성:

  • const pcmData = new Float32Array(analyser.fftSize);
    • analyser.fftSize를 기반으로 하는 Float32Array를 생성합니다.
    • 이 배열은 주기적으로 업데이트되는 오디오 데이터를 저장할 버퍼라고 생각하면 됩니다.

 

5) onFrame 함수 정의:

  • 재귀적으로 실행되어 현재 볼륨값을 계산하는 onFrame 함수입니다.
    • onFrame 함수는 애니메이션 프레임마다 호출되며, analyser를 사용하여 실시간 오디오 데이터를 얻어옵니다.
    • getFloatTimeDomainData 메서드를 사용하여 pcmData 배열에 현재의 주기적인 오디오 데이터를 채웁니다.
  • analyser.getFloatTimeDomainData(pcmData);
    • analyser 로부터 현재 시간의 오디오 데이터를 가져와서 pcmData 배열에 저장합니다.
  • for (const amplitude of pcmData) { sum += amplitude * amplitude; }
    • 오디오 데이터가 저장된 pcmData 배열에서 각 오디오의 진폭(amplitude) 값을 가져온 후, 진폭값을 제곱하여 합을 계산합니다.
    • 제곱을 하는 이유는 음의 값을 가지는 진폭과 양의 진폭이 섞여 있을 때를 고려하기 위해서 입니다.
  • const rms = Math.sqrt(sum / pcmData.length);
    • 제곱된 값의 평균을 구하고, 다시 제곱근을 씌워 Root Mean Square (RMS)를 계산합니다.
    • 이 RMS는 오디오 신호의 평균적인 에너지, 즉 볼륨의 대략적인 크기를 나타내는 값이 됩니다.
  • const normalizedVolume = Math.min(1, rms / 0.5);
    • 계산된 RMS 값을 0~1 사이의 실수로 정규화 해줍니다.
  • colorVolumeMeter(normalizedVolume);
    • 정규화된 RMS(볼륨) 값을 시각화를 담당하는 colorVolumeMeter 함수에 전달합니다.
  • onFrameId = window.requestAnimationFrame(onFrame);
    • 재귀적으로 자기 자신을 호출하여 계속 반복합니다.

 

6) 애니메이션 시작

  • onFrameId = window.requestAnimationFrame(onFrame);
    • onFrame 함수를 최초로 호출하여 애니메이션을 시작합니다.
    • 애니메이션 종료를 위해 onFrameId 는 나중에 종료를 위해 따로 저장해둡니다.

 


📶 3. 정규화된 볼륨을 시각화 하기

const normalizeToInteger = (volume, min, max) => {
  const scaledValue = Math.min(max, Math.max(min, volume * (max - min) + min));
  return Math.round(scaledValue);
};

const colorVolumeMeter = (vol) => {
  const VOL_METER_MAX = 10;
  const childrens = document.querySelectorAll(".volumeBar");
  const numberOfChildToColor = normalizeToInteger(vol, 0, VOL_METER_MAX);

  const coloredChild = Array.from(childrens).slice(0, numberOfChildToColor);
  childrens.forEach((child) => {
    child.style.backgroundColor = "#e6e6e6";
  });

  coloredChild.forEach((child) => {
    child.style.backgroundColor = "#4F4FFB";
  });
};

앞서 0~1 사이의 볼륨으로 정규화된 볼륨 값을 시각화하는 코드입니다.

 

간단하게 내용을 요약하면, 0~1 사이의 실수값으로 전달받은 볼륨을 0~10의 정수로 변환하여

변환한 정수만큼 div 태그를 색칠 하는 방식으로 시각화 하는 코드입니다.

변환된 정수 값이 1이라면 위와 같이 10개의 div 태그 중에 1개가 색칠됩니다.

 

두 함수를 한번 살펴보겠습니다.

const normalizeToInteger = (volume, min, max) => {
  const scaledValue = Math.min(max, Math.max(min, volume * (max - min) + min));
  return Math.round(scaledValue);
};

0~1사이의 실수 volume을 min~max의 정수로 정규화해주는 함수입니다.

볼륨 바의 child 개수가 10개가 아닌 6개 혹은 20개 등으로 바뀌면 max에 해당 값을 전달하면 됩니다.

기본적으로 1개 이상을 색칠하고자 하면 min에 0을 전달해도 됩니다.

const colorVolumeMeter = (vol) => {
  const VOL_METER_MAX = 10;
  const childrens = document.querySelectorAll(".volumeBar");

  childrens.forEach((child) => {
    child.style.backgroundColor = "#e6e6e6";
  });

  const numberOfChildToColor = normalizeToInteger(vol, 0, VOL_METER_MAX);
  const coloredChild = Array.from(childrens).slice(0, numberOfChildToColor);

  coloredChild.forEach((child) => {
    child.style.backgroundColor = "#4F4FFB";
  });
};

0~1사이의 실수 vol을 가지고 normalizeToInteger() 함수를 통해 색칠할 개수를 구한 뒤,

volumeBar의 child를 해당 개수만큼 칠해주는 함수입니다.

 

먼저 volumeBar의 child를 모두 childrens에 불러와준 후, 배경 색을 칠해줍니다.

 

그 다음, slice 메소드를 이용해 색칠할 child만큼을 선택한 다음 해당 child를 원하는 색만큼 칠해줍니다.

onFrame 함수가 이 과정을 반복하며 음성의 변화가 부드럽게 시각화 됩니다.

 

 

최종 코드

See the Pen 마이크 볼륨 바 예제 by AA (@vchgekmq-the-flexboxer) on CodePen.

onFrame 함수가 이 과정을 반복하며 음성의 변화가 부드럽게 시각화 됩니다

 

 


 

참고자료