사용자로부터 오디오 녹음

이제 대부분의 브라우저에서 사용자의 동영상 및 오디오 입력에 액세스할 수 있습니다. 그러나 브라우저에 따라 완전한 동적 인라인 환경이거나 사용자 기기의 다른 앱에 위임될 수 있습니다.

단순하게 점진적으로 시작

가장 쉬운 방법은 사용자에게 미리 녹음된 파일을 요청하는 것입니다. 간단한 파일 입력 요소를 만들고 오디오 파일만 허용됨을 나타내는 accept 필터와 마이크에서 직접 가져오려고 함을 나타내는 capture 속성을 추가하면 됩니다.

<input type="file" accept="audio/*" capture />

이 메서드는 모든 플랫폼에서 작동합니다. 데스크톱에서는 사용자에게 파일 시스템에서 파일을 업로드하라는 메시지가 표시됩니다 (capture 속성 무시). iOS의 Safari에서는 마이크 앱이 열려 오디오를 녹음한 다음 웹페이지로 다시 전송할 수 있습니다. Android에서는 오디오를 녹음할 앱을 사용자가 선택할 수 있는 선택권이 제공되고 오디오 녹음을 웹페이지로 다시 보냅니다.

사용자가 녹화를 완료하고 웹사이트로 돌아오면 어떻게 해서든 파일 데이터를 확보해야 합니다. onchange 이벤트를 입력 요소에 연결한 후 이벤트 객체의 files 속성을 읽어 빠르게 액세스할 수 있습니다.

<input type="file" accept="audio/*" capture id="recorder" />
<audio id="player" controls></audio>
  <script>
    const recorder = document.getElementById('recorder');
    const player = document.getElementById('player');

    recorder.addEventListener('change', function (e) {
      const file = e.target.files[0];
      const url = URL.createObjectURL(file);
      // Do something with the audio file.
      player.src = url;
    });
  </script>
</audio>

파일에 액세스할 수 있게 되면 파일로 원하는 작업을 할 수 있습니다. 예를 들어 다음과 같이 변경할 수 있습니다.

  • 재생할 수 있도록 <audio> 요소에 직접 연결합니다.
  • 사용자 기기에 다운로드
  • XMLHttpRequest에 연결하여 서버에 업로드합니다.
  • Web Audio API를 통해 전달하고 필터를 적용합니다.

오디오 데이터에 액세스하기 위해 입력 요소 메서드를 사용하는 것이 보편적이지만 가장 매력적이지 않은 옵션입니다. 마이크에 액세스하고 페이지에서 바로 만족스러운 경험을 제공하고 싶어요.

대화형으로 마이크에 액세스

최신 브라우저는 마이크에 직접 연결되어 있어 웹페이지와 완전히 통합된 환경을 빌드할 수 있으며 사용자가 브라우저를 떠날 필요가 없습니다.

마이크 액세스 권한 획득

WebRTC 사양 getUserMedia()의 API를 사용하여 마이크에 직접 액세스할 수 있습니다. getUserMedia()는 사용자에게 연결된 마이크 및 카메라에 액세스하라는 메시지를 표시합니다.

성공하면 API가 카메라 또는 마이크의 데이터가 포함된 Stream를 반환합니다. 그러면 API를 <audio> 요소에 연결하거나, WebRTC 스트림에 연결하거나, 웹 오디오 AudioContext에 연결하거나, MediaRecorder API를 사용하여 저장할 수 있습니다.

마이크에서 데이터를 가져오려면 getUserMedia() API에 전달되는 제약 조건 객체에서 audio: true를 설정하면 됩니다.

<audio id="player" controls></audio>
<script>
  const player = document.getElementById('player');

  const handleSuccess = function (stream) {
    if (window.URL) {
      player.srcObject = stream;
    } else {
      player.src = stream;
    }
  };

  navigator.mediaDevices
    .getUserMedia({audio: true, video: false})
    .then(handleSuccess);
</script>

특정 마이크를 선택하려면 먼저 사용 가능한 마이크를 열거할 수 있습니다.

navigator.mediaDevices.enumerateDevices().then((devices) => {
  devices = devices.filter((d) => d.kind === 'audioinput');
});

그런 다음 getUserMedia를 호출할 때 사용할 deviceId를 전달하면 됩니다.

navigator.mediaDevices.getUserMedia({
  audio: {
    deviceId: devices[0].deviceId,
  },
});

그 자체로는 그다지 유용하지 않습니다. 오디오 데이터를 받아서 재생하기만 하면 됩니다.

마이크에서 원시 데이터에 액세스

마이크에서 원시 데이터에 액세스하려면 getUserMedia()에서 만든 스트림을 가져온 다음 Web Audio API를 사용하여 데이터를 처리해야 합니다. Web Audio API는 사용자가 들을 수 있도록 입력 소스를 가져와서 오디오 데이터를 처리할 수 있는 노드 (게인 조정 등)와 스피커에 연결하는 간단한 API입니다.

연결할 수 있는 노드 중 하나는 AudioWorkletNode입니다. 이 노드는 맞춤 오디오 처리를 위한 하위 수준의 기능을 제공합니다. 실제 오디오 처리는 AudioWorkletProcessorprocess() 콜백 메서드에서 이루어집니다. 이 함수를 호출하여 입력 및 매개변수를 제공하고 출력을 가져옵니다.

