JavaScriptでリアルタイムに音を出すときに簡単便利なやつつくった


先日作成した「関西電気保安協会リズムマシーン」と「ONE-LINER-ORCHESTRA」が

で使えるようになりました。


どちらも pico-player.js っていう拙作ライブラリを使っているんだけど、普通に便利なので紹介します。
https://gist.github.com/1342081
CoffeeScriptで書いてコンパイルしています。もうJavaScript書けなくなってきました。

2011/11/08追記
別タブ選択時に音が途切れる減少を解消しました。
関連記事: http://d.hatena.ne.jp/mohayonao/20111108/1320756534

2011/11/07 追記
CoffeeScriptで書いた分は無駄が多いので、JavaScriptで書き直したバージョン更新しました。


具体的にどんなことをしているのかは以前に書いた記事が参考になるかも知れません。
JavaScriptでリアルタイムに音を鳴らす方法を3つほど - つまみ食う

使い方

これだけ、非常に簡単。

// ブラウザに応じたプレイヤーオブジェクトを生成
var player = pico.getplayer(options);

// 引数はオブジェクトで渡す。以下はデフォルト値。
options = {
  samplerate: 44100, // サンプリングレート
  channel: 1,        // チャンネル数
  duration: 40,      // ストリームの分割時間(ミリ秒)
  slice: 8           // ストリームセルの分割数
};
// ジェネレータ(後述)を引数に play
player.play(generator);
// 再生中?
player.isPlaying();
// 音を止める
player.stop();
// プレイヤーの種類 ["WebKitPlayer", "MozPlayer", "HTML5AudioPlayer"]
console.log( player.gettype() );


playerは以下の不変プロパティを持つ

player.SAMPLERATE         // サンプリングレート
player.CHANNEL            // チャンネル数
player.STREAM_FULL_SIZE   // ストリームのサイズ
player.STREAM_CELL_SIZE   // ストリームセルのサイズ
player.STREAM_CELL_COUNT  // ストリームセルの分割数
player.PLAY_INTERVAL      // ストリームの分割時間(ミリ秒)

この中で気にしないといけないのは、サンプリングレートとストリームのサイズくらいであとはおまけ。
あと、オブジェクト生成時の引数と実際に生成されたオブジェクトのプロパティは異なるので注意が必要。たとえば、Chrome(Web Audio API)はサンプリングレート固定なので、起動時の引数は無視されるし、Opera(HTMLAudioElementを大量生成)は他の2つに比べて無理やり処理している関係でストリームの分割時間が長め(400ミリ秒以上)に設定される。このへんは十分に検証できないので調整必要かも。


ジェネレータ

  • ジェネレータは実際に音のシグナルを出力するためのオブジェクト
  • player.play(generator) を実行すると、generator.next が定期的に呼ばれる
  • generator.next は player.STREAM_FULL_SIZE の大きさの Float32Array (-1.0 <= signal <= +1.0)を返す
  • チャンネル数が 2 のときは [L, R, L, R, .. ] という配列を返せばよい


以下はウェーブテーブル方式のジェネレータのサンプル。
あらかじめ音の波形の配列を用意しておいて、配列の要素を飛ばす大きさで音の高さをコントロールしてる。

var TABLE_SIZE = 1024;
var sinetable = new Float32Array(TABLE_SIZE);
for (var i = 0; i < TABLE_SIZE; i++) {
    sinetable[i] = Math.sin(2 * Math.PI * (i / TABLE_SIZE));
}

var ToneGenerator = function(player, wavelet, frequency) {
    this.wavelet = wavelet;
    this.frequency = frequency;
    this.phase = 0;
    this.phaseStep = TABLE_SIZE * frequency / player.SAMPLERATE;
    this.stream_full_size = player.STREAM_FULL_SIZE;
};
ToneGenerator.prototype.next = function() {
    var wavlet = this.wavelet;
    var phase = this.phase;
    var phaseStep = this.phaseStep;
    var table_size = TABLE_SIZE;
   
    var stream = new Float32Array(this.stream_full_size);
    for (var i = 0, imax = this.stream_full_size; i < imax; i++) {
        stream[i] = wavelet[(phase|0) % table_size];
        phase += phaseStep;
    }
    this.phase = phase;
    return stream;
};


// 440Hzのサイン波を再生して 2秒後に止まる
var player = pico.getplayer();
var gen = new ToneGenerator(player, sinetable, 440);

player.play(gen);
setTimeout(function() { player.stop(); }, 2000);


