Choppy/inaudible playback with chunked audio through Web Audio API - html

I brought this up in my last post but since it was off topic from the original question I'm posting it separately. I'm having trouble with getting my transmitted audio to play back through Web Audio the same way it would sound in a media player. I have tried 2 different transmission protocols, binaryjs and socketio, and neither make a difference when trying to play through Web Audio. To rule out the transportation of the audio data being the issue I created an example that sends the data back to the server after it's received from the client and dumps the return to stdout. Piping that into VLC results in a listening experience that you would expect to hear.
To hear the results when playing through vlc, which sounds the way it should, run the example at https://github.com/grkblood13/web-audio-stream/tree/master/vlc using the following command:
$ node webaudio_vlc_svr.js | vlc -
For whatever reason though when I try to play this same audio data through Web Audio it fails miserably. The results are random noises with large gaps of silence in between.
What is wrong with the following code that is making the playback sound so bad?
window.AudioContext = window.AudioContext || window.webkitAudioContext;
var context = new AudioContext();
var delayTime = 0;
var init = 0;
var audioStack = [];
client.on('stream', function(stream, meta){
stream.on('data', function(data) {
context.decodeAudioData(data, function(buffer) {
audioStack.push(buffer);
if (audioStack.length > 10 && init == 0) { init++; playBuffer(); }
}, function(err) {
console.log("err(decodeAudioData): "+err);
});
});
});
function playBuffer() {
var buffer = audioStack.shift();
setTimeout( function() {
var source = context.createBufferSource();
source.buffer = buffer;
source.connect(context.destination);
source.start(context.currentTime);
delayTime=source.buffer.duration*1000; // Make the next buffer wait the length of the last buffer before being played
playBuffer();
}, delayTime);
}
Full source: https://github.com/grkblood13/web-audio-stream/tree/master/binaryjs

You really can't just call source.start(audioContext.currentTime) like that.
setTimeout() has a long and imprecise latency - other main-thread stuff can be going on, so your setTimeout() calls can be delayed by milliseconds, even tens of milliseconds (by garbage collection, JS execution, layout...) Your code is trying to immediately play audio - which needs to be started within about 0.02ms accuracy to not glitch - on a timer that has tens of milliseconds of imprecision.
The whole point of the web audio system is that the audio scheduler works in a separate high-priority thread, and you can pre-schedule audio (starts, stops, and audioparam changes) at very high accuracy. You should rewrite your system to:
1) track when the first block was scheduled in audiocontext time - and DON'T schedule the first block immediately, give some latency so your network can hopefully keep up.
2) schedule each successive block received in the future based on its "next block" timing.
e.g. (note I haven't tested this code, this is off the top of my head):
window.AudioContext = window.AudioContext || window.webkitAudioContext;
var context = new AudioContext();
var delayTime = 0;
var init = 0;
var audioStack = [];
var nextTime = 0;
client.on('stream', function(stream, meta){
stream.on('data', function(data) {
context.decodeAudioData(data, function(buffer) {
audioStack.push(buffer);
if ((init!=0) || (audioStack.length > 10)) { // make sure we put at least 10 chunks in the buffer before starting
init++;
scheduleBuffers();
}
}, function(err) {
console.log("err(decodeAudioData): "+err);
});
});
});
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.05; /// 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
};
}

Related

Why the web audio output from oscillator is not working as expected?

