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

