音の鳴るブログ

鳴らないこともある

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

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

A: https://jsfiddle.net/oz2sfb56/

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 になる場合がある
  • 定義されていない書式でも動作することがある

参照

ECMA-262 11.14 コンマ演算子(,)

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 というのは凄く書きづらいですね。

類似のやつも紹介しておきます。

画像を点字にする

ごめんな。Windowsでは点字が表示できないんだ。

2年くらい前にこの記事を見て、真似してブラウザで動くやつを作ったのだけどすっかり忘れて放置していた。

僕の書いたのは二値化した単色点字で、元になったほうを改めて見るとクオリティが全然違っててビックリするのだけど、今週になってもうちょっと便利にならんか?という問い合わせをいただいたので、せっかくだしnpmモジュール化しました。(ブラウザのほうは修正するのが面倒そうだった)

こういう感じで使う。

$ seurat image/lena.jpg

f:id:mohayonao:20150310220043p:plain

他にサイズや二値化の具合を調整したり、白黒反転させたりできる。

$ 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、こういう感じで書くのが良いよというのがあったら教えてください!!