You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用AudioContext实现多段音频的有序播放?

原生JavaScript实现多段音频有序播放与控制

需求说明

基于原生AudioContext实现以下功能:

  • 创建AudioContext实例
  • 将多段音频加载为缓冲区
  • 按指定时间点编排音频队列
  • 支持播放/暂停/停止控制
  • 支持音量与播放速度调节

示例场景

  • Audio1:0秒开始,时长5秒
  • Audio2:5秒开始,时长5秒
  • Audio3:20秒开始,时长10秒

完整实现代码

<button id="AudioLoad">加载音频</button>
<button id="AudioPlay">播放</button>
<button id="AudioPause">暂停</button>
<button id="AudioStop">停止</button>
<input id="AudioVolume" type="range" step="0.1" min="0" max="1.0" value="0.4">
<input id="AudioSpeed" type="range" step="0.1" min="0.5" max="2.0" value="1.0">

<script>
// 全局状态管理
let audioContext;
let gainNode;
let audioBuffers = [];
// 音频队列:存储{startTime: 计划开始时间(秒), buffer: 音频缓冲区}
let audioQueue = [];
// 播放状态标记
let isPlaying = false;
let isPaused = false;
// 暂停时的时间偏移量
let pauseOffset = 0;
// 当前活跃的音频源节点集合
let activeSources = [];

// 加载并解码音频为缓冲区的工具函数
async function loadAudioBuffer(url) {
  const response = await fetch(url);
  const arrayBuffer = await response.arrayBuffer();
  return await audioContext.decodeAudioData(arrayBuffer);
}

// 初始化播放器
const initPlayer = async () => {
  // 唤醒AudioContext(浏览器要求必须由用户交互触发初始化)
  audioContext = new window.AudioContext();
  gainNode = audioContext.createGain();
  gainNode.connect(audioContext.destination);
  
  // 设置初始音量
  gainNode.gain.value = document.getElementById('AudioVolume').value;

  // 加载指定音频
  audioBuffers[0] = await loadAudioBuffer('https://assets.mixkit.co/sfx/preview/mixkit-game-show-suspense-waiting-667.mp3');
  audioBuffers[1] = await loadAudioBuffer('https://assets.mixkit.co/sfx/preview/mixkit-retro-game-emergency-alarm-1000.mp3');
  audioBuffers[2] = await loadAudioBuffer('https://assets.mixkit.co/sfx/preview/mixkit-trumpet-fanfare-2293.mp3');

  // 编排音频播放队列
  audioQueue = [
    { startTime: 0, buffer: audioBuffers[0] },
    { startTime: 5, buffer: audioBuffers[1] },
    { startTime: 20, buffer: audioBuffers[2] }
  ];

  // 绑定控制按钮事件
  bindControls();
};

// 绑定控制按钮的交互逻辑
function bindControls() {
  // 播放按钮逻辑
  document.getElementById('AudioPlay').addEventListener('click', () => {
    if (!audioContext) return;
    
    if (isPaused) {
      resumePlayback();
    } else if (!isPlaying) {
      startPlayback();
    }
  });

  // 暂停按钮逻辑
  document.getElementById('AudioPause').addEventListener('click', () => {
    if (isPlaying && !isPaused) {
      pausePlayback();
    }
  });

  // 停止按钮逻辑
  document.getElementById('AudioStop').addEventListener('click', () => {
    stopPlayback();
  });

  // 音量调节逻辑
  document.getElementById('AudioVolume').addEventListener('input', (e) => {
    if (gainNode) {
      gainNode.gain.value = e.target.value;
    }
  });

  // 播放速度调节逻辑
  document.getElementById('AudioSpeed').addEventListener('input', (e) => {
    activeSources.forEach(source => {
      source.playbackRate.value = e.target.value;
    });
  });
}

