从 url 中提取音频片段并使用纯网络音频播放 API

Extracting fragment of audio from a url and play it with pure Web Audio API

在下面url:

https://www.tophtml.com/snl/15.mp3

我想在 following range 上使用纯 Web Audio API 播放一首音频:

range from: second: 306.6
  range to: second: 311.8
     total: 5.2 seconds

我将该文件下载到我的桌​​面(我正在使用 Windows 10),然后使用 VLC 打开它并获得以下文件信息:

number of channels: 2
       sample rate: 44100 Hz
   bits per sample: 32 (float32)

在这里你有关于这个概念的信息:

https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Basic_concepts_behind_Web_Audio_API#Audio_buffers_frames_samples_and_channels

从那里我得到了以下摘录:

我要播放上面评论的range(也贴在这里):

range from: second: 306.6
  range to: second: 311.8
     total: 5.2 seconds

通过从服务器下载支持请求的片段 header:Range.

然后我尝试了下面的代码:

...
let num_channels    = 2;
let sample_rate     = 44100;
let range_from      = 0;                                    // Goal: 306.6 seconds
let range_length    = (sample_rate / num_channels) * 5.2;   // Goal:   5.2 seconds
let range_to        = range_from + (range_length - 1);      // "range_to" is inclusive (confirmed)
request.setRequestHeader("Range", "bytes=" + range_from + "-" + range_to);
...

我的问题是:

  1. 我需要为变量找到正确的值:range_from 所以它从秒开始播放:306.6.

  2. 我想知道上面为 range_length 指定的值是否正确,因为可能有用于 headers 等的字节,我的意思是: headers + data.

这里是我目前的代码:

window.AudioContext = window.AudioContext || window.webkitAudioContext; // necessary for iPhone (maybe others). Could change a near future.

const URL = 'https://www.tophtml.com/snl/15.mp3';
const context = new AudioContext();

