Antd Audio自定义音频播放器

1,742 阅读4分钟

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

1. 原生音频播放器

原生音频播放器,使用 HTML5 的 Audio 标签,可以播放音频文件,并且可以控制播放、暂停、音量、下载、倍数等等。

原生效果,如下图所示:

m_audio_1.png

如果要去掉下载和倍速功能,可以配置controlsList属性:

  • nodownload: 禁用音频播放器下载媒体功能。
  • noplaybackrate:禁用音频播放器控制播放速率功能。
// 音频源
const audioSrc = "/audio/mp3Trans14s.mp3";
// 音频元素
const audioRef = useRef<HTMLAudioElement | null>(null);
// 音频播放器
<audio
  ref={audioRef}
  controls
  controlsList="nodownload noplaybackrate"
  className="audio"
  src={audioSrc}
/>;

m_audio_2.png

2. 自定义播放器

原生音频播放器,尽管提供了基本的播放功能,但在界面和功能上往往显得不足。

因此,对于追求更佳用户体验的应用来说,需要自定义实现,满足更漂亮的UI体验。

左边为原生的,自定义实现效果如右边所示:

m_audio_3.png

主要技术栈:

  • React:流行的 JavaScript 库,用于构建用户界面。
  • Ant Design:一套企业级 UI 设计语言和 React 组件库,用于快速搭建美观的用户界面。
  • Tone.js:一个成熟且功能全面的音频库,特别适合音乐制作和音频处理。

2.1 进度条控制

获取总时长:通过 useEffect 钩子用于监听音频元素的生命周期事件,如 canplaytimeupdate,以更新播放状态和播放总时长。

注意:刷新时数据就没了,需要强制加载音频资源,如 audioEle.load()

// 管理音频的总时长
const [allTime, setAllTime] = useState(0);
useEffect(() => {
  // 获取音频元素的引用
  const audioEle = audioRef.current;
  // 1. 定义canplay事件处理器,当音频可以开始播放时触发
  const onCanPlay = () => {
    if (audioEle) {
      // 设置总时长
      setAllTime(audioEle.duration);
    }
  };
  // 2. 定义timeupdate事件处理器,当音频播放时间更新时触发
  const onTimeUpdate = () => {
    if (!audioEle) return;
    // 更新当前播放时间
    setCurrentTime(audioEle.currentTime);
    // 如果当前播放时间等于总时长,设置播放状态为false
    if (audioEle.currentTime === audioEle.duration) {
      setIsPlay(false);
    }
  };

  // 如果音频元素存在,添加事件监听器
  if (audioEle) {
    audioEle.addEventListener("canplay", onCanPlay);
    audioEle.addEventListener("timeupdate", onTimeUpdate);

    // 强制加载音频资源
    audioEle.load();

    // 返回清除事件监听器的函数,确保组件卸载时移除监听器
    return () => {
      audioEle?.removeEventListener("canplay", onCanPlay);
      audioEle?.removeEventListener("timeupdate", onTimeUpdate);
    };
  }
}, []);

进度条控制如下,可通过滑动条的changeTime动态修改播放时间

// 定义一个函数来格式化时间显示
const formatSecond = (time: number) => {
  const second = Math.floor(time % 60); // 计算秒数
  let minute = Math.floor(time / 60); // 计算分钟数
  // 返回格式化的时间字符串
  return `${minute}:${second >= 10 ? second : `0${second}`}`;
};

// 修改播放时间
const changeTime = (value: number) => {
  if (!audioRef.current) return;
  // 设置新的播放时间
  audioRef.current.currentTime = value;
  // 更新当前播放时间状态
  setCurrentTime(value);
  // 如果新的播放时间等于总时长,更新播放/暂停按钮状态
  if (value === audioRef.current.duration) {
    setIsPlay(false);
  }
};

/* 时常/进度条控制 */
<div className="time">
  <div className="time_text">
    <span>{formatSecond(currentTime)}</span>
    <span className="time_text_split">/</span>
    <span>{formatSecond(allTime)}</span>
  </div>
  <Slider max={allTime} value={currentTime} onChange={changeTime} />
</div>;

当然也可以不使用Ant Design的 Slider 组件,可以用原生的input组件实现:

<input
  type="range"
  step="0.01"
  max={allTime}
  value={currentTime}
  onChange={(e: any) => changeTime(e.target.value)}
/>

m_audio_8.png

2.1 开始/停止播放

通过 isPlay 管理播放状态,实现播放(play)/暂停(pause)功能

const [isPlay, setIsPlay] = useState(false);
// 播放或者暂停
const pauseOrPlay = () => {
  if (!audioRef.current) return;
  if (isPlay) {
    audioRef.current.pause();  // 暂停音频
    setIsPlay(false);
  } else {
    audioRef.current.play(); // 播放音频
    setIsPlay(true);
  }
};

