使用 Tone.js 实现一个基本的音频播放器,该播放器不仅支持基本的播放控制,如播放、暂停、进度显示等等,还具备倍速播放和音高调整等功能
上一篇文章Antd Audio 自定义音频播放器基于 HTML5 的 Audio,实现了基本的音频播放器,也通过 Tone.js 实现了音高调整功能,但是该功能的实现没有和 Audio 的其他控制关联起来。
本文介绍如何使用 Tone.js 构建一个完整的音频播放器,并实现音高调整功能。
1. 安装及引入
Tone.js 是一个功能强大且易于使用的 Web Audio 库,专为创作交互式音乐和声音设计而设计。它提供了许多抽象层,使得在浏览器中制作复杂的音频应用程序变得更加容易,适合任何希望在网页上实现高质量音频和音乐体验的开发者。
Tone.js 的目标是为音乐家和音频工程师提供一种熟悉的工具集,类似于传统的数字音频工作站(DAW)软件。
Tone.js 官方文档:tonejs.github.io/docs/15.0.4…
- 安装
1
2npm install tone
- 引入
1import * as Tone from "tone";
2. 创建播放器
Tone.Player
: 是一个多功能组件,旨在播放音频文件,并具备开始、循环和停止播放等功能onload
:加载完成回调。自动加载音频文件,加载完成后,才能开始播放onstop
:停止播放回调。(注意:暂停/拖动进度条时,也会触发,无法当作播放结束事件处理)start
:开始播放事件stop
:停止播放事件dispose
:释放资源,当不再需要播放器时,记得调用 .dispose() 方法来释放资源playbackRate
:设置播放速率volume
:设置播放音量mute
:设置禁音/非禁音状态seek
:设置当前播放位置duration
:获取音频文件的总时长(player.current.buffer.duration)loop
:设置是否循环播放
Tone.PitchShift
:Tone.js 中的一个效果器,对输入信号进行近乎实时的音调转换。该效果是通过调整 DelayNode 的延迟时间来实现的,具体来说,是使用锯齿波来周期性地加速或减速延迟时间,从而产生音高变化的效果。dispose
:释放资源pitch
:设置音调偏移量。参数可以是正数(升高音高)也可以是负数(降低音高)。例如,参数 0.5 表示升高半音,-1 表示降低一个全音。通过改变这个参数,可以实现实时的音高变换效果。
1const player = useRef<Tone.Player | null>(null);
2const pitchShift = useRef<Tone.PitchShift | null>(null);
3const [playPitch, setPlayPitch] = useState(0);
4
5useEffect(() => {
6 if (!audioSrc) return;
7
8
9 player.current = new Tone.Player({
10 url: audioSrc,
11 onload: () => {
12
13 if (!player.current) return;
14
15 setAllTime(player.current.buffer.duration);
16 },
17 onstop: (data) => {
18
19
20 console.log("Tone onstop:");
21 },
22 onerror: (error) => {
23
24 console.error("Tone onerror:", error);
25 },
26 });
27
28
29 pitchShift.current = new Tone.PitchShift(0);
30
31
32 player.current.connect(pitchShift.current);
33
34
35 pitchShift.current.toDestination();
36
37 return () => {
38
39 player.current?.dispose();
40 pitchShift.current?.dispose();
41 };
42}, [audioSrc]);
3. 播放/暂停
3.1 Player 处理
Player
提供了start
和stop
方法,分别用于开始和停止播放音频。
1
2const pauseOrPlay = () => {
3 if (isPlay) {
4 player.current.stop();
5 setIsPlay(false);
6 } else {
7 player.current.start();
8 setIsPlay(true);
9 }
10};
3.2 Transport 处理
但是 Player 没有找到暂停的方法,start()
方法每次都是从头开始播放的。后来查了资料,发现可以用Tone.getTransport()
处理
Tone.Transport
通常与 Player 或 Synth 等其他 Tone.js 组件结合使用,以实现更复杂的音频同步和控制。例如,可以让多个音轨或音效同步启动和停止,或者根据节拍和时间签名来安排音符和效果。
Tone.getTransport()
方法返回的是 Transport 实例,这个实例可以用来控制整个音频应用的节奏和同步,包括启动、停止、暂停、跳转以及节拍和时间签名的管理。- start([time]) - 开始播放,如果提供了 time 参数,它将从指定的时间开始播放。
- stop([time]) - 停止播放
- pause([time]) - 暂停播放
- clear([eventId]) - 清除事件
position
:控制当前播放位置
注意:使用前要先同步一下,player.current.sync().start(0)
注意:重新播放前,需要重置position
1player.current = new Tone.Player({
2 url: audioSrc,
3 onload: () => {
4
5
6
7
8 player.current.sync().start(0);
9 },
10});
11
12const pauseOrPlay = () => {
13 if (isPlay) {
14 Tone.getTransport().pause();
15 setIsPlay(false);
16 } else {
17
18 if (currentTime >= allTime) {
19 Tone.getTransport().position = 0;
20 }
21 Tone.getTransport().start();
22 setIsPlay(true);
23 }
24};
4. 禁音/取消禁音
1
2const onMuteAudio = () => {
3 if (!player.current) return;
4 setIsMuted(!isMuted);
5 player.current.mute = !isMuted;
6};
5. 音量控制
-
value / 100
:将 value(介于 0 到 100 之间的百分比值)转换为 0 到 1 之间的范围,这是 Tone.Gain 节点期望的增益值范围。 -
Tone.gainToDb
:这个函数将线性的增益值转换为分贝值。在内部,它使用以下公式:dB = 20 * log10(gain)
为什么使用分贝?因为分贝能够更好地反映人耳对音量变化的感知。例如,将音量增加一倍(线性增益从 1 增加到 2)在分贝中大约相当于增加了 6dB,而将音量增加到原来的十分之一(线性增益从 1 减少到 0.1)则相当于减少了 20dB。这种对数关系使得分贝成为描述音量变化的更直观的单位。
1
2const changeVolume = (value: number) => {
3 if (!player.current) return;
4
5 player.current.volume.value = Tone.gainToDb(value / 100);
6 setVolume(value);
7 setIsMuted(!value);
8};
6. 倍速控制
1
2const changePlayRate = (num: number) => {
3 if (!player.current) return;
4 setPlayRate(num);
5 player.current.playbackRate = num;
6};
7. 音高控制
1
2const changePlayPitch = (num: number) => {
3 if (pitchShift.current) {
4 setPlayPitch(num);
5 pitchShift.current.pitch = num;
6 }
7};
8. 播放进度条
8.1 Transport 处理
Player 播放器没有找到监听播放时间的事件,onstop 也无法确认播放结束,所以使用 Tone.Transport
处理
scheduleRepeat
:用于按指定的时间间隔重复执行一个回调函数;interval 参数是 “16n”,表示每十六分音符执行一次回调。
1useEffect(() => {
2 const eventId = Tone.getTransport().scheduleRepeat((time) => {
3 const currentTime = Tone.Time(time).toSeconds();
4 console.log("Tone currentTime:", currentTime);
5
6 }, "16n");
7
8 return () => {
9
10 Tone.getTransport().clear(eventId);
11 };
12}, [audioSrc]);
使用时,发现这个不准,停止时也在变动,无法作为进度条的控制。也没找到其他方式,就暂时放弃了
8.2 Audio timeupdate 处理
主要是文档太少了,可参考的资料也少,确实没找到其他方式处理进度条,但是功能还是得实现的呀。。。
最后无奈的处理方式,通过隐藏的 Audio 控件处理。使用了 Audio API 的 timeupdate 事件,通过监听音频的播放时间,实时更新进度条。具体处理逻辑可以看下上一篇文章进度条控制
注意:Audio 一直保持 muted 禁音
处理,只用作进度条同步
所以其他事件(播放/暂停、禁音、音量、倍速等)中,也需要添加 audioRef.current 的处理逻辑,进行播放进度同步,我就不一一添加了。如下以 changeTime 为例:
1
2const changeTime = (value: number) => {
3 if (!player.current) return;
4 audioRef.current!.currentTime = value;
5
6 player.current.seek(value);
7 setCurrentTime(value);
8 if (
9 value === player.current.buffer.duration ||
10 value === audioRef.current!.duration
11 ) {
12
13 setIsPlay(false);
14 }
15};
9. 播放/暂停 bug 修复
(1)问题:测试时,如果同时初始化多个 MAudio 组件,会出现播放时,其他组件也会同时播放的情况。
(2)原因:
Tone.Transport
是 Tone.js 中的全局控制器,它允许以一种统一的方式控制音频的播放、暂停、停止以及各种时间相关的操作。
使用player.current.sync().start(0)
来同步播放器时,实际上是在告诉播放器与 Tone.Transport 的节奏和时间线保持一致。在同步之后,都是从时间点 0 开始的,按照 Transport 的节奏同时开始播放、暂停。
(3)解决:找了好久,没找到解决方法。。。,Transport 也没法用了
皇天不负有心人,最后终于发现了一个解决方案,还是使用 Player 来处理
- start(time?, offset?, duration?):用于指定何时开始播放音频缓冲区(buffer),并且可以指定从缓冲区的哪个位置开始播放,以及播放的持续时间。
- time:表示开始播放的时间
- offset:从音频样本的开始位置偏移多少时间开始播放
- duration:表示播放的持续时间
1
2const pauseOrPlay = () => {
3 if (!player.current) return;
4 if (isPlay) {
5 player.current.stop();
6 audioRef.current!.pause();
7 setIsPlay(false);
8 } else {
9 if (
10 currentTime >= allTime ||
11 currentTime >= player.current.buffer.duration
12 ) {
13
14 player.current.start(0);
15 } else {
16
17 player.current.start(0, currentTime);
18 }
19 audioRef.current!.play();
20 setIsPlay(true);
21 }
22};