window.addEventListener('load', function() {

 const button_option_1   = document.querySelector('.button_option_1');
 const button_option_1_play  = document.querySelector('.button_option_1_play');
 button_option_1_play.disabled = true;

 button_option_1.addEventListener('click', async function() {
  let time_start, duration;
  let buffer;
  log('...', false);
  button_option_1_play.disabled = true;
  button_option_1_play.onclick = () => playBuffer(buffer);
  //---
  time_start = new Date().getTime();
  let arrayBuffer = await fetch(URL);
  // download complete
  duration = sprintf('%.2fs', (new Date().getTime()-time_start)/1000);
  log(sprintf('P2. Delay: +%s for download. Wait...', duration));
  //---
  time_start = new Date().getTime();  
  let audioBuffer = await decodeAudioData(context, arrayBuffer);
  // decoding complete
  duration = sprintf('%.2fs', (new Date().getTime()-time_start)/1000);
  log(sprintf('P3. Delay: +%s for decoding.', duration));
  //---
  button_option_1_play.disabled = false;
  buffer = audioBuffer;
  button_option_1_play.click();
 });

});
function playBuffer(buffer, from, duration) {
 const source = context.createBufferSource(); // type of "source": "AudioBufferSourceNode"
 source.buffer = buffer;
 source.connect(context.destination);
 source.start(context.currentTime, from, duration);
}
function log(text, append = true) {
 let log = document.querySelector('.log');
 if (!append)
  log.innerHTML = '';
 let entry = document.createElement('div');
 entry.innerHTML = text;
 log.appendChild(entry);
}
function decodeAudioData(context, arrayBuffer) {
 return new Promise(async (resolve, reject) => {
  if (false) {}
  else if (context.decodeAudioData.length == 1) {
   // console.log('decodeAudioData / Way 1');
   let audioBuffer = await context.decodeAudioData(arrayBuffer);
   resolve(audioBuffer);
  }
  else if (context.decodeAudioData.length == 2) {
   // necessary for iPhone (Safari, Chrome) and Mac (Safari). Could change a near future.
   // console.log('decodeAudioData / Way 2');
   context.decodeAudioData(arrayBuffer, function onSuccess(audioBuffer) {
    resolve(audioBuffer);
   });
  }
 });
}
function fetch(url) {
 return new Promise((resolve, reject) => {
  var request = new XMLHttpRequest();
  request.open('GET', url, true);
  request.responseType = 'arraybuffer';
  let num_channels = 2;
  let sample_rate  = 44100;
  let range_from  = 0;         // Goal: 306.6 seconds
  let range_length = (sample_rate / num_channels) * 5.2; // Goal:   5.2 seconds
  let range_to  = range_from + (range_length - 1);  // "range_to" is inclusive (confirmed)
  request.setRequestHeader("Range", "bytes=" + range_from + "-" + range_to);
  request.onload = function() {
   let arrayBuffer = request.response;
   let byteArray = new Uint8Array(arrayBuffer);
   // console.log(Array.from(byteArray)); // just logging info
   resolve(arrayBuffer);
  }
  request.send();
 });
}
.log {
 display: inline-block;
 font-family: "Courier New", Courier, monospace;
 font-size: 13px;
 margin-top: 10px;
 padding: 4px; 
 background-color: #d4e4ff;
}
.divider {
 border-top: 1px solid #ccc;
 margin: 10px 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/sprintf/1.1.1/sprintf.min.js"></script>

<button class="button_option_1">Option 1</button>
<button class="button_option_1_play">Play</button><br />
<div class="log">[empty]</div>

这里有对应的CodePen.io:

https://codepen.io/anon/pen/RYXKmP

能否请您为 range_from 提供正确的值并将其用于 CodePen.io 上的分叉代码?

相关问题:https://engineering.stackexchange.com/questions/23929

[编辑 1]

这里有一个更简单的 CodePen.iohttps://codepen.io/anon/pen/YJKVde,它专注于检查浏览器在给定随机位置的情况下移动到下一个有效帧的能力。

在我做的一个快速实验中,使用 { Windows 10, Android, iPhone } x { Native browser, Chrome, Firefox } 的组合,上面右边的代码只适用于:{ (Windows 10, Chrome), (Android, Chrome), (Android, Native browser) }.

可惜不能用:

{ (iPhone, Safari), (iPhone, Chrome), (Windows 10, Firefox), (Android, Firefox) }

有没有什么办法可以向浏览器开发者提出请求,让他们关注这个问题?

Google ChromeWindows 10Android 上表现非常好。

如果其他浏览器也这样做会很有趣。

谢谢!

帧长(秒)= 帧采样数/采样率 38.28 frames/sec.

帧长度(字节)= 144*bitrate/sample速率

那么,您的 fetch() 现在应该可以工作了(我也更改了范围长度):

function fetch(url) {
  return new Promise((resolve, reject) => {
    var request = new XMLHttpRequest();
    request.open('GET', url, true);
    request.responseType = 'arraybuffer';
    let num_channels    = 2;
    let bitrate         = 192000;
    let sample_rate     = 44100;
    let byte_per_sec    = 144 * (bitrate/sample_rate) * 38.28;
    let range_from      = Math.floor(byte_per_sec * 306.6);
    let range_length    = Math.floor(byte_per_sec * 5.2);
    let range_to        = range_from + (range_length - 1);
    request.setRequestHeader("Range", "bytes=" + range_from + "-" + range_to);
    request.onload = function() {
        let arrayBuffer = request.response;
        let byteArray = new Uint8Array(arrayBuffer);
        //******************
            for ( let i = 0; i < byteArray.length; i += 1 ) {
                if (( byteArray[i] === 0b11111111 ) && ( byteArray[ i + 1 ] & 0b11110000 ) === 0b11110000 ){
                    log('we have a winner! Frame header at:'+i, true);
                    console.log((parseInt(byteArray[i], 10)).toString(2)); //frame header 4 bytes
                    console.log((parseInt(byteArray[i+1], 10)).toString(2));
                    console.log((parseInt(byteArray[i+2], 10)).toString(2));
                    console.log((parseInt(byteArray[i+3], 10)).toString(2));
                    resolve(arrayBuffer.slice(i));
                    break;
                }
            }
        //******************
    }
    request.send();
  });
}

编辑 我添加了基本框架 header 搜索,我的天哪,连老狐狸都吃掉了。 对于稳定的解决方案,您必须解析文件 header 以获取元数据,并将其与帧 header 数据进行比较。并在找不到 header 时做一些事情并且... ...