/* 播放/暂停按钮 */
<div onClick={pauseOrPlay} className="toggle">
  {/* 根据播放状态显示播放或暂停图标 */}
  {isPlay ? <PauseCircleOutlined /> : <PlayCircleOutlined />}
</div>;

2.3 音量/禁音控制

音量控制和静音功能通过 volumemuted 属性实现,同时使用 Ant Design 的 Slider 组件提供直观的音量调节界面。

const [isMuted, setIsMuted] = useState(false);
const [volume, setVolume] = useState(100);
// 改变音量
const changeVolume = (value: number) => {
  if (!audioRef.current) return;
  // 设置音量,将百分比转换为0到1之间的值
  audioRef.current.volume = value / 100;
  setVolume(value);
  // 如果音量为0,则设置为静音
  setIsMuted(!value);
};
//禁音/取消禁音
const onMuteAudio = () => {
  if (!audioRef.current) return;
  setIsMuted(!isMuted);
  // 设置音频元素的静音状态
  audioRef.current.muted = !isMuted;
};

/* 音量控制 */
<Tooltip
  overlayClassName="volume-tooltip"
  // 显示垂直滑动条作为音量控制器
  title={
    <Slider vertical value={isMuted ? 0 : volume} onChange={changeVolume} />
  }
  color="#fff"
>
  <div onClick={onMuteAudio} className="volume">
    {/* 根据静音状态显示静音或音量开启图标 */}
    <img src={isMuted ? icAudioMute : icAudioOpen} alt="禁音" />
  </div>
</Tooltip>;

禁音效果:

m_audio_4.png

调整音量效果:

m_audio_5.png

2.4 倍速控制

倍速播放功能通过修改 playbackRate 属性实现,用户可以选择不同的播放速率。

const [playRate, setPlayRate] = useState(1.0);

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

/* 倍速播放 */
<Tooltip
  overlayClassName="rate-tooltip"
  title={rateList.map((o) => (
    <div
      key={o.value}
      className={`item ${playRate === o.value ? "active" : ""}`}
      onClick={() => changePlayPitch(o.value)}
    >
      {o.label}
    </div>
  ))}
  color="#fff"
>
  <Button type="link">倍速</Button>
</Tooltip>;

调整倍速效果:

m_audio_6.png

2.5 音高控制

使用了 Tone.js,Tone.js 是一个成熟的音频库,专为创作交互式音乐和复杂的音频应用而设计。它提供了高级别的构建块,如合成器、采样器、音序器和效果器,同时也提供了低级别的构建块,允许你创建自定义的声音处理器和复杂的控制信号。

音高调整是通过 Tone.js 的 PitchShift 节点实现的。当用户选择不同的音高时,会创建一个新的 Player 实例,并通过 PitchShift 节点进行音高变换。

// 使用引入
import * as Tone from "tone";
// 使用 useRef 来保存 Tone.js 中的 Player 和 PitchShift 对象实例
const player = useRef<Tone.Player | null>(null);
const pitchShift = useRef<Tone.PitchShift | null>(null);
// 存储音高的偏移量
const [playPitch, setPlayPitch] = useState(0);

useEffect(() => {
  return () => {
    // 在组件卸载时清理 Tone.js 的资源,避免内存泄漏
    player.current?.dispose();
    pitchShift.current?.dispose();
  };
}, []);
// 播放音高
const changePlayPitch = async (num: number) => {
  // 更新音高偏移量的状态
  setPlayPitch(num);

  // 已有播放器实例,先停止并断开连接
  if (player.current) {
    player.current.stop();
    player.current.disconnect();
    pitchShift.current?.disconnect();
    player.current.dispose();
    pitchShift.current?.dispose();
  }

  // 1. 创建 Tone.Player 实例,只在首次调用时创建(用于播放音频文件)
  player.current = new Tone.Player(audioSrc);
  // 2. 创建 PitchShift 节点,参数为音高偏移量(用于改变音高)
  // 0.5-升半个调,1-升一个全音
  pitchShift.current = new Tone.PitchShift(num);
  // 3. 将 Player 的输出连接到 PitchShift 节点
  player.current.connect(pitchShift.current);
  // 4. 将 PitchShift 节点的输出连接到音频上下文的目的地
  pitchShift.current.toDestination();

  // 等待所有音频资源加载完成
  await Tone.loaded();
  // 开始播放音频
  player.current!.start();
};

调整音高效果如下所示:

m_audio_7.png

实际上,React 和 Tone.js 的组合可以支持更复杂的功能,如实时音频分析、音轨混音和多音轨同步等。。。大家感兴趣的话,可以多研究研究。