node-midiで仮想MIDIデバイスを作る
Kyoto.js #11 というのがあって、せっかくなので最近ぽちぽち作っていた「node-midiで仮想MIDIデバイスを作る」という内容で LT しました。他には GitBook とか arduino と Node.js で格闘してる話とか自分も似たような事をしそうだな〜的なものから、あんまり知らない話とかあって良かった。戦ってる話が多かった気がする。
要約するとMIDIデバイスって便利だけど持ってないと使えなくて困る。でも node-midi というパッケージを使えば、仮想MIDIポートが作れて、それだとパソコンを騙してMIDIデバイスを持っていることにできるので Electron で UI をつけたら万事オッケー。三つくらい作ってやったわー。くぅ〜。という内容です。
使ったライブラリ
作ったもの
- virtual-midi-keyboard
- Electron-based Virtual MIDI Keyboard
- virtual-midi-knob-knob-pad
- Electron-based Virtual MIDI Controller that like Novation Launch Control
- virtual-midi-matrix-pad
- Electron-based Virtual MIDI Controller that like Novation Launchpad
念のために言っておくと仮想MIDIデバイス最高!!ハードウェアなんかいらんかったんや。というのはなくて、やっぱり触った感じとかやってる感はハードウェアデバイスでないと得られないと思っています。高級箱ティッシュと駅で配っているポケットティッシュくらい差がある。今回は特別にオススメのMIDIデバイスのリンクを貼っておくので2つ3つほど試してみたら良いんじゃないでしょうか。
M-Audio USB MIDIキーボード 32鍵 Ableton Live Lite付属 Keystation Mini 32
- 出版社/メーカー: M-AUDIO
- 発売日: 2014/06/19
- メディア: エレクトロニクス
- この商品を含むブログを見る
Novation MIDIコントローラー Launch Control
- 出版社/メーカー: Novation
- メディア: エレクトロニクス
- この商品を含むブログを見る
Novation パフォーマンスコントローラ Launchpad MK2
- 出版社/メーカー: Novation
- 発売日: 2015/08/28
- メディア: エレクトロニクス
- この商品を含むブログを見る
エリエール 贅沢保湿 200組400枚×3箱入り パルプ100%
- 出版社/メーカー: 大王製紙
- 発売日: 2016/09/21
- メディア: ヘルスケア&ケア用品
- この商品を含むブログを見る
食べたもの
飲み物とピザをもらったんだけど写真はそれらとは関係なくて、前後に食べたサンドイッチとフランクフルトです。なんか写真がいまいちなのが残念です。ごちそうさまでした。
BiquadFilterを聴きくらべる
Web Audio APIのBiquadFilterNodeで設定するQの効きが悪いと思ったことがある。もしくは、Max/MSPのパッチを渡されてJavaScriptで再現しないといけないっ!!となった時に、音の雰囲気がどうも違ってて悩むというウェブオーディオあるあるの噂を聞いたことがあるかも知れない。
そういう諸兄のために、いくつかのフィルタレスポンスを比較して視聴できるページを作った。
係数計算の部分はそれぞれ npm package として使える。
デモページでは以下の4種類を比較できる。
- Audio-EQ-Cookbook.txt - フィルタ係数計算式の定番。他のやつもこれがベースになっている。
- Max/MSP filtergraph~ - Max/MSPで使われるやつ。
- Web Audio API v1 - Web Audio API で Firefox, Safari で使われているやつ。
- Web Audio API v2 - Web Audio API のアップデート版。今のところ Chrome のみ対応している。
ついでにCookbookと比較したそれぞれの違いをまとめる。グラフは横軸が周波数(100Hz, 1000Hz, 10000Hzに線が引いてある)で縦軸が強度(dB)となっている。
lowpass, highpass
- Web Audio API の
Q
は効きが悪い。(仕様に記述あり、デシベルとほぼ一致する) - Web Audio API の v1 と v2 でちょっとだけ違う。
- Max/MSP は
gain
でグラフ全体を持ち上げることができる。
(lowpass, freq: 1000, Q: 6, gain: 2)
bandpass, notch, allpass
(bandpass, freq: 1000, Q: 6, gain: 2)
peaking
(peaking, freq: 1000, Q: 6, gain: 2)
lowshelf, highshelf
- Web Audio API は
Q
が無効で、スロープを作れない。 - Web Audio API の v1 と v2 で違いはない。
- Max/MSP は
Q
の計算方法が違っていて、他のよりも強めになる。
(lowshelf, freq: 1000, Q: 6, gain: 2)
resonant
- Max/MSP にのみあって便利。
(resonant, freq: 1000, Q: 6, gain: 2)
オレのATOM
僕はエディタにAtomを使っている。ほぼデフォルトのまま使っているのだけど、atom-runner というAtomパッケージは超重宝している。これは開いているファイルを Ctrl+R
だか Alt+R
でAtomの中で実行してくれるというもので少し設定して次のように使っている。
textlint
textlintというテキスト向けLintツールをatom-runnerで実行する。
Atomの config.cson
を次のように設定して *.md
ファイルを対象にtextlintを実行している。
"*" runner: extensions: md: "textlint"
textlint自体はグローバルインストールしてある。ルールはプロジェクトルートの .textlintrc
を参照してくれる。
$ npm i -g textlint textlint-rule-preset-ja-technical-writing
テスト
僕はJavaScriptのテストランナーに Mocha を使うことが多い。しかし、MochaはグローバルスコープにAPIを置くので、そのままだと実行できない。困る。例えば単純に次のようなテストコードを実行しようとすると describe
がないと怒られる。
const assert = require("assert"); describe("test", () => { it("ok?", () => { assert(true); }); });
ReferenceError: describe is not defined
そこで run-with-mocha というモジュールを作った。
これは、テストコードを実行した時にMochaのAPIが見つからないと、そのファイルを引数に mocha
コマンドで再実行してくれる。説明を変えると、このモジュールを require しておくと node test.js
で mocha test.js
を実行できる。
require("run-with-mocha"); const assert = require("assert"); describe("test", () => { it("ok?", () => { assert(true); }); });
test ✓ ok? 1 passing (132ms)
気軽な気持ちでテストができて大変便利。
日常的に Babel を使っていて、Mocha以前にシンタックスエラーだよ〜(^_^;)という人は次のように設定すると良い。
"*" runner: extensions: js: "babel-node"
$ npm i -g babel-cli
3Dシーケンサーをつくった
Web Music Hackathon で @aike1000 さんが作っていた 3Dシーケンサー を一人ハッカソンがてら自分でも作ってみた。いちおう出来たといえるレベルにはなっていて優しい感じの音が聴けます。
https://mohayonao.github.io/cubic-sequencer/
アイデア
8x8x8の3次元配列をシーケンスデータのコンテナとして使い、それをピアノロールにみたてた8x8の行列(行が音程、列が時間を表す)と8つのシーン(切り替えできるパターン)とみなす。これはXYZ軸のそれぞれの視点から取り出すことができるので、それをピアノ、パッド、ビープの3つのトラックとする。
使い方
右側の操作パネルを使います。
メインコントローラ
アプリケーション全体の設定
- PLY: 再生のON/OFF
- RND: パラメータを適当に設定
- CLR: パラメータのクリア
- BPM: テンポ
- AXIS: 視点(トラック)の移動
- → 赤: ピアノ / 緑: パッド / 青: ビープ
トラックコントローラ
各トラック(軸)ごとの設定
- PITCH SHIFT: 音高の調整
- LOOP LENGTH: ループの長さ
- → 2に設定すると1列目と2列目を繰り返す
- NOTE LENGTH: 音の長さ
- → 全音符 / 2分音符 / - / 4分音符 / - / - / - / 8分音符
- SCENE: シーンの切り替え
- MATRIX: ピアノロール(音程x時間軸)
使った技術
コントローラ部分は React と Redux、左側のグラフィックは Three.js。音は Web Audio API でサイン波とゲインだけを使用。音のスケジューリングは次の記事とそれを実装したライブラリを使っている。
- 2 つの時計のお話 - Web Audio の正確なスケジューリングについて - HTML5 Rocks
- https://github.com/mohayonao/web-audio-scheduler
ソースコード
課題
作業時間は 6時間+3時間 の休憩という感じでやったのだけど、間に合わなかった機能があったり無駄に時間を取られたりした箇所があった。
MIDIコントローラ
LaunchPad をコントローラとして使えるようにしたかったけど、音のタイミングとLED点滅のタイミングを同期させることとかを考えると、ややこしそうだったのでやめた。具体的には上述の音のスケジューリングの記事のやり方だと音は100msくらい先に予約するので、そのタイミングでLEDを点滅させると微妙にずれる。three.jsを使ったビューワーはそのあたりも考慮したのだけど、同じようなことをMIDIコントローラー向けにもやるのがちょっと面倒そうだった。このあたりは良い方法を考えたい。
3D Panner
ハッカソン時のコメントで音を3次元に配置すると良さそうというコメントがあったので、やろうと思ってたけど、ビューワーで見たままに音を配置しても気持ち悪そうだったのでやめた。ただ、音を立体的に配置するというアイデアは良さそうなので、いい感じに使う方法を考えたい。
Three.js
3Dで考えるのが苦手で軸を選択して回転させるだけのところですごく時間がかかった。Three.js本でも買おうかなと思う。
初めてのThree.js 第2版 ―WebGLのためのJavaScript 3Dライブラリ
- 作者: Jos Dirksen,あんどうやすし
- 出版社/メーカー: オライリージャパン
- 発売日: 2016/07/23
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (2件) を見る
あとは、React/ReduxでのComponentの分割具合をミスった感がある。練習が必要。
web-audio-lesson : Web Audio API初学者向けのテキストを書きました
ひょんなきっかけでIAMASでウェブオーディオの使い方をレクチャーすることになったので、それに合わせて用意したものです。
初学者向けということで、基礎の部分を中心にできるだけ興味をもってもらうことを目的にしています。
主に次の内容を扱っています。
- オーディオコンテキスト
- オーディオノード
- ライフタイム
- オーディオパラメーター
- OscillatorNode
- GainNode
- AudioBufferSourceNode
- システム構成
僕の興味のせいか内容がかなり偏っている気がするのですが、これからウェブオーディオを始めたい!という人のガイドになれば良いかと思います。
肝心の講義については、普段全然しゃべらない人がいきなりたくさん喋ったみたいな状態になってしまった。アットホーム?な雰囲気だったのでまぁどうにかなったのかなという感じでした。なんでもそうだけど普段から練習するしかないですね。
書くときに参考にしたものとか
Web Audio APIについてはこのあたりで勉強した
読みながら実行できる構成というのはこのあたりを参考にした
文章の書き方やチェックについてはツールを使った
写真
- 作者: 結城浩
- 出版社/メーカー: 筑摩書房
- 発売日: 2013/04/11
- メディア: 文庫
- この商品を含むブログ (29件) を見る
今日からはじめる CLI 音楽入門 2
コマンドラインからピロピロ鳴らせると良い。バックグラウンドにあったりして見えないターミナルの状態を音を聴くことで判断できるかもしれないし、判断できなかったとしても単純に音が鳴ると楽しい。
前回の記事では既存のコマンドの組み合わせでテキストファイルから音を取り出しました。
とはいうものの、この方法ではデータの内容がそのまま音となるため欲しい情報を欲しい音で得ることができません。そこで、今回の記事では自分でプログラムを書いて音を生成する方法について説明します。プログラムは Node.js で書いていますが、特別なパッケージは使わないので他の言語への移植/実行も難しくないはずです。
復習: KEN_ALL.CSV を聴く
前回の復習がてら KEN_ALL.CSV の音を聴いてみましょう。ここでは改行以外の文字を "x" に変換してインパルスを出力しています。("x" を 0、改行を 1 とみなした信号を生成しています。) 長い行が続くと低い音、短い行が続くと高い音に聴こえます。
cat KEN_ALL.CSV | awk '{ gsub(/./,"x"); print $0 }' | play -t u8 -c 1 -r 44100 -
プログラムでやってみる
同じことをプログラムでやってみます。さきほどのコマンドで awk は stdin からの入力を "x" か 改行 のどちらかに変換して stdout に 文字(u8) として出力していました。それと同じように stdin からデータを読み込んでオーディオ信号(f32) に変換して stdout から出力するだけです。
// nl-impulse.js "use strict"; const NL = "\n".charCodeAt(0); // (1) stdin からデータを読み込んで process.stdin.on("data", (chunk) => { const output = new Float32Array(chunk.length); // (2) Float32Array にオーディオ信号を書き込んで for (let i = 0; i < output.length; i++) { output[i] = chunk[i] === NL ? 1 : 0; } // (3) Buffer に変換して const buffer = Buffer.from(output.buffer); // Buffer.from がエラーになる場合は new Buffer(output.buffer) とする // (4) stdout に出力する process.stdout.write(buffer); });
このように実行できます。(注: 出力が float なのでオプションは -t f32 で指定します)
cat KEN_ALL.CSV | node nl-impulse.js | play -t f32 -c 1 -r 44100 -
もし Linux ユーザで ALSA aplay コマンドを使いたいなら、こう書きます。( -t f32 を -f float_le とする )
cat KEN_ALL.CSV | node nl-impulse.js | aplay -f float_le -c 1 -r 44100
オーディオループをつくる
しかしながら、このシンプルな方法は出力が文字からオーディオ信号になったたけで、本質的には入力を変換して出力するだけの構造は変わっておらず、いくらプログラムを改良してもデータに対して欲しい音を得ることができないという問題は解決できません。
そこで、オーディオループを導入して入力と出力を分離します。入力と出力を分離することで、入出力の依存関係が弱まり、入力を単なるトリガとして扱えるようになるので、よりデータの特徴を拾いやすい音作りが可能になります。
オーディオループは以下のように定義します。基本的な設定、信号処理のための非同期ループ、そして実際に信号処理を行うためのコールバックを持つ関数です。
// audioloop.js "use strict"; module.exports = (channels, blockSize, sampleRate, callback) => { let currentTime = Date.now() * 0.001; const loop = () => { // 0.1秒以上先はレンダリングしない if (currentTime < (Date.now() * 0.001) + 0.1) { const output = new Float32Array(channels * blockSize); callback(output, currentTime); currentTime += blockSize / sampleRate; const buffer = Buffer.from(output.buffer); // Buffer.from がエラーになる場合は new Buffer(output.buffer) とする // 書き込みバッファがいっぱいになったら 'drain' (書き込み再開可能) まで待機 if (!process.stdout.write(buffer)) { return process.stdout.once("drain", loop); } } setImmediate(loop); }; setImmediate(loop); };
このように使います。簡単のため入力は割愛しています。
// noise.js "use strict"; const audioloop = require("./audioloop"); // channels, blockSize, sampleRate, callback audioloop(1, 128, 44100, (output, playbackTime) => { // ここで信号処理をする for (let i = 0; i < output.length; i++) { output[i] = Math.random() * Math.sin(playbackTime * 0.250); } });
node noise.js | play -t f32 -c 1 -r 44100 -
もう一度 KEN_ALL.CSV を聴く
それでは、オーディオループを使用して KEN_ALL.CSV を聴いてみましょう。新しい例では awk で各行の長さを 0.1秒ごとに出力して、それをMIDIノート番号としてサイン波の周波数を変えながら出力します。最初のプログラム例とは逆で長い行は高い音、短い行は低い音になりますが、最初の例と比較すると同じ長さの行が連続している部分など、よりデータの特徴が掴みやすくなっていると思います。
// midi.js "use strict"; const audioloop = require("./audioloop"); let phase = 0; let phaseIncr = 0; let amp = 0; audioloop(1, 128, 44100, (output) => { for (let i = 0; i < output.length; i++) { output[i] = Math.sin(phase) * amp; phase += phaseIncr; amp *= 0.9999; } }); process.stdin.on("data", (chunk) => { const freq = 440 * Math.pow(2, (+chunk - 69) / 12); phaseIncr = (freq / 44100) * 2 * Math.PI; amp = 1; });
cat KEN_ALL.CSV | awk '{print length($0)-24; system("sleep 0.1")}' | node midi.js | play -t f32 -c 1 -r 44100 -
シーケンサーとしてつかう
ちなみに、入力と出力が独立していることで継続的な音の生成や編集も可能になります。以下の例では tail -f を使って数列ファイルを編集するたびにその音列を再生します。これはデバッグをするときに非常に役立つテクニックです。
79 78 74 69 68 76 80 84
tail -f seq.txt | awk '{print $0; system("sleep 0.15")}' | node midi.js | play -t f32 -c 1 -r 44100 -
テスト失敗のアラートを聴く
最後に実用的な例を紹介します。mocha --watch でファイルに変更があるたびにテストを実行して、失敗した場合 (この例では AssertionError という出力があった場合) に音を出します。notification などを画面に表示することなくテストの失敗を通知してくれてるので、画面を見なくてもテストの失敗が分かりますし、大きな音を出せば離れた場所にいる人にもテストが失敗したことが伝わります。
音しか出力しなくなるとどのテストが失敗したのか分からなくなるので、本来の出力は stdout でなく stderr に出力していますが、カラー表示がなくなってしまうのはどうすれば良いんだろ?
// mocha-alert.js "use strict"; const audioloop = require("./audioloop"); let phase = 0; let phaseIncr = 0; let amp = 0; audioloop(1, 128, 44100, (output, playbackTime) => { for (let i = 0; i < output.length; i++) { output[i] = Math.sin(phase) * amp; phase += phaseIncr; amp *= 0.99975; } phaseIncr = playbackTime % 0.1 + 0.3; }); process.stdin.setEncoding("ascii").on("data", (chunk) => { if (/AssertionError/.test(chunk)) { amp = 1; } process.stderr.write(chunk); });
mocha --watch test.js | node mocha-alert.js | play -t f32 -c 1 -r 44100 -
なるほど〜、といった感じですが、もっと複雑な音を出したいときはどうすればよいのでしょうか? もちろん audioloop の中で信号処理のテクニックを駆使してえげつない音を出すことは可能ですが、もっと簡単にえげつない音を出したいという気持ちもあると思います。
というわけで次回はもう少し簡単な方法で今回より複雑な音を出す方法について説明します。
今日からはじめる CLI 音楽入門 1
コマンドラインからピロピロ鳴らせると良い。バックグラウンドにあったりして見えないターミナルの状態を音を聴くことで判断できるかもしれないし、判断できなかったとしても単純に音が鳴ると楽しい。
そこで、この記事では Mac/Linux でコマンドラインから音を出すための方法について説明します。ただし、Windows については調査不足のため言及しません。
play コマンドをインストールする
sox という音をあれこれするためのツールがあります。 ffmpeg の音声ファイル限定版みたいなものですが、これをインストールすれば play
という再生コマンドがついてくるのでこれを利用します。
Linux を使っていて ALSA aplay コマンドがある場合は sox はなくても構いません。
ReadMe を聴いてみる
では、さっそく適当なファイルから音を生成して聞いてみます。サブディレクトリに README.md が大量に見つかりそうなディレクトリに移動して以下のコマンドを実行します。耳に悪い音がする可能性があるので、念のためにイヤホンではなくボリュームをしぼったスピーカーで聞くほうが良いかと思います。
find . -name "README.md" | xargs cat | play -t u8 -c 1 -r 4000 -
おそらくローパスフィルタのかかったホワイトノイズのような音が聞こえたと思います。これが基本の形です。
それぞれのコマンドの説明はしませんが find から cat までで、カレントディレクトリ以下の README.md の内容を結合して出力、それを u8 (unsinged char) の音声信号として play コマンドで再生しています。
play コマンドのオプションは以下の通り。
- -t : データの形式 (今回は unsigned char)
- -c : チャンネル数 (今回はモノラル)
- -r : サンプルレート (今回は 4000Hz)
- - : stdin から入力する
もし Linux ユーザで ALSA aplay コマンドを使いたいなら、こう書きます。( -t を -f とすれば良いです )
find . -name "README.md " | xargs cat | aplay -f u8 -c 1 -r 4000
KEN_ALL.CSV を聴いてみる
さて、最初の例ではあまり面白みがなかったので、もう少しマシなデータを試してみましょう。 KEN_ALL.CSV という日本郵便が配布している郵便番号データがあります。量も多いしデータの特徴的にも都合が良いので、このデータを使ってみましょう。 「読み仮名データの促音・拗音を小書きで表記するもの - 全国一括」のデータをダウンロードします。
解凍して以下のコマンドを実行します。
cat KEN_ALL.CSV | play -t u8 -c 1 -r 4000 -
最初の例と比べるとうっすら何かが聞こえてきます。いけそうな感触があるとテンション上がりますよね。 ですが、まだまだいまいちなので、もうちょっと工夫して5列目のデータを取り出して聴きいてみます。
cat KEN_ALL.CSV | awk -F , '{print $5}' | play -t u8 -c 1 -r 4000 -
かなり音楽っぽいものが聴こえてきたと思います。なぜこうなるのかは説明しづらいですが、同じデータが繰り返されるとそれが周期となって音程感が出るとかそういうやつです。一番最初の ReadMe だとほぼランダムに文字が出てくるのでパターンがみられずホワイトノイズっぽくなり、KEN_ALL.CSV だとある程度パターンがあるので何かが聴こえる感じ、KEN_ALL.CSV の5列目は市区町村の項目なのでよりパターンが強調されてメロディーが聴こえる。
データがどうなっているのか目視したい場合は head とかを使えば良いです。(KEN_ALL.CSV は Shift_JIS なので nkf で UTF-8 に変換しています)
cat KEN_ALL.CSV | nkf | awk -F , '{print $5}' | head
"サッポロシチュウオウク" "サッポロシチュウオウク" "サッポロシチュウオウク" "サッポロシチュウオウク" "サッポロシチュウオウク" "サッポロシチュウオウク" "サッポロシチュウオウク" "サッポロシチュウオウク" "サッポロシチュウオウク" "サッポロシチュウオウク"
この「"サッポロシチュウオウク"」が 15bytes くらいあるので、その繰り返しが 4000Hz / 15 = 266Hz くらいの音程となり、市区町村の大きさに応じて持続時間が変化しているわけです。
ちなみに6列目なんかはそのまま聴いても面白くないですが、grep コマンドを経由させると非常に緊張感のある良いサウンドが聴けたりします。以下の例では各階に郵便番号がふってあるような高層ビルの名前から音を生成しています。
cat KEN_ALL.CSV | nkf | grep '[0-9]カイ' | awk -F , '{print $6}' | play -t u8 -c 1 -r 4000 -
このあたりは色々試行錯誤してみると面白いのではないでしょうか?
とりあえず今回はここまで。次回はプログラムを書いて好きな音を出してみます。