事例紹介 - Technitone.com の構築

Sean Middleditch
Sean Middleditch
Technitone — ウェブ オーディオ エクスペリエンス。

Technitone.com は、WebGL、Canvas、Web Sockets、CSS3、JavaScript、Flash、Chrome の新しい Web Audio API が統合されています。

この記事では、計画、サーバー、サウンド、映像、インタラクティブなデザインに使用するワークフローなど、制作のあらゆる側面について取り上げます。ほとんどのセクションには、コード スニペット、デモ、ダウンロードが含まれています。記事の最後にあるダウンロード リンクから、すべてを 1 つの zip ファイルとしてダウンロードできます。

gskinner.com 制作チーム

ギグ

私たち gskinner.com はオーディオ エンジニアというわけではありませんが、ある課題にチャレンジして、次のプランを考えます。

  • ユーザーは、AndreToneMatrix を参考にしてトーンをグリッド上にプロットします。
  • トーンはサンプリングされた楽器、ドラムキット、さらにはユーザーが所有するレコーディングに送られます。
  • 接続された複数のユーザーが同じグリッドで同時にプレイできます
  • ...または、1 人モードで単独で探索できます
  • 招待制のセッションでは、バンドを結成して即席でジャムを楽しめます

音声のフィルタやエフェクトをトーンに適用できるツールパネルを使用して、Web Audio API を体験する機会をユーザーに提供します。

gskinner.com による Technitone

また、次のことを行います。

  • ユーザーの楽曲やエフェクトをデータとして保存し、クライアント間で同期
  • かっこいい曲を描画できるように、色をいくつか用意します。
  • 他の人の作品を聴き、愛し、編集できるギャラリーを提供する

おなじみのグリッド メタファーをそのまま使い、3D 空間にフロート表示し、照明、テクスチャ、粒子効果を加え、柔軟な(または全画面表示の)CSS と JS 駆動型のインターフェースに格納しました。

ドライブ旅行

楽器、エフェクト、グリッドのデータは、クライアント上で統合およびシリアル化された後、カスタムの Node.js バックエンドに送信され、Socket.io で複数のユーザーが解決されます。このデータは各プレーヤーの協力とともにクライアントに送り返されてから、マルチユーザー再生中に UI、サンプル、エフェクトのレンダリングを担当する相対 CSS、WebGL、WebAudio レイヤに分散されます。

ソケットを使用したリアルタイム通信により、クライアントには JavaScript が、サーバーには JavaScript がフィードされます。

Technitone のサーバー図

サーバーのあらゆる側面にノードを使用しています。静的ウェブサーバーとソケットサーバーを 1 つにしたものですExpress は、最終的にノード上に構築された完全なウェブサーバーです。非常にスケーラブルで高度にカスタマイズ可能で、(Apache や Windows Server と同様に)低レベルのサーバー要素を処理します。そうすれば、デベロッパーはアプリケーションの構築に集中できます。

マルチユーザー デモ(スクリーンショットのみ)

このデモは Node.js から実行する必要があります。この記事は Node.js ではないため、Node.js をインストールしてウェブサーバーを構成し、ローカルで実行した後のデモがどう見えるかを示すスクリーンショットを添付しています。新しいユーザーがデモのインストールにアクセスするたびに、新しいグリッドが追加され、全員の作業が相互に表示されるようになります。

Node.js デモのスクリーンショット

ノードは簡単。Socket.io とカスタム POST リクエストを組み合わせることで、同期用の複雑なルーティンを構築する必要がなくなりました。これは、Socket.io が透過的に処理するため、JSON が渡されます。

簡単か?これを見てくれ

3 行の JavaScript で、Express でウェブサーバーが稼働します。

//Tell  our Javascript file we want to use express.
var express = require('express');

//Create our web-server
var server = express.createServer();

//Tell express where to look for our static files.
server.use(express.static(__dirname + '/static/'));

リアルタイムの通信のために socket.io を関連付けるためにさらにいくつか。

var io = require('socket.io').listen(server);
//Start listening for socket commands
io.sockets.on('connection', function (socket) {
    //User is connected, start listening for commands.
    socket.on('someEventFromClient', handleEvent);

});

これで、HTML ページからの受信接続のリッスンを開始できます。

