音の鳴るブログ

鳴らないこともある

コマンドラインからウェブオーディオのコードをちょっと試す

先日作った、JavaScript 実装の Web Audio API を使って、コマンドラインからウェブオーディオのコードをちょっと試すやつを作った。

使い方は簡単で、グローバルインストールすると wae というコマンドが使えるようになる。

$ npm install -g wae-cli

で、適当にコードを書いて ( audioContextwae コマンドから自動的に与えられる AudioContext )

// coin.js
const osc = audioContext.createOscillator();
const amp = audioContext.createGain();

osc.type = "square";
osc.frequency.setValueAtTime(987.7666, 0);
osc.frequency.setValueAtTime(1318.5102, 0.075);
osc.start(0);
osc.stop(2);
osc.connect(amp);
osc.onended = () => {
  process.exit();
};

amp.gain.setValueAtTime(0.25, 0);
amp.gain.setValueAtTime(0.25, 0.075);
amp.gain.linearRampToValueAtTime(0, 2);
amp.connect(audioContext.destination);

実行すると音が鳴る。

$ wae coin

オプションを工夫するとWAV形式で出力したりできる。 ドキュメントに書くのを忘れたけど、この場合はスピーカーから音を出す必要がないので、5秒の音声でも0.何秒かで出力してくれる。 しかも setInterval などを使って逐次的に処理するようなコードを書いても上手に実行してくれる。

$ wae coin -o coin.wav

module.exports = function で書けば引数を渡したりもうちょっと高度なコードが書ける。

// beep.js
module.exports = (audioContext, frequency, duration) => {
  const osc = audioContext.createOscillator();
  const amp = audioContext.createGain();

  osc.frequency.value = frequency;
  osc.start(0);
  osc.stop(duration);
  osc.connect(amp);

  amp.gain.setValueAtTime(0.5, 0);
  amp.gain.linearRampToValueAtTime(0, duration);
  amp.connect(audioContext.destination);
};
$ wae beep -- 1760 0.5

ベースで使っている web-audio-engine というライブラリが完全ではない (例えば現状 ConvolverNode や DynamicsCompressor が未実装など) という問題があるけど、ちょっと試したい時にブラウザをわざわざ開かなくて良いので便利だと思う。

関連

JavaScript で Web Audio API を実装した

github.com

以前から Web Audio API に興味があって暇なときには仕様を読んだりしていたのですが、だいたい分かったぞ!!と思って勢いで書きました。すべて JavaScript で書いてあるのでブラウザはもちろん Node.js でも動作します。Node.js で動かす場合はレンダリング結果をストリームとして出力するかwav形式でエクスポートできます。

オンラインのデモ (web-audio-engine と native Web Audio API の比較ができます)

web-audio-engine :: demo

先行する事例の

と比較すると、 チャネルの動的適応 (入力のチャネル構成によって下流のチャネル構成が変化する) や AudioNode のライフサイクル (動作しないエッジを擬似的にばっさりと切断する) を実装していてより仕様に沿ったものとなっています。

まだまだ未対応なノードとかメソッドなどが残っていますが音を出す仕組みのあたりは一段落したという感じです。ちょっと疲れてきて作業をするたびにコードが意味不明になっていく状態になったので、しばらくはコードにコメントを足すぐらいにして、また気力と体力が復活したら少しずつ機能を足していこうかと思います。

実装したAPI

  • AudioBuffer
  • AudioBufferSourceNode
  • AudioContext
  • AudioDestinationNode
  • AudioNode
  • AudioParam
  • BiquadFilterNode (audio rate parameter is not supported)
  • ChannelMergerNode
  • ChannelSplitterNode
  • DelayNode (noisy..)
  • GainNode
  • OscillatorNode (use wave-table synthesis, not use periodic wave)
  • PeriodicWave
  • ScriptProcessorNode
  • StereoPannerNode
  • WaveShaperNode

未実装のAPI

  • AnalyserNode
  • AudioWorkerNode
  • ConvolverNode
  • DynamicsCompressorNode
  • IIRFilterNode
  • PannerNode
  • SpatialPannerNode

お焚き上げ2015

年末なので今年書いた npm モジュールをまとめてみます。


OscMsg v0.3.0

OSC のメッセージを JSON にデコード/エンコードするライブラリ。去年からちょくちょく OSC を使って Node.js と openFrameworks や Max/MSP と連携する案件を手伝ったりしていたので非常に重宝した。似たようなライブラリに node-osc-min というのがあるのだけど、壊れたデータを渡したときに例外が出るのとかブラウザ対応がいまいち気に入らなかったので自分で作った。とはいえブラウザで使うことはないなーという印象。