ONE-LINER-ORCHESTRAでは上記のジェネレータみたいなやつを複数管理して加算合成して返すジェネレータを player に与えて複数の音を出している。大量の音を合成している例


上記のサンプルはここで確認できる。
pico-player.js / pico-player.coffee

ちょっと便利

オプション引数に slice を与えると STREAM_CELL_SIZE というのが計算される。
STREAM_CELL_SIZE は STREAM_FULL_SIZE/slice で計算される値で、player 自体の動作には何の影響もないんだけど、あると便利なので入れている。


たとえばモノフォニックなアルペジエータを作っているとき、音程の切り替わるサンプル数は

sample = samplecount = (60/BPM) * SAMPLERATE * (4/LENGTH)

で計算できるので、samplecount-- していって 0 になったら、音程を切り替えれば良いんだけど、1サンプルごとに if で条件判定するのは非効率で、ある程度大雑把(耳では判別できない)に処理したほうが計算量が減って良い。音量とか低位とかも同じ。その大雑把な数字に使えるのが STREAM_CELL_SIZE。

var samplerate = player.SAMPLERATE;
var stream_cell_size = player.STREAM_CELL_SIZE;

var stream = new Float32Array(player.STREAM_FULL_SIZE);
var k = 0;
for (var i = 0, imax = player.STREAM_CELL_COUNT; i < imax; i++) {
    samplecount -= STREAM_CELL_SIZE;
    if (samplecount <= 0) {
       // 音程を切り替えるための処理
       // phaseStep = TABLE_SIZE * new_frequency / samplerate; // みたいなの
       samplecount += sample;
    }
    for (var j = 0; j < stream_cell_size; j++) {
        stream[k++] = wavelet[(phase|0)&TABLE_SIZE];
        phase += phaseStep;
    }
}
return stream;

ブラウザ戦争の結果、JavaScript がいくら高速になったといってもDSP処理は結構重いので計算を省略するのが重要で、最初の例のサイン波も再生時には Math.sin みたいな複雑な計算はリアルタイムに行わないような工夫をしたり、STREAM_CELL_SIZEを使った例みたいに処理単位を減らしたり地道な努力が必要っぽい。

メモ

Operaでは Float32Array が使えないっぽい (pico-player.js内で偽Float32Arrayをでっち上げている)
Operaと同じやり方はSafariでは固まる。ナゾ。
iOS, Androidもダメ。
・上記の例で phaseStep は毎回計算しているけど、 MIDIノート番号の細かい版みたいなのを用意すると効率よい。

# 半音をresolution個に分割した音番号->phseStepのテーブルをつくる
def calc_steps(size=8192, resolution=32):
    center = size >> 1
    def calcStep(i):
        freq = 440.0 * ((2.0**(1.0/(12*resolution)))**(i-center))
        return TABLE_LENGTH * freq / SAMPLERATE
    return tuple(calcStep(i) for i in xrange(size))

大量の風車みたいなインターフェイスのウェブ楽器をつくった

windmills
http://mohayonao.herokuapp.com/windmills

  • くるくる回っているやつをマウスオーバーすると回転速度が変わる
  • スペースまたは左下の「Sound OFF」をクリックで音が鳴る/止まる (Chrome, Firefox)
  • くるくる回っているやつのスピードで音が変わったりする
  • くるくる回っているやつは3つ1組で音の高さ、大きさ、トレモロが操作できる
  • 音は単純に24のサイン波が鳴っているだけ
  • クエリストリングに「?20」とかつけると、くるくる回っているやつの半径が変わる (15-80)
  • くるくる回っているやつの半径が変わると音の数が変わる (3-130)
  • あの世っぽい

関西電気保安協会リズムマシーンの生存戦略について


KSDN-808II 関西電気保安協会リズムマシーン
http://ksdn808.herokuapp.com


機能追加しました。最初のバージョンと比べると以下のことができるようになった。

  • テンポが(たぶん)正確に
    • 以前のやつ、なぜかループ内でループ変数を書き換えていてテンポがおかくなっていたのを直した。
  • Operaでも再生できるようになった
    • wavデータをリアルタイムに作って再生している。リアルタイム感ひくい。
  • ドラム音のピッチを変更できるようにした
    • 波形データ配列のインデックスを捜査ステップを変えているだけ。
  • ドラムキット、ボーカルセットを変更できるようにした
    • データを作るのが面倒くさくなったので、とりあえずドラム2種類と「生存戦略」を追加。
  • 矢印キーでエディター内を移動
    • viスタイルの移動方法だったけど、僕はvi使いじゃなくて分からなくなるので矢印でも動けるようにした。

