如何基于Mozilla Web Audio API实现循环音频首尾淡入淡出
问题描述
我正在开发一款音频编辑插件,需求之一是让音频播放时循环n次。目前通过设置AudioBufferSourceNode的src.loop = true,再调用src.stop(timeToStop)实现,其中timeToStop = this.ac.currentTime + this.delay + (this.getDuration() * this.loopCount),全程未修改AudioBuffer。
但现在给AudioBuffer添加淡入淡出效果时遇到两个问题:
- 淡入会在每次循环时重复出现
- 淡出完全不生效(推测原因是淡出的结束时间超过了音频本身的时长)
我需要实现仅在第1次循环开始时淡入,第n次(最后一次)循环结束时淡出的效果,请问能否不修改或复制n次AudioBuffer来实现?
相关代码
启动音频播放的方法
play({start} = {}) { //play is called at the end when all effects are applied to audioBuffer if(this.isPaused){ this.isPaused = false; this.isPlaying = true; this.resumeState(); return; } if(!start){ start = 0; } if(this.ac.state == 'suspended'){ this.resumeState(); } this.enqueue(() => this.promiseInit() .then(() => { this.createSource(); this.src.connect(this.masterGain); if(this.isLooping){ this.src.loop = true; this.src.loopStart = 0; } this.startSrc(start); this.src.stop(this.ac.currentTime + this.delay + (this.getDuration() * this.loopCount)); //delay will always be zero this.savePlayPosition(); this.fireEvent('playing', this.name); }) ) this.flushQueue(); }
给AudioBuffer添加淡入淡出的方法
fadeOut({start, end}) { console.log("Fade out called. Start is = " + start + " and end = " + end); this.enqueue(() => this.promiseInit() .then(() => WebAudio.fadeOutBuffer({ buffer: this.getAudioBuffer(), start, end, threshold: this.fadeThreshold })) .then((renderedBuffer) => { this.loadNewBuffer(renderedBuffer); this.fireEvent("effect-applied", this.name, "fadeOut"); }) .catch((err) => this.fireEvent('error', this.name, err)) ); } fadeIn({start, end}) { // start is zero console.log("Fade in called. Start is = " + start + " and end = " + end); this.enqueue(() => this.promiseInit() .then(() => WebAudio.fadeInBuffer({ buffer: this.getAudioBuffer(), start, end, threshold: this.fadeThreshold })) .then((renderedBuffer) => { this.loadNewBuffer(renderedBuffer); this.fireEvent("effect-applied", this.name, "fadeIn"); }) .catch((err) => this.fireEvent('error', this.name, err)) ); }
使用OfflineAudioContext编辑AudioBuffer的静态方法
static fadeOutBuffer({buffer, start = 0, end = buffer.duration, threshold = 0}) { let ac = new OfflineAudioContext(buffer.numberOfChannels, buffer.length, buffer.sampleRate); const source = new AudioBufferSourceNode(ac, {buffer}); const gain = new GainNode(ac); gain.gain.setValueAtTime(1, start); gain.gain.linearRampToValueAtTime(threshold, end); gain.gain.setValueAtTime(1, end); source.connect(gain).connect(ac.destination); source.start(); return ac.startRendering() } static fadeInBuffer({buffer, start = 0, end = buffer.duration, threshold = 0}) { let ac = new OfflineAudioContext(buffer.numberOfChannels, buffer.length, buffer.sampleRate); const source = new AudioBufferSourceNode(ac, {buffer}); const gain = new GainNode(ac); gain.gain.setValueAtTime(threshold, start); gain.gain.linearRampToValueAtTime(1, end); source.connect(gain).connect(ac.destination); source.start(); return ac.startRendering() }
解决方案
完全可以不修改或复制AudioBuffer实现需求,核心思路是放弃离线修改Buffer的方式,改为在播放流程中动态控制GainNode的增益曲线,精准控制淡入淡出的触发时机。
具体实现步骤
- 移除Buffer修改逻辑:删除原有的
fadeInBuffer、fadeOutBuffer静态方法,以及fadeIn、fadeOut中修改Buffer的代码,改为仅记录淡入淡出的配置参数(时长、阈值)。 - 播放时动态控制增益:在
play方法中插入专用的GainNode,根据总播放时长计算淡入、淡出的时间节点,设置对应的增益变化曲线。
修改后的关键代码
淡入淡出配置方法
// 仅保存淡入配置,不修改Buffer fadeIn({duration, threshold = 0}) { this.fadeInDuration = duration; this.fadeThreshold = threshold; this.fireEvent("effect-applied", this.name, "fadeIn"); } // 仅保存淡出配置,不修改Buffer fadeOut({duration, threshold = 0}) { this.fadeOutDuration = duration; this.fadeThreshold = threshold; this.fireEvent("effect-applied", this.name, "fadeOut"); }
调整后的play方法
play({start} = {}) { if(this.isPaused){ this.isPaused = false; this.isPlaying = true; this.resumeState(); return; } if(!start){ start = 0; } if(this.ac.state == 'suspended'){ this.resumeState(); } this.enqueue(() => this.promiseInit() .then(() => { this.createSource(); // 创建专门处理淡入淡出的GainNode,插入到源节点和主增益之间 const fadeGain = new GainNode(this.ac); this.src.connect(fadeGain); fadeGain.connect(this.masterGain); const currentTime = this.ac.currentTime; const audioDuration = this.getDuration(); const totalPlayTime = this.delay + (audioDuration * this.loopCount); const stopTime = currentTime + totalPlayTime; // 处理淡入:仅在第一次循环开始时执行 if (this.fadeInDuration) { fadeGain.gain.setValueAtTime(this.fadeThreshold, currentTime); fadeGain.gain.linearRampToValueAtTime(1, currentTime + this.fadeInDuration); } else { fadeGain.gain.setValueAtTime(1, currentTime); } // 处理淡出:仅在最后一次循环末尾执行 if (this.fadeOutDuration) { const fadeOutStartTime = stopTime - this.fadeOutDuration; // 确保淡出开始时间晚于淡入结束时间 const safeFadeOutStart = Math.max(fadeOutStartTime, currentTime + (this.fadeInDuration || 0)); if (safeFadeOutStart < stopTime) { fadeGain.gain.setValueAtTime(1, safeFadeOutStart); fadeGain.gain.linearRampToValueAtTime(this.fadeThreshold, stopTime); } } if(this.isLooping){ this.src.loop = true; this.src.loopStart = 0; } this.startSrc(start); this.src.stop(stopTime); this.savePlayPosition(); this.fireEvent('playing', this.name); }) ) this.flushQueue(); }
方案优势
- 完全不需要修改或复制
AudioBuffer,避免循环时重复播放淡入效果的问题。 - 淡出时间基于总播放时长计算,和单段音频时长无关,彻底解决淡出不生效的问题。
- 动态控制增益是Web Audio API的标准用法,性能更优,节省内存。
内容的提问来源于stack exchange,提问作者FKhan
相关产品推荐
相关产品推荐

