前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >用Web音频API来做一个音频可视化工具

用Web音频API来做一个音频可视化工具

作者头像
疯狂的技术宅
发布2019-03-27 15:09:45
2.9K0
发布2019-03-27 15:09:45
举报
文章被收录于专栏:京程一灯京程一灯

如果你曾经想过像MilkDrop这样的音乐可视化工具是怎么做的,那么这篇文章就是为你准备的。我们将从使用Canvas API来做简单的可视化入手,然后慢慢转移到用WebGL着色器来做更复杂的可视化。

使用Canvas API的波形图可视化

做一个音频可视化工具所需的第一件东西就是一些音频。现在我们有两个选项:一个是从A3到A6的扫描和我做的一首歌(由Pye Corner Audio重建轨道名字叫“Zero Center”)。

Saw Sweep Play Song(译者注:原文这里是两个按钮可以听这两个音频的效果,下同)

所有的音频可视化工具都需要的第二件事是获取音频数据的方式。Web Audio API为此提供了 AnalyserNode 这个接口。除了提供了原始的波形(也叫做时间域)数据,它还提供了访问音频频谱(也叫频域)数据的方法。使用 AnalyserNode这个接口很简单:创建一个 AnalyserNode.frequencyBinCount长度的类型化数组,然后调用 AnalyserNode.getFloatTimeDomainData这个方法用当前的波形数据来填充这个数组。

代码语言:javascript
复制
const analyser = audioContext.createAnalyser()
masterGain.connect(analyser)

const waveform = new Float32Array(analyser.frequencyBinCount)
analyser.getFloatTimeDomainData(waveform)

此时, waveform数组将包含与通过 masterGain节点播放的音频波形相对应的从-1到1的值。 这只是目前正在播放的一个片刻。为了使之有用,我们需要周期性的更新这个数组。在 requestAnimationFrame的回调函数里更新这个数组是一个好主意。

代码语言:javascript
复制
;(function updateWaveform() {
  requestAnimationFrame(updateWaveform)
  analyser.getFloatTimeDomainData(waveform)
})()

现在将会每秒更新这个 waveform数组60次,这样,我们最后一个需要的东西:一些绘图代码。在这个例子中,我们只需简单地像示波器在y轴上绘制波形。

代码语言:javascript
复制
const scopeCanvas = document.getElementById('oscilloscope')
scopeCanvas.width = waveform.length
scopeCanvas.height = 200
const scopeContext = scopeCanvas.getContext('2d')

;(function drawOscilloscope() {
  requestAnimationFrame(drawOscilloscope)
  scopeContext.clearRect(0, 0, scopeCanvas.width, scopeCanvas.height)
  scopeContext.beginPath()
  for (let i = 0; i < waveform.length; i++) {
    const x = i
    const y = (0.5 + waveform[i] / 2) * scopeCanvas.height;
    if (i == 0) {
      scopeContext.moveTo(x, y)
    } else {
      scopeContext.lineTo(x, y)
    }
  }
  scopeContext.stroke()
})()

Saw Sweep Play Song

尝试多次点击“锯切扫描”按钮,看看波形如何响应

使用Canvas API进行频谱可视化。

AnalyserNode接口还提供有关音频中当前存在的频率的数据。它对波形数据运行FFT(傅立叶变换),并将这些值暴露为一个数组。在这种情况下,我们将要求数据为 Uint8Array,因为0-255范围内的值正是执行Canvas像素操作时所需要的值的范围。

代码语言:javascript
复制
const spectrum = new Uint8Array(analyser.frequencyBinCount)
;(function updateSpectrum() {
  requestAnimationFrame(updateSpectrum)
  analyser.getByteFrequencyData(spectrum)
})()

waveform数组类似, spectrum数组现在将使用当前的音频频谱每秒更新60次。这些值对应于频谱的给定片段的音量,从低频到高频排列。让我们看看如何使用这些数据来创建一个被称为声谱图的可视化。

