VEDA 2.4: GLSLで音楽を演奏できるようになったぞ!!!

こんにちは id:amagitakayosi です。
Atom用GLSL実行環境 VEDA を開発しています。

github.com

昨日リリースしたVEDAの最新版で、GLSLで音楽を演奏できるようになりました!

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上で利用できます。

loadSound にテクスチャ名と時刻を渡すと、音声ファイルのその時刻の値が取得できます。 時刻の値を変更することで、再生速度を変更したりもできます。

実装解説

先月のWebGLアドベントカレンダーの記事で、ShadertoyのSound Shaderの実装について解説しました。

blog.gmork.in

VEDAでは、レンダリングのタイミング等は少々異なりますが、基本的にはShadertoyと同じ処理をしています。
大まかな流れは以下の通りです。

  1. 必要なサンプル数を計算する
  2. 音声バッファを作成し、再生を開始する
  3. 必要なサンプルが集まるまで、以下を繰り返す

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の場合)。

AtomDAWになる日も近い……!!!

ところで

VEDAはもうすぐ100 stars!!
いますぐ fand/veda を Star してください!!!