3

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:

enter image description here

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:

enter image description here

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!

davidesp
  • 3,743
  • 10
  • 39
  • 77

1 Answers1

3

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

Sven Liivak
  • 1,323
  • 9
  • 10
  • Thanks @Sven. Unfortunately, for some reason is not working for me. It is playing from the beginning. Could you please, provide a forked `CodePen.io`?. The fragment that should be played is: `"Mister president, why do you keep attacking Amazon?. Do you really hate Jeff Bezos that much?"`. Thanks! – davidesp Sep 26 '18 at 15:11
  • yeah, range values can't be floats. I fixed 'em. Now it should work, this is the fragment i hear. – Sven Liivak Sep 26 '18 at 15:45
  • still not working, I just tried this code: https://codepen.io/anon/pen/ZMgmxX, and got the following result: https://image.ibb.co/f3u2XU/image.png. Could yo please, provide a forked `CodePen.io` with your changes? Thanks! – davidesp Sep 26 '18 at 16:05
  • just in case, here I opened a related discussion (non programming, though): https://engineering.stackexchange.com/questions/23929 – davidesp Sep 26 '18 at 18:21
  • It.does work for me: [https://ibb.co/e0FwsU](https://ibb.co/e0FwsU) BUT! If it doesn't for you then it is not donable. mp3 is collection of frames - we just cut the sourcefile from random point and most probably do that in the middle of the frame. My browser (or op.sys decoder?) fixes the first, broken frame, yours not. Solution would be to search frame header from incoming stream and throw away broken frame data. Btw. exact frame location is not possible to calculate due to mp3 spec. – Sven Liivak Sep 26 '18 at 18:36
  • thanks @SvenLiivak. This time I tried on Chrome the same `CodePen.io` I linked above: https://codepen.io/anon/pen/ZMgmxX and it worked fine which is good. But on Firefox I'm getting the error you can see here: https://image.ibb.co/kfMuU9/image.png. What's the browser you successfully tried? Do you have any idea on how to make the above code works on Firefox in the same way as on Chrome? By the way, on the console of both browsers I see the same array data. – David Smith Sep 26 '18 at 18:54
  • is there any pattern we could look for to figure out the beginning of the next valid frame? Maybe paying attention to the header of the very first frame, saving it, and then look for the same next header when looking at random positions? – David Smith Sep 26 '18 at 18:56
  • eleven ones... but looking only for that will give lot of false positives. Further analysis request downloading file header first to get some metadata which then will be used for frame identification. [https://www.codeproject.com/Articles/8295/MPEG-Audio-Frame-Header](https://www.codeproject.com/Articles/8295/MPEG-Audio-Frame-Header) – Sven Liivak Sep 26 '18 at 20:52
  • Like I thought - Firefox didn't like broken frame. Now with this audiofile works but for a future you need to implement full header checking. Good luck! – Sven Liivak Sep 26 '18 at 22:45
  • @SvenLiivak, could you read my **[EDIT 1]** on my initial post and probably suggest me how can I do a formal request to main browser developers?, do you think it makes sense?. Thanks! – davidesp Sep 27 '18 at 07:55
  • Nope. Mp3 is not meant to be sliced. It is a packed format, using different ways to compress data. One frame can consist data of other frames, IIRC even 10 frames can be tied this way. Which means - to do everything correctly the file must be downloaded fully. – Sven Liivak Sep 27 '18 at 10:21
  • [https://codepen.io/anon/pen/aRoVjY](https://codepen.io/anon/pen/aRoVjY) I put last edit up, works with Chrome, Firefox, Opera @ Windows and Chrome, Native @ Android. – Sven Liivak Sep 27 '18 at 10:24
  • wow @SvenLiivak, that was really good!. Could you please, paste here the references that helped you to arrive to that approach? Thanks! – davidesp Sep 27 '18 at 13:36
  • Could you also clarify a bit the origin of the constants you use on your code? I mean: `{ 144, 38.28 }`? – davidesp Sep 28 '18 at 04:44