자세한 내용은 오디오 Worklet 입력을 참고하세요.

<script>
  const handleSuccess = async function(stream) {
    const context = new AudioContext();
    const source = context.createMediaStreamSource(stream);

    await context.audioWorklet.addModule("processor.js");
    const worklet = new AudioWorkletNode(context, "worklet-processor");

    source.connect(worklet);
    worklet.connect(context.destination);
  };

  navigator.mediaDevices.getUserMedia({ audio: true, video: false })
      .then(handleSuccess);
</script>
// processor.js
class WorkletProcessor extends AudioWorkletProcessor {
  process(inputs, outputs, parameters) {
    // Do something with the data, e.g. convert it to WAV
    console.log(inputs);
    return true;
  }
}

registerProcessor("worklet-processor", WorkletProcessor);

버퍼에 보관된 데이터는 마이크의 원시 데이터이며 이 데이터로 할 수 있는 다양한 옵션이 있습니다.

  • 서버에 바로 업로드
  • 로컬에 저장
  • WAV와 같은 전용 파일 형식으로 변환한 후 서버 또는 로컬에 저장

마이크의 데이터 저장

마이크의 데이터를 저장하는 가장 쉬운 방법은 MediaRecorder API를 사용하는 것입니다.

MediaRecorder API는 getUserMedia에서 만든 스트림을 가져와 스트림에 있는 데이터를 원하는 대상에 점진적으로 저장합니다.

<a id="download">Download</a>
<button id="stop">Stop</button>
<script>
  const downloadLink = document.getElementById('download');
  const stopButton = document.getElementById('stop');


  const handleSuccess = function(stream) {
    const options = {mimeType: 'audio/webm'};
    const recordedChunks = [];
    const mediaRecorder = new MediaRecorder(stream, options);

    mediaRecorder.addEventListener('dataavailable', function(e) {
      if (e.data.size > 0) recordedChunks.push(e.data);
    });

    mediaRecorder.addEventListener('stop', function() {
      downloadLink.href = URL.createObjectURL(new Blob(recordedChunks));
      downloadLink.download = 'acetest.wav';
    });

    stopButton.addEventListener('click', function() {
      mediaRecorder.stop();
    });

    mediaRecorder.start();
  };

  navigator.mediaDevices.getUserMedia({ audio: true, video: false })
      .then(handleSuccess);
</script>

여기서는 나중에 Blob로 변환할 수 있는 배열에 데이터를 직접 저장합니다. 이 배열은 데이터를 웹 서버 또는 사용자 기기의 저장소에 직접 저장하는 데 사용할 수 있습니다.

책임감 있게 마이크를 사용하기 위한 권한 요청

사용자가 이전에 사이트에 마이크 액세스 권한을 부여하지 않은 경우 getUserMedia를 호출하는 즉시 브라우저에서 사용자에게 사이트에 마이크 액세스 권한을 부여하라는 메시지를 표시합니다.

사용자는 시스템에서 강력한 기기에 액세스하라는 메시지가 표시되는 것을 싫어하며, 메시지가 생성된 상황을 이해하지 못하면 요청을 자주 차단하거나 무시합니다. 처음에 필요할 때만 마이크 액세스를 요청하는 것이 좋습니다. 사용자가 액세스 권한을 부여한 후에는 다시 요청을 받지 않지만, 사용자가 액세스를 거부하면 사용자에게 권한을 다시 요청할 수 없습니다.

권한 API를 사용하여 이미 액세스 권한이 있는지 확인하세요.

getUserMedia API를 사용하면 이미 마이크에 액세스할 수 있는지 알 수 없습니다. 이 경우 문제가 발생합니다. 사용자가 마이크 액세스 권한을 부여할 수 있는 멋진 UI를 제공하려면 마이크 액세스 권한을 요청해야 합니다.

이 문제는 일부 브라우저에서 Permission API를 사용하여 해결할 수 있습니다. navigator.permission API를 사용하면 메시지를 다시 표시하지 않고도 특정 API에 액세스하는 기능의 상태를 쿼리할 수 있습니다.

사용자의 마이크에 액세스할 수 있는지 쿼리하려면 {name: 'microphone'}를 쿼리 메서드에 전달하면 됩니다. 그러면 메서드가 다음 중 하나를 반환합니다.

  • granted: 사용자가 이전에 마이크 액세스 권한을 부여했습니다.
  • prompt: 사용자가 액세스 권한을 부여하지 않았으며 getUserMedia를 호출할 때 메시지가 표시됩니다.
  • denied - 시스템 또는 사용자가 명시적으로 마이크 액세스를 차단하여 마이크에 액세스할 수 없습니다.

이제 사용자가 실행해야 하는 작업을 수용하도록 사용자 인터페이스를 변경해야 하는지 빠르게 확인할 수 있습니다.

navigator.permissions.query({name: 'microphone'}).then(function (result) {
  if (result.state == 'granted') {
  } else if (result.state == 'prompt') {
  } else if (result.state == 'denied') {
  }
  result.onchange = function () {};
});

의견