blog

브라우저에서 녹음한 Blob을 WAV 파일로 변환하기

Front-end / JavaScript#Web Audio API#Blob#WAV#MediaRecorder

MediaRecorder로 녹음한 오디오는 브라우저에 따라 webm·ogg 등으로 저장된다. 서버나 음성 인식 API가 WAV(PCM)만 받는 경우, 브라우저에서 직접 디코딩한 뒤 WAV 헤더를 붙여 변환할 수 있다.

흐름

  1. FileReader로 Blob을 ArrayBuffer로 읽는다.
  2. AudioContext.decodeAudioData로 AudioBuffer(PCM 샘플)로 디코딩한다.
  3. 44바이트 WAV 헤더를 쓰고, 샘플을 16-bit PCM으로 변환해 이어 붙인다.

코드

const writeString = (view, offset, string) => {
  Array.from(string).forEach((char, index) => {
    view.setUint8(offset + index, char.charCodeAt(0));
  });
};

const encodeWav = (audioBuffer) => {
  const format = 1; // PCM
  const { sampleRate } = audioBuffer;
  const numChannels = audioBuffer.numberOfChannels;
  const byteRate = sampleRate * numChannels * 2; // 16-bit
  const blockAlign = numChannels * 2;
  const dataSize = audioBuffer.length * numChannels * 2;

  const buffer = new ArrayBuffer(44 + dataSize);
  const view = new DataView(buffer);

  // WAV 헤더
  writeString(view, 0, 'RIFF');
  view.setUint32(4, 36 + dataSize, true);
  writeString(view, 8, 'WAVE');
  writeString(view, 12, 'fmt ');
  view.setUint32(16, 16, true);
  view.setUint16(20, format, true);
  view.setUint16(22, numChannels, true);
  view.setUint32(24, sampleRate, true);
  view.setUint32(28, byteRate, true);
  view.setUint16(32, blockAlign, true);
  view.setUint16(34, 16, true);
  writeString(view, 36, 'data');
  view.setUint32(40, dataSize, true);

  // 오디오 데이터
  const dataView = new DataView(buffer, 44);
  Array.from({ length: numChannels }).forEach((_, channelIndex) => {
    const channelData = audioBuffer.getChannelData(channelIndex);

    channelData.forEach((sample, sampleIndex) => {
      const clampedSample = Math.max(-1, Math.min(1, sample));
      const scaledSample = clampedSample * 0x7fff;
      const byteOffset = sampleIndex * 2 + channelIndex * audioBuffer.length * 2;

      dataView.setInt16(byteOffset, scaledSample, true);
    });
  });

  return new Blob([buffer], { type: 'audio/wav' });
};

const convertBlobToWav = (blob) =>
  new Promise((resolve, reject) => {
    const reader = new FileReader();

    reader.onload = (event) => {
      const arrayBuffer = event.target.result;
      const audioContext = new (window.AudioContext || window.webkitAudioContext)();

      audioContext.decodeAudioData(arrayBuffer, (audioBuffer) => {
        resolve(encodeWav(audioBuffer));
      }, reject);
    };

    reader.onerror = reject;
    reader.readAsArrayBuffer(blob);
  });

export { convertBlobToWav };

참고

  • 위 코드는 채널 데이터를 채널별로 이어 쓴다(planar). 표준 WAV 플레이어 호환이 필요하면 스테레오일 때 샘플을 채널 순서대로 교차 배치(interleave)해야 한다. 모노 녹음이라면 차이가 없다.
  • decodeAudioData는 AudioContext의 샘플레이트로 리샘플링한다. 특정 샘플레이트(예: 16kHz)가 필요하면 new AudioContext({ sampleRate: 16000 })처럼 지정한다.