音の鳴るブログ

鳴らないこともある

OSC in the browser

openFrameworks や Max/MSP, SuperCollider などで良く使われるメッセージングプロトコルOSC (Open Sound Control) というのがあって、どんなものかについては この辺 を参照して欲しいのだけど、node-osc-min というライブラリを使えば OSC のメッセージを JavaScript でも解釈できるのでそれで遊ぶ話です。

名前に node と付いているけどバージョン 0.2.0 からブラウザにも対応していて、以下のコマンドでブラウザバージョンをビルドできます。

$ git clone https://github.com/russellmcc/node-osc-min.git
$ cd node-osc-min
$ npm install --dev
$ npm run-script browserify
$ ls build
  osc-min.js  osc-min.min.js

簡単な使い方

osc.toBuffer(json)JSONからOSC形式のバイナリ(Uint8Array)に変換。

uint8 = osc.toBuffer({ address: "/message", args: [ "hello, world" ] });

osc.fromBuffer(uint8) でバイナリからJSONに変換。

JSON.stringify(osc.fromBuffer(uint8), null, 2)
// {
//   "address": "/message",
//   "args": [
//     {
//       "type": "string",
//       "value": "hello, world"
//     }
//   ],
//   "oscType": "message"
// }

こういう感じで OSC形式のバイナリデータとJSONを相互変換できる。JSONの形式はちょっと癖があるのでリポジトリのドキュメントを要参照です。

WebSocketでブラウザと接続

OSC が使えて何が嬉しいのかというと、最初にあげた openFrameworks なんかとブラウザで連携させたいときに便利です。もうちょっと具体的に言うと、いつもは openFrameworks や Max/MSP を使って作品制作をしているんだけど、その出力先として iPhone とかモバイル端末を使ってみたい。でも、JavaScript はあまり良く分からんので、お前ちょっとやってくれや。と言われたときに便利です。 ただし、直接通信させるのは困難なので、今回は WebSocket を介して openFrameworks とブラウザを socket.io 経由で接続する簡単な例を紹介します。順番に openFrameworks, サーバー(CoffeeScript), クライアント(JavaScript) のコードです。

// ofApp.h
#include "ofxOsc.h"

class ofApp : public ofBaseApp {
  // 省略
private:
  ofxOscSender sender;
}

// ofApp.cpp
void ofApp::setup(){
    sender.setup("127.0.0.1", 57120);
}

void ofApp::mousePressed(int x, int y, int button){
    ofxOscMessage m;

    m.setAddress("/synth/note");
    m.addFloatArg(440.0f);
    m.addFloatArg(0.8f);
    m.addFloatArg(2.1f);

    sender.sendMessage(m); // (1) マウスのボタンを押したらメッセージ送信
}
# server.coffee
udp = require("dgram").createSocket("udp4")
app = require("express")();
io  = require("socket.io")(app)
emitter = new require("events").EventEmitter()

sock = udp.createSocket "udp4", (msg, rinfo)->
  emitter.emit "osc", msg # (2) oFからメッセージを受信
sock.bind 57120

app.get "/", (req, res)->
  res.sendFile "index.html"

io.on "connection", (socket)->
  emitter.on "osc", (msg)->
    socket.emit "osc", msg # (3) ブラウザにメッセージを送信
app.listen 3000
// client.js in index.html
var socket = io();

socket.on("osc", function(msg) {
  emitter.emit("osc", osc.fromBuffer(msg)); // (4) メッセージを受信
});

emitter.on("osc", function(msg) {
  var val0 = msg.args[0].value;
  var val1 = msg.args[1].value;
  var val2 = msg.args[1].value;
  // (5) ここで何かする
});

クライアントの部分だけ jsFiddle で書いてみた。

neume.js example - receive osc message - JSFiddle

このサンプルは実際にソケットサーバーからOSCメッセージを受け取っているわけではなく、サンプル内で擬似的にOSCメッセージを生成して送信→受信しているけど、こういう感じのことが簡単にできる。openFrameworks や Max/MSP、それらに接続した Arduino からのデータで制御していて、しかも iPhone なんかで簡単に見たり聴いたり出来ると思えば色々と夢が広がるのではないでしょうか。

他にこういうこともやりたい

あまり良く分かってないけど、WebRTC を使って直接接続できるともっと良さそう。誰か分かる人おしえてください。

他の事例

この記事とは逆で iOS -> WebSocket -> Max/MSP のパターン。

