StereoPannerNode の shim をつくった
なんかカンファレンスとかあって一部で非常に盛り上がっているっぽいウェブオーディオ界隈ですが、そろそろ Chrome あたりに StereoPannerNode というのがやってきそうな気配があります。
今のウェブオーディオで音の定位を扱うには PannerNode というのがあるのですが、なぜか3D空間に音を配置するみたいな超高級な仕様で、パンといえばミキサーについているつまみくらいの感覚の一般市民には使いにくかったりします。
で、新しくステレオ定位を扱う StereoPannerNode というのが検討されています。
http://webaudio.github.io/web-audio-api/#the-stereopannernode-interface
interface StereoPannerNode : AudioNode { readonly attribute AudioParam pan; };
見てのとおりインターフェースは単純で、pan 属性があるのみ。-1 で左、0 で真ん中、+1 で右という按配です。 なるほど分かった簡単だし便利そうだ。ですが、代替できる仕組みがないとすべてのブラウザに実装されるまで使いたくないというのもあるので、今のウェブオーディオだけで作ってみた。
使い方
<script src="/path/to/stereo-panner-shim.js"></script>
これで AudioContext.prototype.createStereoPanner が使えるようになります。
var audioContext = new AudioContext(); var audioElement = document.getElementById("audioElement"); var mediaSource = audioContext.createMediaElementSource(audioElement); var autoPanRate = audioContext.createOscillator(); var autoPanAmount = audioContext.createGain(); var stereoPanner = audioContext.createStereoPanner(); // これが新しく使える autoPanRate.frequency.value = 0.05; autoPanRate.start(audioContext.currentTime); mediaSource.connect(stereoPanner); // 普通に接続して使います autoPanRate.connect(autoPanAmount); autoPanAmount.connect(stereoPanner.pan); stereoPanner.connect(audioContext.destination);
仕組み
既存のオーディオノードをごちゃごちゃ組み合わせてやっています。詳細はここの README を参照してください。
JSタイマー三種盛りとそのインターフェース
最近タイマーAPIを書くブームが来ていて、とりあえず3つほど書いたので順番に紹介していきます。
ブラウザのタイマーはタブの裏に入ると精度が悪くなる。それを解消する方法として WebWorker でタイマーを作動させてメッセージ送信→コールバック関数を実行するやり方がある。ウェブオーディオとか見えなくてもちゃんと処理してほしい時に使える。
setInterval で 100 を指定すると、だいたい 100ms 単位でコールバック関数が呼ばれるのだけど、これは 100ms ± 10% くらいのアバウトな間隔でコールバックされるやつ。"酔う"感じとかは設定で変えられる。
sinon の FakeTimer などのように、手動で時間を進められるやつ。テストに使う。
いずれもネイティブと同じインターフェースで setInterval
とか setTimeout
を呼んで使う。例えば、n回タイマー処理をしたい場合、以下のようなインターフェースにしておくと、状況に応じてタイマーを切り替えられるという算段です。
var nTimesInterval = (n, callback, delay, timerAPI = global) => { var count = 0; var timerId = timerAPI.setInterval(() => { callback(count++); if (count >= n) { timerAPI.clearInterval(timerId); } }, delay); }; // 普通のとき nTimesInterval(10, callback, 100); // バックグラウンドでも絶対に実行したいとき nTimesInterval(10, callback, 100, WorkerTimer); // ぐだぐだな感じにしたいとき nTimesInterval(10, callback, 100, DrunkTimer); // テストのとき nTimesInterval(10, callback, 100, TickableTimer);
APIを引数で受け取ると良いというのは前回書いた記事と同じ。
新しいタイマーを作るぞ!というときにオブジェクティブな感じで new して start / stop みたいなインターフェースにするのではなくて、ネイティブのに合わせておくと使用性が上がる。
具体的には関数名を同じにするのと、出来たらメソッドでなくて関数にする。例えば乱数を生成するのは Math.random
と同じように random
という名前でアクセスできて、fn = foo.random; fn()
ができた方が良い。
乱数を使う関数のテスト
function coin(x) { return Math.random() < x; }
こういう関数があったとして、どうテストを書くのか。
ぱっと思いつくのはこういう感じで、Math.random
自体を上書きするやり方。
describe("coin(x)", function() { it("works", sinon.test(function() { this.stub(Math, "random", function() { return 0.5; }); assert(coin(0.4) === false); assert(coin(0.5) === false); assert(coin(0.6) === true); })); });
このやり方、たしかに間違っていないんだけど、関数のインターフェース自体を変更した方が良いと思う。
function coin(x, random) { random = random || Math.random; return random() < x; }
これだとテストがしやすいだけじゃなく、偏った乱数生成器とかシード付きの乱数生成器を使用したりできて便利さ100万倍になる。
describe("coin(x)", function() { it("works", function() { var random = function() { return 0.5; }; assert(coin(0.4, random) === false); assert(coin(0.5, random) === false); assert(coin(0.6, random) === true); }); });
参照
JavaScript の this が分からない
ライブラリをES6で書いて公開する所から始めよう | Web Scratch
この記事を読んで、よっしゃ僕も練習がてらライブラリのES6化するぞ!!と思って、とりあえず既存ライブラリのテストだけでもES6化しようと思って脳内コーディングをしたところ、ライブラリ作者にとって Arrow Function が意外と厄介なんじゃないかと感じた。
(実際に試したわけではないので、全体的に勘違いしている可能性があります)
例えば、ちょっと雑いけど以下のようなテストコード。
describe("test", function() { it("Math.random", sinon.test(function() { // <- test sandbox this.stub(Math, "random", function() { return 0.5; }); assert(Math.random() === 0.5); })); // sinon.test を使えばテストが失敗しても書き換えたスタブは元に戻る });
sinon の sandbox は this を書き変えるので Arrow Function (this が外側のスコープに束縛される) は使えない。結果 function
と =>
が混在して嫌な感じがする。デンジャラスメル。
describe("test", ()=>{ it("Math.random", sinon.test(function() { // <- Arrow Function が使えない this.stub(Math, "random", ()=>{ return 0.5; }); assert(Math.random() === 0.5); })); });
これの対処は以下のどちらかになると思うのだけど。まあ後者だろうなぁと思う。
- ライブラリ使用者が Function式 と Arrow Function を使い分ける
- ライブラリ作者が
call
とかapply
を使わない
sinon のインターフェースを自分なりに修正するならこんな感じにすると思う。this に頼らず、それ相当のインターフェースを引数として別途提供するみたいな感じ。
describe("test", ()=>{ it("Math.random", sinon.test((sinon)=>{ sinon.stub(Math, "random", ()=>{ return 0.5; }); assert(Math.random() === 0.5); })); });
っていうか CoffeeScript の ->
みたいの、なんでないのだろう?
OSCを共有する osc-hub というのを作った
openFrameworks や Max/MSP, SuperCollider などで良く使われるメッセージングプロトコルに OSC (Open Sound Control) というのがあって、どんなものかについては この辺 を参照して欲しいのだけど、通信プロトコルにUDPが使われていて、基本的には A -> B というように一対一でしかメッセージを送信できない*1。
そこでOSCを中継するハブのようなものがあれば、複数端末にメッセージ送信ができて便利そうなので作ってみた。頑張って作った構成図とGIFアニメが以下。
使い方
具体的なコマンド等は README を参照してもらうとして、大雑把な流れを示す。
- あらかじめ OscHubServer を立ち上げる
- 各クライアントで OscHubClient を立ち上げて OscHubServer に接続
- Max/MSP とかで OscHubClient に設定したポートでOSCを送受信する
用途
インターネット越しに同じMaxパッチを共有して、スクリーン共有みたいな感じでOSCだけを共有して動作を伝えるとか、まったく別々のパッチを同じコントローラで制御するとかに使えると思う。たぶん便利なのでアイデア次第でいろいろ面白いことできそうだけど、便利すぎるのですでに同じようなのがあるのかも知れない。
留意点
ネットワークを介するのでレイテンシーが発生する。
*1:ブロードキャストアドレスを使えば、同一ネットワーク内の全端末に送信できる
ウェブオーディオが使われているライブの様子を見てきた
楽×学(ラクガク)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 );