僕も「非常に短いコードから音楽を生成して再生するやつ」つくった


非常に短いコードから音楽を生成して再生するやつ - hitode909のダイアリー
http://d.hatena.ne.jp/hitode909/20111024/1319204646


これを見て面白かったので僕もやってみた。


ONE-LINER-ORCHESTRA
http://mohayonao.herokuapp.com/one-liner-orchestra



同じものを作ってもしょうがないので、いくつか機能をつけたら、たったひとつの式で全てを生み出す無骨さが無くなった代わりに非常に複雑な音楽が簡単に作れるようになった。


非常に長い周期のノコギリ波はフェードイン/アウトっぽいし、三角波はオートパンっぽい、それらのパラメータも同じような式で操作できたら面白そう。それをターミナルのようなインターフェイスで式(コマンド)を次々に入力していくことで、複数トラックを制御すれば...!!というものです。


Chrome(Web Audio API)とFirefox(Audio Data API)ハイブリッドに動く。
たまにブラウザが落ちる.. そうなったらごめんなさい。


リファレンス

システムコマンド

start プレイヤーの開始
stop プレイヤーの停止


ミキサーコマンド

add (exp) トラックを追加 (addは省略可)
del [id] トラックを削除
sel (id) トラックを選択
ch (id) トラックを選択 (selと同じ)
mute [id] ミュート
solo [id] ソロ

[id]は省略可能、その場合は選択中のトラックに適用される


トラックコマンド

vol (exp) ボリューム1を設定
amp (exp) ボリューム2を設定
pan (exp) パンを設定
pitch [num] ピッチシフトを設定 (1=1オクターブ, 省略時は0と同じ)
filter (NONE,LP,HP,BP,BR) フィルタを設定
cutoff (exp) カットオフを設定
res (exp) レゾナンスを設定
famp (exp) フィルターデプスを設定


exp の部分は t<<1 とかの式を入れられる。単純に数値でもOK。128が真ん中。


つくった曲

冒頭デモのスコア

t*(((t>>12)|(t>>8))&(63&(t>>4)))
filter HP
cutoff t<<1|t>>5
pan t<<3
t<<1|t>>5
pitch 4
filter LP
res t<<1|t>>7
cutoff t<<2
(t<<1)/(~t&(1<<(t&15)))
pan 224
del 1
del 0
amp 64
amp 32
amp 16
amp 8
del
stop

ビート感あるやつ

(t>>4)&((t<<5)|(Math.sin(t)*3000))
filter LP
cutoff t<<1|t>>5
res 224
t<<5|t>>2
pan t<<1|t>>6
pitch 1
amp t

id:hitode909 のモチーフを使った曲

t&(t<<(t/800)) | (t-16000)*((t-16000)<<((t-16000)/800)) | (t-32000)&((t-32000)<<((t-32000)/800))
(Math.sin(t/((t>>10)%7+2))*0.7 + Math.sin(t/((t>>11)%4+3)) + Math.sin(t/((t>>13)%6+4)))*30+128
ch 0
amp t<<5
pan (t>>2)|(t&0x0f0)<<2
filter BP
ch 1
pan 96
(t<<5)|(t>>2)|(t<<3)
amp 64
pan 240

参考

countercomplex: Algorithmic symphonies from one line of code -- how and why?
http://countercomplex.blogspot.com/2011/10/algorithmic-symphonies-from-one-line-of.html


1行のコードからアルゴリズム交響曲 - どのように、そしてなぜ? - 閉村観光
http://d.hatena.ne.jp/miujun/20111003


非常に短いコードから音楽を生成して再生するやつ - hitode909のダイアリー
http://d.hatena.ne.jp/hitode909/20111024/1319204646


Pythonインタプリタを楽器にしてしまう簡単な方法。 - 蟲!虫!蟲! - #!/usr/bin/bugrammer
http://bugrammer.g.hatena.ne.jp/nisemono_san/20111022/1319289787

JavaScriptで画期的なリズムマシーンをつくった

誇大広告です。


2日ほど前に「おまえはアホか」を作って音程だけなんだけど言葉のイントネーションで遊ぶのって面白いと思って、そもそも面白いイントネーションのものを素材にして別のものを作ってみた。


KSDN-808
http://ksdn808.herokuapp.com/

KSDN-808はJavaScriptで作られた4つのドラム音と関西電気保安協会が鳴る実用的なリズムマシーンです。あなたの作成したクールなリズムトラックを保存して共有することができます。無料です。

