问题
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:
I need to find the right value for variable:
range_from
so it starts playing from second:306.6
.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