読者です 読者をやめる 読者になる 読者になる

音の鳴るブログ

鳴らないこともある

node-midiで仮想MIDIデバイスを作る

Kyoto.js #11 というのがあって、せっかくなので最近ぽちぽち作っていた「node-midiで仮想MIDIバイスを作る」という内容で LT しました。他には GitBook とか arduino と Node.js で格闘してる話とか自分も似たような事をしそうだな〜的なものから、あんまり知らない話とかあって良かった。戦ってる話が多かった気がする。

kyotojs.connpass.com

要約するとMIDIバイスって便利だけど持ってないと使えなくて困る。でも node-midi というパッケージを使えば、仮想MIDIポートが作れて、それだとパソコンを騙してMIDIバイスを持っていることにできるので Electron で UI をつけたら万事オッケー。三つくらい作ってやったわー。くぅ〜。という内容です。

speakerdeck.com

使ったライブラリ

作ったもの

念のために言っておくと仮想MIDIバイス最高!!ハードウェアなんかいらんかったんや。というのはなくて、やっぱり触った感じとかやってる感はハードウェアデバイスでないと得られないと思っています。高級箱ティッシュと駅で配っているポケットティッシュくらい差がある。今回は特別にオススメのMIDIバイスのリンクを貼っておくので2つ3つほど試してみたら良いんじゃないでしょうか。

Novation MIDIコントローラー Launch Control

Novation MIDIコントローラー Launch Control

Novation パフォーマンスコントローラ Launchpad MK2

Novation パフォーマンスコントローラ Launchpad MK2

エリエール 贅沢保湿 200組400枚×3箱入り パルプ100%

エリエール 贅沢保湿 200組400枚×3箱入り パルプ100%

食べたもの

飲み物とピザをもらったんだけど写真はそれらとは関係なくて、前後に食べたサンドイッチとフランクフルトです。なんか写真がいまいちなのが残念です。ごちそうさまでした。

f:id:mohayonao:20161022115437j:plain

f:id:mohayonao:20161022182508j:plain

BiquadFilterを聴きくらべる

Web Audio APIのBiquadFilterNodeで設定するQの効きが悪いと思ったことがある。もしくは、Max/MSPのパッチを渡されてJavaScriptで再現しないといけないっ!!となった時に、音の雰囲気がどうも違ってて悩むというウェブオーディオあるあるの噂を聞いたことがあるかも知れない。

そういう諸兄のために、いくつかのフィルタレスポンスを比較して視聴できるページを作った。

係数計算の部分はそれぞれ npm package として使える。

デモページでは以下の4種類を比較できる。

ついでにCookbookと比較したそれぞれの違いをまとめる。グラフは横軸が周波数(100Hz, 1000Hz, 10000Hzに線が引いてある)で縦軸が強度(dB)となっている。

lowpass, highpass

  • Web Audio APIQ は効きが悪い。(仕様に記述あり、デシベルとほぼ一致する)
  • Web Audio API の v1 と v2 でちょっとだけ違う。
  • Max/MSPgain でグラフ全体を持ち上げることができる。

(lowpass, freq: 1000, Q: 6, gain: 2) f:id:mohayonao:20161002104508p:plain

bandpass, notch, allpass

  • Web Audio API は Cookbook と同じ。
  • Web Audio API の v1 と v2 で違いはない。
  • Max/MSPgain でグラフ全体を持ち上げることができる。

(bandpass, freq: 1000, Q: 6, gain: 2) f:id:mohayonao:20161002104934p:plain

peaking

  • Web Audio API は Cookbook と同じ。
  • Web Audio API の v1 と v2 で違いはない。
  • Max/MSPQ の計算方法が違っていて、他のよりも強めになる。

(peaking, freq: 1000, Q: 6, gain: 2) f:id:mohayonao:20161002105211p:plain

lowshelf, highshelf

  • Web Audio APIQ が無効で、スロープを作れない。
  • Web Audio API の v1 と v2 で違いはない。
  • Max/MSPQ の計算方法が違っていて、他のよりも強めになる。

(lowshelf, freq: 1000, Q: 6, gain: 2) f:id:mohayonao:20161002105551p:plain

resonant

  • Max/MSP にのみあって便利。

(resonant, freq: 1000, Q: 6, gain: 2) f:id:mohayonao:20161002105639p:plain

オレのATOM

僕はエディタにAtomを使っている。ほぼデフォルトのまま使っているのだけど、atom-runner というAtomパッケージは超重宝している。これは開いているファイルを Ctrl+R だか Alt+RAtomの中で実行してくれるというもので少し設定して次のように使っている。

textlint

textlintというテキスト向けLintツールをatom-runnerで実行する。

Atomconfig.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.jsmocha 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/

f:id:mohayonao:20160905091456p:plain:w360

イデア

8x8x8の3次元配列をシーケンスデータのコンテナとして使い、それをピアノロールにみたてた8x8の行列(行が音程、列が時間を表す)と8つのシーン(切り替えできるパターン)とみなす。これはXYZ軸のそれぞれの視点から取り出すことができるので、それをピアノ、パッド、ビープの3つのトラックとする。

