事例紹介 - Chrome での JAM

優れたオーディオを実現する Google の音楽

はじめに

JAM with Chrome は、Google が制作したウェブベースの音楽プロジェクトです。JAM with Chrome を使えば、世界中のユーザーがブラウザ上でリアルタイムでバンドを結成してジャムを楽しめます。DinahMoe にとって、このプロジェクトに参加できて光栄でした。私たちの役割は、アプリケーション用の音楽の制作と、音楽コンポーネントの設計と開発でした。開発には、MIDI 再生、ソフトウェア サンプラー、オーディオ エフェクト、ルーティング、ミキシングを含む「音楽ワークステーション」、音楽をインタラクティブにリアルタイムで制御する音楽ロジック エンジン、セッション内のすべてのプレーヤーがまったく同時に音楽を聴くことができる同期コンポーネント(一緒に演奏するための前提条件)の 3 つの主要領域から構成されました。

できるだけ高いレベルの信頼性、精度、音質を実現するため、Web Audio API を使用することに決めました。このケーススタディでは、直面したいくつかの課題と、それらをどのように解決したかについて説明します。HTML5Rocks には、ウェブ オーディオを始めるのに役立つ優れた紹介記事がすでに数多くあります。それでは、いよいよ本題に入ります。

カスタム オーディオ エフェクトの作成

Web Audio API の仕様には便利なエフェクトが多数含まれていますが、Chrome で JAM で使用する楽器には、もっと手の込んだエフェクトが必要でした。たとえば、ウェブオーディオにはネイティブの遅延ノードがありますが、ステレオ ディレイ、ピンポン ディレイ、スラップバック ディレイなど、さまざまな遅延があります。幸いなことに、これらはすべて、ネイティブ エフェクト ノードとある程度の想像力を駆使してウェブ オーディオで作成できます。

ネイティブ ノードと独自のカスタム エフェクトを可能な限り透過的に使用できるようにするため、これを実現できるラッパー形式を作成する必要がありました。ウェブ オーディオのネイティブ ノードは、connect メソッドを使用してノードをリンクするため、この動作をエミュレートする必要がありました。基本的な考え方は次のようになります。

var MyCustomNode = function(){
    this.input = audioContext.createGain();
    var output = audioContext.createGain();

    this.connect = function(target){
       output.connect(target);
    };
};

このパターンでは、ネイティブ ノードにかなり近づきます。これをどのように使用するか見てみましょう。

//create a couple of native nodes and our custom node
var gain = audioContext.createGain(),
    customNode = new MyCustomNode(),
    anotherGain = audioContext.createGain();

//connect our custom node to the native nodes and send to the output
gain.connect(customNode.input);
customNode.connect(anotherGain);
anotherGain.connect(audioContext.destination);
カスタムノードのルーティング

カスタムノードとネイティブ ノードの唯一の違いは、カスタムノードの入力プロパティに接続する必要があることです。これを回避する方法があることは確かですが、私たちの目的にはこれで十分です。このパターンをさらに開発して、ネイティブ AudioNode の切断方法をシミュレートしたり、接続時のユーザー定義の入出力に対応したりすることもできます。ネイティブ ノードでできることについては、仕様をご覧ください。

カスタム効果を作成するための基本的なパターンがわかったので、次のステップでは、実際にカスタムノードにカスタムの動作を指定します。スラップバック遅延ノードを見てみましょう。

意図どおりのスラップバック

スラップバック ディレイはスラップバック エコーとも呼ばれ、50 年代スタイルのボーカルからサーフギターまで、さまざまな楽器で使用される古典的なエフェクトです。このエフェクトは、着信したサウンドを取得して、約 75 ~ 250 ミリ秒のわずかな遅延でサウンドのコピーを再生します。これにより、音が跳ね上がったような感じになります。次のような効果を作成できます。

