切换语言为:繁体

Tone.js 音频播放器详细使用方法,及遇到的bug解决方法

  • 爱糖宝
  • 2024-07-23
  • 2145
  • 0
  • 0

使用 Tone.js 实现一个基本的音频播放器,该播放器不仅支持基本的播放控制,如播放、暂停、进度显示等等,还具备倍速播放和音高调整等功能

本文介绍如何使用 Tone.js 构建一个完整的音频播放器,并实现音高调整功能。

1. 安装及引入

Tone.js 是一个功能强大且易于使用的 Web Audio 库,专为创作交互式音乐和声音设计而设计。它提供了许多抽象层,使得在浏览器中制作复杂的音频应用程序变得更加容易,适合任何希望在网页上实现高质量音频和音乐体验的开发者。

Tone.js 的目标是为音乐家和音频工程师提供一种熟悉的工具集,类似于传统的数字音频工作站(DAW)软件。

Tone.js 官方文档:tonejs.github.io/docs/15.0.4…

  1. 安装

# 我的版本是v15.0.4 
npm install tone


  1. 引入

import * 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 表示降低一个全音。通过改变这个参数,可以实现实时的音高变换效果。

const player = useRef<Tone.Player | null>(null); // 播放器
const pitchShift = useRef<Tone.PitchShift | null>(null); // 音高效果器
const [playPitch, setPlayPitch] = useState(0); // 音高控制

useEffect(() => {
  if (!audioSrc) return;

  // 1. 创建 Tone.Player 实例,只在首次调用时创建
  player.current = new Tone.Player({
    url: audioSrc, // 音频文件的 URL
    onload: () => {
      // 当音频加载完成时执行的回调函数
      if (!player.current) return;
      // 获取音频总时长
      setAllTime(player.current.buffer.duration);
    },
    onstop: (data) => {
      // 当播放停止时执行的回调函数
      // 注意:暂停/拖动进度条时,也会触发,无法当作播放结束事件处理
      console.log("Tone onstop:");
    },
    onerror: (error) => {
      // 当加载音频文件出错时执行的回调函数
      console.error("Tone onerror:", error);
    },
  });
  // 2. 创建 PitchShift 节点,参数为音高偏移量
  // 初始音高偏移量为 0,意味着没有音高变化
  pitchShift.current = new Tone.PitchShift(0);
  // 3. 将 Player 的输出连接到 PitchShift 节点
  // 这样音频数据会先经过 PitchShift 处理,再输出
  player.current.connect(pitchShift.current);
  // 4. 将 PitchShift 节点的输出连接到音频上下文的目的地
  // 这是音频输出的最终目的地,通常是用户的扬声器
  pitchShift.current.toDestination();

  return () => {
    // 清理资源,释放 Player 和 PitchShift 实例占用的资源
    player.current?.dispose();
    pitchShift.current?.dispose();
  };
}, [audioSrc]);


3. 播放/暂停

3.1 Player 处理

Player提供了startstop方法,分别用于开始和停止播放音频。

// 播放或者暂停
const pauseOrPlay = () => {
  if (isPlay) {
    player.current.stop();
    setIsPlay(false);
  } else {
    player.current.start();
    setIsPlay(true);
  }
};


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

player.current = new Tone.Player({
  url: audioSrc,
  onload: () => {
    // 设置循环播放
    // player.current.loop = true;
    // 在音频加载完成后,与Transport同步
    // 注意:要加该代码,Tone.getTransport().start()才能起作用
    player.current.sync().start(0);
  },
});

const pauseOrPlay = () => {
  if (isPlay) {
    Tone.getTransport().pause();
    setIsPlay(false);
  } else {
    // 注意:确保在开始播放前,position被重置为0,才能开始重新播放
    if (currentTime >= allTime) {
      Tone.getTransport().position = 0;
    }
    Tone.getTransport().start();
    setIsPlay(true);
  }
};


4. 禁音/取消禁音

//禁音/取消禁音
const onMuteAudio = () => {
  if (!player.current) return;
  setIsMuted(!isMuted);
  player.current.mute = !isMuted;
};