<!-- Socket-io will serve it-self when requested from this url. -->
<script type="text/javascript" src="/socket.io/socket.io.js"></script>

 <!-- Create our socket and connect to the server -->
 var sock = io.connect('http://localhost:8888');
 sock.on("connect", handleConnect);

 function handleConnect() {
    //Send a event to the server.
    sock.emit('someEventFromClient', 'someData');
 }
 ```

## Sound check

A big unknown was the effort entailed with using the Web Audio API. Our initial findings confirmed that [Digital Signal Processing](http://en.wikipedia.org/wiki/Digital_Signal_Processing) (DSP) is very complex, and we were likely in way over our heads. Second realization: [Chris Rogers](http://chromium.googlecode.com/svn/trunk/samples/audio/index.html) has already done the heavy lifting in the API.
Technitone isn't using any really complex math or audioholicism; this functionality is easily accessible to interested developers. We really just needed to brush up on some terminology and [read the docs](https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html). Our advice? Don't skim them. Read them. Start at the top and end at the bottom. They are peppered with diagrams and photos, and it's really cool stuff.

If this is the first you've heard of the Web Audio API, or don't know what it can do, hit up Chris Rogers' [demos](http://chromium.googlecode.com/svn/trunk/samples/audio/index.html). Looking for inspiration? You'll definitely find it there.

### Web Audio API Demo

Load in a sample (sound file)…

```js
/**
 * The XMLHttpRequest allows you to get the load
 * progress of your file download and has a responseType
 * of "arraybuffer" that the Web Audio API uses to
 * create its own AudioBufferNode.
 * Note: the 'true' parameter of request.open makes the
 * request asynchronous - this is required!
 */
var request = new XMLHttpRequest();
request.open("GET", "mySample.mp3", true);
request.responseType = "arraybuffer";
request.onprogress = onRequestProgress; // Progress callback.
request.onload = onRequestLoad; // Complete callback.
request.onerror = onRequestError; // Error callback.
request.onabort = onRequestError; // Abort callback.
request.send();

// Use this context to create nodes, route everything together, etc.
var context = new webkitAudioContext();

// Feed this AudioBuffer into your AudioBufferSourceNode:
var audioBuffer = null;

function onRequestProgress (event) {
    var progress = event.loaded / event.total;
}

function onRequestLoad (event) {
    // The 'true' parameter specifies if you want to mix the sample to mono.
    audioBuffer = context.createBuffer(request.response, true);
}

function onRequestError (event) {
    // An error occurred when trying to load the sound file.
}

モジュラー ルーティングを設定し、

/**
 * Generally you'll want to set up your routing like this:
 * AudioBufferSourceNode > [effect nodes] > CompressorNode > AudioContext.destination
 * Note: nodes are designed to be able to connect to multiple nodes.
 */

// The DynamicsCompressorNode makes the loud parts
// of the sound quieter and quiet parts louder.
var compressorNode = context.createDynamicsCompressor();
compressorNode.connect(context.destination);

// [other effect nodes]

// Create and route the AudioBufferSourceNode when you want to play the sample.

実行時の効果(インパルス応答を使用した畳み込み)を適用...

/**
 * Your routing now looks like this:
 * AudioBufferSourceNode > ConvolverNode > CompressorNode > AudioContext.destination
 */

var convolverNode = context.createConvolver();
convolverNode.connect(compressorNode);
convolverNode.buffer = impulseResponseAudioBuffer;

...別の実行時の効果(遅延)を適用...

/**
 * The delay effect needs some special routing.
 * Unlike most effects, this one takes the sound data out
 * of the flow, reinserts it after a specified time (while
 * looping it back into itself for another iteration).
 * You should add an AudioGainNode to quieten the
 * delayed sound...just so things don't get crazy :)
 *
 * Your routing now looks like this:
 * AudioBufferSourceNode -> ConvolverNode > CompressorNode > AudioContext.destination
 *                       |  ^
 *                       |  |___________________________
 *                       |  v                          |
 *                       -> DelayNode > AudioGainNode _|
 */

var delayGainNode = context.createGainNode();
delayGainNode.gain.value = 0.7; // Quieten the feedback a bit.
delayGainNode.connect(convolverNode);

var delayNode = context.createDelayNode();
delayNode.delayTime = 0.5; // Re-sound every 0.5 seconds.
delayNode.connect(delayGainNode);

delayGainNode.connect(delayNode); // make the loop

...聴けるようにします。