var SlapbackDelayNode = function(){
    //create the nodes we'll use
    this.input = audioContext.createGain();
    var output = audioContext.createGain(),
        delay = audioContext.createDelay(),
        feedback = audioContext.createGain(),
        wetLevel = audioContext.createGain();

    //set some decent values
    delay.delayTime.value = 0.15; //150 ms delay
    feedback.gain.value = 0.25;
    wetLevel.gain.value = 0.25;

    //set up the routing
    this.input.connect(delay);
    this.input.connect(output);
    delay.connect(feedback);
    delay.connect(wetLevel);
    feedback.connect(delay);
    wetLevel.connect(output);

    this.connect = function(target){
       output.connect(target);
    };
};
slapback ノードの内部ルーティング

すでにご存じの方もいらっしゃるかもしれませんが、このディレイを大きなディレイタイムでも使用すると、フィードバックを伴う通常のモノディレイになります。次に、この遅延を使用して音を聴く例を示します。

オーディオのルーティング

プロフェッショナル オーディオ アプリケーションでさまざまな楽器や音楽パートを操作する場合、効果的にサウンドのミキシングやモジュレーションを行える柔軟なルーティング システムが必要です。JAM with Chrome では、物理的なミキシング ボードに搭載されているものと同様のオーディオ バスシステムを開発しました。これにより、リバーブ効果が必要なすべてのインストゥルメントを共通のバス(チャンネル)に接続し、そのバスにリバーブを追加できます。各インストゥルメントにリバーブを追加する必要はありません。これは重要な最適化です。より複雑なアプリケーションを作成し始めたら、すぐに同様の処理を行うことを強くおすすめします。

AudioBus のルーティング

これはウェブ オーディオで簡単に行えます。基本的に、エフェクトに対して定義したスケルトンを使用して、同じように使用できます。

var AudioBus = function(){
    this.input = audioContext.createGain();
    var output = audioContext.createGain();

    //create effect nodes (Convolver and Equalizer are other custom effects from the library presented at the end of the article)
    var delay = new SlapbackDelayNode(),
        convolver = new tuna.Convolver(),
        equalizer = new tuna.Equalizer();

    //route 'em
    //equalizer -> delay -> convolver
    this.input.connect(equalizer);
    equalizer.connect(delay.input);
    delay.connect(convolver);
    convolver.connect(output);

    this.connect = function(target){
       output.connect(target);
    };
};

これは、次のように使用されます。

//create some native oscillators and our custom audio bus
var bus = new AudioBus(),
    instrument1 = audioContext.createOscillator(),
    instrument2 = audioContext.createOscillator(),
    instrument3 = audioContext.createOscillator();

//connect our instruments to the same bus
instrument1.connect(bus.input);
instrument2.connect(bus.input);
instrument3.connect(bus.input);
bus.connect(audioContext.destination);

こうして、ディレイ、イコライゼーション、リバーブ(パフォーマンスの面ではかなり高価なエフェクト)を、個別のインストゥルメントに適用した場合の半分の費用で適用できました。バスにスパイスを追加したい場合は、2 つの新しいゲイン ノード(preGain と post ゲイン)を追加できます。これにより、2 つの異なる方法でバス内のサウンドをオフまたはフェードできます。preGen はエフェクトの前に置かれ、post ゲインはチェーンの最後に配置されます。pre ゲインをフェードすると、ゲインが一番下になった後もエフェクトは共鳴しますが、post ゲインをフェードするとすべてのサウンドが同時にミュートされます。

ここからどこへ行く?

ここで説明した手法は、開発の余地があり、開発する必要があります。カスタムノードの入出力、connect メソッドなどは、プロトタイプ ベースの継承を使用して実装できます(また、実装する必要があります)。バスはエフェクトのリストを渡すことで、動的にエフェクトを作成できる必要があります。

Chrome での JAM のリリースを記念して、エフェクトのフレームワークをオープンソースにすることにしました。この簡単な紹介にご興味がおありでしたら、ぜひご覧になり、ご投稿ください。カスタムのウェブ オーディオ エンティティの形式の標準化については、こちらで議論されています。参加しましょう