前言

最近看了一下钟文泽的 Macbook Pro 测评视频(唉,最近又想买电子产品了),他在测评音响的时候,点播了一首蔡琴的《渡口》。

当听到这首歌的时候,我真的是情不自禁地感叹:“爷青回!!”,想当年,第一次听这首歌的时候还是在 Windows XP 系统上的 千千静听 这个播放器里听到的,那时印象最深刻的就是里面的 音频可视化(频谱图) 了。

当我在发呆、无聊的时候,音频频谱图里的小浮块总能让我盯上一整天。而如今,在各大音乐软件中已很少看到这样的频谱图了。那今天就跟大家一起用原生的 Audio API 来实现这个频谱图吧。

项目已经放在 Github[1],也可以在 这里预览[2]

由于原来的拟物风格实现太难实现了,只能做个粗糙的版本 :)

解决思路

首先我们要理解频谱图里的这些“长条”是什么意思。实际上这是音频里的 频率 Frequency,我们常说的低音炮和美高音就是指在声音在低频区和高频区的表现。

了解了音频频率后,我们可以先理清一下这个小玩具的实现思路:

从音频获取音频流 stream,通过中间的解析器分析出频率值 freqency,将这些频率值通过“长条”的方式绘制在 <canvas> 上,然后以此不断循环就可以实现这样的频谱动态图了。

根据上面的思路,我们首先要准备好这样的页面结构:

const Player: FC = () => {const {visualize} = useAudioVisualization('#canvas', 50);const audioRef = useRef<HTMLAudioElement>(null);const onPlay = async () => {if (audioRef.current) {await audioRef.current.play();const stream = (audioRef.current as any).captureStream();visualize(stream)}}return (<div className={styles.player}><div className={styles.canvas}><canvas id="canvas" width={500} height={300}/></div><div className={styles.controls}><audio ref={audioRef} src={audioUrl} onPlay={onPlay} controls /></div></div>)
}

useAudioVisualization

这里使用 React Hook 的方式来封装可视化逻辑:

const useAudioVisualization = (selector: string, length = 50) => {// 开始可视化const visualize = (stream: MediaStream) => {}return { visualize };
}

visualize

在拿到音频的流之后,我们就可以调用 Audio API 来创建解析器并分析音频了。

// 开始可视化
const visualize = (stream: MediaStream) => {const canvasEl: HTMLCanvasElement | null = document.querySelector(selector);if (!canvasEl) {throw new Error('找不到 canvas');}// 创建解析器audioCtxRef.current = new AudioContext()analyserRef.current = audioCtxRef.current.createAnalyser();// 获取音频源const source = audioCtxRef.current.createMediaStreamSource(stream);// 将音频源连接解析器source.connect(analyserRef.current);// 准备数据数组analyserRef.current.fftSize = 256;const bufferLength = analyserRef.current.frequencyBinCount;const dataArray = new Uint8Array(bufferLength);// 开始递归画图drawEachFrame(canvasEl, dataArray);
}

这里主要做了几件事:

  • 通过 AudioContext 创建 analyser

  • 将音频输入源连接 analyser,每次播放的时候,音频都会经过 analyser 进行处理

  • 设置 fft,从 analyser 获取音频频率数据 dataArray

经过上面的操作我们已经拿到了音频的数据,接下来就是渲染 <canvas> 的时候了,开始实现 drawEachFrame

drawEachFrame

我们日常所看到的动画本质上都是一个画面一个画面连续播放的效果。

只要画面足够快就可以让画面动起来,那究竟要多快呢?相信有的同学已经开始拿起纸和笔来算了。

其实并不用这么复杂,这里给大家推荐一个 API requestAnimationFrame。它会以浏览器的显示频率来作为其动画动作的频率,比如浏览器每 10ms 刷新一次,动画回调也每 10ms 调用一次,这样就不会存在过度绘制的问题,动画不会掉帧,自然流畅。

只要我们在 requestAnimationFramecallback 里不断地绘制 <canvas> 就可以获得一个流畅的频谱图了。