使い方

右側の操作パネルを使います。

メインコントローラ

アプリケーション全体の設定

f:id:mohayonao:20160905092906p:plain:w240:left

  • PLY: 再生のON/OFF
  • RND: パラメータを適当に設定
  • CLR: パラメータのクリア
  • BPM: テンポ
  • AXIS: 視点(トラック)の移動
    • → 赤: ピアノ / 緑: パッド / 青: ビープ

トラックコントローラ

各トラック(軸)ごとの設定

f:id:mohayonao:20160905093223p:plain:w240:left

  • PITCH SHIFT: 音高の調整
  • LOOP LENGTH: ループの長さ
    • → 2に設定すると1列目と2列目を繰り返す
  • NOTE LENGTH: 音の長さ
    • 全音符 / 2分音符 / - / 4分音符 / - / - / - / 8分音符
  • SCENE: シーンの切り替え
  • MATRIX: ピアノロール(音程x時間軸)

使った技術

コントローラ部分は ReactRedux、左側のグラフィックは Three.js。音は Web Audio API でサイン波とゲインだけを使用。音のスケジューリングは次の記事とそれを実装したライブラリを使っている。

ソースコード

課題

作業時間は 6時間+3時間 の休憩という感じでやったのだけど、間に合わなかった機能があったり無駄に時間を取られたりした箇所があった。

MIDIコントローラ

LaunchPad をコントローラとして使えるようにしたかったけど、音のタイミングとLED点滅のタイミングを同期させることとかを考えると、ややこしそうだったのでやめた。具体的には上述の音のスケジューリングの記事のやり方だと音は100msくらい先に予約するので、そのタイミングでLEDを点滅させると微妙にずれる。three.jsを使ったビューワーはそのあたりも考慮したのだけど、同じようなことをMIDIコントローラー向けにもやるのがちょっと面倒そうだった。このあたりは良い方法を考えたい。

3D Panner

ハッカソン時のコメントで音を3次元に配置すると良さそうというコメントがあったので、やろうと思ってたけど、ビューワーで見たままに音を配置しても気持ち悪そうだったのでやめた。ただ、音を立体的に配置するというアイデアは良さそうなので、いい感じに使う方法を考えたい。

Three.js

3Dで考えるのが苦手で軸を選択して回転させるだけのところですごく時間がかかった。Three.js本でも買おうかなと思う。

初めてのThree.js 第2版 ―WebGLのためのJavaScript 3Dライブラリ

初めてのThree.js 第2版 ―WebGLのためのJavaScript 3Dライブラリ

あとは、React/ReduxでのComponentの分割具合をミスった感がある。練習が必要。

web-audio-lesson : Web Audio API初学者向けのテキストを書きました

ひょんなきっかけでIAMASでウェブオーディオの使い方をレクチャーすることになったので、それに合わせて用意したものです。

初学者向けということで、基礎の部分を中心にできるだけ興味をもってもらうことを目的にしています。

主に次の内容を扱っています。

  • オーディオコンテキスト
  • オーディオノード
  • ライフタイム
  • オーディオパラメーター
  • OscillatorNode
  • GainNode
  • AudioBufferSourceNode
  • システム構成

僕の興味のせいか内容がかなり偏っている気がするのですが、これからウェブオーディオを始めたい!という人のガイドになれば良いかと思います。


肝心の講義については、普段全然しゃべらない人がいきなりたくさん喋ったみたいな状態になってしまった。アットホーム?な雰囲気だったのでまぁどうにかなったのかなという感じでした。なんでもそうだけど普段から練習するしかないですね。


書くときに参考にしたものとか

Web Audio APIについてはこのあたりで勉強した

読みながら実行できる構成というのはこのあたりを参考にした

文章の書き方やチェックについてはツールを使った


写真

f:id:mohayonao:20160611105004j:plain

f:id:mohayonao:20160611110106j:plain

f:id:mohayonao:20160611113821j:plain

数学文章作法 基礎編 (ちくま学芸文庫)

数学文章作法 基礎編 (ちくま学芸文庫)

今日からはじめる 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

オーディオループをつくる

しかしながら、このシンプルな方法は出力が文字からオーディオ信号になったたけで、本質的には入力を変換して出力するだけの構造は変わっておらず、いくらプログラムを改良してもデータに対して欲しい音を得ることができないという問題は解決できません。

f:id:mohayonao:20160512205941p:plain

そこで、オーディオループを導入して入力と出力を分離します。入力と出力を分離することで、入出力の依存関係が弱まり、入力を単なるトリガとして扱えるようになるので、よりデータの特徴を拾いやすい音作りが可能になります。

f:id:mohayonao:20160512205953p:plain

オーディオループは以下のように定義します。基本的な設定、信号処理のための非同期ループ、そして実際に信号処理を行うためのコールバックを持つ関数です。

// 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.CSVShift_JIS なので nkfUTF-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 -

このあたりは色々試行錯誤してみると面白いのではないでしょうか?

とりあえず今回はここまで。次回はプログラムを書いて好きな音を出してみます。