Technitone.com は、WebGL、Canvas、Web ソケット、CSS3、JavaScript、Flash、Chrome の新しい Web Audio API を融合させたものです。
この記事では、制作のあらゆる側面(計画、サーバー、サウンド、ビジュアル、インタラクティブなデザインに活用するワークフローなど)について説明します。ほとんどのセクションには、コード スニペット、デモ、ダウンロードが含まれています。記事の最後に、これらをすべて 1 つの ZIP ファイルとしてダウンロードできるリンクがあります。
ギグ
gskinner.com のスタッフは音響エンジニアではありませんが、課題があれば解決策を考えます。
- Andre の ToneMatrix に「インスパイアされた」グリッドに音をプロットする
- トーンには、サンプリングされた楽器、ドラムキット、さらにはユーザーの独自の録音を接続できます。
- 複数の接続済みユーザーが同じグリッドで同時にプレイする
- または、ソロモードに切り替えて自分で探索することもできます。
- 招待制セッションでは、バンドを結成して即興のジャムセッションを楽しむことができます。
ユーザーは、音色にオーディオ フィルタとエフェクトを適用するツールパネルを使用して、Web Audio API を試すことができます。
また、以下も行っています。
- ユーザーの合成とエフェクトをデータとして保存し、クライアント間で同期する
- 色のオプションをいくつか用意して、かっこいい曲を描画できるようにする
- ギャラリーを用意して、他のユーザーの作品を視聴、高評価、編集できるようにする
使い慣れたグリッドのメタファーを維持し、3D 空間に浮かせて、ライティング、テクスチャ、パーティクル エフェクトを追加し、柔軟な(または全画面)CSS と JS ドリブンのインターフェースに収めました。
ドライブ旅行
楽器、エフェクト、グリッドのデータはクライアントで統合され、シリアル化された後、カスタム Node.js バックエンドに送信され、Socket.io のように複数のユーザーに対して解決されます。このデータは、各プレーヤーの貢献を含めてクライアントに送り返され、マルチユーザー再生中の UI、サンプル、エフェクトのレンダリングを担当する相対 CSS、WebGL、WebAudio レイヤに分散されます。
ソケットとのリアルタイム通信は、クライアント側の JavaScript とサーバー側の JavaScript にフィードされます。
Google では、サーバーのあらゆる側面で Node を使用しています。静的ウェブサーバーとソケット サーバーが 1 つに統合されています。最終的に使用したのは Express です。これは、Node で完全に構築された完全なウェブサーバーです。非常にスケーラブルでカスタマイズ性が高く、低レベルのサーバー処理を自動的に行います(Apache や Windows Server と同様に)。デベロッパーは、アプリケーションの構築に集中できます。
マルチユーザー デモ(実際はスクリーンショットです)
このデモは Node サーバーから実行する必要がありますが、この記事では Node サーバーを使用していません。そのため、Node.js をインストールしてウェブサーバーを構成し、ローカルで実行した後のデモのスクリーンショットを掲載しています。新しいユーザーがデモ インストールにアクセスするたびに、新しいグリッドが追加され、全員の作業が相互に表示されます。
Node は簡単です。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 で別のタブに移動した場合に、このような動作がすべて停止するように、Google は真剣に取り組んできました。
ライトショー
グリッドとパーティクル トンネルが前面に表示されます。これは Technitone の WebGL レイヤです。
WebGL は、GPU にプロセッサと連携して動作するようタスクを割り当てることで、ウェブ上でビジュアルをレンダリングする他のほとんどのアプローチよりも優れたパフォーマンスを提供します。パフォーマンスの向上には、学習曲線が非常に急で、開発が大幅に複雑になるというコストが伴います。ただし、ウェブ上のインタラクティブ性にこだわり、パフォーマンスの制限をできるだけ少なくしたい場合は、WebGL がFlash に匹敵するソリューションを提供します。
WebGL デモ
WebGL コンテンツはキャンバス(HTML5 キャンバス)にレンダリングされ、次のコア構成要素で構成されます。
- オブジェクト頂点(ジオメトリ)
- 位置マトリックス(3D 座標)
- シェーダー(GPU に直接リンクされたジオメトリの外観の説明)
- コンテキスト(GPU が参照する要素への「ショートカット」)
- バッファ(コンテキスト データを GPU に渡すためのパイプライン)
- メインコード(目的のインタラクティブに固有のビジネス ロジック)
- draw メソッド(シェーダーを有効にしてキャンバスにピクセルを描画します)
WebGL コンテンツを画面にレンダリングする基本的なプロセスは次のとおりです。
- パースペクティブ マトリックスを設定します(3D 空間を覗き込むカメラの設定を調整し、画像面を定義します)。
- 位置行列を設定します(位置が相対的に測定される 3D 座標の原点を宣言します)。
- バッファにデータ(頂点位置、色、テクスチャなど)を入力して、シェーダーを介してコンテキストに渡します。
- シェーダーを使用してバッファからデータを抽出して整理し、GPU に渡します。
- 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 で作成されています。
最初から、ユーザーがグリッドをできるだけ早く操作できるようにすることを決めていました。スプラッシュ画面、手順、チュートリアルはありません。ただ「Go」と表示されます。インターフェースが読み込まれている場合は、遅くなる原因はありません。
そのため、初めて利用するユーザーをどのようにガイドするかを慎重に検討する必要がありました。WebGL 空間内のユーザーのマウス位置に基づいて CSS カーソル プロパティを変更するなど、微妙な手がかりが追加されています。カーソルがグリッド上にある場合は、手形カーソルに切り替わります(音をプロットして操作できるため)。グリッドの周囲の空白部分にカーソルを合わせると、方向クロスカーソルに切り替わります(グリッドを回転または分割してレイヤにできることを示します)。
番組の準備
LESS(CSS プリプロセッサ)と CodeKit(ウェブ開発の強化版)により、デザイン ファイルをスタブ化された HTML/CSS に変換する時間を大幅に短縮できました。変数、ミックスイン(関数)、さらには数学を活用することで、CSS をより柔軟に整理、記述、最適化できます。
ステージ効果
CSS3 遷移と backbone.js を使用して、アプリケーションに生命感を与え、使用している楽器を視覚的に示す非常にシンプルなエフェクトを作成しました。
Backbone.js を使用すると、色変更イベントをキャッチし、適切な DOM 要素に新しい色を適用できます。GPU アクセラレーションによる CSS3 遷移により、パフォーマンスにほとんど影響を与えることなく、カラースタイルの変更を処理できました。
インターフェース要素の色の変化のほとんどは、背景色の変化によって作成されています。この背景色の上に、背景色が透けて見えるように、戦略的に透明な部分を設けた背景画像を配置します。
HTML: 基盤
このデモでは、ユーザーが選択した 2 つの色域と、3 つ目の混合色域の 3 つの色域が必要でした。この例では、CSS3 遷移をサポートし、HTTP リクエストを最小限に抑えた、考えられる限り最もシンプルな DOM 構造を構築しました。
<!-- Basic HTML Setup -->
<div class="illo color-mixed">
<div class="illo color-primary"></div>
<div class="illo color-secondary"></div>
</div>
CSS: スタイルを使用したシンプルな構造
絶対配置を使用して各地域を正しい位置に配置し、background-position プロパティを調整して、各地域内の背景イラストを配置しました。これにより、すべての領域(それぞれに同じ背景画像を使用)が 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 で検索し、ユーザーが選択した色に基づいて background-color を設定します。クラスを追加して、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 要素の背景色を画像の透明領域に表示できます。
色付きのボックスは、異なる色が接する部分に硬いエッジを作成します。これはリアルな照明効果の妨げになり、イラストをデザインする際の大きな課題の一つでした。
解決策として、色付き領域の端が透明な領域に透けて見えないようにイラストをデザインしました。
ビルドの計画が重要でした。デザイナー、デベロッパー、イラストレーターの間で簡単な計画セッションを実施し、組み立てたときにすべてが連携するように、すべてをどのように構築する必要があるかをチームで理解しました。
レイヤ名付けによって CSS の作成に関する情報を伝達する方法の例として、Photoshop ファイルをご確認ください。
Encore
Chrome を使用しないユーザーについては、アプリのエッセンスを 1 つの静的画像に凝縮することを目標としました。グリッドノードが主役となり、背景タイルにはアプリの目的が示唆され、反射に写るパースペクティブはグリッドの没入型 3D 環境を暗示しています。
Technitone について詳しくは、ブログをご覧ください。
バンド
ここまでお読みいただきありがとうございました。今後、一緒に演奏できる日を楽しみにしています。