/**
 * Once your routing is set up properly, playing a sound
 * is easy-shmeezy. All you need to do is create an
 * AudioSourceBufferNode, route it, and tell it what time
 * (in seconds relative to the currentTime attribute of
 * the AudioContext) it needs to play the sound.
 *
 * 0 == now!
 * 1 == one second from now.
 * etc...
 */

var sourceNode = context.createBufferSource();
sourceNode.connect(convolverNode);
sourceNode.connect(delayNode);
sourceNode.buffer = audioBuffer;
sourceNode.noteOn(0); // play now!

Technitone で再生するためのアプローチは、スケジュールがすべてです。ビートごとにサウンドを処理するテンポと同じタイマー間隔を設定するのではなく、キュー内のサウンドを管理してスケジュール設定する間隔を短くしました。これにより、API は音声データを解決し、フィルタとエフェクトを処理して、CPU に実際に可聴化の処理を任せることができます。そのビートがようやく到着すると、スピーカーに最終的な結果を届けるために必要な情報がすべて揃っています。

全体として、すべてを最適化する必要がありました。CPU に負担をかけすぎると、スケジュールに遅れずにプロセスをスキップ(ポップ、クリック、傷)する処理がスキップされました。Chrome の別のタブに移動したときに、混乱を招かないように真剣に取り組んでいます。

ライトショー

正面と中央はグリッドと粒子のトンネルです。これは Technitone の WebGL レイヤです。

WebGL は、プロセッサと連携するように GPU にタスクが課されるため、ウェブ上でのビジュアル レンダリングにおいて、他のほとんどのアプローチよりも優れたパフォーマンスを発揮します。このパフォーマンスの向上には、非常に複雑な開発と、習得に要する時間の短縮という代償が伴います。ウェブでのインタラクティブ操作に真剣に取り組んでおり、パフォーマンスの制約をできるだけ少なくしたい場合は、WebGL が Flash に匹敵するソリューションとなります。

WebGL のデモ

WebGL コンテンツはキャンバス(文字どおり HTML5 Canvas)にレンダリングされ、次の主要な構成要素で構成されます。

  • オブジェクトの頂点(ジオメトリ)
  • 位置行列(3 次元座標)
    • シェーダー(ジオメトリの外観の記述。GPU に直接リンク)
    • コンテキスト(GPU が参照する要素への「ショートカット」)
    • バッファ(コンテキスト データを GPU に渡すパイプライン)
    • メインコード(目的のインタラクティブに固有のビジネス ロジック)
    • 「draw」メソッド(シェーダーを有効にしてピクセルをキャンバスに描画)

WebGL コンテンツを画面にレンダリングする基本的なプロセスは次のようになります。

  1. 遠近マトリックスを設定します(3D 空間に突入するカメラの設定を調整して、画面を定義します)。
  2. 位置行列を設定します(位置の測定基準とする 3D 座標の原点を宣言します)。
  3. バッファにデータ(頂点の位置、色、テクスチャなど)を埋めて、シェーダーを介してコンテキストに渡します。
  4. シェーダーを使用してバッファからデータを抽出して整理し、GPU に渡します。
  5. draw メソッドを呼び出して、シェーダーを有効化してデータを実行し、キャンバスを更新するようコンテキストに指示します。

実際の動作は次のようになります。

視点行列を設定...

// Aspect ratio (usually based off the viewport,
// as it can differ from the canvas dimensions).
var aspectRatio = canvas.width / canvas.height;

// Set up the camera view with this matrix.
mat4.perspective(45, aspectRatio, 0.1, 1000.0, pMatrix);

// Adds the camera to the shader. [context = canvas.context]
// This will give it a point to start rendering from.
context.uniformMatrix4fv(shader.pMatrixUniform, 0, pMatrix);

...位置行列を設定...

// This resets the mvMatrix. This will create the origin in world space.
mat4.identity(mvMatrix);

// The mvMatrix will be moved 20 units away from the camera (z-axis).
mat4.translate(mvMatrix, [0,0,-20]);

// Sets the mvMatrix in the shader like we did with the camera matrix.
context.uniformMatrix4fv(shader.mvMatrixUniform, 0, mvMatrix);

...ジオメトリと外観を定義...

// Creates a square with a gradient going from top to bottom.
// The first 3 values are the XYZ position; the last 4 are RGBA.
this.vertices = new Float32Array(28);
this.vertices.set([-2,-2, 0,    0.0, 0.0, 0.7, 1.0,
                   -2, 2, 0,    0.0, 0.4, 0.9, 1.0,
                    2, 2, 0,    0.0, 0.4, 0.9, 1.0,
                    2,-2, 0,    0.0, 0.0, 0.7, 1.0
                  ]);

