ウェブオーディオが使われているライブの様子を見てきた
楽×学(ラクガク)2014~私の「やる!」を興すために~ を開催します/浜松市
浜松で「楽×学 2014~私の「やる!」を興すために~」というイベントが開催されて、その最期を飾る SjQ ライブパフォーマンスでウェブオーディオが使われていたので様子を見に行った。
さらに今回はプログラマーmohayonao氏をゲストに迎え、cellがオンライン上に生成したサウンドをお客さんのスマホからアウトプットするシステムを開発。SjQの演奏中にcellの電子音がスマホに降り注ぎ、見事なサウンドスケープを描き出しました(スタッフa)
— SjQ++ (@sjqpp) 2014, 11月 30
SjQというのはこういう感じ。
【SjQ++】生演奏とコンピュータを組み合わせ、新しい即興音楽を生み出すバンド - PICK UP | KAKEHASHI
前回 、Cell というインスタレーションを手伝った流れで、そのシステムをライブ仕様に調整して使用することになった。インスタレーションでは、Cell という人工生命デバイスが環境と自身に反応しながらドローンを生成、その合間合間に音の素材をドロップする構造で、現地の音響空間を楽しむ他に、その生成された音をブラウザ経由で聴けるようになっていたのだけど、今回のライブは人工生命デバイスと人間の演奏が相互に反応しながら、生成された音の素材をお客さんのスマートフォンに送信して、演奏に合わせて光り、そして音が鳴るという仕組みを構築した。
仕組み
おおまかな構成はこういう感じ。
+-----+ +--------------+ +------------------+ | SjQ | - OSC -> | SocketServer | - WiFi -> | MobileDevice * N | +-----+ +--------------+ +------------------+
僕は SocketServer と MobileDevice のプログラムを書いた。SocketServer は SjQ のシステムから受信したOSCメッセージをJSONに変換してブラウザに送るだけで、ブラウザは受信したメッセージに応じて、音を出したり(Web Audio API)、画面を光らせたりする(Canvas)。 音を出す部分は以前に書いたものを使用したので、実際にライブの仕様に合わせて書いたのはサーバー側、クライアント側あわせて 500 行程度。前日のスタジオ練習で雰囲気を確認していったん寝た翌朝、当日の朝に全部書き直す開発手法(development in sleeping)を採用した。
以下は抜粋したコード。
// server.js var express = require("express"); var oscmin = require("osc-min"); var udp = require("dgram"); var http = require("http"); var socketIO = require("socket.io"); var app = express(); var server = http.createServer(app); var io = socketIO(server); app.use(express.static(__dirname + "/public")); server.listen(SERVER_PORT, function() { console.log("Listening on port %d", server.address().port); }); var numUsers = 0; var prevOsc = {}; // お客さんの端末とのソケット接続 io.on("connection", function(socket) { numUsers += 1; console.log("connected: " + numUsers); // 接続数に応じて制御の調整を行う io.emit("numUsers", { count: numUsers, osc: prevOsc }); socket.on("disconnect", function() { numUsers -= 1; console.log("disconnected: " + numUsers); io.emit("numUsers", { count: numUsers, osc: prevOsc }); }); // お客さんの全端末を強制的に音を鳴らす socket.on("fire", function(data) { io.emit("fire", data); console.log("fire!"); }); }); // SjQのシステムからOSCを受信 udp.createSocket("udp4", function(data) { var osc = oscmin.fromBuffer(data); osc.args = osc.args.map(function(x) { return x.value; }); // お客さんの端末に送信 io.emit("osc", osc); prevOsc = osc; console.log(JSON.stringify(osc)); }).bind(OSC_PORT);
// client.js var socket = io(); var app = new CellLiveApp(); socket.on("osc", function(osc) { app.onReceiveOsc(osc); }); socket.on("numUsers", function(data) { app.numUsers = data.count; app.setOsc(data.osc); }); socket.on("fire", function() { app.fire(); });
ライブの様子
50分弱のパフォーマンスの間、サーバーに接続があったのは最大時で50台、平均して35台程度で演奏やプロジェクターで投影している映像に合わせて周りを取り囲むお客さんの端末がチカチカ光る。演奏がスパースになった瞬間にスマートフォンからの音が入り込むなど絶妙な融合ができて、パフォーマンスとそれを鑑賞するお客さんという区別が溶けて全員が当事者であるみたいな一体感というか緊張感があった。自分のiPhoneもサーバーに接続していたのだけど、自分の所有物である端末に演奏のフィードバックがダイレクトに伝わってくる感覚は非常に刺激的だった。当初、お客さん全員の端末が一気に爆発したらどうしようみたいな心配をしていたのだけど、爆発した端末がなかったのもよかった。
ウェブオーディオとアート/音楽
以前、自分で書いた内容の引用だけど。目の前で確認したことで、より強く感じた。
現地で生成される音をネットを介して体験することができるのはかなり面白くて、応用方法を色々考えるとアートや音楽と体験者との距離感の新しい選択肢になりえそう。 音楽系のメディアアートだとMax/MSPとかSuperColliderとかが良く使わていて、そういう蓄積のある技術に比べると Web Audio API はまだまだ見劣りする感じだけど、ブラウザがあれば*1専用のアプリケーションやデバイスを用意することなく誰でも手軽に体験を共有できるという点で重要なポジションを担えるんじゃないかなと思います。
今後の課題
いくつか反省点もあったのだけど、書くのが面倒になってきたので箇条書き。
FunctionからWebWorkerを作るやつ作った
今まで Worker を作るときは
- worker用のファイルを作る
- 普通のやりかた
- 1つのファイルで main と worker 用のコードを切り分ける
- ファイルたくさん作りたくないとき
- 文字列で worker の中身を記述して Blob 経由で URL を生成する
- Workerの規模が小さいとき
みたいにしていたけど、3番目のやり方をもうちょっと便利にした。
使い方
バックグラウンドで動くタイマー (タブを切り替えても精度が落ちないやつ)
var timer = new WorkerBuilder(function(onmessage, postMessage) { // ここが worker の中身 // 引数は無視されるのでなくても良い (lint対策) var t = 0; onmessage = function(e) { if (t) { clearInterval(t); } t = 0; if (typeof e.data === "number" && e.data > 0) { t = setInterval(function() { postMessage(null); }, e.data); } }; }).build(); // build() で Worker を生成する // wb.createURL() で URL だけ取得することもできる // あとは普通の Worker として使える timer.onmessage = function() { console.log("!"); }; timer.postMessage(1000);
仕組み
単純に関数を文字列化して中身を取り出す -> Blob -> URL.createURL しているだけです。
用途
Web Audio APIに将来的に導入される AudioWorkerNode はノード毎にファイルを用意させるみたいな仕様っぽいので、そんな面倒なこと絶対にしたくない気持ちで作った。動かせる環境がないので確認できないけど以下のように使えると思う。
var bitcrusher_worker = new WorkerBuilder(function(onaudioprocess) { var phaser = 0; var lastDataValue = 0; onaudioprocess = function (e) { for (var channel = 0; channel < e.inputBuffers.length; channel++) { var inputBuffer = e.inputBuffers[channel]; var outputBuffer = e.outputBuffers[channel]; var bufferLength = inputBuffer.length; var bitsArray = e.parameters.bits; var frequencyReductionArray = e.parameters.frequencyReduction; for (var i = 0; i < bufferLength; i++) { var bits = bitsArray ? bitsArray[i] : 8; var frequencyReduction = frequencyReductionArray ? frequencyReductionArray[i] : 0.5; var step = Math.pow(1 / 2, bits); phaser += frequencyReduction; if (phaser >= 1.0) { phaser -= 1.0; lastDataValue = step * Math.floor(inputBuffer[i] / step + 0.5); } outputBuffer[i] = lastDataValue; } } }; }); var bitcrusherNode = audioContext.createAudioWorker(bitcrusher_worker.createURL(), 1, 1); // Custom parameter - number of bits to crush down to - default 8 bitcrusherNode.addParameter( "bits", 8 ); // Custom parameter - frequency reduction, 0-1, default 0.5 bitcrusherNode.addParameter( "frequencyReduction", 0.5 );
クソコードは世界を救う
以前に書いた一部のマニアには垂涎もののウェブアプリがFirefoxで動いていなかったので修正した。簡単に内容を説明するとWeb Audio APIのAudioParamの遷移をグラフ化するやつです。
グラフの値を取得するにあたって 1 だけを出力する AudioBufferSource を GainNode に接続して使っていたのだけど、どうもバッファのデータが反映されていないらしく、 0 を出力 → GainNode を通しても 0 のままとなっていたっぽい。
+--------------------+ | AudioBufferSource | * 常に 1 を出力 | - buffer: [ 1, 1 ] | +--------------------+ | +-----------+ | GainNode | * 入力と x を積算して出力 | - gain: x | = AudioParam の値の遷移が分かる +-----------+ | +--------------------------+ | audioContext.destination | +--------------------------+
修正前 (WebKit系なら動くけどFirefoxでは動いてなかった)
bufSrc = audioContext.createBufferSource() bufSrc.buffer = audioContext.createBuffer(1, 2, 44100) bufSrc.buffer.getChannelData(0).set [ 1, 1 ] bufSrc.loop = true
修正後 (コメントと1行追加)
bufSrc = audioContext.createBufferSource() bufSrc.buffer = audioContext.createBuffer(1, 2, 44100) bufSrc.buffer.getChannelData(0).set [ 1, 1 ] # 不安だからもう一度 bufSrc.buffer = bufSrc.buffer bufSrc.loop = true
どうも bufSrc.buffer
に AudioBuffer が代入された時点でバッファの内容がコピーされて、それ以降の変更がC++側に無視されている気がする。ので、再代入することで変更を反映させた。JITコンパイラが賢くなると再代入の行自体が無視されてやっぱり動かなくなるかも知れない。
この変が該当箇所だと思うけど良く分からなかった。
- gecko-dev/AudioBufferSourceNode.cpp at master · mozilla/gecko-dev · GitHub
- gecko-dev/nsRefPtr.h at master · mozilla/gecko-dev · GitHub
次回のクソコード選手権にエントリーして、散々馬鹿にされたいと思います。
ウェブオーディオが使われているインスタレーションの様子を見てきた
今、奈良で 町家の芸術祭 はならぁと というイベントが開催されていて、そこで展示されているインスタレーション作品にウェブオーディオが使われているので様子を見に行った。SjQの魚住勇太さんの作品で詳しくはここに書いてある。
http://yutauozumi.com/cell/details/
時計仕掛けの生命をイメージして設置された装置。装置は部屋の気温や光といった環境と反応しながら、音を紡ぎ、空間を満たして行く。その動きや音は、刻々と変化していく。装置の内部にはいくつかのアームが設置されている。それらのひとつひとつが、単なる部品ではなく、個体として、互いに反応する動きを見せる。その様子は、分ごと、時間ごと、日ごとに、変化する。それは常時、音に変換されることで、一つの流れをもったサウンドスケープとしてリアルタイムで奏でられる。
会場は複数の人工生命デバイスがカチャカチャと反応しながら音楽を作り出す空間になっていて、人工生命デバイス自身の状態や空間の環境を元に音楽パラメーターを生成して、それをサーバー経由のブラウザでリアルタイムに処理してサウンド化しているということなので相当にコンピューティングなことをしているのだけど、現地ではそう見えなくて機械と生物が曖昧に溶け合うというような感じで心地よかった。音自体は以下のサイトで現地で鳴っているものがリアルタイムに聴けて充分楽しめるのだけど、やっぱり音は場であるし、実際に人工生命デバイスが音楽を生成している現場というのは緊張感があって断然良いので、みなさん現地まで見に行けば良いと思います。
- "Cell" 環境や自身と反応し音を織り続ける装置
- 演奏時間 10:00-17:00
奈良は遠いという方はこのサイトを上手く使うと簡単に来られます。
語り
ウェブオーディオ部分を担当したエンジニアの人によると、作家さんからMax/MSP等で綿密に作り込まれた音のイメージを伝えられて、そのイメージを壊すことなくWeb Audio APIで再現させることが求められたけど、Web Audio APIだけではやりたいことがなかなかできなくて結構ガツガツ書いたということで、うわー、そうとう辛そうだなと思ったし、実際辛かったし、体重が3kgくらい増えたけど、疲れた時のハッピーターン250%は格別です。
という感じで、フロントの部分は僕が書かせてもらった。事前にたまたま neume.js というウェブオーディオライブラリを作っていたので、それを使っている。
ただ、ライブラリは早い段階での試作には強力でも、要求に合わせてキチキチにチューニングしようとすると拡張用コードを結構書く必要があったり、チューニング作業はWeb Audio APIの仕様とWebKitの実装とライブラリの実装とアプリケーションの性質とやりたいことのバランスを考える必要があったりと、まだまだノウハウが足りないのを実感した。夢の中でウェブオーディオ仙人みたいな人に弟子入りして色々教えてもらいながらどうにか完成させたという感じ。このあたりは色々あるのでそのうち書けたら書きたい。
ウェブオーディオとアート/音楽
現地で生成される音をネットを介して体験することができるのはかなり面白くて、応用方法を色々考えるとアートや音楽と体験者との距離感の新しい選択肢になりえそう。 音楽系のメディアアートだとMax/MSPとかSuperColliderとかが良く使わていて、そういう蓄積のある技術に比べると Web Audio API はまだまだ見劣りする感じだけど、ブラウザがあれば*1専用のアプリケーションやデバイスを用意することなく誰でも手軽に体験を共有できるという点で重要なポジションを担えるんじゃないかなと思います。
ところでカレーの情報です
会場の近くのカフェ、居心地もカレーも最高だったので合わせて寄りたいところです。
オーディオファイルの波形を表示するやつ
ドラムの音素材とかコンボリューションリバーブに使う Impulse Response データとかの波形をちょっと確認したいとき用に作った。そういう音は立ち上がりに特徴があるので先頭部分だけを拡大したり振幅を拡大したりできるようにした。無意味に拡大して模様っぽくして楽しむこともできる。
Web Audio API を駆使して Firefox かどうか調べる
if (typeof window.PannerNode !== "undefined") { alert("お使いのブラウザは Firefox ですね!!"); } else { alert("お使いのブラウザは Firefox ではありません!!"); }
おまけ: PannerNode のインスタンスかどうか調べる
function isPannerNode(node) { if (typeof window.PannerNode === "function") { return node instanceof window.PannerNode; } if (node instanceof window.AudioNode) { return node instanceof node.context.createPanner().constructor; } return false; }
AudioParam Viewer
Web Audio API の AudioParam の値を可視化できるやつを作った。
コードを書いて実行(eval)すると30秒分の値の遷移を記録してグラフ表示します。param
というのが操作する AudioParam のインスタンスで、Ctrl+O
か Ctrl+Space
で操作関数を補完できます。
仕組み
Web Audio API はリアルタイムでオーディオ処理をするためのAPIだと思われがちだけど、OfflineAudioContext というのを使えば静的なオーディオ処理もできます。以下は簡単な使い方。レンダリング用のインターフェースがあるだけで基本的には AudioContext と同じように使えます。これを使うと30秒の処理を一瞬で行うことができるので、それを使って常に 1 を出力する AudioBufferSource とパラメータ操作用の GainNode を接続し、レンダリング結果をグラフ化しています。
// (1) OfflineAudioContext を作る var context = new OfflineAudioContext(numOfChannels, length, sampleRate); context.oncomplete = function(e) { // (4) 処理の結果 e.renderedBuffer; // AudioBuffer }; // (2) ここで処理をする(同期処理) audioProcessing(context); // (3) レンダリング開始 context.startRendering();
AudioContext と OfflineAudioContext の違い
AudioContext と OfflineAudioContext の大きな違いは処理を行うタイミングにあります。良くある AudioContext のアプリケーションでは setInterval 等を使って数ミリ秒単位で少しずつ行いますが、OfflineAudioContext の場合は一気に処理を行う必要があります。また、複数トラックを扱うようなアプリケーションを書くときに AudioContext の場合はすべてのトラックをリアルタイムに並列で処理する必要がありますが、OfflineAudioContext の場合はリアルタイムでないので各トラックずつ順番にといったように直列に処理することができます。そして、AudioContext ではボタンクリック等のイベントを介して今すぐ音を鳴らすというような書き方ができるのに対して、OfflineAudioContext はイベントを介した操作はまずできなくて、いつ音を出して、いつ音を止めるか、いつパラメーターをどう変化させるかを厳密に指定する必要があります。
お前それ OfflineAudioContext でも同じこと言えんの?
上記のように時間にシビアな OfflineAudioContext では osc.start(0)
で今すぐ音を出すみたいな書き方や、一定時間経過後にオシレーターの周波数を変えたいので、それっぽいタイミングで osc.frequency.value = newValue
をするというような使い方はできません。シンセサイザーアプリのようにユーザーイベントがきっかけになるような場合は仕方がないケースもありますが、それでもきっちりと時間を指定して osc.start(context.currentTime)
や osc.frequency.setValueAtTime(newValue, startTime)
のように書いておいた方が後々に機能の追加や、コードの流用などの点で有利になります*1。OfflineAudioContext を制するものは Web Audio API を制するといっても過言ではないのです。
ということで練習です
AudioParam Viewer は OfflineAudioContext を使っているので厳密に時間を指定する方法で書く必要がありますが、逆にこれを使いこなして思い通りのグラフが書けるようになると AudioContext でもかっちりしたコードが書けるようになります。以下、適当に練習問題を考えてみましたので、どうぞお試しください。
ヒント: timeConstant = 0.1
答え
ヒント: var a = 0.2, d = 0.1, s = 0.4, r = 0.2, keyOff = 0.8;
答え
keyOff = 0.1
とかしても適切なカーブを描かせる
答え
*1:例外として変化しないパラメーターは param.value で直接代入したほうが良いです