代码语言:javascript
复制
const spectroCanvas = document.getElementById('spectrogram')
spectroCanvas.width = spectrum.length
spectroCanvas.height = 200
const spectroContext = spectroCanvas.getContext('2d')
let spectroOffset = 0

;(function drawSpectrogram() {
  requestAnimationFrame(drawSpectrogram)
  const slice = spectroContext.getImageData(0, spectroOffset, spectroCanvas.width, 1)
  for (let i = 0; i < spectrum.length; i++) {
    slice.data[4 * i + 0] = spectrum[i] // R
    slice.data[4 * i + 1] = spectrum[i] // G
    slice.data[4 * i + 2] = spectrum[i] // B
    slice.data[4 * i + 3] = 255         // A
  }
  spectroContext.putImageData(slice, 0, spectroOffset)
  spectroOffset += 1
  spectroOffset %= spectroCanvas.height
})()

Saw Sweep Play Song

我发现谱图是分析音频的最有用的工具之一,例如找出正在播放的和弦或调试不正确的合成器。光谱图也适用于寻找复活节彩蛋!

可视化与WebGL着色器

我最喜欢的电脑图形技术是使用WebGL的全屏像素着色器。通常,几个像素着色器与3D几何结合使用来呈现场景,但是今天我们将使用单个像素着色器(也称为片段着色器)来跳过几何图形并渲染整个场景。与Canvas API相比,它需要引用更多的文件,但最终的结果是非常值得的。

首先,我们需要绘制一个覆盖整个屏幕的矩形(也称为四边形)。片段着色器将被绘制的在这上面。

代码语言:javascript
复制
function initQuad(gl) {
  const vbo = gl.createBuffer()
  gl.bindBuffer(gl.ARRAY_BUFFER, vbo)
  const vertices = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1])
  gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW)
  gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0)
}

function renderQuad(gl) {
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
}

现在我们有全屏四边形(技术上它是两个半屏三角形),我们需要一个着色器程序。 这是一个使用顶点着色器和片段着色器的函数,并返回一个已经编译好的着色器程序。

代码语言:javascript
复制
function createShader(gl, vertexShaderSrc, fragmentShaderSrc) {
  const vertexShader = gl.createShader(gl.VERTEX_SHADER)
  gl.shaderSource(vertexShader, vertexShaderSrc)
  gl.compileShader(vertexShader)
  if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
    throw new Error(gl.getShaderInfoLog(vertexShader))
  }

  const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)
  gl.shaderSource(fragmentShader, fragmentShaderSrc)
  gl.compileShader(fragmentShader)
  if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
    throw new Error(gl.getShaderInfoLog(fragmentShader))
  }

  const shader = gl.createProgram()
  gl.attachShader(shader, vertexShader)
  gl.attachShader(shader, fragmentShader)
  gl.linkProgram(shader)
  gl.useProgram(shader)

  return shader
}

这个可视化的顶点着色器非常简单,它只是穿过顶点位置而不会修改它。

代码语言:javascript
复制
attribute vec2 position;

void main(void) {
  gl_Position = vec4(position, 0, 1);
}

这个片段着色器就比较有趣了,我们将从由Danguafer提供的这个着色器开始,并做出一些战略性的修改,以便对音频进行响应。

代码语言:javascript
复制
precision mediump float;

uniform float time;
uniform vec2 resolution;
uniform sampler2D spectrum;

void main(void) {
  vec3 c;
  float z = 0.1 * time;
  vec2 uv = gl_FragCoord.xy / resolution;
  vec2 p = uv - 0.5;
  p.x *= resolution.x / resolution.y;
  float l = 0.2 * length(p);
  for (int i = 0; i < 3; i++) {
    z += 0.07;
    uv += p / l * (sin(z) + 1.0) * abs(sin(l * 9.0 - z * 2.0));
    c[i] = 0.01 / length(abs(mod(uv, 1.0) - 0.5));
  }
  float intensity = texture2D(spectrum, vec2(l, 0.5)).x;
  gl_FragColor = vec4(c / l * intensity, time);
}