neume.js - library for web audio programming

またか。と言われてしまいそうだけど、またウェブ楽器プログラミング用のライブラリを作った。

以前に timbre.js というライブラリを作ったのだけど、それの刷新版という感じです。

特徴1: 機能性よりパフォーマンスを重視

以前は多くの動作環境をサポートするためにすべての信号処理を JavaScript で書いていたのですが、それゆえにパフォーマンスに問題があって(Issueでよく指摘された)、neume.js では IE や node.js での動作を諦めて Web Audio API の機能を大活用することでパフォーマンス問題を解決しました。デモでは three.jsgretro と一緒に動作する様子が確認できます。

特徴2: Web Audio API の面倒な部分を抽象化

Web Audio API は多機能なんだけど、部品が細かすぎて使いこなすのが難しいと思います。

例えば 1秒で減衰する三角波 880(±20)Hz を書きたい場合、素の Web Audio API だとこういう感じになると思うのですが、色々書くことが多すぎて辛い気持ちになります。

var tri    = context.createOscillator();
var lfo    = context.createOscillator();
var lfoAmp = context.createGain();
var xline  = context.createGain();

// 基準の三角波(880Hz)
tri.type = "triangle";
tri.frequency.value = 880;

// 揺らすためのサイン波(2Hz)
lfo.type = "sine";
lfo.frequency.value = 2;

// 揺らす大きさ (±20)
lfoAmp.gain.value = 20;

// 1秒の減衰
xline.gain.setValueAtTime(0.25, context.currentTime);
xline.gain.exponentialRampToValueAtTime(0.001, context.currentTime + 1);

// 各ノードを接続
lfo.connect(lfoAmp);
lfoAmp.connect(osc.frequency);
osc.connect(xline);
xline.connect(context.destination);

// オシレーターを開始
lfo.start();
osc.start();

// 一秒たったら止める
setTimeout(function() {
  lfo.stop();
  osc.stop();
  xline.disconnect();
}, 1000);

neume.js だとこういう風に書けます。これが分かりやすいと思うのにも時間がかかりそうだけど、オーディオグラフがそのままオブジェクトになっている感じです。

var Synth = new Neume(function($) {
  return $("xline", { start: 0.25, end: 0.001, dur: 1 }, 
    $("tri", { freq: $("sin", { freq: 2, mul: 20, add: 880 }) })
  ).on("end", function(e) {
    // xline が終わったら呼び出される
    this.stop(e.playbackTime);
  });
});

new Synth().start();

以下のようなオーディオグラフが生成されます。

                           +-------------------+
                           | OscillatorNode    |
                           | - type     : sine |
                           | - frequency: 2    |
                           | - detune   : 0    |
                           +-------------------+
+-----------------------+    |
| OscillatorNode        |  +------------+
| - type     : triangle |  | GainNode   |
| - frequency: 880      |--| - gain: 20 |
| - detune   : 0        |  +------------+
+-----------------------+
  |
+---------------------+
| GainNode            |
| - gain: 0.25->0.001 |
+---------------------+
  |

揺れるスピードをどんどん早くしたくなってもちょっとの修正でOK。

var Synth = new Neume(function($) {
  return $("xline", { start: 0.25, end: 0.001, dur: 1 }, 
    $("tri", {
      freq: $("sin", {
        // ここが変わった
        freq: $("xline", { start: 0.5, end: 10, dur: 20 }),
        mul: 20, add: 880
      })
    })
  ).on("end", function(e) {
    this.stop(e.playbackTime);
  });
});

new Synth().start();

どうですか?

音を作る作業にはどうしても試行錯誤が必要なので、だいたい同じようなインターフェースで適当に書いたら適切になるように工夫しました。まだ書き方によっては無駄のあるオーディオグラフが生成されたり、必要なインターフェースが足りていない等の改善点は色々あるのですが、とりあえず作業的にひと段落したので、使ってもらって様子を見たいという感じです。

ドキュメントも雑な感じになってしまったので、興味があるけど良く分からないという方はツイッター@mohayonao に聞くとか、GitHub Issue で聞くとか、関西なら呼び出しに応じられると思うので呼び出すとかしてください。

という感じです。よろしくお願いします。

Web Audio API でもテストがしたい

開発中に node.js でもテストできるように Web Audio API のテスト用APIを書いた。

最終的なオーディオ処理の結果については耳で確認するしかないのだけど、音を聞くまでもない段階のときに AudioNode(音を出す部品) がどう接続されているのかを調べたりできる。便利だと思うけど、これがあって嬉しい人は少ないと思う。あるのはインターフェースと型のチェックくらいで、信号処理的なのは一切していません。こういう事ができます。

グラフの確認

JSON化すると接続状態が返ってくるのでテストできる。

var ctx = new AudioContext();
var osc = ctx.createOscillator();
var amp = ctx.createGain();

osc.type = "sawtooth";
osc.frequency.value = 220;

osc.connect(amp);
amp.connect(ctx.destination);

ctx.toJSON();
{
  "name": "AudioDestinationNode",
  "inputs": [
    {
      "name": "GainNode",
      "gain": {
        "value": 1,
        "inputs": []
      },
      "inputs": [
        {
          "name": "OscillatorNode",
          "type": "sawtooth",
          "frequency": {
            "value": 220,
            "inputs": []
          },
          "detune": {
            "value": 0,
            "inputs": []
          },
          "inputs": []
        }
      ]
    }
  ]
}

オーディオデコード

var ctx = new AudioContext();

it("AudioContext#decodeAudioData", function(done) {

  // このフラグで成功/失敗を切り替える
  // ctx.DECODE_AUDIO_DATA_FAILED = true;

  ctx.decodeAudioData(buffer, function(e) {
    expect(e).to.be.instanceOf(AudioBuffer);
    done();
  }, function() {
    // failed
  });

});

スクリプティング

var ctx = new AudioContext();

it("ScriptProcessorNode#onaudioprocess", function(done) {
  var scp = ctx.createScriptProcessor(1024, 2, 2);
  var count = 0;

  scp.onaudioprocess = function(e) {
    count += 1;
    expect(e).to.be.instanceOf(AudioProcessingEvent);
    if (count === 5) {
      done();
    }
  };

  // 0.5 秒分処理する
  ctx.process(0.5);
});

型チェック

Web Audio API よりも厳しく型のチェックをするので、動くけど厳密には間違っているみたいなのを検出できます。

var ctx = new AudioContext();

var osc = ctx.createOscillator();

osc.frequency = 880; // エラー (代入は frequency.value にする)
osc.frequency.value = "880"; // エラー (型が違う)

内部モジュールを依存順に並べる

内部モジュールを依存順に並べる node.js のモジュールを作りました。

たとえば以下のようなファイル構成のとき、

  +---------+
  | main.js |
  +---------+
    | require
    V
  +---------+    +---------+
  | foo.js  | -> | baz.js  |
  +---------+    +---------+
    |               |    ^
    |     +---------+    |
    |     |              |
    V     V              |
  +---------+    +---------+
  | bar.js  |    | qux.js  |
  +---------+    +---------+

main.js から見た依存関係で並び替えると以下のようになります。下にあるモジュールは上にあるモジュールに依存しているといった按配です。

[ 
  "path/to/bar.js", 
  "path/to/baz.js", 
  "path/to/foo.js", 
  "path/to/main.js" 
]

そういうことをしてくれます。

仕組み

ソースファイルから require 式を抽出してグラフをつくってトポロジカルソートしています。

  • esprima ソースファイルをASTにして
  • estraverse require式を抽出、
  • toposort トポロジカルソートをする

require("./path/to/module") のように文字リテラルで指定してる部分のみ対応していて、require("module-name") のような外部モジュールや require(filepath) のように文字リテラル以外で読み込んでいる場合は無視しています。

使いどころ

結構あると思うのだけど、個人的には以前に書いたこの記事、

大雑把に説明すると(リンク先を読んでも大雑把だけど)

  • モジュール化した JavaScript をブラウザ向けにビルドするとき
  • 各モジュールの依存順で並べて単純に結合すると簡単
  • require で名前空間の拡張だけして結果を代入しなければその行ごと削除できる

こういうケースで使えます。

AudioContext をたくさん作る会

やりかた間違っているんだけど、100個のオシレーターを100個の AudioContext で作りたいとき。webkit系(ChromeOperaで確認した)では上限があって怒られる。

_.range(100).map(function(i) {
  var ctx = new AudioContext();
  var osc = ctx.createOscillator();

  osc.frequency.value = i * 20 + 200;
  osc.start(0);
  osc.connect(ctx.destination);

  return osc;
});

// SyntaxError: Failed to construct 'AudioContext':
//   number of hardware contexts reached maximum (6).

