Help us understand the problem. What is going on with this article?

ブラウザで録音してwavで保存

More than 1 year has passed since last update.

はじめに

ブラウザ経由で音声を取得して、いろいろやりたくて調べた。最終的に、getUserMediaしてScriptProcessorNodeで音声データをバッファにためて、wavヘッダをつけてBlobで保存した。

最初は、MediaRecorderとか、AnalyserNodeとかで、録音したり解析したりしようとしたけど、なんだか思ったのと挙動が違った。

MediaRecorderで保存しようとすると、webmとかいうのんで保存された。wavじゃできなかった。ブラウザ依存なんかもしれん。ChromeとFirefoxで試したけど、wavは無理やった。

AnalyserNodeは、データをFFTしたりできるけど、どうもすべてのバッファに対して処理するんじゃなくて、一定タイミングとか画面の更新タイミングで、そのときたまってるデータを解析してるっぽい。たぶん。

すべてのデータに対して処理するには、ScriptProcessorNodeとかAudioWorkletっての使うらしい。最近は、ScriptProcessorNodeじゃなくてAudioWorkletを使った方が良さそう。後継っぽい。AudioWorkletでもよかったんだけど、やりたいこと調べてたら、ScriptProcessorNode使って、似たようなことしてる人見つけたので、そっちを使うことにした。

調べてると、下記サイトが出てきた。これを真似したらできるはずなんやけど、Javascriptのこと、あまりわかってないので、試行錯誤した。中身ほぼコピペで恐縮やけど、僕みたいな初心者でも真似できるように、やったことを残しておく。

getUserMediaで録音したデータをWAVファイルとして保存する - Qiita

コード

こんな感じ。ひとつのコードに書いてるからわかりにくいけど、やってることはシンプルなはず。exportWAVでwavに変換してて、データを処理するだけなら、bufferDataとかaudioDataとかを使ったらいい。

wav.html
<!DOCTYPE html>
<html lang="ja">

<head>
  <title>Save audio data</title>
</head>

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

    // for audio
    let audio_sample_rate = null;
    let scriptProcessor = null;
    let audioContext = null;

    // audio data
    let audioData = [];
    let bufferSize = 1024;

    let saveAudio = function () {
      downloadLink.href = exportWAV(audioData);
      downloadLink.download = 'test.wav';
      downloadLink.click();

      audioContext.close().then(function () {
        stopButton.setAttribute('disabled', 'disabled');
      });
    }

    // export WAV from audio float data
    let exportWAV = function (audioData) {

      let encodeWAV = function (samples, sampleRate) {
        let buffer = new ArrayBuffer(44 + samples.length * 2);
        let view = new DataView(buffer);

        let writeString = function (view, offset, string) {
          for (let i = 0; i < string.length; i++) {
            view.setUint8(offset + i, string.charCodeAt(i));
          }
        };

        let floatTo16BitPCM = function (output, offset, input) {
          for (let i = 0; i < input.length; i++ , offset += 2) {
            let s = Math.max(-1, Math.min(1, input[i]));
            output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
          }
        };

        writeString(view, 0, 'RIFF');  // RIFFヘッダ
        view.setUint32(4, 32 + samples.length * 2, true); // これ以降のファイルサイズ
        writeString(view, 8, 'WAVE'); // WAVEヘッダ
        writeString(view, 12, 'fmt '); // fmtチャンク
        view.setUint32(16, 16, true); // fmtチャンクのバイト数
        view.setUint16(20, 1, true); // フォーマットID
        view.setUint16(22, 1, true); // チャンネル数
        view.setUint32(24, sampleRate, true); // サンプリングレート
        view.setUint32(28, sampleRate * 2, true); // データ速度
        view.setUint16(32, 2, true); // ブロックサイズ
        view.setUint16(34, 16, true); // サンプルあたりのビット数
        writeString(view, 36, 'data'); // dataチャンク
        view.setUint32(40, samples.length * 2, true); // 波形データのバイト数
        floatTo16BitPCM(view, 44, samples); // 波形データ

        return view;
      };

      let mergeBuffers = function (audioData) {
        let sampleLength = 0;
        for (let i = 0; i < audioData.length; i++) {
          sampleLength += audioData[i].length;
        }
        let samples = new Float32Array(sampleLength);
        let sampleIdx = 0;
        for (let i = 0; i < audioData.length; i++) {
          for (let j = 0; j < audioData[i].length; j++) {
            samples[sampleIdx] = audioData[i][j];
            sampleIdx++;
          }
        }
        return samples;
      };

      let dataview = encodeWAV(mergeBuffers(audioData), audio_sample_rate);
      let audioBlob = new Blob([dataview], { type: 'audio/wav' });
      console.log(dataview);

      let myURL = window.URL || window.webkitURL;
      let url = myURL.createObjectURL(audioBlob);
      return url;
    };

    // stop button
    stopButton.addEventListener('click', function () {
      saveAudio();
      console.log('saved wav');
    });

    // save audio data
    var onAudioProcess = function (e) {
      var input = e.inputBuffer.getChannelData(0);
      var bufferData = new Float32Array(bufferSize);
      for (var i = 0; i < bufferSize; i++) {
        bufferData[i] = input[i];
      }

      audioData.push(bufferData);
    };

    // getusermedia
    let handleSuccess = function (stream) {
      audioContext = new AudioContext();
      audio_sample_rate = audioContext.sampleRate;
      console.log(audio_sample_rate);
      scriptProcessor = audioContext.createScriptProcessor(bufferSize, 1, 1);
      var mediastreamsource = audioContext.createMediaStreamSource(stream);
      mediastreamsource.connect(scriptProcessor);
      scriptProcessor.onaudioprocess = onAudioProcess;
      scriptProcessor.connect(audioContext.destination);

      console.log('record start?');

      // when time passed without pushing the stop button
      setTimeout(function () {
        console.log("10 sec");
        if (stopButton.disabled == false) {
          saveAudio();
          console.log("saved audio");
        }
      }, 10000);
    };

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

  </script>
