WebAudioとThree.jsでSpectrogramつくる
#WebAudio #Three.js #DSP #GLSL #シェーダー
WebAudioとThree.jsを組み合わせてSpectrogramを作ります
Visualizing Sound as an Audio Spectrogramとかを参考にしつつ
完成品
https://gyazo.com/651bd158f70bfef615e9c46870e1f424
https://codesandbox.io/p/sandbox/794wyr
WebAudioパート
AnalyserNode.getFloatFrequencyData()を使う
返り値の横軸は、 sampleRate の半分(24000 Hzとか)までの周波数を線形に収録
サイズは AnalyserNode.fftSize で指定した値の半分まで
例えば、横軸に12音階やオクターブなどを等間隔で表示しようとすると、周波数領域は線形でなく対数で表示しなければいけない
$ \log_2(f_i) = {\rm lerp} \left( \log_2(f_0), \log_2(f_1), \frac{n_o-1}{N_o-1} \right)
みたいな感じで出力のインデックスと入力のインデックスをマッピングするといいんでないでしょうか
$ f_0・$ f_1はお好みですが、下は10-20Hz・上はサンプリング周波数くらいがいいんじゃないでしょうか
See also: 対数で等間隔
当然上の方法だと、インデックスが整数値にならなくなってしまうので、lerpを使って線形補間して取ってやる
返り値の縦軸は、デシベル値
AnalyserNode.getByteFrequencyData() では、数値が 0 - 255 の範囲で取れるが、これは AnalyserNode.minDecibels と AnalyserNode.maxDecibels の範囲内の値がマッピングされて帰ってきている
これにならって、linearstepを使って 0.0 - 1.0 の範囲内の値にしてやる
また、AnalyserNode.smoothingTimeConstantの値によって見た目が大きく変わる
基本的には 0.0 で良いと思う
code:js
const _intermediate = new Float32Array( analyserNode.fftSize / 2.0 );
analyserNode.getFloatFrequencyData( _intermediate );
for ( let i = 0; i < target.length; i ++ ) {
const t = i / ( target.length - 1 );
const logJ = lerp( 3.0, Math.log2( this._intermediate.length ), t );
const j = Math.pow( 2.0, logJ ) - 1.0;
const jf = j % 1.0;
const ji = Math.floor( j );
const db0 = _intermediate ji ;
const db1 = _intermediate ji + 1.0 ;
const db = lerp( db0, db1, jf );
const v = linearstep( analyserNode.minDecibels, analyserNode.maxDecibels, db );
target i = 255.0 * v;
}
Three.jsパート
Three.js: FullScreenQuadを使って描画を行う
基本アイデアは、横に流れていくバッファをJS側で持って毎回描画なんかしたくないので、フレームバッファに取っておく、というもの
RenderTargetを2枚用意し、それらの間をピンポンさせる
データの書き込みはThree.js: DataTextureを使って行う
1x512みたいな解像度のテクスチャに、さっき作ったデータを詰める
左端のピクセル列のみ、データを書き込む
それ以外のピクセルは、フレームバッファから左隣のテクセルを参照して、横に流してやる
code:glsl
uniform float time;
uniform vec2 resolution;
uniform sampler2D samplerPrev;
uniform sampler2D samplerData;
varying vec2 vUv;
void main() {
float value = 0.0;
if ( vUv.x < 1.0 / resolution.x ) {
value = texture2D( samplerData, vUv ).x;
} else {
value = texture2D( samplerPrev, vUv - vec2( 1.0, 0.0 ) / resolution ).x;
}
gl_FragColor = vec4( vec3( value ), 1.0 );
}
DataTextureでピンポンさせても画面には出力されないので、画面に出力する用のFullScreenQuadをもう一つ作る
この時点ではDataTextureには白黒のデータが入っているが、ついでにこのパスでグラデーションを当ててあげよう
グラデーションにはInferno Colormapを使います
https://www.shadertoy.com/view/WlfXRN が多項式補間を作ってくれているので、これをありがたく拝借する
code:glsl
uniform float time;
uniform sampler2D sampler0;
varying vec2 vUv;
// Source: https://www.shadertoy.com/view/WlfXRN (CC0)
vec3 inferno(float t) {
const vec3 c0 = vec3(0.0002189403691192265, 0.001651004631001012, -0.01948089843709184);
const vec3 c1 = vec3(0.1065134194856116, 0.5639564367884091, 3.932712388889277);
const vec3 c2 = vec3(11.60249308247187, -3.972853965665698, -15.9423941062914);
const vec3 c3 = vec3(-41.70399613139459, 17.43639888205313, 44.35414519872813);
const vec3 c4 = vec3(77.162935699427, -33.40235894210092, -81.80730925738993);
const vec3 c5 = vec3(-71.31942824499214, 32.62606426397723, 73.20951985803202);
const vec3 c6 = vec3(25.13112622477341, -12.24266895238567, -23.07032500287172);
return c0+t*(c1+t*(c2+t*(c3+t*(c4+t*(c5+t*c6)))));
}
void main() {
float value = texture2D( sampler0, vUv ).x;
gl_FragColor = vec4( inferno( value ), 1.0 );
}