なんでシンタックスエラーなのかは分からないけどとにかく上限がある。 Firefoxだと怒られなかったけど音が全然違う。プルルルとダサい音が鳴ってしまう。大丈夫か?Firefox

_.range(100).map(function() {
  return new AudioContext();
});

// AudioContext[100]

おっ、これは大丈夫・・

_.range(100).map(function() { 
  return new AudioContext();
}).map(function(ctx, i) {
  var osc = ctx.createOscillator();

  osc.frequency.value = i * 20 + 200;
  osc.start(0);
  osc.connect(ctx.destination);

  return osc;
});

// OscillatorNode[100]

これだと動く。なんなんだ...


webkit のソースを確認したところ、new AudioContext() をしたときに、ここ で上限をチェックしているのだけど、実際に初期化されるのは ちょっと遅れる みたいで、とりあえず最初に AudioContext だけを大量に生成するとチェックをすり抜けてしまう。

Web Audio API用のMMLイベントシーケンサー wamml です

2014.08.20 名前を変更しました。 wamml => MMLEmitter

Web Audio API用のMMLイベントシーケンサーを作りました。

概要

音楽プログラムを大雑把に説明すると

  • 楽器 (音色)
  • 譜面 (音程とタイミング)

の二つの要素を解決するプログラムと言えます。MMLEmitter はこの 譜面 の部分のみを解決するライブラリです。AudioContext と MML を引数にシーケンサーを生成して起動すると、MML に記述したタイミングでイベントが発火するので、そこで音を出す処理を行います。Web Audio API 依存なので今のところ(サポートされないかぎり) IE では動作しません。あ、MML というのは楽譜を文字列で表現するための記法です。

Web Audio API で音を出す

まずは手始めに Web Audio API を使って、ボタンを押したら 1秒間 880Hz の三角波を鳴らすウェブ楽器を作ってみます。

var audiotContext = new AudioContext();

$("#button").on("click", function() {
  var osc  = audioContext.createOscillator(); // 音を出す部品を作る
  var amp  = audioContext.createGain();       // 音量を制御する部品を作る
  var when = audioContext.currentTime;        // 今の時間

  // 音を出す部品に周波数 880Hz を設定する
  osc.frequency.value = 880;
  // 波形を三角波にする
  osc.type = "triangle";
  // 音量の制御, 1秒で減衰させる
  amp.gain.setValueAtTime(0.25, when);
  amp.gain.linearRampToValueAtTime(0.0, when + 1.0);

  // 音を出す部品を起動させる (鍵盤を押すみたいなイメージ)
  osc.start(when);

  // こういう感じで各部品を接続する
  // osc(tri, 880Hz) -> amp(decay, 1sec) -> Web Audio API output
  osc.connect(amp)
  amp.connect(audioContext.destination);

  setTimeout(function() {
    // 終わったら接続を解除する
    amp.disconnect();
  }, 1000);
});

Web Audio API は結構低レベルなインターフェースしかなくて、それゆえに組み合わせれば可能性無限大ッ!!みたいな感じなんだけど、こういう感じでやっと楽器の部分を作ったあと、さらに譜面をどうするかを考えないといけないのが大変で、僕の場合は面倒になって setInterval と Math.random で適当に音を鳴らし続けるみたいなものしか書けませんでした。

Web Audio API の詳しい使いかたはここをざっと読めば分かるようになります。

MMLEmitter で演奏する

つぎに MMLEmitter を使ってボタンを押したらちょっとしたフレーズを演奏させてみます。

音を生成する部分はさっきのコードの流用です。ほとんど追記することなく、音を鳴らすだけのレベルから演奏といえるまでレベルアップします。このライブラリで解決するのはタイミング制御の部分だけなので、音を生成する部分は頑張って書かないといけませんが、そのぶん汎用性があります。

var audioContext = new AudioContext();

var mml= new MMLEmitter(audioContext, "t100 l8 cege [>eg<c]2");

mml.tracks[0].on("note", noteEventHandler); // 発音のタイミングで呼ばれる

$("#button").on("click", function() {
  mml.start();
});


/**
  * ここで Web Audio API を駆使して音を出す
  *
  * @param {object} event
  *   @param {float}    event.when      : 発音されるべき時間 (タイミング)
  *   @param {int}      event.midi      : MIDIノート番号
  *   @param {float}    event.duration  : 鳴っている時間
  *   @param {function} event.noteOff   : duration 経過後に呼ぶコールバックを設定するための関数
  *   @param {int}      event.chordIndex: 和音の場合のインデックス
  */