// Set the order of which the vertices are drawn. Repeating values allows you
// to draw to the same vertex again, saving buffer space and connecting shapes.
this.indices = new Uint16Array(6);
this.indices.set([0,1,2, 0,2,3]);

バッファにデータを入れて、コンテキストに渡します。

// Create a new storage space for the buffer and assign the data in.
context.bindBuffer(context.ARRAY_BUFFER, context.createBuffer());
context.bufferData(context.ARRAY_BUFFER, this.vertices, context.STATIC_DRAW);

// Separate the buffer data into its respective attributes per vertex.
context.vertexAttribPointer(shader.vertexPositionAttribute,3,context.FLOAT,0,28,0);
context.vertexAttribPointer(shader.vertexColorAttribute,4,context.FLOAT,0,28,12);

// Create element array buffer for the index order.
context.bindBuffer(context.ELEMENT_ARRAY_BUFFER, context.createBuffer());
context.bufferData(context.ELEMENT_ARRAY_BUFFER, this.indices, context.STATIC_DRAW);

次に、draw メソッドを呼び出します。

// Draw the triangles based off the order: [0,1,2, 0,2,3].
// Draws two triangles with two shared points (a square).
context.drawElements(context.TRIANGLES, 6, context.UNSIGNED_SHORT, 0);

アルファベースのビジュアルを重ねて表示したくない場合は、フレームごとに必ずキャンバスをクリアしてください。

The Venue

グリッドと粒子のトンネル以外にも、他のすべての UI 要素は HTML / CSS と JavaScript のインタラクティブ ロジックで構築されています。

Google は当初から、ユーザーができるだけ速くグリッドを操作すべきだと考えていました。スプラッシュ画面、手順、チュートリアルはなく、ただ「進む」だけです。インターフェースが読み込まれている場合は、速度を低下させないようにする必要があります。

そのため、初めてのユーザーとのやり取りをどのようにガイドするかを慎重に検討する必要がありました。WebGL 空間内のユーザーのマウス位置に応じて CSS カーソル プロパティを変更するなど、微妙な手がかりも追加しました。カーソルがグリッド上にある場合は、手のカーソルに切り替えます(トーンをプロットすることで操作できるため)。グリッドの周囲の空白部分にカーソルを合わせると、十字カーソル(グリッドを回転させたり、レイヤに展開したりできることを示すため)に入れ替えます。

イベントの準備

LESS(CSS プリプロセッサ)と CodeKit(ステロイドのウェブ開発)により、デザイン ファイルをスタブ化された HTML/CSS に変換するのにかかる時間を大幅に短縮できます。これにより、変数、ミックスイン(関数)、さらには数学まで、より汎用性の高い方法で CSS を整理、記述、最適化できます。

ステージ エフェクト

CSS3 トランジションbackbone.js を使用して、アプリケーションに命を吹き込み、ユーザーが使用している楽器を示すビジュアル キューを提供する非常にシンプルな効果を作成しました。

Technitone の色。

Backbone.js を使用すると、色の変更イベントを捕捉して、新しい色を適切な DOM 要素に適用できます。GPU による高速化の CSS3 遷移は、パフォーマンスにほとんど、あるいはまったく影響を与えずに色スタイルの変更を処理しました。

インターフェース要素の色遷移のほとんどは、背景色の遷移によって作成されていました。この背景色の上に、背景色が透けて見えるように、戦略的に透明な領域を持つ背景画像を配置します。

HTML: 基礎

このデモには 3 つの色領域が必要でした。2 つはユーザーが選択した色領域、もう 1 つは混合色領域です。今回は、CSS3 の遷移をサポートするシンプルな DOM 構造を構築し、HTTP リクエスト数を最小限に抑えました。

<!-- Basic HTML Setup -->
<div class="illo color-mixed">
  <div class="illo color-primary"></div>
  <div class="illo color-secondary"></div>
</div>

CSS: スタイルを使用したシンプルな構造

絶対位置を使用して各領域を正しい場所に配置し、背景位置のプロパティを調整して各領域内に背景のイラストを配置しました。これにより、同じ背景画像を持つすべての領域が 1 つの要素のように見えます。