</body>

</html>

動作

Chromeで実行。

はじめに、マイクの許可を聞かれる。許可したら録音開始される。
image.png

インターフェースはStopボタンがあるだけ。Stop押したら、録音止まる。
image.png

録音が終わったら、自動でデータ保存のポップアップがでる。そのあとは、Downloadがリンクになって、保存のポップアップでるようになる。Stopボタンはdisabledされる。あんまり、長いデータを貯めるのも微妙かなと思って、10秒間押されなかったら、勝手に保存するようにしてみた。
image.png

おわりに

やりたいことから、紆余曲折して、最終的に、ScriptProcessorNodeに落ち着くまでが、それなりに時間かかった。でも、JavaScriptのクラスっぽい動きとか、変数のスコープとか、コールバックとかをそこそこ勉強できたので、よかった。変数を関数間で受け渡しするときに、めんどくさくてグローバル変数的なことしちゃったけど、もっとうまいことやりたいなと思う。エラー処理も、もっとちゃんとやりたい。

参照

ここを大いに参照。
getUserMediaで録音したデータをWAVファイルとして保存する - Qiita
AudioContext.createScriptProcessor() - Web API | MDN
JavaScriptのWeb Audio APIで録音してみる - saitodev.co
Blobを使って(jQueryを使わずに)JavaScriptでデータをファイルに保存 - Qiita

参照しなかったところ

調べてたけど、使わなかった。

ユーザーから音声データを取得する  |  Web  |  Google Developers
MediaRecorder.ondataavailable - Web API | MDN
[WebAudio API] AudioWorklet の使い方 | g200kg Music & Software
Web Audio API – AudioWorklet で遊ぶ – – rilakkuma3xjapan's blog
AnalyserNode.getByteTimeDomainData() - Web APIs | MDN

optimisuke
Feedback Welcome
acall
「Life in Work and Work in Life for Happiness」をビジョンに掲げ、様々なワークスペース、ハードウェア、ソフトウェアを統合して"はたらく"の体験を向上するWorkstyleOSを通じ、暮らしと仕事の幸福度の最大化と追求します。
https://www.acall.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした