Whiteboard Drum の紹介 – WebRTC と Web Audio API の魔法

原文:Introducing the Whiteboard Drum – WebRTC and Web Audio API magic on November 14, 2013 at 12:18 am, by Tatsuya Shinyagaito and Robert Nyman [Editor]

ブラウザの機能は急速に拡大し、単に文書を “ブラウズ” するだけのものではなくなってきています。そして最近、Web Audio APIによって、ついにWebブラウザでオーディオ処理ができるようになりました。それは本格的な音楽アプリケーションを構築できるほどパワフルなものです。

そしてそれだけではなく、他のAPIと組み合わせて使用すると、また非常に面白い応用が可能になります。その1つに、ローカルPCのマイク/カメラデバイスからオーディオやビデオ信号を取り込む事ができるgetUserMedia()があります。Whiteboard Drum (GitHub上のソース)は、Web Audio APIとgetUserMedia()を使った面白い音楽アプリケーションの一例です。

私は10月に東京で開催されたWeb Music ハッカソンでWhiteboard Drum (GitHub上のソース) を紹介しました。このハッカソンは Web Audio API と Web MIDI APIをテーマにしたエキサイティングなイベントです。色々な機器がブラウザと強調動作する事で、現実世界への新しいインターフェースを作り出す事ができます。

私はこれはWebベースの音楽アプリケーション、特にWeb Audio API とその他のAPIの組み合わせに対する新しい可能性を示唆するものだと考えています。以下では、Whiteboard Drum がどうやって動作しているのか、コードを交えながら説明して行きたいと思います。

概要

まずは、ハッカソンでの写真をご覧ください:

そして、デモビデオはこちらにあります:

見ていただければわかるように、Whiteboard Drum はホワイトボード上に描かれたマトリックス・パターンに従ってリズムを演奏します。ホワイトボードには何も仕掛けはなく、Webカメラが向けられているだけです。このデモではマグネットを使っていますが、マーカーペンでパターンを書き込んでも構いません。マトリックスのそれぞれの行が、シンバル、ハイハット、スネアドラム、バスドラムというそれぞれの楽器の表していて、それぞれのカラムは時間のステップをを表しています。この実装では、シーケンスは8ステップになっています。色が青いマス目は通常の音、赤い部分はアクセントが付いた音になります。

処理のフローは次の通りです:

  1. ホワイトボードの画像がWebカメラでキャプチャーされます
  2. マトリックス・パターンが解析されます
  3. このパターンがドラム音のジェネレータに送られ、リズムが再生されます

ブラウザの最新の技術を使用してはいますが、それぞれの処理はそれほど難しいものではありません。幾つかのキーポイントを以下に説明します。

getUserMedia()によるイメージ・キャプチャー

getUserMedia() はWebカメラ/マイクからビデオ/オーディオを取り込む機能です。これはWebRTCの一部であり、機能としてはかなり新しいものです。Webカメラからのイメージの取り込みではユーザーの許可を得る事が必要になりますので、注意が必要です。単にWebカメラの画像をスクリーン上に出すだけならば非常に簡単にできますが、画像の生のピクセルデータに対して JavaScript で処理を行いたい場合、canvas および createImageData()関数が必要になります。

このアプリケーションでは、ピクセル毎の処理が後で必要になるため、取り込む画像の解像度を400 x 200pxに落としています。これはつまり、リズムパターンの各マス目が50 x 50pxになる事を意味しています。

注:最近のラップトップ/ノートブックPCは大抵Webカメラを内蔵しているのですが、Whiteboard Drumでは、カメラは正確にホワイトボード上のパターンを捉える必要がありますので、外部のカメラを準備した方が良いでしょう。なお、複数のデバイス/カメラが接続されている場合の入力選択の手段は今のところ標準化されていません。Firefox では接続時の許可ダイアログで、またGoogle Chromeの場合はセットアップ画面の”コンテンツ設定”のオプションで選択が可能です。

Webカメラのビデオを取得する

この部分はスクリーンに表示せずに処理したいので、まず、videoを非表示にしています:

<video id="video" style="display:none"></video>

そして、ビデオを取り込みます:

video = document.getElementById("video");
navigator.getUserMedia=navigator.getUserMedia||navigator.webkitGetUserMedia||navigator.mozGetUserMedia;
navigator.getUserMedia({"video":true},
    function(stream) {
        video.src= window.URL.createObjectURL(stream);
        video.play();
    },
    function(err) {
        alert("Camera Error");
    });

キャプチャーおよびピクセルの取得

canvasも非表示にします:

<canvas id="capture" width=400 height=200 style="display:none"></canvas>

そしてビデオデータをcanvas上にキャプチャーします:

function Capture() {
    ctxcapture.drawImage(video,0,0,400,200);
    imgdatcapture=ctxcapture.getImageData(0,0,400,200);
}

このWebカメラからの映像は周期的にcanvas上に描画されます。

 

画像の解析

次に400×200ピクセルの値をgetImageData()で取得し、解析フェーズでは、400 x 200 の画像から 1マスを50 x 50 pxとして、8 x 4 のマトリックス・パターンを作り出します。入力となるデータはimgdatcapture.data配列にRGBAで1ピクセル当たり4要素の形式で入っています。

var pixarray = imgdatcapture.data;
var step;
for(var x = 0; x < 8; ++x) {
    var px = x * 50;
    if(invert)
        step=7-x;
    else
        step=x;
    for(var y = 0; y < 4; ++y) {
        var py = y * 50;
        var lum = 0;
        var red = 0;
        for(var dx = 0; dx < 50; ++dx) {
            for(var dy = 0; dy < 50; ++dy) {
                var offset = ((py + dy) * 400 + px + dx)*4;
                lum += pixarray[offset] * 3 + pixarray[offset+1] * 6 + pixarray[offset+2];
                red += (pixarray[offset]-pixarray[offset+2]);
            }
        }
        if(lum < lumthresh) {
            if(red > redthresh)
                rhythmpat[step][y]=2;
            else
                rhythmpat[step][y]=1;
        }
        else
            rhythmpat[step][y]=0;
    }
}

これは単純に各マス目毎のループについて、ピクセル毎のループを回しているだけです。この実装では、「明度」と、「赤さ」について解析しています。もしマス目が「暗い」場合はそのマス目はアクティブになります。またマス目が「赤い」場合はアクセントが付きます。

「明度」の計算は単純化したマトリックスで R * 3 + G * 6 + B としていて、ピクセル値の10倍になります。つまり、各ピクセルについての計算値は0~2550の範囲です。また「赤さ」については R – B としていますがこれは赤か青かを判定するだけで良いために使用している実験的な値です。結果はrhythmpat配列に保存され、マス目に何もなければ0、青ならば1、赤ならば2となります。

Web Audio APIによる音の生成

Web Audio API は非常に新しい機能で、まだ全てのブラウザでサポートされている訳ではありません。Google Chrome / Safari / Webkit ベースの Opera / Firefox (25以上)でこのAPIがサポートされています。注: Firefox 25 は10月の終わりにリリースされた最新版です。

その他のブラウザ用としては、Flashにフォールバックするポリフィルの開発もしていますので、WAAPISim (GitHub) を参照ください。これにより Web Audio APIのほとんどの機能を Internet Explorerなどの非対応ブラウザ上で使えるようになります。

Web Audio API は大規模な仕様のものですが、このアプリケーションでは音の生成のために、それぞれの楽器に対応したそれぞれの音をロードし、正確な時間にそれをトリガーするというだけという Web Audio API のごく簡単な使い方をしています。最初にベンダープリフィックスに配慮しつつ、オーディオコンテキストの作成を行います。プリフィックスは現在、webkit かプリフィックス無しになります。

audioctx = new (window.AudioContext||window.webkitAudioContext)();

次にXMLHttpRequestを使って音をバッファにロードします。このケースでは、それぞれの楽器に対応した個別の音(bd.wav / sd.wav / hh.wav / cy.wav)がbuffers配列にロードされます。

var buffers = [];
var req = new XMLHttpRequest();
var loadidx = 0;
var files = [
    "samples/bd.wav",
    "samples/sd.wav",
    "samples/hh.wav",
    "samples/cy.wav"
];
function LoadBuffers() {
    req.open("GET", files[loadidx], true);
    req.responseType = "arraybuffer";
    req.onload = function() {
        if(req.response) {
            audioctx.decodeAudioData(req.response,function(b){
                buffers[loadidx]=b;
                if(++loadidx < files.length)
                    LoadBuffers();
            },function(){});
        }
    };
    req.send();
}

Web Audio API はノードを接続したルーティンググラフによって音を作り出します。Whiteboard Drumでは、AudioBufferSourceNodeGainNodeによる単純なグラフを使用しています。AudioBufferSourceNodeはAudioBufferを再生し、(通常の「青」の場合)、直接destination(出力)に信号を流し、また(アクセント付の「赤」の場合)はGainNodeを通じてdestinationに接続されます。AudioBufferSourceNodeは一度だけしか使えませんので、トリガー毎に新しく作成されます。

アクセント付の音の出力ポイントとして、次のように GainNode を準備しています。

gain=audioctx.createGain();
    gain.gain.value=2;
    gain.connect(audioctx.destination);

そして、トリガーする関数は次のようになっています:

function Trigger(instrument,accent,when) {
    var src=audioctx.createBufferSource();
    src.buffer=buffers[instrument];
    if(accent)
        src.connect(gain);
    else
        src.connect(audioctx.destination);
    src.start(when);
}

さて、後必要な事は、リズムパターンに従ってを正確なタイミングで再生するだけです。このトリガーのタイミングを作り出すのは、setInterval() タイマーを使うのが簡単ですが、これはあまり推奨できません。CPUの負荷によってタイミングは簡単に乱されてしまいます。

正確なタイミングを維持するためには、Web Audio API が持っている時間の管理機構を使用します。それはつまり、上述のTrigger()関数における、whenパラメータの計算によります。

// console.log(nexttick-audioctx.currentTime);
while(nexttick - audioctx.currentTime < 0.3) {
    var p = rhythmpat[step];
    for(var i = 0; i < 4; ++i)
        Trigger(i, p, nexttick);
    if(++step >= 8)
        step = 0;
    nexttick += deltatick;
}

Whiteboard Drum では、このコードがコアの部分になります。nexttick は、次のステップの正確な時刻(秒)、audioctx.currentTime は正確な現在時刻(これも秒)となります。つまり、このルーチンは今後の300ミリ秒を先読みしています(nextticktime – currenttime < 0.3 の間、あらかじめトリガーしてしまいます)。
コメントされている console.log の行は、タイムマージンを出力します。このルーチンが周期的に呼ばれている間、この値がマイナスになるとタイミングが間に合っていない事を示します。

より詳しい解説については、A Tale of Two Clocks – Scheduling Web Audio with Precision に有用な文書があります。

UIについて

DAWやVSTプラグインなどの音楽制作ソフトウェアでは、特にUIが重要となっています。Webアプリケーションの場合はこれと同じにしなくてはならないと言う訳ではありませんが、似たようなやり方を取るのも良い方法です。これに適した、非常に便利なwebaudio-controls WebComponentライブラリがあり、ツマミやスライダーをHTMLタグ1つで定義する事ができます。

: webaudio-controls は Polymer.js を使用していますが、まだ時として安定性に問題がある事があり、特に複雑なAPIと併用する際には稀に予想外の挙動を起こす場合があります。

今後の展開

これは既に面白いアプリケーションではありますが、まだ改善の余地があります。明らかにカメラの位置の調整をどうするかは問題です。解析フェーズで(何らかのマーカーを使って)位置の自動調整をしたり、適応型の色検出を行うなどもっとスマートなやり方も可能です。音の生成もまだ改善可能で、楽器を増やしたり、ステップを増やしたり、エフェクトを掛ける事もできます。

挑戦してみませんか?

Whiteboard Drum は http://www.g200kg.com/whiteboarddrum/ で、ソースコードは GitHub で公開しています。

是非触ってみてどんなリズムが作れるか試してみてください!

1 件のコメント

  1. Pingback from Web Music ハッカソン #2 を開催します | Japan Buzz on :

    […] また、WebRTC の映像・音声入力機能(getUserMeida)を使った作品も複数制作され、そのうちの 1 つ「Whiteboard Drum」は Mozilla Developer Street(modest)でも紹介されました。 […]