音の鳴るブログ

鳴らないこともある

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);
}