这个关键是将输出颜色与声谱强度相乘。 另一个区别是我们将“l”缩放为0.2,因为大部分音频都在频谱纹理里的前20%。

到底什么是频谱纹理?它是从之前的声谱数组,复制到1024x1的图像。 以下讲的是如何实现(波形数据可以使用相同的技术):

代码语言:javascript
复制
function createTexture(gl) {
  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)
  return texture
}

function copyAudioDataToTexture(gl, audioData, textureArray) {
  for (let i = 0; i < audioData.length; i++) {
    textureArray[4 * i + 0] = audioData[i] // R
    textureArray[4 * i + 1] = audioData[i] // G
    textureArray[4 * i + 2] = audioData[i] // B
    textureArray[4 * i + 3] = 255          // A
  }
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, audioData.length, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, textureArray)
}

随着所有这一切准备完毕,我们终于准备绘制可视化了。 首先,我们初始化画布并编译着色器。

代码语言:javascript
复制
const fragCanvas = document.getElementById('fragment')
fragCanvas.width = fragCanvas.parentNode.offsetWidth
fragCanvas.height = fragCanvas.width * 0.75
const gl = fragCanvas.getContext('webgl') || fragCanvas.getContext('experimental-webgl')
const vertexShaderSrc = document.getElementById('vertex-shader').textContent
const fragmentShaderSrc = document.getElementById('fragment-shader').textContent
const fragShader = createShader(gl, vertexShaderSrc, fragmentShaderSrc)

接下来,我们初始化这个着色器变量: position, time, resolution,还有我们最感兴趣的一个变量 spectrum

代码语言:javascript
复制
const fragPosition = gl.getAttribLocation(fragShader, 'position')
gl.enableVertexAttribArray(fragPosition)
const fragTime = gl.getUniformLocation(fragShader, 'time')
gl.uniform1f(fragTime, audioContext.currentTime)
const fragResolution = gl.getUniformLocation(fragShader, 'resolution')
gl.uniform2f(fragResolution, fragCanvas.width, fragCanvas.height)
const fragSpectrumArray = new Uint8Array(4 * spectrum.length)
const fragSpectrum = createTexture(gl)

现在设置了这些变量,我们初始化全屏四边形并启动渲染循环。 在每个框架上,我们更新 time变量和 spectrum纹理,并渲染这个四边形。

代码语言:javascript
复制
initQuad(gl)

;(function renderFragment() {
  requestAnimationFrame(renderFragment)
  gl.uniform1f(fragTime, audioContext.currentTime)
  copyAudioDataToTexture(gl, spectrum, fragSpectrumArray)
  renderQuad(gl)
})()

Saw Sweep Play Song(两个按钮,可以演示效果)

如您所见,全屏片段着色器相当强大。 如果有更多的想法,可以花一些时间探索Shadertoy和The Book of Shaders。 使着色器对音频作出反应是吸引更多生命力的好方法,正如我们所看到的,Web Audio API使其易于操作。 如果您最终制作出酷炫的音乐可视化,请在评论中分享!


往期精选文章

使用虚拟dom和JavaScript构建完全响应式的UI框架

扩展 Vue 组件

使用Three.js制作酷炫无比的无穷隧道特效

一个治愈JavaScript疲劳的学习计划

全栈工程师技能大全

WEB前端性能优化常见方法

一小时内搭建一个全栈Web应用框架

干货:CSS 专业技巧

四步实现React页面过渡动画效果

让你分分钟理解 JavaScript 闭包



小手一抖,资料全有。长按二维码关注京程一灯,阅读更多技术文章和业界动态。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2017-10-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 京程一灯 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 使用Canvas API的波形图可视化
  • 使用Canvas API进行频谱可视化。
  • 可视化与WebGL着色器
相关产品与服务
云点播
面向音视频、图片等媒体,提供制作上传、存储、转码、媒体处理、媒体 AI、加速分发播放、版权保护等一体化的高品质媒体服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档