Web Audio API と Audio Data API を使っていて、ChromeFirefox で動く。


画像が悪くてよく見えないけど、デモ動画も作ってみた。全部キーボードで操作している。


デモソング
http://ksdn808.herokuapp.com/U0Sw7yAZ
http://ksdn808.herokuapp.com/U0SfBFae
http://ksdn808.herokuapp.com/U0S+CY8r

ソースコードとか

heroku + node.js/CoffeeScript + mongoDB という構成。使いやすいので気に入ってる。


ソースコードGitHubに置いた。
https://github.com/mohayonao/ksdn808


メモ

ChromeはWeb Audio APIFirefoxはAudio Data APIを使ってリアルタイムに処理
・Web Audio APIはサンプリングレートを変えられない (48kHzだった)
Chromeでは音が変、Firefoxの音は格好よい(素材のサンプルレートと揃えているため?)
Firefoxでは表示と音がずれる(どんどんずれる)
・テンポが計算よりはやい(分かる程度微妙にはやい)
・別タブを選択すると変になって格好よい
・おまけでフィルタもつけてみた
・UIけっこういけてると思う
・よく考えたら関西ローカルなネタだわ
関西電気保安協会はKSDNじゃなくてKSDHやった..


参考にしたもの

YouTube

「おまえはアホか」をつくった


カラフル鍵盤をクリックすれば様々な音程で喋りますし、あなたのオペレーションを保存したり再生することも出来ますのでアホな友達にアホと伝えることができてとても便利。無料です。


おまえはアホか



おまアホサンプル
http://hotbros.herokuapp.com/300SpaV
http://hotbros.herokuapp.com/300klBYE

ソースコードとか

heroku + node.js/CoffeeScript + mongoDB という構成。


ソースコードGitHubに置いた。
https://github.com/mohayonao/hotbros


サーバー、クライアントどちらもCoffeeScriptで書いて、それぞれJavaScriptコンパイルして動かしている。wcオプションで自動コンパイルするようにすれば楽で良い。

$ coffee -wc app.coffee public/javascripts/main.coffee


ついでにサーバー側のスクリプトはnode-devで更新するたびに再起動するようにしておくと楽できる。

$ node-dev app.js

技術っぽいこと

・リアルタイムにAudioオブジェクトを作って再生
・事前に各音のwaveデータを用意しておいてfloatの配列で持っておく
・配列のインデックスを位相として捉えて位相の増分(配列をどう捜査するか)で音程を出す
・位相が2ずつ増えるなら元の音の1オクターブ高くなるし、0.5ずつ増えるなら1オクターブ低くなる
・1.5ずつ増えるなら5度上の音だし、0.75ずつ増えると4度下の音になる
・原音から離れるにつれて不自然さが大きくなる
・音程によって持続時間が異なることになるけど気にしない
・本当は配列の真ん中あたりをループするなりして長さを揃うようにするのがベター


参考にしたもの

node.jsのプログラムを自動で再起動してくれるnode-devコマンド
http://d.hatena.ne.jp/replication/20110224/1298474534


JavaScriptで音を鳴らす方法を3つほど
http://d.hatena.ne.jp/mohayonao/20110808/1312803835


おまえはアホか
http://fc2.in/s60195

DevQuiz2011のコードを晒してみる

やったのはウォーミングアップ、Web Game、Go!とスライドパズル。スコアが120.8だった。

Web Game

https://gist.github.com/1210841#file_web_game.js

Chrome Extensionとかダルそうやなーと思いながら、HTMLソースを見ると普通に色の情報が書いてあったので目視で10問くらい解いた後、カードが増えてきたのでブックマークレットを書いた。

Go!

https://gist.github.com/1210841#file_golang.go

Goが出始めたときにインストールしたっきり触ってなかったけど問題が簡単そうだったので選んだ。でもコンパイルの仕方がわからなかったり、リファレンスどおりやってもコンパイルできなかったりかなり悩むはめに...

このページがなかったらクリアできなかった
http://golang.jp/install#releases

スライドパズル

https://gist.github.com/1210841#file_slidepuzzle.py

幅優先探索でちょっとやってみたら全然解けなかったので、ちゃんと調べようと思ったけど面倒くさくなったので適当に優先順位つけるようにしたら、思ったよりは回答してたのでそれでいいやってなった。夜に動かす→仕事行く→帰ったら止める。みたいなやりかたで2,3回動かした。

やってみて

一応ボーダーは超えてるっぽいけど、プログラム書く友達とかいないしそういうイベント行ったことないので怖いです。