ヤッターGLSLで音が鳴るぞ!!
この記事はWebGL Advent Calendar 2017 の15日目の記事です。
こんにちは、 id:amagitakayosi です! AtomでGLSLをライブコーディングできるパッケージを作ったりしています。
今日はShadertoyのコードを読んで、Three.jsで真似してみる、ということをやってみました。
経緯
ShadertoyにはGLSLで音声を出力できる機能があります。(以下 Sound Shader
と呼びます)
デモ用の効果音を生成したり、以下のようにガッチリ曲を作ったりできます。
しかし、その仕組みがどのようになってるか、どこにも解説されていない! というわけで、Shadertoyのコードを読みつつ、ちゃんと理解するためにThree.jsで実装してみました。
解説
ShadertoyのSound Shaderは、以下の2つのメソッドを読むと仕組みが分かります。
- EffectPass.prototype.Create_Sound
- EffectPass.prototype.Paint_Sound
Create_Sound
(https://www.shadertoy.com/js/effect.js の243行目〜)
ここでは、Sound Shaderを実行するための準備を行っています。
Sound Shaderではメイン関数を vec2 mainSound
として書きます。
xが左、yが右チャンネルです。
Create_Soundでは、ユーザーが書いたSound Shaderに、main関数を含む文字列を結合します。 main関数を読むと、ピクセルごとに異なる引数でmainSoundを呼び出していることがわかります。
レンダリングするテクスチャのサイズは 512x512
であることがわかります。
音声のサンプルレートは 44100
なので、SoundShaderは1回のレンダリングで約6秒の音声データを作成できるようですね。
(512 * 512 / 44100 ≒ 5.94
)
また、 this.mPlayTime = 60*3;
とある通り、Sound Shaderが生成できる音声には180秒までの制限があるようです。
Paint_Sound
(https://www.shadertoy.com/js/effect.js の1900行目〜)
Paint_Soundは、シェーダーの変更後はじめてPaintが実行されたとき呼ばれるようになっています。 つまり、Paint_Soundは1回の実行で180秒分の音声データをすべて作成します。
uniform変数をガチャガチャ設定してる部分はどうでもいいので読み飛ばすと、forループがネストした箇所が見つかります(2053行目〜)。 ここが音声生成のコア部分です。
やってることは単純です。
外側のループは、180秒分の音声データが生成し終わるまでレンダリングを繰り返す、というループです。
内側のループでは、レンダリング結果の各ピクセルの値を音声データに変換し、AudioBufferに詰める、ということをやっています。
この時、 bufL[off+i] = -1.0 + 2.0*(this.mData[4*i+0]+256.0*this.mData[4*i+1])/65535.0;
とある通り、一つのサンプルを2バイトで表現しています。
mainSound
が返すのはvec2でしたが、Create_Soundで追加されたmain関数の中身をよく見てみると、 以下のように上位ビットと下位ビットに分けて出力していることがわかります。
vec2 vl = mod(v,256.0)/255.0; vec2 vh = floor(v/256.0)/255.0; gl_FragColor = vec4(vl.x,vh.x,vl.y,vh.y);
これはおそらく、GLSLの世界では音声はfloatとして表現されるけれど、出力すると1バイト (0 ~ 255) に丸められてしまい、音声として使い物にならないという問題があったのだと思います。 そのため、1サンプルをr+g or b+aの2バイトを使って表現することで、より精度の高い音声出力を実現したのでしょう。
ループが終わったら、AudioBufferSourceNodeをstartして、音声を再生しています。
感想
Three.jsでもシュッと実装できて楽しかった。 あとShadertoyのコード意外と読みやすい。
やり方はわかったので、VEDAにも機能追加しようと思います。 音声をループ再生したり、音声の長さを指定できたりすると、ライブコーディングでの音楽パフォーマンスが楽しくなりそうですね。