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とブラウザでの簡単で意味のないやり取りの例。
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); }
JS クイズ
Q: 以下のテストをパスしてください
function assert(cond) { if (!cond) throw new Error("failed!!"); } // Foo と Bar は異なる assert(Foo !== Bar); // new Foo() は Foo / Bar のインスタンスである assert(new Foo() instanceof Foo); assert(new Foo() instanceof Bar); // new Bar() も Foo / Bar のインスタンスである assert(new Bar() instanceof Foo); assert(new Bar() instanceof Bar);
JavaScriptの小銭リテラルが便利
コンピュータプログラムのソースコードなどで、特定のデータ型による値を直接表記する際の書式。また、そのような書式に従って記載された値。
JavaScriptにも数値や文字列、オブジェクト、正規表現などのリテラル表現があるのだけど、意外と知られていないものに 小銭リテラル がある。小銭リテラルはその名のとおり小銭を表現する書式で仕様では以下のように定義されている。
KozeniLiteral :: DecimalIntegerLiteral KozeniParts(opt) KozeniParts :: KozeniPart KozeniParts KozeniPart KozeniPart :: KozeniDelimiter KozeniDigits KozeniDigits :: DecimalDigit DecimalDigit DecimalDigit KozeniDelimiter :: ,
使用例
変数(price)には小銭の部分だけが代入されるので余計な演算をすることなく小銭を取得できる。
var price = 1,234,567; // 小銭リテラル console.log("小銭は " + price + "円です"); // => 小銭は 567 円です
試しに数値リテラルで同じことをする例も載せるが複雑な演算子を必要としない小銭リテラルの方が明らかにシンプルで便利なのが分かると思う。
var price = 1234567; // 数値リテラル console.log("小銭は " + (price % 1000) + "円です"); // => 小銭は 567 円です
このように便利な小銭リテラルだけど、エッジすぎる機能なのか残念なことに現時点ではきちんと実装されておらず非常にバギーなので、実装が安定するまでは使うのを控えた方がよさそう。
var price = 1,234,067; // 小銭リテラル console.log("小銭は " + price + "円です"); // => 小銭は 55 円です (12円の損)
確認されているバグ的なやつ
- 小銭が少なくなる場合がある
- strict mode だと SyntaxError になる場合がある
- 定義されていない書式でも動作することがある
参照
読み込んだファイルを格好良い変数に格納する
./index.html
を読み込んで index.html
でアクセスできるようにする。
var fs = require("fs"); var index = { html: fs.readFileSync("index.html", "utf-8") }; console.log(index.html);
以上です。
prominence - node.jsコールバックをPromise化するやつ
と、いうのを作りました。
node.jsの非同期APIはコールバックでエラーと結果を受け取りますが、それをPromiseベースのAPIに変換します。
こうやって使う。
var fs = require("fs"); var prominence = require("prominence"); // prominence するとそのオブジェクトで使える全メソッドが Promise 仕様になる prominence(fs).readFile("hoge.txt", "utf-8").then(function(text) { console.log(text); }).catch(console.error.bind(console)); // こういう書き方もできる prominence(fs, "readFile", [ "hoge.txt", "utf-8" ]).then(function(text) { console.log(text); }).catch(console.error.bind(console)); // 中でやっていること new Promise(function(resolve, reject) { fs.readFile("hoge.txt", "utf-8", function(err, text) { if (err) { return reject(err); } return resolve(text); }); }).then(function(text) { console.log(text); }).catch(console.error.bind(console));
prominence というのは凄く書きづらいですね。
類似のやつも紹介しておきます。
画像を点字にする
2年くらい前にこの記事を見て、真似してブラウザで動くやつを作ったのだけどすっかり忘れて放置していた。
僕の書いたのは二値化した単色点字で、元になったほうを改めて見るとクオリティが全然違っててビックリするのだけど、今週になってもうちょっと便利にならんか?という問い合わせをいただいたので、せっかくだしnpmモジュール化しました。(ブラウザのほうは修正するのが面倒そうだった)
こういう感じで使う。
$ seurat image/lena.jpg
他にサイズや二値化の具合を調整したり、白黒反転させたりできる。
$ seurat --help Usage: seurat [options] path/to/image -w, --width Number width(cols) of the converted text -h, --height Number height(rows) of the converted text -t, --threshold Number threshold for binarization -i, --invert invert to negative -o, --output String write the converted text to this file -p, --print print out the converted text -v, --version show version --help show help
勢いで作ってみたは良いものの問い合わせをくれたのは技術系の人ではなくて node.js が使えないとのことなのでどう連絡しようか悩んでいるところです。
ES6の書き方が定まらない
最近ちょっとしたコードは ES6 で書くようにしているのだけど、慣れていないせいか書き方が定まらない。
例えば ArrowFunction の => の前後にスペースを入れるかどうか
let foo = ()=>{}; // 最初はこうしていたけど let foo = ()=> {}; // これを経て let foo = () =>{}; let foo = () => {}; // 結局これになった
ArrowFunction の {} を省略するかどうか
let foo = array.map(x => x * 2); // 単純な演算なら省略する let foo = array.map((x) => { // メソッド呼び出しが入っていたら return x.valueOf(); // {} は 省略しないようにしていたけど }); let foo = array.map(x => x.valueOf()); // 結局は省略するようになった // でも(感覚的に)長いときや入れ子になっているときは省略しない let foo = array.map((x) => { return x.slice(10).concat(x.slice(0, 10).reverse().map(x => x.valueOf())); }); // {} を使うときは引数の () も省略しない (これはまだ悩んでいる) // 関係ないけど、よく括弧を書き忘れる let a = aray.map x => x.clone();
export のタイミングとか
class Foo {} export default Foo; // 最初はファイルの最後にこうしていたけど export default class Foo {} // 良く考えたらこっちの方が良いと思う export class Bar {} // 上の書き方の場合、default 以外が書きにくくなる
上の書き方だとこういうインポートをする羽目になる
import Foo, {Bar} from "./foobar"; // なんか格好悪い
今、気が付いたけどこう書けばよかったのかも
export class Foo {} // export はその場で書く export class Bar {} export default Foo; // export default はファイルの最後に書く
// 格好良い import Foo from "./foobar"; import { Foo, Bar } from "./foobar"; // こっちの方が分かりやすいのかも知れない.. import * as foobar from "./foobar";
あと var と let を使い分けたり..
let a = () => { // 一番外側は let var a = 10; // 関数の最初は var { let a = 5; // ブロックの中は let } return a; }; // 今は var はいっさい使わない
本当に最初のときはすごく乱暴にこういう感じだった
export default new (class Bar extends require("./foo") { })();
ES6、こういう感じで書くのが良いよというのがあったら教えてください!!