.illo {
  background: url('../img/illo.png') no-repeat;
  top:        0;
  cursor:     pointer;
}
  .illo.color-primary, .illo.color-secondary {
    position: absolute;
    height:   100%;
  }
  .illo.color-primary {
    width:                350px;
    left:                 0;
    background-position:  top left;
  }
  .illo.color-secondary {
    width:                355px;
    right:                0;
    background-position:  top right;
  }

色の変化イベントをリッスンする GPU アクセラレーションによる遷移が適用されています。時間を長くし、.color-mixed のイージングを変更して、色が混ざるのに時間がかかるような印象を与えました。

/* Apply Transitions To Backgrounds */
.color-primary, .color-secondary {
  -webkit-transition: background .5s linear;
  -moz-transition:    background .5s linear;
  -ms-transition:     background .5s linear;
  -o-transition:      background .5s linear;
}

.color-mixed {
  position:           relative;
  width:              750px;
  height:             600px;
  -webkit-transition: background 1.5s cubic-bezier(.78,0,.53,1);
  -moz-transition:    background 1.5s cubic-bezier(.78,0,.53,1);
  -ms-transition:     background 1.5s cubic-bezier(.78,0,.53,1);
  -o-transition:      background 1.5s cubic-bezier(.78,0,.53,1);
}

現在のブラウザのサポートと、CSS3 移行の推奨使用方法については、HTML5Please をご覧ください。

JavaScript: 仕組み

色を動的に割り当てるのは簡単です。カラークラスを使用して DOM を検索し、ユーザーが選択した色に基づいて背景色を設定します。DOM の任意の要素に遷移効果を適用するには、クラスを追加します。これにより、軽量かつ柔軟でスケーラブルなアーキテクチャが構築されます。

function createPotion() {

    var primaryColor = $('.picker.color-primary > li.selected').css('background-color');
    var secondaryColor = $('.picker.color-secondary > li.selected').css('background-color');
    console.log(primaryColor, secondaryColor);
    $('.illo.color-primary').css('background-color', primaryColor);
    $('.illo.color-secondary').css('background-color', secondaryColor);

    var mixedColor = mixColors (
            parseColor(primaryColor),
            parseColor(secondaryColor)
    );

    $('.color-mixed').css('background-color', mixedColor);
}

プライマリ カラーとセカンダリ カラーを選択したら、混合色の値を計算し、その結果を適切な DOM 要素に割り当てます。

// take our rgb(x,x,x) value and return an array of numeric values
function parseColor(value) {
    return (
            (value = value.match(/(\d+),\s*(\d+),\s*(\d+)/)))
            ? [value[1], value[2], value[3]]
            : [0,0,0];
}

// blend two rgb arrays into a single value
function mixColors(primary, secondary) {

    var r = Math.round( (primary[0] * .5) + (secondary[0] * .5) );
    var g = Math.round( (primary[1] * .5) + (secondary[1] * .5) );
    var b = Math.round( (primary[2] * .5) + (secondary[2] * .5) );

    return 'rgb('+r+', '+g+', '+b+')';
}

HTML/CSS アーキテクチャの説明: 3 つのカラー シフティング ボックスに個性を与える

その目的は、隣接する色領域にコントラストの効いた色を配置しても整合性が保たれる、楽しくリアルな照明効果を作成することでした。

24 ビットの PNG では、画像の透明な領域に HTML 要素の背景色が透けて表示されます。

画像の透明度

色付きのボックスは、さまざまな色の接合部分が硬くなっています。これは、リアルな照明効果の妨げになっていて、イラストをデザインする際の大きな課題の 1 つでした。

色領域

解決策として、色領域の端が透明な領域に透けて見えないようにイラストをデザインしました。

色領域のエッジ

構築の計画は極めて重要でした。デザイナー、デベロッパー、イラストレーターの簡単な計画セッションにより、組み立て時に連携させるために必要なすべてがどのように組み立てられるかについて、チームは把握できました。

CSS の構造に関する情報を伝えるレイヤの命名方法の例として、Photoshop ファイルをご覧ください。

色領域のエッジ

Encore

Chrome を使用していないユーザーのために、アプリケーションの本質を 1 枚の静止画像にまとめることを目標にしています。グリッドノードが主役となり、背景タイルはアプリケーションの目的をほのめかし、リフレクションに含まれる視点がグリッドの臨場感あふれる 3D 環境を示唆しています。

色領域のエッジ。

Technitone について詳しくは、ブログをご覧ください。

バンド

お読みいただきありがとうございました。また近いうちにお会いしましょう