Как работают звуковые шейдеры Shadertoy?

Чтобы начать, я не мог найти подходящее сообщество для публикации этого вопроса, поэтому я выбрал этот. Мне было интересно, как работают звуковые шейдеры популярного шейдерного инструмента на основе webGL, потому что, когда я, очевидно, слышал о "нормальных" шейдерах GLSL, я впервые услышал о шейдерах для процедурного генерации звука, я был поражен. Любые подсказки?

Ответы

Ответ 1

В основном это функция, которая при задании 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);
}

Теперь передайте аудиоданные в 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 вы должны использовать их для генерации данных в текстуру, прикрепленную к фреймбуферу. Затем генерируемые ими данные должны быть скопированы с GPU в основную память с помощью gl.readPixels, а затем переданы в API Web Audio, который будет медленным и, по крайней мере, в WebGL, он блокирует обработку, поскольку нет возможности асинхронно читать данные обратно в WebGL. Кроме того, вы не можете легко прочитать данные с плавающей запятой в WebGL. Конечно, если shadertoy действительно использует WebGL, то он может переписать аудио-шейдер для кодирования данных в 8-битные RGBA-текстуры, а затем преобразовать их обратно в float в JavaScript. Еще больше причин НЕ использовать WebGL для этого. Основная причина использования WebGL заключается в том, что он просто симметричный. Все шейдеры используют один и тот же язык.

Приведенный выше пример bytebeat полностью запущен в JavaScript. По умолчанию используется значение bytebeat, означающее значение, возвращаемое функцией, от 0 до 255 unsigned int, но есть параметр для floatbeat, и в этом случае он ожидает значение от -1 до 1, как и shadertoy shaders.


Update

Итак, я проверил Shadertoy и использует шейдеры WebGL и кодирует значения в 8-битные текстуры

Вот настоящий шейдер (я использовал хром-шейдерный редактор, чтобы легко смотреть на шейдер).

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 просто предварительно генерирует N секунд звука, используя шейдер, когда вы нажимаете кнопку воспроизведения, где N, по-видимому, составляет 60 секунд. Итак, нет блокировки, но затем звук длится всего 60 секунд.