// 开始播放队列音频
function startPlayback() {
  isPlaying = true;
  isPaused = false;
  pauseOffset = 0;
  
  const baseTime = audioContext.currentTime;
  
  audioQueue.forEach(item => {
    const source = audioContext.createBufferSource();
    source.buffer = item.buffer;
    source.connect(gainNode);
    source.playbackRate.value = document.getElementById('AudioSpeed').value;
    
    // 安排在指定时间播放(替代已废弃的noteOn方法)
    source.start(baseTime + item.startTime);
    
    activeSources.push(source);
    
    // 播放结束后从活跃列表移除
    source.onended = () => {
      activeSources = activeSources.filter(s => s !== source);
      if (activeSources.length === 0) {
        isPlaying = false;
      }
    };
  });
}

// 暂停播放
function pausePlayback() {
  isPaused = true;
  pauseOffset = audioContext.currentTime;
  
  // 停止所有活跃的音频源(BufferSource停止后无法复用,需重新创建)
  activeSources.forEach(source => {
    source.stop();
  });
  activeSources = [];
}

// 从暂停位置恢复播放
function resumePlayback() {
  isPaused = false;
  const currentTime = audioContext.currentTime;
  const elapsedSincePause = currentTime - pauseOffset;
  
  audioQueue.forEach(item => {
    // 计算该音频剩余的计划播放时间
    const scheduledStartTime = item.startTime - elapsedSincePause;
    
    if (scheduledStartTime <= 0) {
      // 该音频已到播放时间或正在播放中,计算已播放时长,从剩余位置开始
      const alreadyPlayed = elapsedSincePause - item.startTime;
      if (alreadyPlayed < item.buffer.duration) {
        const source = audioContext.createBufferSource();
        source.buffer = item.buffer;
        source.connect(gainNode);
        source.playbackRate.value = document.getElementById('AudioSpeed').value;
        source.start(currentTime, alreadyPlayed);
        activeSources.push(source);
        
        source.onended = () => {
          activeSources = activeSources.filter(s => s !== source);
          if (activeSources.length === 0) {
            isPlaying = false;
          }
        };
      }
    } else {
      // 该音频还未到播放时间,重新安排播放
      const source = audioContext.createBufferSource();
      source.buffer = item.buffer;
      source.connect(gainNode);
      source.playbackRate.value = document.getElementById('AudioSpeed').value;
      source.start(currentTime + scheduledStartTime);
      activeSources.push(source);
      
      source.onended = () => {
        activeSources = activeSources.filter(s => s !== source);
        if (activeSources.length === 0) {
          isPlaying = false;
        }
      };
    }
  });
}

// 停止播放并重置状态
function stopPlayback() {
  isPlaying = false;
  isPaused = false;
  pauseOffset = 0;
  
  activeSources.forEach(source => {
    source.stop();
  });
  activeSources = [];
}

// 绑定加载按钮事件
document.getElementById('AudioLoad').addEventListener('click', initPlayer);
</script>

实现要点说明

  1. AudioContext唤醒规则:浏览器要求AudioContext必须通过用户交互(如点击)初始化,否则会处于暂停状态,因此初始化逻辑绑定在加载按钮的点击事件中。
  2. 音频缓冲区管理:封装统一的加载函数处理音频的请求与解码,避免重复代码,同时将解码后的缓冲区存储在数组中方便复用。
  3. 队列编排逻辑:用数组存储每个音频的计划开始时间和对应缓冲区,便于后续播放控制时遍历处理。
  4. 播放控制核心:
    • 播放:遍历队列创建BufferSource节点,使用start()方法(替代已废弃的noteOn())安排在指定时间播放。
    • 暂停:记录当前时间偏移量,停止所有活跃的BufferSource(该节点一旦停止无法复用,必须重新创建)。
    • 恢复:根据暂停偏移量计算每个音频的剩余播放位置,重新创建BufferSource并从对应位置开始播放。
    • 停止:清空所有活跃节点,重置播放状态。
  5. 音量与速度调节:
    • 音量通过GainNode的gain.value实时控制,响应滑块输入。
    • 播放速度通过BufferSource的playbackRate.value控制,对当前所有活跃的音频源生效。

内容的提问来源于stack exchange,提问作者svarlitskiy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 18:21:27