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

你说的曾经没有我的故事 提交于 2019-12-08 03:38:36

问题


On the following url:

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

there is one audio I want to play using pure Web Audio API on the following range:

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

I downloaded that file to my desktop (I'm using Windows 10), then opened it with VLC and got the following file info:

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

Here you have info about concepts on this:

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

from where I got the following excerpt:

I want to play the range commented above (also pasting it here):

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

by downloading just that fragment from the server, which supports the request header: Range.

Then I tried the following code:

...
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);
...

My questions are:

  1. I need to find the right value for variable: range_from so it starts playing from second: 306.6.

  2. I want to know if the value specified above for: range_length is correct or not since probably there are bytes used for headers, etc., I mean: headers + data.

Here you have the code I have so far:

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>

Here you have the corresponding CodePen.io:

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

Could you please, provide the right value for: range_from and use it on a forked code on CodePen.io?

Related question: https://engineering.stackexchange.com/questions/23929

[EDIT 1]

Here is a simpler CodePen.io: https://codepen.io/anon/pen/YJKVde, which is focused on check the ability of the browser to move, given a random position, to the next valid frame.

On a quick experiment I did, using combinations of { Windows 10, Android, iPhone } x { Native browser, Chrome, Firefox }, the right above code only works on: { (Windows 10, Chrome), (Android, Chrome), (Android, Native browser) }.

It's a pity it doesn't work on:

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

Is there a way we can submit a request to the browser developers to pay attention to this?

Google Chrome is doing really well on Windows 10 and Android.

It would be interesting that the rest of the browsers do the same.

Thanks!


回答1:


Frame length (sec) = frame samples / sample rate which makes 38.28 frames/sec.

Fram length (byte) = 144*bitrate/sample rate

So, your fetch() should work now (I changed range length too):

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();
  });
}

EDIT I added basic frame header search and, my'o'my, even old fox eats that. For a stabile solution you'll have to parse file header to get metadata, to compare that against frame header data. And do something when header is not found and... ...



来源:https://stackoverflow.com/questions/52513425/extracting-fragment-of-audio-from-a-url-and-play-it-with-pure-web-audio-api

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!