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

    不改变音调情况下Audio音频的倍速合成JS实现

    张 鑫旭发表于 2024-02-29 14:22:22
    love 0

    by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=11124 鑫空间-鑫生活
    本文欢迎分享与聚合,全文转载就不必了,尊重版权,圈子就这么大,若急用可以联系授权。

    音频倍速音调不变封面图

    一、以为很简单,结果…

    遇到需求,需要对音频倍速合成,可能是0.5倍速,也可能是2倍速。

    这还不简单,0.5倍速那就采样的时候两两重复,2倍速间隔取样就好了,相关代码如下所示(假设音频地址是url,播放速率变量是rate):

    fetch(url)
        .then(response => response.arrayBuffer())
        .then(arrayBuffer => new AudioContext().decodeAudioData(arrayBuffer))
        .then(originAudioBuffer => {
            // 创建新的AudioBuffer
            const audioBuffer = new AudioContext().createBuffer(
                originAudioBuffer.numberOfChannels,
                originAudioBuffer.length / rate,
                originAudioBuffer.sampleRate
            );
    
            // 复制原始音频的数据到新的AudioBuffer
            for (let channel = 0; channel < originAudioBuffer.numberOfChannels; channel++) {
                const originChannelData = originAudioBuffer.getChannelData(channel);
                const newChannelData = audioBuffer.getChannelData(channel);
                for (let i = 0; i < audioBuffer.length; i += 1) {
                     newChannelData[i] = originChannelData[Math.floor(i * rate)] || 0;
                }
            }
    
            // 此时 audioBuffer 就是变速后的音频数据
            // 你可以用来播放,或者上传,或者下载
        });
    

    上面代码中的audioBuffer就是就是变速后的音频数据,你可以用来播放,或者上传,或者下载,具体如何实现,可参见之前“纯JS实现多个音频的拼接或者合并”一文。

    代码一跑,嘿嘿,时长符合预期,但是竖起耳朵一听,不对劲,很不对劲,这声音的音调怎么变了。

    慢速的时候声音变得特别的低沉,快速的时候音调非常高。

    大家也可以来感受下,您可以狠狠地点击这里:JS音频倍速后音调发生变化demo

    比方说demo页面中这段“斗气化马”的录音,两倍速后,声音听起来就像是捏着嗓子的孩童音。

    声音变调截图

    怎么回事,难道是代码逻辑有问题。

    于是花时间研究了一番,发现事情不简单。

    二、变速变音调的原因解密

    众所周知,声音其实就是一种波,在数学上常常用三角函数表示。

    播放速率变快,也就意味着波形越陡峭,自然音调也就越高。

    下面这个GIF动图很好地示意了这一点(原图访问这里):

    GIF与音调原理示意

    此图源自非常优质的一片外文“Everything You Should Know About Sound”,可以加深大家对音频本质的了解。

    如何解决?

    音频拉伸影响音调其实是业界比较知名的一个问题了,非常考验算法功力,所以各路大神分分出马,诞生了很多经典的算法。

    例如颗粒合成(Granular synthesis)算法,大致原理是这样的。

    音频采样是一个一个的点,这些点连在一起,就会构成波峰或曲线,为了方便示意,我们用一条直线代替,如下所示:

    采样点曲线

    现在缩短音频播放时长,也就是加快音频的播放速率,那么采样点曲线就会变得更加陡峭,就会是这样子:

    曲线陡峭

    陡峭也就意味着音调变高了,显然不是我们想要的,所以需要保持速率的同时让曲线不陡峭,可以这么处理,把曲线截成一段一段的颗粒,让这段颗粒的曲度和默认播放曲线一致,然后这些颗粒的起点位置偏移,保持设定的播放速率,就像下图这样:

    颗粒算法说明

    于是,用户听起来播放速度快了,同时音调并未发生变化。

    当然,还有很多其他的算法,我并未去深究。

    这些算法本质上是数学计算,有些还牵扯到傅里叶变换,而这些知识,我早已经还给教授了,所以,当机立断,找别人写好的算法,直接套用实现。

    三、某个简单的算法实现

    一开始找的是这个项目,原因无他,代码少,如何使用一目了然,参见:https://github.com/danigb/timestretch

    其算法代码不多,直接粘贴出来都可以:

    /**
     * https://github.com/danigb/timestretch/blob/master/lib/index.js
     * Copy `len` bytes generated by a function to `array` starting at `pos`
     */
    function copy (len, array, pos, fn) {
      for (var i = 0; i < len; i++) {
        array[pos + i] = fn(i)
      }
    }
    
    function stretch (ac, input, scale, options) {
      // OPTIONS
      var opts = options || {}
      // Processing sequence size (100 msec with 44100Hz sample rate)
      var seqSize = opts.seqSize || 4410
      // Overlapping size (20 msec)
      var overlap = opts.overlap || 882
      // Best overlap offset seeking window (15 msec)
      // var seekWindow = opts.seekWindow || 662
    
      // The theoretical start of the next sequence
      var nextOffset = Math.round(seqSize / scale)
    
      // Setup the buffers
      var numSamples = input.length
      var output = ac.createBuffer(1, numSamples * scale, input.sampleRate)
      var inL = input.getChannelData(0)
      var outL = output.getChannelData(0)
    
      // STATE
      // where to read then next sequence
      var read = 0
      // where to write the next sequence
      var write = 0
      // where to read the next fadeout
      var readOverlap = 0
    
      while (numSamples - read > seqSize) {
        // write the first overlap
        copy(overlap, outL, write, function (i) {
          var fadeIn = i / overlap
          var fadeOut = 1 - fadeIn
          // Mix the begin of the new sequence with the tail of the sequence last
          return (inL[read + i] * fadeIn + inL[readOverlap + i] * fadeOut) / 2
        })
        copy(seqSize - overlap, outL, write + overlap, function (i) {
          // Copy the tail of the sequence
          return inL[read + overlap + i]
        })
        // the next overlap is after this sequence
        readOverlap += read + seqSize
        // the next sequence is after the nextOffset
        read += nextOffset
        // we wrote a complete sequence
        write += seqSize
      }
    
      return output
    }

    使用非常简单,几行代码的事情,示意如下(假设原音频数据对象是 audioBuffer,播放速率是rate):

    // 倍速变化,返回新的AudioBuffer
    const newAudioBuffer = stretch(new AudioContext(), audioBuffer, 1 / rate, {
        seqSize: audioBuffer.sampleRate / 10,
        overlap: audioBuffer.sampleRate / 100
    });

    上面代码返回的newAudioBuffer就是倍速处理后的新的音频。

    我们来看看这段代码真实应用后的效果,您可以狠狠地点击这里:JS音频倍速采用音调保持不变简易算法demo

    还是斗气化马的音频,2倍速,细细听一下,可以听出是我的声音,可却有很多噪点杂音,听起来并不舒服:

    噪声明显

    跑了下音频波形图,诸多细节丢失,很多地方过于平滑,感觉像是有些高音调的采样数据没能调好,混在一起,就好似噪点这般。

    波形图细节

    我琢磨着,会不会是因为我录音的时候,本身就有背景噪音导致,于是,就使用生产环境的AI语音进行测试。

    我嘞个擦,正常声音的背后还有一道连续不断的刺耳的重金属噪音。

    结论很明显,虽然此项目算法简单,但是效果真的是差,不能用在真实的项目中。

    作者似乎也意识到自己这段代码的算法不是很牛逼的那种,于是有说:

    This is not the best implementation if you’re looking for sound quality and/or performance (see FAQ).

    如果您正在寻找音质和/或性能,这不是最好的实现(请参阅FAQ)。

    而此项目的FAQ正好收罗了诸多音频时间拉伸算法,我差不多都研究了下,随后决定使用 OLA-TS.js 。

    音频时间拉伸算法截图

    四、声音质量更好OLA算法实现

    项目地址:https://github.com/echo66/OLA-TS.js

    作者是这么描述的:

    OLA-TS.js是一种改进的重叠和添加(OLA)算法的音频时间拉伸实现。

    一番使用下来,效果确实好。

    如何使用?

    此项目非常老,年久失修,要是无人指引,想要上手还是要费一番功夫的。

    经过我的一番整合,调试,终于弄了个可用的版本(原本的实现只支持多通道音频)。

    很简单,首先,引入bufferedOla.js:

    import BufferedOLA from './bufferedOla.js';

    然后,通用下面的语法进行音频拉伸处理(假设原音频数据对象是 audioBuffer,播放速率是rate):

    // 创建新的audiobuffer
    const newAudioBuffer = new AudioContext().createBuffer(
      audioBuffer.numberOfChannels,
      audioBuffer.length / rate,
      audioBuffer.sampleRate,
    );
    const myOLATS = new BufferedOLA();
    myOLATS.set_audio_buffer(audioBuffer);
    myOLATS.alpha = 1 / rate;
    
    myOLATS.process(newAudioBuffer);

    执行完上面的代码后,newAudioBuffer就是处理后的倍速音频AudioBuffer数据了。

    眼见为实,您可以狠狠地点击这里:JS音频时间拉伸改进算法demo

    此时,再去2倍速听“斗气化马”,喔噢,没有任何杂音,非常流畅,非常完美。

    完美效果使用示意

    五、结束说明

    看看时间,12点之前写不完了,前段时间感冒发烧,家里领导规定必须12点之前关机休息,写到这里23:53,加上还要配图,写摘要,写关键字,社交账号写周知消息,12点之前定是来不及了。

    保存草稿,明天发布,断更就断更吧,身体要紧。

    最后,附上那些与音频时间速率音调相关的项目:

    • https://github.com/sebpiq/paulstretch.js
    • https://github.com/echo66/OLA-TS.js
    • https://github.com/echo66/PhaseVocoderJS
    • https://github.com/Infinity/Kali
    • https://github.com/0xfe/vexwarp
    • https://github.com/mikolalysenko/pitch-shift
    • https://github.com/GTCMT/pitchshiftjs

    全都是很多年之前的项目了,就这样,我们下篇文章再见。

    😀😃
    😄😁
    😆😀

    本文为原创文章,欢迎分享,勿全文转载,如果实在喜欢,可收藏,永不过期,且会及时更新知识点及修正错误,阅读体验也更好。
    本文地址:https://www.zhangxinxu.com/wordpress/?p=11124

    (本篇完)



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