こんにちは id:amagitakayosi です。
Atom用GLSL実行環境 VEDA を開発しています。
昨日リリースしたVEDAの最新版で、GLSLで音楽を演奏できるようになりました!
🔥🔥 VEDA v2.3.0 🔥🔥
— amagi (@amagitakayosi) 2018年1月7日
Sound Shader!!!
Play music by GLSL...😎https://t.co/xH8dz690UN#VEDAJS #GLSL #Atom #Shadertoy pic.twitter.com/6z3tdRgmTY
VEDAでは、この機能を Sound Shader
と呼んでいます。
mainSound
関数に時刻から音声を合成する処理を書くことで、GPU上で音声合成できてしまいます!
Sound Shaderの使い方
mainSound()
関数を定義alt-enter
で実行alt-.
で停止
普通のフラグメントシェーダーや頂点シェーダーを ctrl-enter
で実行すると、映像と音声を同時にGLSLで生成できます。
Shadertoyとの違い
Sound Shader機能は、ShadertoyのSoundバッファと同様の機能です。
基本的にShadertoyのコードがそのまま動きますが、以下のような違いがあります。
長さを変更できる
Shadertoyでは、生成する音声の長さは180秒固定でした。
VEDAでは、実行したいGLSLファイルの先頭に /*{ soundLength: 10 }*/
等と指定することで、生成する音声の長さを変更できます。
ループ再生される
Shadertoyでは、180秒を過ぎたら音声が停止してしまいます。
VEDAでは、音声は soundLength
の長さでループするようになっています。
ループ再生しつつ徐々に演奏内容を変更できるため、よりライブコーディングに向いた仕様となっています。
音声を停止したい時は alt-.
を入力してください。
音声ファイルをロードできる
mp3及びwav形式のファイルをテクスチャとして読み込み、GLSL上で利用できます。
Of course, you can use samples with generated sounds, change the speed, add some effects...... like this 😎 pic.twitter.com/wKur6I0KxU
— amagi (@amagitakayosi) 2018年1月10日
loadSound
にテクスチャ名と時刻を渡すと、音声ファイルのその時刻の値が取得できます。
時刻の値を変更することで、再生速度を変更したりもできます。
実装解説
先月のWebGLアドベントカレンダーの記事で、ShadertoyのSound Shaderの実装について解説しました。
VEDAでは、レンダリングのタイミング等は少々異なりますが、基本的にはShadertoyと同じ処理をしています。
大まかな流れは以下の通りです。
- 必要なサンプル数を計算する
- 音声バッファを作成し、再生を開始する
- 必要なサンプルが集まるまで、以下を繰り返す
1. 必要なサンプル数を計算
soundLength から、必要なサンプル数を計算します。
soundLength = 10, sampleRate = 48000
の時、必要なサンプルは 480000 個となります。
2. 音声バッファを作成し、再生
次に、生成したデータを格納するための音声バッファを作成します。
Web Audio APIの AudioBufferSourceNodeでは、Uint8Array
で音声を生成できます。
今回は Uint8Array(480000)
することになります。
AudioBufferSourceNodeでは、再生開始後にバッファにデータを詰める事もできます。
そのため、レンダリング前に再生を開始することで、ユーザーの操作から再生までのタイムラグを抑えています。
3. レンダリング
続いて、GPUで音声のレンダリングを開始します。
一度のレンダリングでは必要なサンプルが集まらないので、繰り返しレンダリングする必要があります。
VEDAでは、アニメーションのラグを回避するため、一度にレンダリングするサイズを小さくしています。
具体的には 32x64
としているので、音声をすべて生成するには 234 (= 480000 / (32 * 64)) 回のレンダリングが必要です。
レンダリングでは、各ピクセルごとに mainSound(time)
を実行し、各時刻に対応する音声サンプルを計算します。
time
の値は、ピクセルの位置とレンダリング回数から求めます。
例えば、(x,y) = (10, 20) のピクセルの3回目のレンダリングでは、
10 + (20 * 32) + (32 * 64 * 3) = 6794
より、6794個目のサンプルを計算することになります。
レンダリング結果はRGBAで出力されるので、計4byteとなります。
mainSound
の結果はステレオであり、floatの値が2つ生成されますが、画像に出力する際に1byteずつ出力してしまうと精度が悪くなってしまいます。
そのため、次のようにして、各チャンネルの値を2byteに分割して保存するようにします。
vec2 v = mainSound(t); vec2 h = floor(v/256.0)/255.0; vec2 l = mod(v,256.0)/255.0; gl_FragColor = vec4(h.x, l.x, h.y, l.y);
画像を生成したら、これを再度JSで音声に変換してあげます。
outputDataL[i] = (pixels[i * 4 + 0] * 256 + pixels[i * 4 + 1]) / 65535 * 2 - 1; outputDataR[i] = (pixels[i * 4 + 2] * 256 + pixels[i * 4 + 3]) / 65535 * 2 - 1;
これを繰り返すことで、VEDAで alt-enter
を押したら即再生を開始し、アニメーションを続けつつ音声を生成することができるのです。
音声ファイルをロードする実装
音声ファイルは、JS側で画像に変更してテクスチャとしてGPUにロードしています。
この時、画像から音声に変換するのとちょうど逆の処理をしてあげれば良いのです。
// 音声データを格納するUint8Arrayを作成 const array = new Uint8Array(_constants.SAMPLE_WIDTH * _constants.SAMPLE_HEIGHT * 4); // 音声を画像に変換 for (let i = 0; i < c0.length; i++) { const off = i * 4; // -1〜1 の値を 0〜65536 に変換 const l = c0[i] * 32768 + 32768; const r = c1[i] * 32768 + 32768; // 4bytesに分割 array[off] = l / 256; array[off + 1] = l % 256; array[off + 2] = r / 256; array[off + 3] = r % 256; } // Uint8Arrayからテクスチャを作成 const texture = new THREE.DataTexture(array, _constants.SAMPLE_WIDTH, _constants.SAMPLE_HEIGHT, THREE.RGBAFormat);
簡単ですね!
現在、音声テクスチャのサイズは 1280x720
固定にしています。
その為、ロードできる音声の長さは19.2秒までとなっています (サンプルレートが48000の場合)。
ところで
VEDAはもうすぐ100 stars!!
いますぐ fand/veda を Star してください!!!