5. 音量控制

  • value / 100:将 value(介于 0 到 100 之间的百分比值)转换为 0 到 1 之间的范围,这是 Tone.Gain 节点期望的增益值范围。

  • Tone.gainToDb:这个函数将线性的增益值转换为分贝值。在内部,它使用以下公式:dB = 20 * log10(gain)

为什么使用分贝?因为分贝能够更好地反映人耳对音量变化的感知。例如,将音量增加一倍(线性增益从 1 增加到 2)在分贝中大约相当于增加了 6dB,而将音量增加到原来的十分之一(线性增益从 1 减少到 0.1)则相当于减少了 20dB。这种对数关系使得分贝成为描述音量变化的更直观的单位。

// 改变音量
const changeVolume = (value: number) => {
  if (!player.current) return;
  // 将百分比音量值转换为分贝,就可以在Tone.js的音频处理链中使用了
  player.current.volume.value = Tone.gainToDb(value / 100);
  setVolume(value);
  setIsMuted(!value);
};


6. 倍速控制

// 播放倍数
const changePlayRate = (num: number) => {
  if (!player.current) return;
  setPlayRate(num);
  player.current.playbackRate = num;
};


7. 音高控制

// 播放音高
const changePlayPitch = (num: number) => {
  if (pitchShift.current) {
    setPlayPitch(num);
    pitchShift.current.pitch = num;
  }
};


8. 播放进度条

8.1 Transport 处理

Player 播放器没有找到监听播放时间的事件,onstop 也无法确认播放结束,所以使用 Tone.Transport 处理

scheduleRepeat:用于按指定的时间间隔重复执行一个回调函数;interval 参数是 "16n",表示每十六分音符执行一次回调。

useEffect(() => {
  const eventId = Tone.getTransport().scheduleRepeat((time) => {
    const currentTime = Tone.Time(time).toSeconds();
    console.log("Tone currentTime:", currentTime);
    // setCurrentTime(currentTime);
  }, "16n");

  return () => {
    // 清理资源
    Tone.getTransport().clear(eventId);
  };
}, [audioSrc]);


使用时,发现这个不准,停止时也在变动,无法作为进度条的控制。也没找到其他方式,就暂时放弃了

8.2 Audio timeupdate 处理

主要是文档太少了,可参考的资料也少,确实没找到其他方式处理进度条,但是功能还是得实现的呀。。。

最后无奈的处理方式,通过隐藏的 Audio 控件处理。使用了 Audio API 的 timeupdate 事件,通过监听音频的播放时间,实时更新进度条。

注意:Audio 一直保持 muted 禁音处理,只用作进度条同步

所以其他事件(播放/暂停、禁音、音量、倍速等)中,也需要添加 audioRef.current 的处理逻辑,进行播放进度同步,我就不一一添加了。如下以 changeTime 为例:

// 修改播放时间
const changeTime = (value: number) => {
  if (!player.current) return;
  audioRef.current!.currentTime = value;
  // 控制播放进度
  player.current.seek(value);
  setCurrentTime(value);
  if (
    value === player.current.buffer.duration || // Tone.js播放器总时长
    value === audioRef.current!.duration // 音频播放器总时长
  ) {
    // 上述两个总时长不是完全相等的,有些误差
    setIsPlay(false);
  }
};


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:表示播放的持续时间

// 播放或者暂停
const pauseOrPlay = () => {
  if (!player.current) return;
  if (isPlay) {
    player.current.stop();
    audioRef.current!.pause();
    setIsPlay(false);
  } else {
    if (
      currentTime >= allTime ||
      currentTime >= player.current.buffer.duration
    ) {
      // 重新播放
      player.current.start(0);
    } else {
      // 继续播放,使用offset控制
      player.current.start(0, currentTime);
    }
    audioRef.current!.play();
    setIsPlay(true);
  }
};

0条评论

您的电子邮件等信息不会被公开,以下所有项均必填

OK! You can skip this field.