将大文件加载分成块,拼接到 AudioBuffer?

Splitting large file load into chunks, stitching to AudioBuffer?

在我的应用程序中,我有一个长达一小时的音频文件,完全是音效。不幸的是,我确实需要它们——它们是特定物种的声音,所以我无法删除它们中的任何一个。之前它们是分开的,但我将它们全部放入一个大文件中。

导出文件压缩后约为 20MB,但对于网速较慢的用户来说下载量仍然很大。我需要此文件位于 AudioBuffer 中,因为我正在寻找 audioSprite 的部分并使用 loopStart/loopEnd 仅循环该部分。我或多或少需要在开始播放之前下载全部内容,因为请求的物种是在应用程序启动时随机选择的。他们可能在文件开头或结尾处寻找声音。

我想知道的是,如果我将这个文件分成四份,我可以并行加载它们,并在加载完成后将它们拼接成完整的 AudioBuffer 吗?我猜我会合并多个数组,但只执行 decodeAudioData() 一次?请求 ~100 个单独的文件(太多)是我首先想到音频精灵的原因,但我想知道是否有一种方法可以利用一定数量的异步加载来减少所需的时间。我想过有四个 <audio> 元素并使用 createMediaElementSource() 加载它们,但我的理解是我不能(?)将 MediaElementSource 变成 AudioBuffer

我认为原则上可以。只需将每个块下载为 ArrayBuffer,将所有块连接在一起并将其发送到 decodeAudioData.

但是如果你的速度很慢 link,我不确定并行下载会有什么帮助。

编辑: 这段代码是有效的,但有时会产生非常讨厌的音频故障,所以我不建议在没有进一步测试的情况下使用它。我把它留在这里以防它帮助其他人弄清楚如何使用 Uint8Arrays.

这是它的基本版本,基本上就是 Raymond 所描述的内容。我还没有用大文件的拆分版本对此进行测试,所以我不知道它是否完全提高了加载速度,但它确实有效。 JS在下面,但是如果你想自己测试,here's the pen.

// mp3 link is from: https://codepen.io/SitePoint/pen/JRaLVR

(function () {
  'use strict';

  const context = new AudioContext();
  let bufferList = [];

  // change the urlList for your needs
  const URL = 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/123941/Yodel_Sound_Effect.mp3';
  const urlList = [URL, URL, URL, URL, URL, URL];

  const loadButton = document.querySelector('.loadFile');
  const playButton = document.querySelector('.playFile');

  loadButton.onclick = () => loadAllFiles(urlList, loadProgress);

  function play(audioBuffer) {
    const source = context.createBufferSource();
    source.buffer = audioBuffer;
    source.connect(context.destination);
    source.start();
  }

  // concatenates all the buffers into one collected ArrayBuffer
  function concatBufferList(buflist, len) {
    let tmp = new Uint8Array(len);
    let pos = 0;
    for (let i = 0; i < buflist.length; i++) {
      tmp.set(new Uint8Array(buflist[i]), pos);
      pos += buflist[i].byteLength;
    }
    return tmp.buffer;
  }

  function loadAllFiles(list, onProgress) {
    let fileCount = 0;
    let fileSize = 0;

    for (let i = 0; i < list.length; i++) {
      loadFileXML(list[i], loadProgress, i).then(e => {
        bufferList[i] = e.buf;
        fileSize += e.size;
        fileCount++;

        if (fileCount == bufferList.length) {
          let b = concatBufferList(bufferList, fileSize);
          context.decodeAudioData(b).then(audioBuffer => {
            playButton.disabled = false;
            playButton.onclick = () => play(audioBuffer);  
          }).catch(error => console.log(error));  
        }
      });
    }
  }

  // adapted from petervdn's audiobuffer-load on npm
  function loadFileXML(url, onProgress, index) {
    return new Promise((resolve, reject) => {
      const request = new XMLHttpRequest();

      request.open('GET', url, true);
      request.responseType = 'arraybuffer';

      if (onProgress) {
        request.onprogress = event => {
          onProgress(event.loaded / event.total);
        };
      }

      request.onload = () => {
        if (request.status === 200) {
          const fileSize = request.response.byteLength;
          resolve({
            buf: request.response,
            size: fileSize
          });
        }
        else {
          reject(`Error loading '${url}' (${request.status})`);
        }
      };

      request.onerror = error => {
        reject(error);
      };

      request.send();
    });  
  }

  function loadProgress(e) {
    console.log("Progress: "+e);
  }
}());

考虑立即分批播放文件,而不是等待整个文件下载完毕。您可以使用 Streams API 和:

执行此操作
  1. 使用 MediaSource Extensions (MSE) 排队块 API 并在缓冲区之间切换。
  2. 使用网络音频播放解码的 PCM 音频 API 和 AudioBuffer

See examples 用于在接收到文件块时进行低延迟音频播放。