Here is the code:
I want to create an audio program that can play audio from very low frequency to high frequency.
However, this code results in different output (even with the same device):
The sound comes out suddenly - the expected result is it comes out gradually. I am sure my hearing is okay because I've asked my friends to hear;
The audio sounds different on the same frequency.
WARNING: Please adjust your volume to very low in case of any hurting before running this script.
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
// create Oscillator node
var oscillator = audioCtx.createOscillator();
var osc_arr = [];
function purgeSound(){
osc_arr.forEach(function(v){
try {
v.stop();
v.disconnect(audioCtx.destination);
} catch (e) {}
})
}
function playSoundAtFreq(fq){
purgeSound();
var osc = audioCtx.createOscillator();
osc_arr.push(osc);
osc.type = 'square';
osc.frequency.setValueAtTime(fq, audioCtx.currentTime); // value in hertz
$('#fff').val(fq);
osc.connect(audioCtx.destination);
osc.start();
}
$('#stop').click(function(){
purgeSound();
_break = true;
})
var _break = false;
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
var pointer = 0;
var go = appendAttemptAsync(10000);
async function appendAttemptAsync(range) {
if(_break) return;
var target = pointer+range;
for (pointer; pointer<range; pointer++) {
playSoundAtFreq(pointer);
console.log(pointer)
//if(pointer % 1 == 0) {
await sleep(100)
//}
}
return 5221;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<button id='stop'>stop</button>
<input id="fff" type="text" />
WARNING: Please adjust your volume to very low in case of any hurting before running this script.
Thanks for any kind of suggestions to improve my code.
If you want an Oscillator to sweep like in the YouTube video that you mentioned, you can do something like:
let osc = new OscillatorNode(audioCtx);
osc.connect(audioCtx.destination);
osc.frequency.setValueAtTime(20, audioCtx.currentTime);
osc.frequency.linearRampToValueAtTime(audioCtx.sampleRate / 2, audioCtx.currentTime + 300);
osc.start();
Change the 300 to some appropriate time over which the tone sweeps. I arbitrarily chose 5 minutes.
I do not know why your example doesn't work, but this snippet is the typical way to sweep a tone using WebAudio.

progressive load and play video from base64 pieces

I have many pieces of a video in base64.
Just that I want is to play the video progressively as I receive them.
var fileInput = document.querySelector('input#theInputFile');//multiple
fileInput.addEventListener('change', function(e) {
var files = fileInput.files;
for (var i = 0; i < files.length; i++) {
var file = fileInput.files[i]
fileLoaded(file, 0, 102400, file.size);
};
e.preventDefault();
});
videoA=[];
function fileLoaded(file, ini, end, size) {
if (end>size){end=size}
var reader = new FileReader();
var fr = new FileReader();
fr.onloadend = function(e) {
if (e.target.readyState == FileReader.DONE) {
var piece = e.target.result;
display(piece.replace('data:video/mp4;base64,', ''));
}
};
var blob = file.slice(ini, end, file.type);
fr.readAsDataURL(blob);
var init = end;
var endt = init+end;
if (end<size){
fileLoaded(file, init, end, size);
}
}
Trying to display the video by chunks:
var a=0;
function display(vid, ini, end) {
videoA.push(vid);
$('#video').attr('src','data:video/mp4;base64,'+videoA[a]);
a++;
}
I know this is not the way but I`m trying to search and any response adjust to that I'm searching.
Even I'm not sure if it is possible.
Thanks!
EDIT
I've tried to play the chunks one by one and the first one is played well but the rest of them give the error:
"Uncaught (in promise) DOMException: Failed to load because no supported source was found".
If I could make the chunks to base64 correctly it's enough for me
Ok, the solution is to solve the creation of base64 pieces from the original uploaded file in the browser that can be played by an html5 player
So I've put another question asking for that.
Chunk video mp4 file into base64 pieces with javascript on browser

WebAudio streaming with fetch : DOMException: Unable to decode audio data

I'm trying to play an infinite stream coming from the fetch API using Chrome 51. (a webcam audio stream as Microsoft PCM, 16 bit, mono 11025 Hz)
The code works almost OK with mp3s files, except some glitches, but it does not work at all with wav files for some reason i get "DOMException: Unable to decode audio data"
The code is adapted from this answer Choppy/inaudible playback with chunked audio through Web Audio API
Any idea if its possible to make it work with WAV streams ?
function play(url) {
var context = new (window.AudioContext || window.webkitAudioContext)();
var audioStack = [];
var nextTime = 0;
fetch(url).then(function(response) {
var reader = response.body.getReader();
function read() {
return reader.read().then(({ value, done })=> {
context.decodeAudioData(value.buffer, function(buffer) {
audioStack.push(buffer);
if (audioStack.length) {
scheduleBuffers();
}
}, function(err) {
console.log("err(decodeAudioData): "+err);
});
if (done) {
console.log('done');
return;
}
read()
});
}
read();
})
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.01; /// 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
};
}
}
Just use play('/path/to/mp3') to test the code. (the server needs to have CORS enabled, or be on the same domain your run script from)
AudioContext.decodeAudioData just isn't designed to decode partial files; it's intended for "short" (but complete) files. Due to the chunking design of MP3, it sometimes works on MP3 streams, but wouldn't on WAV files. You'll need to implement your own decoder in this case.
Making the wav stream sound correctly implies to add WAV headers to the chunks as Raymond suggested, plus some webaudio magic and paquet ordering checks;
Some cool guys helped me to setup that module to handle just that and it works beautifully on Chrome : https://github.com/revolunet/webaudio-wav-stream-player
Now works on Firefox 57+ with some config flags on : https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/getReader#Browser_compatibility

Chrome's webkitSpeechRecognition ends randomly

I'm using the Web Speech API to capture voice commands on my webpage, but the recognizer ends (it stops listening and fires the onend event) after a certain period of time.
Why does this happen? Can I prevent it?
Here is all the code needed to have a voice recognizing page (40 Lines) and reproduce the error. It will alert "end" when the recognizer stops listening.
<h1>Voice Recognizer</h1>
<script>
if (!('webkitSpeechRecognition' in window)) {
alert('Your browser does not support speech recognition.');
} else {
var recognition = new webkitSpeechRecognition();
recognition.continuous = true;
recognition.interimResults = true;
recognition.onstart = function() {
console.log('started');
}
recognition.onresult = function() {
interim_transcript = '';
for (var i = event.resultIndex; i < event.results.length; ++i) {
if (event.results[i].isFinal) {
final_transcript += event.results[i][0].transcript;
} else {
interim_transcript += event.results[i][0].transcript;
}
}
console.log('interim result:', interim_transcript);
console.log('final reuslt:', final_transcript);
}
recognition.onerror = function() { alert('error'); }
recognition.onend = function() { alert('end'); }
function startListening(e){
final_transcript = '';
recognition.start();
}
startListening();
}
</script>
Google tries to limit the amount of processed data because it loads their servers. Restart speech recognition once it is over or use some offline processing like Pocketsphinx.JS
First of all google has given 60 seconds of recording time as of now, and it is very efficient in listening, so i would suggest that you increase your speaking speed. If your speech takes more than 60 secs, then webkit will trigger the onspeechend() function that leads to stopping of your code. either way if you can write your own trigger for onspeechend() and again call your function from within, then it should work for you as it would start a new instance and would continue with your text, unless you clear it.

Live audio via socket.io 1.0

As from socket.io website
Binary streaming
Starting in 1.0, it's possible to send any blob back and forth: image, audio, video.
I'm now wondering, if this couldn't be the solution for something I'm trying to achieve recently.
I'm actually looking for a way how to broadcast live audio stream from (A - ie, mic input..) to all clients connected to a website of mine. Is something like this possible? I've been messing with WebRTC (https://www.webrtc-experiment.com/) examples but I haven't been able to manage the goal for more than few connected clients.
My idea is about something like getUserMedia or any other audio source (PCM, whatever..) on side A being chopped to chunks and provided to client and played for example by html5 audio element or anything.. I need to make that stream as much realtime as possible, no shout/ice cast services werent fast enough (indeed, they arent solution to my problem, they're meant to be used this way) and I don't really care about the audio quality. Crossplatform compatibility would be awesome.
Is something like that possible? By using socket.io as way how to provide those data to clients?
I would be very grateful for any reference, hint or source that could help me achieve this.
Thanks a lot.
This example shows you how to use the MediaRecorder to upload audio and then forward it using socket.io. This code will only broadcast after you're called mediaRecorder.stop(). You can choose to broadcast inside of ondataavailable. If you do that, you might want to pass a timeslice to mediaRecorder.start(), so that it doesn't trigger ondataavailable so often.
This solution isn't truly live, but I think it will help people who come back and find this question.
Client Code
var constraints = { audio: true };
navigator.mediaDevices.getUserMedia(constraints).then(function(mediaStream) {
var mediaRecorder = new MediaRecorder(mediaStream);
mediaRecorder.onstart = function(e) {
this.chunks = [];
};
mediaRecorder.ondataavailable = function(e) {
this.chunks.push(e.data);
};
mediaRecorder.onstop = function(e) {
var blob = new Blob(this.chunks, { 'type' : 'audio/ogg; codecs=opus' });
socket.emit('radio', blob);
};
// Start recording
mediaRecorder.start();
// Stop recording after 5 seconds and broadcast it to server
setTimeout(function() {
mediaRecorder.stop()
}, 5000);
});
// When the client receives a voice message it will play the sound
socket.on('voice', function(arrayBuffer) {
var blob = new Blob([arrayBuffer], { 'type' : 'audio/ogg; codecs=opus' });
var audio = document.createElement('audio');
audio.src = window.URL.createObjectURL(blob);
audio.play();
});
Server Code
socket.on('radio', function(blob) {
// can choose to broadcast it to whoever you want
socket.broadcast.emit('voice', blob);
});
In the Client Code you can write setInterval() instead of setTimeout() and then recursively call mediaRecorder.start() so that every 5 seconds the blob will be emitted continuously.
setInterval(function() {
mediaRecorder.stop()
mediaRecorder.start()
}, 5000);
Client Code
var constraints = { audio: true };
navigator.mediaDevices.getUserMedia(constraints).then(function(mediaStream) {
var mediaRecorder = new MediaRecorder(mediaStream);
mediaRecorder.onstart = function(e) {
this.chunks = [];
};
mediaRecorder.ondataavailable = function(e) {
this.chunks.push(e.data);
};
mediaRecorder.onstop = function(e) {
var blob = new Blob(this.chunks, { 'type' : 'audio/ogg; codecs=opus' });
socket.emit('radio', blob);
};
// Start recording
mediaRecorder.start();
// Stop recording after 5 seconds and broadcast it to server
setInterval(function() {
mediaRecorder.stop()
mediaRecorder.start()
}, 5000);
});
// When the client receives a voice message it will play the sound
socket.on('voice', function(arrayBuffer) {
var blob = new Blob([arrayBuffer], { 'type' : 'audio/ogg; codecs=opus' });
var audio = document.createElement('audio');
audio.src = window.URL.createObjectURL(blob);
audio.play();
});
Server Code
socket.on('voice', function(blob) {
// can choose to broadcast it to whoever you want
socket.broadcast.emit('voice', blob);
});