IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    独家,MP3音频淡入淡出播放和转换的JS实现

    张 鑫旭发表于 2024-07-28 17:02:44
    love 0

    by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=11293
    本文可全文转载,独立域名个人网站无需授权,但需要保留原作者、出处以及文中链接,任何网站均可摘要聚合,商用请联系授权。

    封面占位图

    一、需求背景

    希望特效音可以有淡入淡出效果,同时能够在视频合成的时候体现在音轨中。

    二、播放淡入淡出

    先讲下偏浅层应用的技术实现,那就是播放淡入淡出。

    1. 使用howlerjs实现

    howlerjs这个项目(https://github.com/goldfire/howler.js)我多次提到过,凡是音频播放的需求(除了那种简单的点击播放),无需任何迟疑,也不用考虑其他,就是要howlerjs。

    两三万Star的项目,品质保证。

    下面演示下如何实现。

    假设页面上有个按钮,点击按钮播放某音频,HTML示意:

    <button id="button">点击播放</button>

    则下面几行JavaScript代码可以实现音频播放淡入,结束淡出的效果。

    const url = './htmlbook.wav';
    
    const sound = new Howl({
        src: [url]
    });
    
    // 点击按钮,音频淡入淡出播放
    button.onclick = function () {
        sound.play();
        sound.fade(0, 1, 1000);
        setTimeout(() => {
            sound.fade(1, 0, 1000);
        }, sound.duration() * 1000 - 1500);
    }

    根据我查找的资料,howlerjs并未提供内置的结尾淡出方法,只有一个fade()方法,因此,结束时候声音淡出使用了定时器。而之所以定时器offset的时间是1500ms而不是淡出的1000ms时间,是因为案例使用的htmlbook.wav末尾有一段声音是静音,为了让淡出效果明显,才如此处理的,不代表通用场景。

    眼见为实,您可以狠狠地点击这里:使用Howler.js实现音频播放淡入淡出demo

    点击下图所示的按钮,就可以听到WAV音频声音fade播放的效果了。

    音频播放demo示意

    2. 使用原生的GainNode实现

    JS Web Audio API提供了原生的音量动态函数变化方法。包括:

    • linearRampToValueAtTime() 音量线性变化
      线性音量变化示意图
    • exponentialRampToValueAtTime() 音量指数变化
      音量指数变化示意图
    • setValueCurveAtTime() 音量曲线变化
      音量曲线变化示意图
    • setTargetAtTime() 指数级音量变化,无限接近。
      指数接近变化示意图
    • setValueAtTime() 即时改变音量。

    下面,我们使用GainNode对象和linearRampToValueAtTime()方法示意如何实现音频播放首尾淡入淡出效果,和上面按钮同样的HTML代码,就一个按钮,然后JS代码如下所示。

    const url = './htmlbook.wav';
    
    // 点击按钮,音频淡入淡出播放
    button.onclick = function () {
        // 请求数据
        fetch(url).then(res => res.arrayBuffer()).then(buffer => {
            const audioContext = new AudioContext();
            audioContext.decodeAudioData(buffer, function (audioBuffer) {
                const source = audioContext.createBufferSource();
                const gainNode = audioContext.createGain();
                // 起始音量静音
                gainNode.gain.value = 0;
                gainNode.connect(audioContext.destination);
                // buffer数据设置
                source.buffer = audioBuffer;
                source.connect(gainNode);
                source.start();
    
                gainNode.gain.linearRampToValueAtTime(1.0, audioContext.currentTime + 1);
    
                // 时长计算
                const duration = audioBuffer.duration;
    
                setTimeout(() => {
                    gainNode.gain.linearRampToValueAtTime(0.01, audioContext.currentTime + 1);
                }, duration * 1000 - 1200);
            });
        });
    }

    此时,点击按钮,就可以听到声音淡入淡出播放的效果了。例如,点击下面这个按钮:

    如果没有效果,多半是在第三方网站,狠击这里访问实例:使用GainNode实现音频淡入淡出播放demo

    补充说明

    根据自己和官方demo测试,exponentialRampToValueAtTime()函数执行的时候,声音是突然变化的,不知道是不是我设备的问题。

    三、音频底层淡入淡出转换

    如果我们希望对音频进行转换,也就是直接把原始音频转换成淡入淡出的音频,就需要处理音频的AudioBuffer数据。

    这个就相对深入些了,原理什么的大家应该都不关心,不啰嗦,3,2,1,直接上代码,见:

    const sliceAudio = function (buffer, start, end, fadeIn = 0, fadeOut = 0) {
      const sampleRate = buffer.sampleRate;
      const audioContext = new AudioContext();
    
      const length = Math.round((end - start) * sampleRate);
      const offset = Math.round(start * sampleRate);
      const newBuffer = audioContext.createBuffer(buffer.numberOfChannels, length, sampleRate);
    
      for (let channel = 0; channel < buffer.numberOfChannels; channel++) {
        const inputData = buffer.getChannelData(channel);
        const outputData = newBuffer.getChannelData(channel);
    
        for (let i = 0; i < length; i++) {
          outputData[i] = inputData[offset + i];
    
          // Apply fade in
          if (i < fadeIn * sampleRate) {
            outputData[i] *= i / (fadeIn * sampleRate);
          }
    
          // Apply fade out
          if (i > length - fadeOut * sampleRate) {
            outputData[i] *= (length - i) / (fadeOut * sampleRate);
          }
        }
      }
    
      return newBuffer;
    }

    其中:

    buffer
    需要转换的AudioBuffer数据
    start
    音频复制起始时间
    end
    音频复制结束时间,如果结束时间就是音频时长,此参数可以设置为buffer.duration。
    fadeIn
    开头淡入的时长,单位是秒
    fadeOut
    结束淡出的时长,单位是秒

    具体如何应用呢?可以参见下面的演示页面。

    您可以狠狠地点击这里:MP3/Wav AudioBuffer转换成淡入淡出音频demo

    点击“转换”按钮,下面的音频播放器播放的就是被淡入淡出处理后的音频,点击现在我们可以得到这个新的WAV音频文件。

    淡入淡出转换截图示意

    主要应用代码示意(从点击按钮开始):

    const url = './htmlbook.wav';
    
    // 点击按钮,音频淡入淡出播放
    button.onclick = function () {
      // 请求数据
      fetch(url).then(res => res.arrayBuffer()).then(buffer => {
        const audioContext = new AudioContext();
        audioContext.decodeAudioData(buffer, (audioBuffer) => {
          const convertBuffer = sliceAudio(audioBuffer, 0, audioBuffer.duration, 1, 1.5);
          // 转blob WAV
          const wavBuffer = audioBufferToWav(convertBuffer);
          const wavBlob = new Blob([wavBuffer], { type: 'audio/wav' });
          const urlBlob = URL.createObjectURL(wavBlob);
    
          // create download link and append to Dom
          const downloadLink = document.createElement('a');
          downloadLink.href = urlBlob;
          downloadLink.setAttribute('download', 'htmlbook-fade.wav');
          downloadLink.textContent = '下载';
    
          // 按钮禁用
          this.disabled = true;
          // 试听支持
          audio.src = urlBlob;
          // 下载支持
          audio.after(downloadLink);
        });
      });
    }

    更完整的处理代码,参见上面的演示页面的源代码(演示页面省略了AudioBuffer转Wav blob数据的代码)。

    四、来了来了,本周的碎碎念

    周三的时候,我的新书已经是京东日榜第1了,看来口碑发酵了,感谢大家的正版支持。

    日榜第1

    稍后补充,先发布。

    本文为原创文章,会经常更新知识点以及修正一些错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验。
    本文地址:https://www.zhangxinxu.com/wordpress/?p=11293

    (本篇完)



沪ICP备19023445号-2号
友情链接