WebAudioScheduler v1.0.0

Web Audio API を使うときに必読の この記事 をベースに作ったスケジューリングライブラリ。時間と関数とパラメータを登録しておくと良いタイミングで呼び出してくれる。Web Audio API を使うときはこれがないと何もできないというレベルで重宝した。名前に反して Node.js でも動くので、そちらでも大活躍。(Web Audio API で使うときは AudioContext#currentTime の時間を指定、そうでないときは Date.now() / 1000 の時間を指定できる)


ADSREnvelope v1.0.0

Web Audio API で ADSRエンベロープを作るのが意外と難しいので、それだけのために作った。ADSR の各パラメータを設定すれば、それに応じた AudioParam 向けのメソッドを生成してくれたり、x秒時点の値を計算してくれたりする。後者の機能は Node.js でも動くので、そちらでも大活躍。デモサイトではエンベロープを可視化したりできる。


Ciseaux v0.4.0

Web Audio API の AudioBuffer を切ったり貼ったり編集できるやつ。がーっと編集して最後に WebWorker でレンダリングするみたいな少し凝った構成になっていて、作って満足した感があった。wav の読み込み/書き出しのみなら Node.js でも動く。


InlineWorker v1.0.0

github.com

関数からWebWorkerを作るやつ。上のライブラリで使っている。Node.js でも動く。ここに書いたやつ。


SeqEmitter v1.2.0

簡単にシーケンサーを作るためのベースモジュール。楽譜情報のイテレーター (timeという要素を返すオブジェクトを返すやつ) を渡すと良いタイミングでイベントとして発火してくれる。


IntervalIterator v1.0.0

上のモジュールで使っているやつ。time という要素を持つオブジェクトを返すイテレーターをラップして、1秒単位とかでまとめて返してくれるイテレータ


MIDIKeyboard v0.2.1

MIDI キーボードの演奏情報をイベントエミッターの API で受信できるやつ。Web MIDI API と Node.js で使える。


LaunchControl v0.4.3

Novation LaunchControl の捜査情報をイベントエミッターの API で受信できるやつ。Web MIDI API と Node.js で使える。


MIDIDevice v0.4.1

上記の2つのベースになっているモジュール。Web MIDI API と node-midi の差を吸収するみたいなやつ。ここに書いたように親クラスを切り替えて継承して使う。


他にもあったと思うけど忘れた。という感じ。

だいぶ前に作ったライブラリが未だに人気があるので、来年はこれをやり直そうかなと思っていて、さいこうの仕様を考えながら年越しをしたいと思います。

Google Chrome の右上の名前をだるくなくした

Google Chrome の右上に本名が表示され続けてだるい。前は簡単に消せたけど今回のは黒い画面で何か入力しないといけないとか意味不明で困っていたのだけど、名前のところをクリックすると編集可能っぽいので、できるだけだるくない名前にした。

f:id:mohayonao:20150729061252p:plain

f:id:mohayonao:20150729061302p:plain

f:id:mohayonao:20150729061317p:plain

f:id:mohayonao:20150729061847p:plain

f:id:mohayonao:20150729061852p:plain

Web Music ハッカソン で Tシャツ をもらいました

Web Music ハッカソン で WebSocket で演奏情報を分散配信して複数のブラウザでひとつの音楽を演奏するシーケンサを作って、頑張った賞みたいなので LittleBits のTシャツをもらいました。

コンセプト的なやつ

f:id:mohayonao:20150726201422p:plain

  • サーバーがある
  • サーバーは音楽の演奏情報を持っている
  • MIDIコントローラー等でサーバーの演奏情報を操作できる
  • たくさんのクライアントがある (10-50台くらいを想定), WebSocket で接続
  • それぞれのクライアントにバラバラに演奏情報 (JSON) を送信する
  • 空間全体でひとつの音楽になる

経緯

去年からちょくちょくウェブオーディオを使ったインスタレーションとかライブを手伝っているのですが、そこで得られたノウハウが全然形として残っていなかったので整理したかったのと、あと今後のために試しておきたかったこととか React とか flux とかの最新のトレンドとかを全部ぶっこんでみたかった。

当日のハック

諸事情あってかなりの張り切りモードになっていたせいで事前に準備しすぎて、あとは細かい調整するくらいしかできない状態だったのだけど、当日一緒になった方々に機能を追加してもらえたりハッカソンぽい感じで作業できた。僕は最強JSビルド環境厨みたいなところがあって、何も考えずに Node.js で ES6 で React で Flux (しかも自分で実装したやつ) で npm run scripts を使っていてかなり制限のきつい状況だったのだけど、うまい具合に対応してもらえてよかった。

スマートフォンの向きでシンセリードのフィルターのかかり具合を制御できる機能を追加してくれた

1x1 の Canvas がチカチカするだけだった画面に 波形 と スペクトラム を表示してくれた

ありがとうございました!

デモ

会場のWiFiが想定外に悪くて全然できなかった。どこか10台以上のクライアントを使ってデモさせてくれるところないですかね?

一台のPCで9つのブラウザウインドウを開いて実行している画面。違う音が出ているので画面の表示はバラバラ、同時に聞くとひとつの音楽になる。本来はこれをハッカソン参加者のそれぞれのPCで実行するつもりだった。

f:id:mohayonao:20150727102503p:plain

一応サーバーがなくても動くスタンドアロン版も用意しているので、雰囲気をつかむだけなら試せる。こっちはめっちゃ React ってる。演奏情報のメッセージングをサーバーを介するかクライアント単体で完結させるかだけなのでベースのコードはほとんど共有できている。

http://mohayonao.github.io/web-music-hackathon-04/

f:id:mohayonao:20150727103701p:plain

懇親会

  • @sascacci さんと楽器の手触り感の気持ちよさの話
  • @aike1000 さんと開発環境とかテストの話
  • @g200kg さんのGPUを使った信号処理の話

あと、今度 YAMAHA から発売される reface はスピーカーがついてて良いみたいなこととか DX7II の思い出みたいなことを話していた気がする。

WebMusicハッカソンで使えそうな俺俺ライブラリ 5選

7月25日に WebMusicハッカソン #4 @kyoto というのがあるのですが、ちょうど良い機会なので僕が作ったウェブ音楽用簡単超絶便利ライブラリを紹介したいと思います。


WEB AUDIO SCHEDULER

2 つの時計のお話 - Web Audio の正確なスケジューリングについて - HTML5 Rocks

Web Audio で必須な割に難しいのがスケジュール管理で、いちおう上の記事のやり方が推奨なのですが、そのままやるとかなり面倒くさい。のだけど、このライブラリを使うと面倒な部分は気にせず、いつ何をしたいのかを書くだけで良くなる。以下は簡単なメトロノームの例。

var gcguard = [];
var audioContext = new AudioContext();
var scheduler = new WebAudioScheduler({ context: audioContext });

function metronome(e) {
  // e.playbackTime が WebAudio 的な時間
  // 0.5秒間隔で ticktack 関数の呼び出しを引数付きで登録
  scheduler.insert(e.playbackTime + 0.000, ticktack, [ 880, 1.00 ]);
  scheduler.insert(e.playbackTime + 0.500, ticktack, [ 440, 0.05 ]);
  scheduler.insert(e.playbackTime + 1.000, ticktack, [ 440, 0.05 ]);
  scheduler.insert(e.playbackTime + 1.500, ticktack, [ 440, 0.05 ]);
  // 2秒後にこの関数を呼び出ししなおす
  scheduler.insert(e.playbackTime + 2.000, metronome);
}

function ticktack(e, frequency, duration) {
  var playbackTime = e.playbackTime;
  var osc = audioContext.createOscillator();
  var amp = audioContext.createGain();
  var t0 = playbackTime;
  var t1 = t0 + duration;

  osc.frequency.value = frequency;
  amp.gain.setValueAtTime(0.4, t0);
  amp.gain.linearRampToValueAtTime(0, t1);
  
  osc.start(t0);
  osc.stop(t1);

  osc.onended = function() {
    osc.disconnect();
    amp.disconnect();
    gcguard.splice(gcguard.indexOf(osc), 1);
  };
  gcguard.push(osc);

  osc.connect(amp);
  amp.connect(audioContext.destination);
}

scheduler.start(metronome);

WEB AUDIO API SHIM

JavaScript - Web Audioの新しいAPIについてざっくり解説 - Qiita

Web Audio の新しい API のポリフィル。StereoPannerNode でオートパンったり、getFloatTimeDomainData で きれいな波形を描画 したり、promise-based API でモダンなプログラミングができるようになる。light版がおすすめ。以下は fetch API からの簡単なオートパンの例。

var gcguard = [];

fetch("amen.wav").then(function(res) {
  return res.arrayBuffer();
}).then(function(audioData) {
  return audioContext.decodeAudioData(audioData);
}).then(function(buffer) {
  var bufSrc = audioContext.createBufferSource();
  var panLFO = audioContext.createOscillator();
  var panner = audioContext.createStereoPanner();
  var t0 = audioContext.currentTime;
  var t1 = t0 + 30;

  bufSrc.buffer = buffer;
  bufSrc.loop = true;
  panLFO.frequency.value = 2;
  
  bufSrc.start(t0);
  panLFO.start(t0);
  bufSrc.stop(t1);
  panLFO.stop(t1);

  bufSrc.onended = function() {
    bufSrc.disconnect();
    panLFO.disconnect();
    panner.disconnect();
    gcguard.splice(gcguard.indexOf(bufSrc), 1);
  };
  gcguard.push(bufSrc);
  
  bufSrc.connect(panner);
  panLFO.connect(panner.pan);
  panner.connect(audioContext.destination);
});

MIDI KEYBOARD

MIDIキーボードの演奏情報をイベントエミッターなAPIで受信できるやつ。Web MIDI API はもちろん、Node.js でも動くのでサーバーサイドのコントローラーとしても使える。以下は M-AUDIO の Keystation Mini 32 を開いて、演奏情報をダンプする簡単な例。

// 開きたいデバイスの名前を指定して new する
var midiKey = new MIDIKeyboard("Keystation Mini 32");

midiKey.open().catch(function(e) {
  console.error(e);
});

midiKey.on("message", function(e) {
  console.log("dataType  : " + e.dataType);
  console.log("noteNumber: " + e.noteNumber);
  console.log("velocity  : " + e.velocity);
  console.log("value     : " + e.value);
  console.log("channel   : " + e.channel);
});

MIDIキーボードの名前は以下のように取得できる。

MIDIKeyboard.requestDeviceNames().then(function(devies) {
  console.log(devices);
});

MIDI DEVICE

MIDIキーボード以外も使いたい人向け、継承するなりして _onmidimessage を上書きすれば簡単にMIDIコントローラーのライブラリが作れる。上の MIDI キーボード以外に Novation LAUNCH CONTROL 用のがあるので参考になりそう。Web MIDI API, Node.js 以外にテスト用インターフェースもあるので CI とか実デバイスがない状態でも使えたりもする。以下はなんか適当で簡単な例。

var midiDevice = new MIDIDevice("Super MIDI Controller");

midiDevice.open().catch(function(e) {
  console.error(e);
});

midiDevice._onmidimessage = function(e) {
  if (e.data[0] & 0x90 === 0x90) {
    this.emit("noteOn");
  }
};

midiDevice.bang = function() {
  this.send([ 0x90, 0x64, 0x64 ]);
};

OSC MSG

ブラウザでOSCを読み書きするライブラリ。もともと osc-min というライブラリを使っていたのだけど、browserify するとサイズが大きくなるのと、壊れた OSC を受信したときに例外を出すのが困るので自分で書いた。基本的なAPIは osc-min と互換性があるし、もちろん Node.js でも使える。以下は Max/MSPとブラウザでの簡単で意味のないやり取りの例。

f:id:mohayonao:20150707210301p:plain

var path = require("path");
var express = require("express");
var socketIO = require("socket.io");
var http = require("http");
var dgram = require("dgram");
var app = express();
var server = http.createServer(app);
var webSocket = socketIO(server);
var oscSocket = dgram.createSocket("udp4");

app.use(express.static(path.join(__dirname, "./public")));

server.listen(8000, function() {
  console.log("Listening HTTP on port %d", server.address().port);
});

webSocket.on("connect", function(socket) {
  // ブラウザから Max/MSP に転送
  socket.on("/osc", function(buffer) {
    oscSocket.send(buffer, 0, buffer.length, 7401, "127.0.0.1");
  });
});

// Max/MSP からブラウザに転送
oscSocket.on("message", function(buffer) {
  webSocket.emit("/osc", buffer);
});

oscSocket.bind(7400, function() {
  console.log("Listening OSC on port %d", oscSocket.address().port);
});
<button id="bang">BANG</button>
<script src="/socket.io/socket.io.js"></script>
<script src="/osc-msg.js"></script>
<script>
window.onload = function() {
  var socket = io();

  function receiveOSC(buffer) {
    var msg = OscMsg.fromBuffer(buffer);

    if (msg.elements) {
      msg = msg.elements[0];
    }

    msg.args = msg.args.map(function(value) {
      return value.value;
    });

    console.log(JSON.stringify(msg));
  }

  // WebSocket 経由で Max/MSP に送信
  function sendOSC(msg) {
    socket.emit("/osc", OscMsg.toBuffer(msg));
  }

  function bang() {
     sendOSC({
      address: "/noteOn",
      args: [
        { type: "integer", value: ((Math.random() * 24) + 48)|0 },
        Math.random() * 128, // 型を指定しない数値は Float になる
      ],
    });
  }

  // Max/MSP から WebSocket経由で受信
  socket.on("/osc", receiveOSC);

  document.getElementById("bang").onclick = bang;
}
</script>

CISEAUX

AudioBuffer を切り貼り編集するやつ。分割したり逆回転させたり重ねたり色々できる 。wavファイルなら Node.js でも使える。以下は AudioBuffer を 100 分割して stutter して適当に並び替える簡単な例。

var gcguard = [];

Ciseaux.from("amen.wav").then(function(tape) {
  // 100分割
  var tapes = tape.split(100).map(function(tape) {
    // それぞれを3回繰り返し
    return tape.repeat(3);
  });
  
  // くっつけてちょっと早くする
  tape = Ciseaux.concat(_.shuffle(tapes)).pitch(1.5);

  return tape.render();
}).then(function(buffer) {
  var bufSrc = audioContext.createBufferSource();
  var t0 = audioContext.currentTime;
  var t1 = t0 + 30;

  bufSrc.buffer = buffer;
  bufSrc.loop = true;

  bufSrc.start(t0);
  bufSrc.stop(t1);

  bufSrc.onended = function() {
    bufSrc.disconnect();
    gcguard.splice(gcguard.indexOf(bufSrc), 1);
  };
  gcguard.push(bufSrc);

  bufSrc.connect(audioContext.destination);
});

STEREO ANALYSER NODE

AnalyserNode のステレオ版。getFloatFrequencyData などのメソッドが左右 2つ指定できる。以下はオーディオファイルを読み込んでからの左右の周波数スペクトラムを表示する簡単な例。

var gcguard = {};

fetch("amen.wav").fetch(function(res) {
  return res.arrayBuffer();
}).then(function(audioData) {
  return new Promise(function(resolve, reject) {
    audioContext.decodeData(audioData, resolve, reject);
  });
}).then(function(buffer) {
  var bufSrc = audioContext.createBufferSource();
  var analyser = new StereoAnalyserNode(audioContext);
  var t0 = audioContext.currentTime;
  var t1 = t0 + 30;
  var timerId = 0;

  bufSrc.buffer = buffer;
  bufSrc.loop = true;

  bufSrc.start(t0);
  bufSrc.stop(t1);

  bufSrc.onended = function() {
    bufSrc.disconnect();
    analyser.disconnect();
    clearInterval(timerId);
    gcguard.splice(gcguard.indexOf(bufSrc), 1);
  };
  gcguard.push(bufSrc);

  var L = new Float32Array(analyser.frequencyBinCount);
  var R = new Float32Array(analyser.frequencyBinCount);

  timerId = setInterval(function() {
    analyser.getFloatFrequencyData(L, R);
    drawSpectrum(L, R);
  }, 100);

  bufSrc.connect(analyser);
  analyser.connect(audioContext.destination);
});

MML EMITTER

MMLで書いた演奏情報を良い感じのタイミングでイベント発火してくれるやつ。便利なので良いのだけど、変な機能があったりコードが酷いのでもうちょっと綺麗な感じに破壊的に書き直ししたい。以下はドレミファソラシドを演奏する簡単な例。

var gcguard = [];
var mml = new MMLEmitter(audioContext, "t120 l8 $ cdef gab<c c>bag fedc; t120 l2 o3 $ ccee>aa<dd");

mml.tracks[0].on("note", noteOn);
mml.tracks[1].on("note", noteOn);

function noteOn(e) {
  var frequency= e.frequency;
  var duration = e.duration;
  var playbackTime = e.playbackTime;
  var osc = audioContext.createOscillator();
  var amp = audioContext.createGain();
  var t0 = playbackTime;
  var t1 = t0 + duration;

  osc.frequency.value = frequency;
  amp.gain.setValueAtTime(0.4, t0);
  amp.gain.linearRampToValueAtTime(0, t1);
    
  osc.start(t0);
  osc.stop(t1);

  osc.onended = function() {
    osc.disconnect();
    amp.disconnect();
    gcguard.splice(gcguard.indexOf(osc), 1);
  };
  gcguard.push(osc);
  
  osc.connect(amp);
  amp.connect(audioContext.destination);
}