function noteEventHandler(e) {
  var osc  = audioContext.createOscillator(); // 音を出す部品を作る
  var amp  = audioContext.createGain();       // 音量を制御する部品を作る
  var when = e.when;  

  // MIDIノート番号 -> 周波数 変換
  osc.frequency.value = 440 * Math.pow(2, (e.midi - 69) * 1 / 12);
  // 波形を三角波にする
  osc.type = "triangle";
  // 音量の制御, duration で減衰させる
  amp.gain.setValueAtTime(0.25, when);
  amp.gain.linearRampToValueAtTime(0.0, when + e.duration);

  // 音を出す部品を起動させる (鍵盤を押すみたいなイメージ)
  osc.start(when);

  // こういう感じで各部品を接続する
  // osc(tri) -> amp(decay) -> Web Audio API output
  osc.connect(amp)
  amp.connect(audioContext.destination);

  e.noteOff(function() {
    // 終わったら接続を解除する
    amp.disconnect();
  }, 0.1); // duration 経過 + 0.1秒で実行される
}

アニメーションへの応用

note イベントで必ず音を出す必要はなくて、アニメーションの制御など、あらゆるタイミング制御に適用できます。その際の各フレームの単位は 11.61 (512 / 44100) msec 程度になります。

「あらゆるタイミング制御に適用できる」と書いたところで思ったけど、それならば Web Audio API への依存をなくすのも良いかも知れない。タイマーとしてしか使ってないし。

予定の機能

最近はやりの directive を導入したい。外部から動的に MML を操作したり、内部から外部へのトリガに使えると利用範囲はかなり広がるはず。一応構想としては vue.js みたいなやり方で、あと固有の機能として $ で始まる識別子は全トラックで共有されるみたいなのを考えている。途中まで作ったのだけど、どうなっていると最高に便利なのか検討が足りていない気がして最初のバージョンからは削除した。MML + directive で何かひらめいた人には助言をおねがいしたいです。

あと、MMLを拡張している部分のシンタックスがちょっと変わるかも知れないです。

おまけだょ

高校生のときに作った曲を思い出しながらMML化したのでおまけに貼っておきます。このページに貼り付けると聞けます。

t180 l8 q7 o5 ab<cd
  e2.d4 g4f4e4f4 d2.e8c8^2 > ab<cd
  e2.d4 g4f4e4f4 d2.e8e8^2 > ab<cd
  e2.d4 g4f4e4f4 d2.e8c8^2 > ab<cd >
  a2 ab<cd > a2 ab<cd> a2 r2 r1 <;
t180 l1 q8 o4 r2
  [fa<ce] [fgb<d] [egb<d] [ea<ce]
  [fa<ce] [fgb<d] [egb<d]2 [g+b<e]2 [a<cd]2 [ea<c]2
  [fa<ce] [fgb<d] [egb<d] [ea<ce]
  [fa<c] [gb<d] l8< r[ce][>b<d]r [ce][>b<d]r[ce] r[ce][>b<d]r [ce]>eee; 
t180 l8 q6 o3 r2
  f4r<f4rc4> ffbb<dd>gg e4r<e4r>b4  aa<ccee>aa
  f4r<f4rc4> ffbb<dd>gg e4r<e4r>g+4 aagagab<c>
  f4r<f4rc4> ffbb<dd>gg e4r<e4r>b4  aa<ccee>aa
  f4f4afff g4g4bggg eaeeaeea4 aaeaeee;

gretro お試しページを作りました

古めかしいCGを描くためのライブラリ gretro のお試しページを作った。

f:id:mohayonao:20140814164733p:plain

すごいやっつけ仕事で見にくいけど、コードで絵を描いて遊べる。

ポリゴンを描画するやつバグってた。。


GitHub Pages と Gist API の組み合わせが良い

このページは GitHub Pages なんだけど、サンプルコードは Gist に置いてある。 賢い機能がついていて Gist API を使ってサンプルコード一覧を取得するようにしてある。ライブラリのサンプルプログラムとか微妙な更新をしまくって gh-pages ブランチのコミットログがごちゃごちゃになることが良くあったのでこの方法はよさそう。

こういう感じで #gistid: に続けて gistid を指定すると、その gist から gr-***.js という名前のファイルが右の Examples のところに表示されるので、格好良い画像ができたら誰でも URL で共有できる。