我用WebGL构建了audio spectrogram
我正在创建一个基于画布高度的缓冲区(依次是基于窗口的高度):
const buffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
gl.bufferData(gl.ARRAY_BUFFER, 1024 * h, gl.DYNAMIC_DRAW);
gl.vertexAttribPointer(a_value, 1, gl.BYTE, true, 1, 0)
gl.enableVertexAttribArray(a_value)
// within rAF
// Assigning values with a rolling offset
gl.bufferSubData(gl.ARRAY_BUFFER, idx * 1024, freqData, 1024);
gl.drawArrays(gl.POINTS, 0, w * h)
idx = (idx + 1) % h
我的问题是 - 我觉得我应该限制我正在使用的顶点/点的数量;但是我该如何选择这个限制呢?
在测试中(调整页面缩放调整生成的点数) - 超过2M点似乎可以在我的macbook上工作;虽然提高了我的CPU使用率。
注意:我正在计划另一个使用图像纹理的版本(我认为可以解决这个问题),但我在不同的项目中已经有过几次这个问题
答案 0 :(得分:1)
我不知道这是否真的是你的问题的答案,但你可能应该使用纹理。使用纹理有多个优点
您只需使用一个四边形即可渲染整个屏幕。
这是基于目的地的意义,意味着它将完成最少量的工作,每个目标像素的一个工作单元,而对于线/点,您可能每个目标像素执行更多的工作。这意味着您不必担心性能。
纹理是随机访问意味着您使用数据的方式多于缓冲区/属性
对纹理进行抽样,以便处理更好地处理freqData.length !== w
的情况。
因为纹理是随机访问,您可以将idx
传递到着色器并使用它来操纵纹理坐标,以便顶部或底部线始终是最新数据,其余部分滚动。使用属性/缓冲区
可以通过将纹理附加到帧缓冲区来从GPU写入纹理。这也可以让您滚动使用2个纹理的位置,每个帧从tex1到tex2复制h - 1
行,但向上或向下移动一行。然后将freqData
复制到第一行或最后一行。下一帧做同样的事情但是使用tex2作为源,使用tex1作为destiantion。
这也可以让您滚动数据。它可以说比将idx
传递到着色器并操纵纹理坐标要慢一些,但它会使纹理坐标的使用保持一致,所以如果你想做任何更高级的可视化,你就不必采取{ {1}}考虑您对纹理进行采样的每个地方。
vertexshaderart.com使用此技术,因此着色器不必考虑像idx
这样的值来确定纹理中最新数据的位置。最新数据始终位于纹理坐标idx
这是一个样本。它不会执行最后两件事,只使用纹理而不是缓冲区。
v = 0

function start() {
const audio = document.querySelector('audio');
const canvas = document.querySelector('canvas');
const audioCtx = new AudioContext();
const source = audioCtx.createMediaElementSource(audio);
const analyser = audioCtx.createAnalyser();
const freqData = new Uint8Array(analyser.frequencyBinCount);
source.connect(analyser);
analyser.connect(audioCtx.destination);
audio.play();
const gl = canvas.getContext('webgl');
const frag = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(frag, `
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D tex;
float P = 5.5;
void main() {
// these 2 lines convert from 0.0 -> 1.0 to -1. to +1
// assuming that signed bytes were put in the texture.
// This is what the previous buffer based code was doing
// by using BYTE for its vertexAttribPointer type.
// The thing is AFAICT the audio data from getByteFrequencyData
// is unsigned data. See
// https://webaudio.github.io/web-audio-api/#widl-AnalyserNode-getByteFrequencyData-void-Uint8Array-array
// But, this is what the old code was doing
// do I thought I should repeat it here.
float value = texture2D(tex, v_texcoord).r * 2.;
value = mix(value, -2. + value, step(1., value));
float r = 1.0 + sin(value * P);
float g = 1.0 - sin(value * P);
float b = 1.0 + cos(value * P);
gl_FragColor = vec4(r, g, b, 1);
}
`);
gl.compileShader(frag);
const vert = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vert, `
attribute vec2 a_position;
varying vec2 v_texcoord;
void main() {
gl_Position = vec4(a_position, 0, 1);
// we can do this because we know a_position is a unit quad
v_texcoord = a_position * .5 + .5;
}
`);
gl.compileShader(vert);
const program = gl.createProgram();
gl.attachShader(program, vert);
gl.attachShader(program, frag);
gl.linkProgram(program);
const a_value = gl.getAttribLocation(program, 'a_value');
const a_position = gl.getAttribLocation(program, 'a_position');
gl.useProgram(program);
const w = freqData.length;
let h = 0;
const pos_buffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, pos_buffer)
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-1, -1,
1, -1,
-1, 1,
-1, 1,
1, -1,
1, 1,
]), gl.STATIC_DRAW);
gl.vertexAttribPointer(a_position, 2, gl.FLOAT, true, 0, 0);
gl.enableVertexAttribArray(a_position);
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
let idx = 0
function render() {
resizeCanvasToDisplaySize(gl.canvas);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
if (gl.canvas.height !== h) {
// reallocate texture. Note: more work would be needed
// to save old data. As is if the user resizes the
// data will be cleared
h = gl.canvas.height;
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, w, h, 0,
gl.LUMINANCE, gl.UNSIGNED_BYTE, null);
idx = 0;
}
analyser.getByteFrequencyData(freqData);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, idx, w, 1,
gl.LUMINANCE, gl.UNSIGNED_BYTE, freqData);
gl.drawArrays(gl.TRIANGLES, 0, 6);
idx = (idx + 1) % h;
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
function resizeCanvasToDisplaySize(canvas) {
const w = canvas.clientWidth;
const h = canvas.clientHeight;
if (canvas.width !== w || canvas.height !== h) {
canvas.width = w;
canvas.height = h;
}
}
document.querySelector("#ui").addEventListener('click', (e) => {
e.target.style.display = 'none';
start();
});

body{ margin: 0; font-family: monospace; }
canvas {
position: absolute;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
display: block;
z-index: -1;
}
#ui {
position: fixed;
top: 0;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
#ui>div {
padding: 1em;
background: #8ef;
cursor: pointer;
}