// 每个动画帧都画图
const drawEachFrame = (canvasEl: HTMLCanvasElement, dataArray: Uint8Array) => {// 递归调用requestAnimateFrameIdRef.current = requestAnimationFrame(() => drawEachFrame(canvasEl, dataArray));if (analyserRef.current) {// 读取当前帧新的数据analyserRef.current.getByteFrequencyData(dataArray);// 更新长度const bars = dataArray.slice(0, Math.min(length, dataArray.length));// 画图clearCanvas(canvasEl);// 绘制小浮块drawFloats(canvasEl, bars);// 绘制条状图drawBars(canvasEl, bars);}
}

上面的 drawEachFrame 里又调用了一次 requestAnimationFrame,以此来实现递归循环调用的效果。

这里我们还会把 requestAnimateFrameId 给记录下来,以防之后销毁时要调用 window.cancelAnimationFrame(id) 来清除。

clearCanvas

在绘制 <canvas> 前,我们先把它给清空一下:

export const clearCanvas = (canvasEl: HTMLCanvasElement) => {const canvasWidth = canvasEl.width;const canvasHeight = canvasEl.height;const canvasCtx = canvasEl.getContext("2d");if (!canvasCtx) {return;}// 绘制图形canvasCtx.fillStyle = 'rgb(29,19,62)';canvasCtx.fillRect(0, 0, canvasWidth, canvasHeight);
}

这样就能得到一个纯色的 “白板” 了:

drawBars

接下来实现条状图,图示:

代码实现:

// 浮动的小块
let floats: any = [];
// 高度
const FLOAT_HEIGHT = 4;
// 下落高度
const DROP_DISTANCE = 1;
// Bar 的 border 宽度
const BAR_GAP = 2;export const drawBars = (canvasEl: HTMLCanvasElement, dataArray: Uint8Array) => {const canvasWidth = canvasEl.width;const canvasHeight = canvasEl.height;const canvasCtx = canvasEl.getContext("2d");if (!canvasCtx) {return;}const barWidth = canvasWidth / dataArray.lengthlet x = 0;dataArray.forEach((dataItem) => {const barHeight = dataItem;// 添加渐变色const gradient = canvasCtx.createLinearGradient(canvasWidth / 2, canvasHeight / 2, canvasWidth / 2, canvasHeight);gradient.addColorStop(0, '#68b3ec');gradient.addColorStop(0.5, '#4b5fc9');gradient.addColorStop(1, '#68b3ec');// 画 barcanvasCtx.fillStyle = gradient;canvasCtx.fillRect(x, canvasHeight - barHeight, barWidth, barHeight);x += barWidth + BAR_GAP;})
}

这里有几个点要注意:

  • 画长方形的时候,原点是在左上角,所以 y 的值为 canvasHeight - barHeight,即 总高度 - 条形高度

  • 画下一个 bar 的时候,需要 + BORDER_WIDTH 来空出一个空隙,不然 bar 就都黏在一起了

  • <canvas> 中画渐变,需要用 addColorStop 来实现

最后效果:

drawFloats

有了上面画条状 bar 的经验后,我们很容易就能想到怎么画这些小块了:

图示:

export const drawFloats = (canvasEl: HTMLCanvasElement, dataArray: Uint8Array) => {const canvasWidth = canvasEl.width;const canvasHeight = canvasEl.height;const canvasCtx = canvasEl.getContext("2d");if (!canvasCtx) {return;}// 找到最大值,以及初始化高度dataArray.forEach((item, index) => {// 默认值floats[index] = floats[index] || FLOAT_HEIGHT;// 处理当前值const pushHeight = item + FLOAT_HEIGHT;const dropHeight = floats[index] - DROP_DISTANCE;// 取最大值floats[index] = Math.max(dropHeight, pushHeight);})const barWidth = canvasWidth / dataArray.length;let x = 0;floats.forEach((floatItem: number) => {const floatHeight = floatItem;canvasCtx.fillStyle = '#3e47a0';canvasCtx.fillRect(x, canvasHeight - floatHeight, barWidth, FLOAT_HEIGHT);x += barWidth + BAR_GAP;})
}

这里最关键就是这个小浮块的高度,我们直接取浮想块下降了的高度 dropHeight 以及被 bar 推高的高度 pushHeight 他们两的最大值就可以了 floats[index] = Math.max(dropHeight, pushHeight)

在实现好了之后,来一首试音原声大碟《渡口》,即可享受频谱图带来的快乐:

stopVisualize

有开始就有结束,由于这里动用了 <canvas>, requestAnimationFrame 这些资源,所以当组件销毁时应该清空他们:

const useAudioVisualization = (selector: string, length = 50) => {...// 重置 canvasconst resetCanvas = () => {const canvasEl: HTMLCanvasElement | null = document.querySelector(selector);if (canvasEl) {const emptyDataArray = (new Uint8Array(length)).map(() => 0);clearFloats();clearCanvas(canvasEl);drawFloats(canvasEl, emptyDataArray);}}// 停止const stopVisualize = () => {if (requestAnimateFrameIdRef.current) {window.cancelAnimationFrame(requestAnimateFrameIdRef.current);resetCanvas();}};return {visualize,stopVisualize,resetCanvas,requestAnimateFrameId: requestAnimateFrameIdRef.current};
}

这里我们也把 requestAnimateFrameId 扔出来,可由开发者自己处理。

完整的使用方式是这样的:

const Player = () => {const {visualize, stopVisualize, resetCanvas} = useAudioVisualization('#canvas', 50);const audioRef = useRef<HTMLAudioElement>(null);const onPlay = async () => {if (audioRef.current) {stopVisualize();await audioRef.current.play();const stream = (audioRef.current as any).captureStream();visualize(stream)}}const onPause = async () => {resetCanvas();}useEffect(() => {resetCanvas();return () => {stopVisualize()}}, []);return (<div className={styles.player}><div className={styles.canvas}><canvas id="canvas" width={500} height={300}/></div><div className={styles.controls}><audio ref={audioRef} src={audioUrl} onPlay={onPlay} onPause={onPause} controls /></div></div>)
}

更好看的样式就交给同学们自己实现了 :) 当然你也可以在 我的 Github 项目 里直接 Copy 我的丑陋样式。

总结

最后总结一下这个频谱图的实现:

  1. 使用 Audio API 创建 analyser,将音频流 stream 连接到 analyser

  2. 设置 analyserfft 参数,以此获取音频数据

  3. 通过递归调用 requestAnimationFrame 来实现动画效果

  4. 使用 Canvas API 来绘制条形图以及小浮块,将这绘制操作放在 requestAnimationFrame 的回调中,从而展示动态的频谱图

如果你看完还是做不出自己的千千静听,可以在 我的 Github 项目 里直接看源码实现。

好了,这个千千静听的项目就给大家带到这里。如果你喜欢我的文章,可以走一波关注,一键三连我也不介意,比心 ❤️

参考资料

[1]

Github 地址: https://github.com/haixiangyan/ttplayer

[2]

预览地址: https://github.yanhaixiang.com/ttplayer/

爷青回!用原生 Audio API 实现一个千千静听相关推荐

  1. 用原生 Audio API 实现一个千千静听

    前言 哈喽,大家好,我是海怪. 最近看了一下钟文泽的 Macbook Pro 测评视频(唉,最近又想买电子产品了),他在测评音响的时候,点播了一首蔡琴的<渡口>. 当听到这首歌的时候,我真 ...

  2. 【游戏开发创新】手把手教你使用Unity制作一个高仿酷狗音乐播放器,滨崎步,旋律起,爷青回(声音可视化 | 频谱 | Audio)

    文章目录 一.前言 二.获取UI素材 三.使用UGUI制作界面 1.界面布局 2.账号圆形头像 3.搜索框 4.调节UI层 5.黑色按钮悬浮高亮效果 6.纯文字按钮 7.滚动列表自适应 8.歌名与视频 ...

  3. 在google play开放平台上closed texting如何删除_“爷青回”!如何抢先体验《英雄联盟》手游?这份攻略送给你...

    如果你要问我最近什么手游最火?那我的回答肯定就是<英雄联盟>手游了!自从拳头公司在前几天爆出部分地区公测的消息之后,很多国内玩家就已经按耐不住自己激动的心了.由于国服没有公测,所以许多国内 ...

  4. 爷青结是什么意思,爷青回是什么意思,爷青结和爷青回是什么梗

    经常上网看视频评论或者留言的朋友,就会看到很多网友刷"爷青结"或者"爷青回"那么这两个词都是什么意思那,今天我们就具体来了解一下. 首先大家要明白爷青结是词缩写 ...

  5. 爷青回!最近很火的朋友圈怀旧小电视源码来啦!看到最后一个视频我大呼好家伙!

    原文首发于公众号:[golang小白成长记] 爷青回!最近很火的朋友圈怀旧小电视源码来啦!看到最后一个视频我大呼好家伙! 体验一把怀旧小电视 最近朋友圈被怀旧小电视刷爆啦! 点开来,是一台老式电视机! ...

  6. 爷青回!AI把《灌篮高手》角色真人化,最帅的居然不是流川枫?

    金磊 假装发自 神奈川 量子位 报道 | 公众号 QbitAI "湘北!加油!" 就这一句话,得勾起多少人难忘的青葱岁月啊. 最近,一位外国博主 AIみかん搞了个事情,更是让网友们 ...

  7. 《灌篮高手》电影版终于定档了!网友:爷青回!!!

    灌篮高手电影版终于要来了!在日本当地时间7月2日下午18点,<灌篮高手>新剧场版在造势许久后,终于定档在今年的 12 月 3 日在日本国内上映,这让网友们掀起了一波超强回忆杀······ ...

  8. 天谕手游服务器今日可创建账号数已达上限,天谕堪称网易2020头号手游?内测人数爆满,玩家直呼爷青回...

    网易旗舰级IP大作<天谕>手游在8月21号开启了删档测试,作为经典IP,<天谕>手游在测试期间就吸引了众多玩家关注,其中有不少端游玩家也纷纷参与测试.由于<天谕>手 ...

  9. 高爷魅族android,爷青回:Flyme上架魅族M8、M9经典主题

    魅族这段时间消停了很多了,唯一的信息可能就是魅族18系列官方宣布降价吧!其实魅族这也是无奈之举,在魅族18系列正式发布之后,官方搞出了那么多的活动,无奈市场和消费者真不买账.在这样的情况之下只有降价了 ...

最新文章

  1. IntelliJ IDEA快捷键与使用小技巧
  2. ERROR: Could not install packages due to an EnvironmentError: [Errno 2] No such file or directory: ‘
  3. Aizu 2170 Marked Ancestor
  4. php excel 下拉菜单,使用 PHPExcel 遇到的一个问题:下拉列表的数据来源过长时,显示了别的正常的下拉列表的数据来源...
  5. 帝国cms linux伪静态规则,帝国cms7.2伪静态规则怎么写
  6. python paramiko exec_command()和invoke_shell()
  7. (67)FPGA模块调用(Verilog调用system Verilog)
  8. ASP.NET MVC应用程序把文字写在图片上
  9. cmake的一个编译报错
  10. 详解animate.css动画插件用法
  11. SPSS统计术语与思维【SPSS 002期】
  12. StackPanel与Grid交叉使用
  13. 三阶魔方复原操作方法
  14. php中大于等于的表示方法,php大于等于符号
  15. 中国最全亲戚关系图谱
  16. Android 平台电容式触摸屏硬件基本原理
  17. CKA真题 :2019年12月英文原题和分值
  18. android art 远程控制,IT之家学院:认识Android中的Dalvik与ART虚拟机
  19. Halcon中的基于区域的形态学处理(腐蚀膨胀开闭预算顶底帽运算)
  20. 生死狙击九天取密(逍遥工作室)

热门文章

  1. 利用excel进行栅格图像逐像元计算
  2. Qt之简约按钮导航栏
  3. 荣耀play4t能升级鸿蒙吗,荣耀30 Pro已开始测试华为鸿蒙HarmonyOS 2.0 荣耀Play4 Pro下月升级...
  4. 《乔布斯传》圈点(2)
  5. 初级软件测试工程师的面试
  6. 电脑开机各种蓝屏错误代码,U盘重装系统彻底解决
  7. 如何查看linux服务器是否为amd64架构还是x86_64架构
  8. 【IDEA 报错 ERROR 16720 --- [ restartedMain] o.a.coyote.http11.Http11NioProtocol : Failed to sta】
  9. Python中numpy.ix_ 的用法
  10. ERR_SSL_PROTOCOL_ERROR浏览器解决办法