Media Capture and Streams と Web Audio API で実現する録画・録音・ WAVファイルの生成

前回のエントリではマイク経由の音声認識を行う Speech Recognition API を試しましたが、今回はカメラ・マイクからの動画・音声キャプチャを行う Media Capture and Streams を試してみます。 また、キャプチャした音声の録音については MediaStream Recording を使用することで可能ですが、現時点(2015年4月)では Firefox にしか実装されていません。 そこで Web Audio API を使用することで他のブラウザでも音声録音を可能にし、それらの音声データを wav 形式で保存する方法についても試してみました。

はじめに

ベンダープレフィックス対策として以下処理を行います。

code demo

script
navigator.getUserMedia = 
    navigator.getUserMedia ||
    navigator.webkitGetUserMedia ||
    navigator.mozGetUserMedia ||
    navigator.msGetUserMedia;
window.URL = 
    window.URL || 
    window.webkitURL || 
    window.mozURL || 
    window.msURL;
window.AudioContext = 
    window.AudioContext||
    window.webkitAudioContext;

動画をキャプチャする

カメラ経由の動画をキャプチャするには navigator.getUserMedia を使用します。下記コードを実行するとカメラへのアクセス許可を求められるので、許可すると動画のキャプチャが始まります。

  1. navigator.getUserMedia(
  2. {video: true, audio: false},
  3. function(stream){
  4. ・・・
  5. },
  6. function(err){
  7. console.log(err.name ? err.name : err);
  8. }
  9. );


コールバック関数で得られる stream を URL.createObjectURL によりオブジェクト URL 化した後、video 要素に割り当てることでリアルタイムに動画を再生させることができます。

  1. function(stream){
  2. var video = document.querySelector('.video');
  3. video.src = URL.createObjectURL(stream);
  4. video.play();
  5. },


実際に試してみます。
以下デモの REC ボタンをクリックしてみてください。 カメラへのアクセス許可をするとカメラのキャプチャが行われ、動画がリアルタイム再生されます。

Chrome の場合、カメラがキャプチャ中である事を示す録画マークがタブ脇に表示されます。 キャプチャを停止するにはストリームの stop() メソッドを実行します。 REC ボタンをクリックするとラベルが STOP に切り替わるので、再クリックしキャプチャ状態を停止してください。

code demo

html
<button class="rec">REC</button><br/>
<video class="video" width="320" height="200"></video><br/>
script
var stream;
var recControl = function(){
    if(!stream){
        el.rec.textContent = 'STOP';
        navigator.getUserMedia(
            {
                video: true,
                audio: false
            },
            function(s){
                stream = s;
                el.video.src = URL.createObjectURL(stream);
                el.video.play();
            },
            function(err){
                console.log(err.name ? err.name : err);
            }
        );
    }
    else{
        el.rec.textContent = 'REC';
        stream.stop();
        stream = undefined;
    }
}
var el = {
    video : $DEMO[0].querySelector('.video'),
    rec : $DEMO[0].querySelector('.rec')
}
el.rec.addEventListener('click', recControl, false);

audio パラメータに false を指定してるのは、リアルタイムでキャプチャ音声を再生してしまうとハウリングを起こしてしまうためです。 音声のストリームデータが必要な場合は、audio パラメータに true を指定し、video 要素側でミュートします。

動画のスクリーンショットを行う

以下のように canvas に対し video 要素を割り当てることで、再生中の動画のスクリーショットをとることができます。

  1. var video = document.querySelector('.video');
  2. var canvas = document.querySelector('.canvas');
  3. canvas.getContext("2d").drawImage(video, 0, 0, 320, 240);


以下デモでは動画の再生中に SCREEN SHOT ボタンを押すと、その瞬間の画像が canvas に表示されます。 表示された画像は画像ファイルとして保存することができます。

code demo

html
<button class="rec">REC</button><br/>
<video class="video" width="320" height="200"></video><br/>
<button class="shot">SCREEN SHOT</button><br/>
<canvas class="canvas" width="320" height="240"></canvas>
script
var stream;
var recControl = function(){
    if(!stream){
        el.rec.textContent = 'STOP';
        navigator.getUserMedia(
            {
                video: true,
                audio: false
            },
            function(s){
                stream = s;
                el.video.src = URL.createObjectURL(stream);
                el.video.play();
            },
            function(err){
                console.log(err.name ? err.name : err);
            }
        );
    }
    else{
        el.rec.textContent = 'REC';
        stream.stop();
        stream = undefined;
    }
}
var shotControl = function(){
    el.canvas.getContext("2d").drawImage(el.video, 0, 0, 320, 240);
}
var el = {
    video : $DEMO[0].querySelector('.video'),
    rec : $DEMO[0].querySelector('.rec'),
    canvas : $DEMO[0].querySelector('.canvas'),
    shot : $DEMO[0].querySelector('.shot')
}
el.rec.addEventListener('click', recControl, false);
el.shot.addEventListener('click', shotControl, false);

Firefox で録音・再生・保存を行う

前述のデモではリアルタイム再生するとハウリングを起こすという理由で音声データのキャプチャは行いませんでしたが、音声データを録音(バッファリング)し、キャプチャ終了後に再生させることでハウリングを回避できます。 現時点(2015年4月)では Firefox にしか実装されてない MediaStream Recording という機能を使用します。

以下ロジックでは 5 秒間の音声録音後、録音した音声を再生させています。

  1. var audio = document.querySelector('.audio');
  2. var recorder;
  3. navigator.getUserMedia({video: false, audio: true},
  4. function(stream){
  5. recorder = new MediaRecorder(stream);
  6. recorder.start();
  7. // 録音が停止されると呼び出される
  8. recorder.ondataavailable = function(event) {
  9. var blob = event.data;
  10. audio.src = URL.createObjectURL(blob);
  11. audio.play();
  12. }
  13. },
  14. ・・・
  15. });
  16. // 5秒後に録音を停止
  17. setTimeout(function(){
  18. recorder.stop();
  19. }, 5000);

recorder.stop() メソッドが実行されると ondataavailable(event) が呼び出され、その中で blob 形式の音声データを取得しています。


以下デモでは REC ボタンで録音の開始・終了を行い、録音の終了時に録音した音声の再生と、音声ファイルのダウンロードリンクを表示します。
Firefox でお試しください。

code demo

html
<button class="rec">REC</button>
<a class="download"></a>
<audio class="audio"></audio>
script
var stream;
var recorder;
var recControl = function(){
    if(typeof MediaRecorder == 'undefined') return;
    if(!stream){
        el.rec.textContent = 'STOP';
        navigator.getUserMedia(
            {
                video: false,
                audio: true
            },
            function(s){
                stream = s;
                recorder = new MediaRecorder(stream);
                recorder.start();
                recorder.ondataavailable = function(event) {
                    var blob = event.data;
                    var url = el.audio.src = URL.createObjectURL(blob);
                    el.audio.play();
                    //download link
                    el.download.href = url;
                    el.download.textContent = 'download'
                    el.download.download = 'output.wav';
                };
            },
            function(err){
                console.log(err.name ? err.name : err);
            }
        );
    }
    else{
        el.rec.textContent = 'REC';
        recorder.stop();
        stream.stop();
        stream = undefined;
    }
}
var el = {
    audio : $DEMO[0].querySelector('.audio'),
    rec : $DEMO[0].querySelector('.rec'),
    download: $DEMO[0].querySelector('.download')
}
el.rec.addEventListener('click', recControl, false);

ライブラリで録音・再生・保存を行う

MediaStreamRecorder.js というライブラリを使用すると、Firefox 以外のブラウザでも前述の MediaStream Recording に近いロジックで録音処理を行えます。

但し、MediaStream Recording とは異なり recorder.stop() に相当するメソッドがなく、 録音開始時に recorder.start( time ) といった具合に録音時間を指定する仕様になっています。 また、Firefox の場合は問題ないのですが、一度でも stream.stop() でキャプチャを停止してしまうと、再度 getUserMedia を実行しキャプチャを行おうとしても、stream からの音声データが取得できなくなってしまうようです。

以下デモでは 5 秒間の録音状態後、録音した音声の再生と音声ファイルのダウンロードリンクを表示します。

code demo

html
<button class="rec">REC</button>
<a class="download"></a>
script
var recControl = function(){
    navigator.getUserMedia(
        {
            video: false,
            audio: true
        },
        function(stream){
            el.rec.textContent = 'now Recording...';
            el.rec.disabled = true;
            var recorder = new MediaStreamRecorder(stream);
            recorder.mimeType = 'audio/ogg';
            recorder.ondataavailable = function (blob) {
                var audio_el = document.createElement("audio");
                var url = audio_el.src = URL.createObjectURL(blob);
                audio_el.play();
                recorder.stop();
                stream.stop();
                el.rec.textContent = 'REC';
                el.rec.disabled = false;

                //download link
                el.download.href = url;
                el.download.textContent = 'download';
                el.download.download = 'output.wav';
                $DEMO[0].appendChild(el.download);
            };
            recorder.start(5000);
        },
        function(err){
            console.log(err.name ? err.name : err);
        }
    );
}
var el = {
    rec : $DEMO[0].querySelector('.rec'),
    download: $DEMO[0].querySelector('.download')
}
el.rec.addEventListener('click', recControl, false);

Web Audio API で録音・再生・保存を行う

Web Audio API を使用した音声データの録音・再生する方法を考えてみます。
Web Audio API で音声を再生させるには音源ノードを音声出力ノードに接続する必要があります。

  1. var audioContext = new AudioContext();
  2. var audioBufferSourceNode = audioContext.createBufferSource(); // 音源
  3.  
  4. audioBufferSourceNode.buffer = 音源データ
  5.  
  6. //接続
  7. audioBufferSourceNode.connect(
  8. audioContext.destination // 音声出力
  9. );
  10. audioBufferSourceNode.start(); //再生

上記処理では音源ノードとして AudaioBufferSourceNode を使用し、音声出力ノードである AudioContext.destination に接続してます。 この状態で AudaioBufferSourceNode に音源データを割り当てることで音声データを再生させることができます。

例えば wav ファイルを音源とする場合は以下のように記述します。(詳細は「HTML5 の Web Audio API で音楽してみる 」を参照ください。)

  1. audioContext.decodeAudioData(
  2. 'xhr で取得した wav データ',
  3. function(wavBuffer){
  4. audioBufferSourceNode.buffer = wavBuffer;
  5. }
  6. )

今回のケースでは Media Capture で取得した Stream を変換し、上記の audioBufferSourceNode.buffer に割り当てる必要があります。

まず取得した音声データをバッファリング用の配列変数(audioBufferArray)に保存していきます。

  1. var bufferSize = 4096;
  2. var mediaStreamSource = audioContext.createMediaStreamSource(stream);
  3. var scriptProcessor = audioContext.createScriptProcessor(bufferSize, 1, 1);
  4. mediaStreamSource.connect(scriptProcessor);
  5. audioBufferArray = [];
  6. scriptProcessor.onaudioprocess = function(event){
  7. var channel = event.inputBuffer.getChannelData(0);
  8. var buffer = new Float32Array(bufferSize);
  9. for (var i = 0; i < bufferSize; i++) {
  10. buffer[i] = channel[i];
  11. }
  12. audioBufferArray.push(buffer);
  13. }
  14. scriptProcessor.connect(audioContext.destination);

mediaStreamSource に stream を割り当て、以下接続をすると scriptProcessor の onaudioprocess イベントよりFloat32Array 型の音声データを取得することができます。

  1. mediaStreamSource(stream) ---> scriptProcessor ---> audioContext.destination


次に audioBufferSourceNode.buffer に割り当てるための AudioBuffer を生成し、これに上記処理で音声データを記録した audioBufferArray を割り当てます。

  1. var audioBuffer = audioContext.createBuffer(
  2. 1,
  3. audioBufferArray.length * bufferSize,
  4. audioContext.sampleRate
  5. );
  6. var channel = audioBuffer.getChannelData(0);
  7. for (var i = 0; i < audioBufferArray.length; i++) {
  8. for (var j = 0; j < bufferSize; j++) {
  9. channel[i * bufferSize + j] = audioBufferArray[i][j];
  10. }
  11. }


最後に AudioBuffer を audioBufferSourceNode.buffer に割り当てることで再生可能な状態になります。

  1. audioBufferSourceNode.buffer = audioBuffer


以下に録音処理部分のみを汎用化してみました。

demo code

script
window.Recorder = function(audioContext, bufferSize){
    var o = this;
    o.audioContext = audioContext;
    o.bufferSize = bufferSize || 4096;
}
Recorder.prototype = {
    audioContext : '',
    bufferSize : '',
    audioBufferArray : [],
    stream : '',
    recording : function(stream){
        var o = this;
        o.stream = stream;
        var mediaStreamSource =
            o.audioContext.createMediaStreamSource(stream);
        var scriptProcessor =
            o.audioContext.createScriptProcessor(o.bufferSize, 1, 1);
        mediaStreamSource.connect(scriptProcessor);
        o.audioBufferArray = [];
        scriptProcessor.onaudioprocess = function(event){
            var channel = event.inputBuffer.getChannelData(0);
            var buffer = new Float32Array(o.bufferSize);
            for (var i = 0; i < o.bufferSize; i++) {
                buffer[i] = channel[i];
            }
            o.audioBufferArray.push(buffer);
        }
        //この接続でonaudioprocessが起動
        scriptProcessor.connect(o.audioContext.destination);
        o.scriptProcessor = scriptProcessor;
    },
    recStart : function(){
        var o = this;
        if(o.stream){
            o.recording(o.stream);
        }
        else{
            navigator.getUserMedia(
                {video: false, audio: true},
                function(stream){o.recording(stream)},
                function(err){
                    console.log(err.name ? err.name : err);
                }
            );
        }
    },
    recStop : function(){
        var o = this;
        o.scriptProcessor.disconnect();
        if(o.stream){
            o.stream.stop();
            o.stream = null;
        }
    },
    getAudioBufferArray : function(){
        var o = this;
        return o.audioBufferArray
    },
    getAudioBuffer : function(){
        var o = this;
        var buffer = o.audioContext.createBuffer(
            1,
            o.audioBufferArray.length * o.bufferSize,
            o.audioContext.sampleRate
        );
        var channel = buffer.getChannelData(0);
        for (var i = 0; i < o.audioBufferArray.length; i++) {
            for (var j = 0; j < o.bufferSize; j++) {
                channel[i * o.bufferSize + j] = o.audioBufferArray[i][j];
            }
        }
        return buffer;
    }
}


以下のように使用します。

  1. var audioContext = new AudioContext();
  2. var recorder = new Recorder(audioContext);
  3. recorder.recStart(); // 録音開始
  4. recorder.recStop(); // 録音停止
  5. recorder.getAudioBufferArray(); //音声配列データの取得
  6. recorder.getAudioBuffer(); //AudioBuffer の取得


音声を再生する場合は以下のようにします。 今回は単純な音声再生しかしませんが、Web Audio API ベースなのでいろいろな音声加工が可能になります。

  1. var src = audioContext.createBufferSource();
  2. src.buffer = recorder.getAudioBuffer();
  3. src.connect(audioContext.destination);
  4. src.start()


次に音声ファイルの保存ですが、これはこちらの記事のロジックを若干改変したものを拝借させて頂きました。

code demo

script
window.exportWAV = function(audioData, sampleRate) {
    var encodeWAV = function(samples, sampleRate) {
        var buffer = new ArrayBuffer(44 + samples.length * 2);
        var view = new DataView(buffer);
        var writeString = function(view, offset, string) {
            for (var i = 0; i < string.length; i++){
                view.setUint8(offset + i, string.charCodeAt(i));
            }
        };
        var floatTo16BitPCM = function(output, offset, input) {
            for (var i = 0; i < input.length; i++, offset += 2){
                var 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;
    };
    var mergeBuffers = function(audioData) {
        var sampleLength = 0;
        for (var i = 0; i < audioData.length; i++) {
          sampleLength += audioData[i].length;
        }
        var samples = new Float32Array(sampleLength);
        var sampleIdx = 0;
        for (var i = 0; i < audioData.length; i++) {
          for (var j = 0; j < audioData[i].length; j++) {
            samples[sampleIdx] = audioData[i][j];
            sampleIdx++;
          }
        }
        return samples;
    };
    var dataview = encodeWAV(mergeBuffers(audioData), sampleRate);
    var audioBlob = new Blob([dataview], { type: 'audio/wav' });
    return audioBlob;
};

exportWav 関数に音声配列データを渡すと blob 形式で返してくれます。

  1. var blob = exportWAV(recorder.getAudioBufferArray())
  2. var url = URL.createObjectURL(blob);


以下デモになります。REC で録音を開始し、STOP で停止と wav ファイルのダウンロードリンクを表示し、PLAY で再生します。

code demo

css
.disabled,
[disabled=true]{
    color: #aaa;
}
html
<button class="rec">REC</button>
<button class="stop" disabled="true">STOP</button>
<button class="play" disabled="true">PLAY</button>
<a class="download"></a>
script
var audioContext = new AudioContext();
var recorder = new Recorder(audioContext);
var el = {
    rec : $DEMO[0].querySelector('.rec'),
    stop : $DEMO[0].querySelector('.stop'),
    play : $DEMO[0].querySelector('.play'),
    download : $DEMO[0].querySelector('.download')
}
var disabled = function(el, bool){
    el.classList[bool ? 'add' : 'remove']('disabled');
    el.disabled = bool;
}
el.rec.addEventListener('click', function(){
    recorder.recStart();
    disabled(el.rec, true);
    disabled(el.stop, false);
    disabled(el.play, true);
});
el.stop.addEventListener('click', function(){
    recorder.recStop();
    disabled(el.rec, false);
    disabled(el.stop, true);
    disabled(el.play, false);

    //download link
    var blob = exportWAV(recorder.getAudioBufferArray(), audioContext.sampleRate)
    var url = URL.createObjectURL(blob);
    el.download.href = url;
    el.download.textContent = 'download'
    el.download.download = 'output.wav';
});
el.play.addEventListener('click', function(){
    //play
    var src = audioContext.createBufferSource();
    src.buffer = recorder.getAudioBuffer();
    src.connect(audioContext.destination);
    src.start()
});

最後に

HTML5 の Web Audio API で音楽してみる」で紹介した作曲向けライブラリを改変して、キャプチャ音声をボイスパーカッションとして使用できるようにしてみたら面白いかもしれません。時間があったらいろいろ試してみたいと思います。