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

如何基于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的增益曲线,精准控制淡入淡出的触发时机。

具体实现步骤

  1. 移除Buffer修改逻辑:删除原有的fadeInBuffer、fadeOutBuffer静态方法,以及fadeIn、fadeOut中修改Buffer的代码,改为仅记录淡入淡出的配置参数(时长、阈值)。
  2. 播放时动态控制增益:在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 01:54:57