-
-
Save hillct/b1b993470f0294e818c52df730448fa2 to your computer and use it in GitHub Desktop.
web audio + wav buffering
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Goal : instantly play any wav source without download the file and without <audio/> | |
Idea is to use the fetch streaming API and pass raw data to web audio | |
My use case is playng a wav file | |
following http://stackoverflow.com/questions/38589614/webaudio-streaming-with-fetch-domexception-unable-to-decode-audio-data/38593356#38593356 | |
you can try this example here : http://www.ee.columbia.edu/~dpwe/sounds/music/ | |
(just paste the code in the chrome console) | |
Problem : dirty sound | |
*/ | |
// from https://gist.github.com/asanoboy/3979747 | |
class Wav { | |
constructor(opt_params) { | |
this._sampleRate = opt_params && opt_params.sampleRate ? opt_params.sampleRate : 44100; | |
this._channels = opt_params && opt_params.channels ? opt_params.channels : 2; | |
this._eof = true; | |
this._bufferNeedle = 0; | |
this._buffer; | |
} | |
setBuffer(buffer) { | |
this._buffer = this.getWavInt16Array(buffer); | |
this._bufferNeedle = 0; | |
this._internalBuffer = ''; | |
this._hasOutputHeader = false; | |
this._eof = false; | |
} | |
getBuffer(len) { | |
var rt; | |
if( this._bufferNeedle + len >= this._buffer.length ){ | |
rt = new Int16Array(this._buffer.length - this._bufferNeedle); | |
this._eof = true; | |
} | |
else { | |
rt = new Int16Array(len); | |
} | |
for(var i=0; i<rt.length; i++){ | |
rt[i] = this._buffer[i+this._bufferNeedle]; | |
} | |
this._bufferNeedle += rt.length; | |
return rt.buffer; | |
} | |
eof() { | |
return this._eof; | |
} | |
getWavInt16Array(buffer) { | |
var intBuffer = new Int16Array(buffer.length + 23), tmp; | |
intBuffer[0] = 0x4952; // "RI" | |
intBuffer[1] = 0x4646; // "FF" | |
intBuffer[2] = (2*buffer.length + 15) & 0x0000ffff; // RIFF size | |
intBuffer[3] = ((2*buffer.length + 15) & 0xffff0000) >> 16; // RIFF size | |
intBuffer[4] = 0x4157; // "WA" | |
intBuffer[5] = 0x4556; // "VE" | |
intBuffer[6] = 0x6d66; // "fm" | |
intBuffer[7] = 0x2074; // "t " | |
intBuffer[8] = 0x0012; // fmt chunksize: 18 | |
intBuffer[9] = 0x0000; // | |
intBuffer[10] = 0x0001; // format tag : 1 | |
intBuffer[11] = this._channels; // channels: 2 | |
intBuffer[12] = this._sampleRate & 0x0000ffff; // sample per sec | |
intBuffer[13] = (this._sampleRate & 0xffff0000) >> 16; // sample per sec | |
intBuffer[14] = (2*this._channels*this._sampleRate) & 0x0000ffff; // byte per sec | |
intBuffer[15] = ((2*this._channels*this._sampleRate) & 0xffff0000) >> 16; // byte per sec | |
intBuffer[16] = 0x0004; // block align | |
intBuffer[17] = 0x0010; // bit per sample | |
intBuffer[18] = 0x0000; // cb size | |
intBuffer[19] = 0x6164; // "da" | |
intBuffer[20] = 0x6174; // "ta" | |
intBuffer[21] = (2*buffer.length) & 0x0000ffff; // data size[byte] | |
intBuffer[22] = ((2*buffer.length) & 0xffff0000) >> 16; // data size[byte] | |
for (var i = 0; i < buffer.length; i++) { | |
tmp = buffer[i]; | |
if (tmp >= 1) { | |
intBuffer[i+23] = (1 << 15) - 1; | |
} | |
else if (tmp <= -1) { | |
intBuffer[i+23] = -(1 << 15); | |
} | |
else { | |
intBuffer[i+23] = Math.round(tmp * (1 << 15)); | |
} | |
} | |
return intBuffer; | |
} | |
} | |
// factory | |
function createWavFromBuffer(buffer, sampleRate) { | |
var wav = new Wav({ | |
sampleRate: sampleRate, | |
channels: 1 | |
}); | |
wav.setBuffer(buffer); | |
return wav; | |
} | |
// ArrayBuffer -> Float32Array | |
var convertBlock = function(buffer) { | |
var incomingData = new Uint8Array(buffer); | |
var i, l = incomingData.length; | |
var outputData = new Float32Array(incomingData.length); | |
for (i = 0; i < l; i++) { | |
outputData[i] = (incomingData[i] - 128) / 128.0; | |
} | |
return outputData; | |
} | |
// function createLinkFromBlob(b) { | |
// var URLObject = window.webkitURL || window.URL; | |
// var url = URLObject.createObjectURL(b); | |
// // console.log(url); | |
// var link = document.createElement('a'); | |
// link.href = url | |
// link.textContent = 'play'; | |
// document.body.appendChild(link); | |
// } | |
// Wav -> blob | |
function getBlobFromWav(wav) { | |
var srclist = []; | |
while( !wav.eof() ){ | |
srclist.push(wav.getBuffer(1000)); | |
} | |
return new Blob(srclist, {type:'audio/wav'}); | |
} | |
// adapted from http://stackoverflow.com/questions/20475982/choppy-inaudible-playback-with-chunked-audio-through-web-audio-api | |
function play(url) { | |
var context = new (window.AudioContext || window.webkitAudioContext)(); | |
var audioStack = []; | |
// fetch API + streaming | |
// see https://jakearchibald.com/2015/thats-so-fetch/ | |
fetch(url).then(function(response) { | |
var reader = response.body.getReader(); | |
function read() { | |
// on each received chunk | |
return reader.read().then(({ value, done })=> { | |
if (value && value.buffer) { | |
// create a new .wav with the chunk | |
let wav = createWavFromBuffer(convertBlock(value.buffer), 44100); | |
// transform to blob | |
let blob = getBlobFromWav(wav); | |
// read blob and send to audio | |
var reader = new FileReader(); | |
reader.onloadend = function() { | |
context.decodeAudioData(reader.result, function(buf) { | |
audioStack.push(buf); | |
if (audioStack.length) { | |
scheduleBuffers(); | |
} | |
}); | |
}; | |
reader.readAsArrayBuffer(blob); | |
} | |
if (done) { | |
console.log('done'); | |
return; | |
} | |
// read next | |
read() | |
}); | |
} | |
// start reading | |
read(); | |
}) | |
var nextTime = 0; | |
function scheduleBuffers() { | |
while ( audioStack.length) { | |
var buffer = audioStack.shift(); | |
var source = context.createBufferSource(); | |
source.buffer = buffer; | |
source.connect(context.destination); | |
if (nextTime == 0) | |
nextTime = context.currentTime + 0.1; /// add 50ms latency to work well across systems - tune this if you like | |
source.start(nextTime); | |
nextTime += source.buffer.duration; // Make the next buffer wait the length of the last buffer before being played | |
}; | |
} | |
} | |
// try http://www.ee.columbia.edu/~dpwe/sounds/music/ | |
play('http://www.ee.columbia.edu/~dpwe/sounds/music/africa-toto.wav') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment