Shadertoy的音频着色器是如何工作的

How do Shadertoy's audio shaders work?

本文关键字:何工作 工作 音频 Shadertoy      更新时间:2023-09-26

首先,我真的找不到任何合适的社区来发布这个问题,所以我选择了这个问题。我想知道流行的基于 webGL 的着色器工具的音频着色器是如何工作的,因为即使我显然听说过"普通"GLSL 着色器,我第一次听说用于程序生成音频的着色器,我也很惊讶。有什么线索吗?

它们基本上是一个函数,给定time为音频单曲(左声道和右声道)返回 2 个值。值从 -1 到 1。

粘贴这个着色器,也许你会得到它

vec2 mainSound( float time )
{
    return vec2( sin(time * 1000.0), sin(time * 1000.0) );
}

您可以在此处看到类似声音风格的更多实时示例。

你可以这样想象

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);
}

现在将音频数据传递给 Web 音频 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,那么它可以重写音频着色器,将数据编码为8位RGBA纹理,然后将其转换回JavaScript中的浮点数。更有理由不使用WebGL。使用WebGL的主要原因是它只是使它对称。所有着色器都使用相同的语言。

上面链接的字节节拍示例完全在 JavaScript 中运行。它默认为 bytebeat,这意味着函数预期返回的值为 0 到 255 无符号 int,但有一个 floatbeat 设置,在这种情况下,它期望的值从 -1 到 1,就像 shadertoy 的着色器一样。

<小时 />

更新

所以我检查了Shadertoy,它正在使用WebGL着色器,并将值编码为8位纹理。

这是一个实际的着色器(我使用chrome着色器编辑器轻松查看着色器)。

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 只是使用着色器预生成 N 秒的音频,其中 N 显然是 60 秒。所以,没有阻塞,但声音只持续60秒。