Shadertoy 的音频着色器如何工作?
How do Shadertoy's audio shaders work?
首先,我真的找不到任何合适的社区来 post 这个问题,所以我选择了这个。我想知道流行的基于 webGL 的着色器工具的音频着色器是如何工作的,因为尽管我显然听说过 'normal' GLSL 着色器,但我第一次听说用于程序生成音频的着色器时,我感到很惊讶。有什么线索吗?
它们基本上是一个函数,为单个音频(左右声道)给出 time
returns 2 个值。值从 -1 到 1。
粘贴到这个着色器中,也许你会得到它
vec2 mainSound( float time )
{
return vec2( sin(time * 1000.0), sin(time * 1000.0) );
}
你可以看到一个更生动的例子a similar style of making sounds here。
你可以这样想象
function generateAudioSignal(time) {
return Math.sin(time * 4000); // generate a 4khz sign wave.
}
var audioData = new Float32Array(44100 * 4); // 4 seconds of audio at 44.1khz
for (var sample = 0; sample < audioData.length; ++sample) {
var time = sample / 44100;
audioData[sample] = generateAudioSignal(time);
}
现在将 audioData 传递给 Web Audio API
对于立体声,它可能是
function generateStereoAudioSignal(time) {
return [Math.sin(time * 4000), Math.sin(time * 4000)]; // generate a 4khz stereo sign wave.
}
var audioData = new Float32Array(44100 * 4 * 2); // 4 seconds of stereo audio at 44.1khz
for (var sample = 0; sample < audioData.length; sample += 2) {
var time = sample / 44100 / 2;
var stereoData = generateAudioSignal(time);
audioData[sample + 0] = stereoData[0];
audioData[sample + 1] = stereoData[1];
}
他们真的没有充分的理由加入 WebGL(假设他们是)。在 WebGL 中,您将使用它们将数据生成到附加到帧缓冲区的纹理中。然后,它们生成的数据必须使用 gl.readPixels
从 GPU 复制回主内存,然后传递到 Web Audio API,这会很慢,至少在 WebGL 中,它会阻止处理为在 WebGL 中无法异步读回数据。最重要的是,您无法轻松地在 WebGL 中读回浮点数据。当然,如果 shadertoy 真的在使用 WebGL,那么它可以 re-write 音频着色器将数据编码为 8 位 RGBA 纹理,然后在 JavaScript 中将其转换回浮点数。更有理由不为此使用 WebGL。使用 WebGL 的主要原因是它只是让它对称。所有 着色器 使用相同的语言。
上面链接的 bytebeat 示例在 JavaScript 中完全 运行。它默认为 bytebeat 意味着函数预期 return 的值是 0 到 255 unsigned int 但是有一个 floatbeat 的设置在这种情况下,它需要一个从 -1 到 1 的值,就像 shadertoy 的着色器一样。
更新
所以我检查了 Shadertoy,它使用 WebGL 着色器并将值编码为 8 位纹理
这是一个实际的着色器(我使用 chrome shader editor 轻松查看着色器)。
precision highp float;
uniform float iChannelTime[4];
uniform float iBlockOffset;
uniform vec4 iDate;
uniform float iSampleRate;
uniform vec3 iChannelResolution[4];
uniform sampler2D iChannel0;
uniform sampler2D iChannel1;
uniform sampler2D iChannel2;
uniform sampler2D iChannel3;
vec2 mainSound( float time )
{
return vec2( sin(time * 1000.0), sin(time * 1000.0) );
}
void main() {
// compute time `t` based on the pixel we're about to write
// the 512.0 means the texture is 512 pixels across so it's
// using a 2 dimensional texture, 512 samples per row
float t = iBlockOffset + ((gl_FragCoord.x-0.5) + (gl_FragCoord.y-0.5)*512.0)/iSampleRate;
// Get the 2 values for left and right channels
vec2 y = mainSound( t );
// convert them from -1 to 1 to 0 to 65536
vec2 v = floor((0.5+0.5*y)*65536.0);
// separate them into low and high bytes
vec2 vl = mod(v,256.0)/255.0;
vec2 vh = floor(v/256.0)/255.0;
// write them out where
// RED = channel 0 low byte
// GREEN = channel 0 high byte
// BLUE = channel 1 low byte
// ALPHA = channel 2 high byte
gl_FragColor = vec4(vl.x,vh.x,vl.y,vh.y);
}
这指出了在这种特殊情况下使用 WebGL 的一个优势,即您可以获得与片段着色器相同的音频着色器输入(因为它是片段着色器)。这意味着例如音频着色器最多可以引用 4 个纹理
在 JavaScript 然后你会用 gl.readPixels
读取纹理,然后用
之类的东西将样本转换回浮点数
var pixels = new Uint8Array(width * height * 4);
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
for (var sample = 0; sample < numSamples; ++sample) {
var offset = sample * 4; // RGBA
audioData[sample * 2 ] = backToFloat(pixels[offset + 0], pixels[offset + 1]);
audioData[sample * 2 + 1] = backToFloat(pixels[offset + 2], pixels[offset + 3]);
}
float backToFloat(low, high) {
// convert back to 0 to 65536
var value = low + high * 256;
// convert from 0 to 65536 to -1 to 1
return value / 32768 - 1;
}
此外,虽然我在上面说过我认为这不是一个好主意,但我认为 shadertoy 一直在调用音频着色器,因此我提出的关于阻塞处理的问题是正确的,但是......显然当您按下播放时,shadertoy 仅 pre-generates N 秒使用着色器的音频,其中 N 显然是 60 秒。所以,没有阻塞,但声音只持续 60 秒。
首先,我真的找不到任何合适的社区来 post 这个问题,所以我选择了这个。我想知道流行的基于 webGL 的着色器工具的音频着色器是如何工作的,因为尽管我显然听说过 'normal' GLSL 着色器,但我第一次听说用于程序生成音频的着色器时,我感到很惊讶。有什么线索吗?
它们基本上是一个函数,为单个音频(左右声道)给出 time
returns 2 个值。值从 -1 到 1。
粘贴到这个着色器中,也许你会得到它
vec2 mainSound( float time )
{
return vec2( sin(time * 1000.0), sin(time * 1000.0) );
}
你可以看到一个更生动的例子a similar style of making sounds here。
你可以这样想象
function generateAudioSignal(time) {
return Math.sin(time * 4000); // generate a 4khz sign wave.
}
var audioData = new Float32Array(44100 * 4); // 4 seconds of audio at 44.1khz
for (var sample = 0; sample < audioData.length; ++sample) {
var time = sample / 44100;
audioData[sample] = generateAudioSignal(time);
}
现在将 audioData 传递给 Web Audio API
对于立体声,它可能是
function generateStereoAudioSignal(time) {
return [Math.sin(time * 4000), Math.sin(time * 4000)]; // generate a 4khz stereo sign wave.
}
var audioData = new Float32Array(44100 * 4 * 2); // 4 seconds of stereo audio at 44.1khz
for (var sample = 0; sample < audioData.length; sample += 2) {
var time = sample / 44100 / 2;
var stereoData = generateAudioSignal(time);
audioData[sample + 0] = stereoData[0];
audioData[sample + 1] = stereoData[1];
}
他们真的没有充分的理由加入 WebGL(假设他们是)。在 WebGL 中,您将使用它们将数据生成到附加到帧缓冲区的纹理中。然后,它们生成的数据必须使用 gl.readPixels
从 GPU 复制回主内存,然后传递到 Web Audio API,这会很慢,至少在 WebGL 中,它会阻止处理为在 WebGL 中无法异步读回数据。最重要的是,您无法轻松地在 WebGL 中读回浮点数据。当然,如果 shadertoy 真的在使用 WebGL,那么它可以 re-write 音频着色器将数据编码为 8 位 RGBA 纹理,然后在 JavaScript 中将其转换回浮点数。更有理由不为此使用 WebGL。使用 WebGL 的主要原因是它只是让它对称。所有 着色器 使用相同的语言。
上面链接的 bytebeat 示例在 JavaScript 中完全 运行。它默认为 bytebeat 意味着函数预期 return 的值是 0 到 255 unsigned int 但是有一个 floatbeat 的设置在这种情况下,它需要一个从 -1 到 1 的值,就像 shadertoy 的着色器一样。
更新
所以我检查了 Shadertoy,它使用 WebGL 着色器并将值编码为 8 位纹理
这是一个实际的着色器(我使用 chrome shader editor 轻松查看着色器)。
precision highp float;
uniform float iChannelTime[4];
uniform float iBlockOffset;
uniform vec4 iDate;
uniform float iSampleRate;
uniform vec3 iChannelResolution[4];
uniform sampler2D iChannel0;
uniform sampler2D iChannel1;
uniform sampler2D iChannel2;
uniform sampler2D iChannel3;
vec2 mainSound( float time )
{
return vec2( sin(time * 1000.0), sin(time * 1000.0) );
}
void main() {
// compute time `t` based on the pixel we're about to write
// the 512.0 means the texture is 512 pixels across so it's
// using a 2 dimensional texture, 512 samples per row
float t = iBlockOffset + ((gl_FragCoord.x-0.5) + (gl_FragCoord.y-0.5)*512.0)/iSampleRate;
// Get the 2 values for left and right channels
vec2 y = mainSound( t );
// convert them from -1 to 1 to 0 to 65536
vec2 v = floor((0.5+0.5*y)*65536.0);
// separate them into low and high bytes
vec2 vl = mod(v,256.0)/255.0;
vec2 vh = floor(v/256.0)/255.0;
// write them out where
// RED = channel 0 low byte
// GREEN = channel 0 high byte
// BLUE = channel 1 low byte
// ALPHA = channel 2 high byte
gl_FragColor = vec4(vl.x,vh.x,vl.y,vh.y);
}
这指出了在这种特殊情况下使用 WebGL 的一个优势,即您可以获得与片段着色器相同的音频着色器输入(因为它是片段着色器)。这意味着例如音频着色器最多可以引用 4 个纹理
在 JavaScript 然后你会用 gl.readPixels
读取纹理,然后用
var pixels = new Uint8Array(width * height * 4);
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
for (var sample = 0; sample < numSamples; ++sample) {
var offset = sample * 4; // RGBA
audioData[sample * 2 ] = backToFloat(pixels[offset + 0], pixels[offset + 1]);
audioData[sample * 2 + 1] = backToFloat(pixels[offset + 2], pixels[offset + 3]);
}
float backToFloat(low, high) {
// convert back to 0 to 65536
var value = low + high * 256;
// convert from 0 to 65536 to -1 to 1
return value / 32768 - 1;
}
此外,虽然我在上面说过我认为这不是一个好主意,但我认为 shadertoy 一直在调用音频着色器,因此我提出的关于阻塞处理的问题是正确的,但是......显然当您按下播放时,shadertoy 仅 pre-generates N 秒使用着色器的音频,其中 N 显然是 60 秒。所以,没有阻塞,但声音只持续 60 秒。