How to play audio byte array (not file!) with JavaScript in a browser - streaming

For mostly security reasons, I'm not allowed to store a WAV file on the server to be accessed by a browser. What I have is a byte array contains audio data (the data portion of a WAV file I believe) on the sever, and I want it to be played on a browser through JavaScript (or Applet but JS preferred), I can use JSON-PRC to send the whole byte[] over, or I can open a socket to stream it over, but in either case I don't know who to play the byte[] within the browser?

The following code will play the sine wave at 0.5 and 2.0. Call the function play_buffersource() in your button or anywhere you want.
Tested using Chrome with Web Audio flag enabled. For your case, all that you need to do is just to shuffle your audio bytes to the buf.
<script type="text/javascript">
const kSampleRate = 44100; // Other sample rates might not work depending on the your browser's AudioContext
const kNumSamples = 16834;
const kFrequency = 440;
const kPI_2 = Math.PI * 2;
function play_buffersource() {
if (!window.AudioContext) {
if (!window.webkitAudioContext) {
alert("Your browser sucks because it does NOT support any AudioContext!");
return;
}
window.AudioContext = window.webkitAudioContext;
}
var ctx = new AudioContext();
var buffer = ctx.createBuffer(1, kNumSamples, kSampleRate);
var buf = buffer.getChannelData(0);
for (i = 0; i < kNumSamples; ++i) {
buf[i] = Math.sin(kFrequency * kPI_2 * i / kSampleRate);
}
var node = ctx.createBufferSource(0);
node.buffer = buffer;
node.connect(ctx.destination);
node.noteOn(ctx.currentTime + 0.5);
node = ctx.createBufferSource(0);
node.buffer = buffer;
node.connect(ctx.destination);
node.noteOn(ctx.currentTime + 2.0);
}
</script>
References:
http://epx.com.br/artigos/audioapi.php
https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html
If you need to resample the audio, you can use a JavaScript resampler: https://github.com/grantgalitz/XAudioJS
If you need to decode the base64 data, there are a lot of JavaScript base64 decoder: https://github.com/carlo/jquery-base64

I accomplished this via the following code. I pass in a byte array containing the data from the wav file to the function playByteArray. My solution is similar to Peter Lee's, but I could not get his to work in my case (the output was garbled) whereas this solution works well for me. I verified that it works in Firefox and Chrome.
window.onload = init;
var context; // Audio context
var buf; // Audio buffer
function init() {
if (!window.AudioContext) {
if (!window.webkitAudioContext) {
alert("Your browser does not support any AudioContext and cannot play back this audio.");
return;
}
window.AudioContext = window.webkitAudioContext;
}
context = new AudioContext();
}
function playByteArray(byteArray) {
var arrayBuffer = new ArrayBuffer(byteArray.length);
var bufferView = new Uint8Array(arrayBuffer);
for (i = 0; i < byteArray.length; i++) {
bufferView[i] = byteArray[i];
}
context.decodeAudioData(arrayBuffer, function(buffer) {
buf = buffer;
play();
});
}
// Play the loaded file
function play() {
// Create a source node from the buffer
var source = context.createBufferSource();
source.buffer = buf;
// Connect to the final output node (the speakers)
source.connect(context.destination);
// Play immediately
source.start(0);
}

If you have the bytes on the server then I would suggest that you create some kind of handler on the server that will stream the bytes to the response as a wav file. This "file" would only be in memory on the server and not on disk. Then the browser can just handle it like a normal wav file.
More details on the server stack would be needed to give more information on how this could be done in your environment.

I suspect you can achieve this with HTML5 Audio API easily enough:
https://developer.mozilla.org/en/Introducing_the_Audio_API_Extension
This library might come in handy too, though I'm not sure if it reflects the latest browser behaviours:
https://github.com/jussi-kalliokoski/audiolib.js

Related

Web audio playback contains clicks

I am trying to build a midi player using web audio API. I used tonejs to parse midi file into JSON. I am using mp3 files to play notes. Following are the relevant parts of the code:
//create audio samples
static async setupSample(audioContext, filepath) {
const response = await fetch(filepath);
const arrayBuffer = await response.arrayBuffer();
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
return audioBuffer;
}
//play a single sample
static playSample(audioContext, audioBuffer, time) {
const sampleSource = new AudioBufferSourceNode(audioContext, {
buffer: audioBuffer,
playbackRate: 1,
});
sampleSource.connect(audioContext.destination);
sampleSource.start(time);
return sampleSource;
}
Scheduling samples:
async start() {
this.startTime = this.audioCtx.currentTime;
this.play();
}
play() {
let nextNote = this.notes[this.noteIndex];
//schedule samples
while ((nextNote.time + this.startTime) - this.audioCtx.currentTime <= 0.250) {
let s = Audio.playSample(this.audioCtx, this.samples[nextNote.midi], this.startTime + nextNote.time);
s.stop(this.startTime + nextNote.time + nextNote.duration);
this.noteIndex++;
if (this.noteIndex == this.notes.length) {
break;
}
nextNote = this.notes[this.noteIndex];
}
if (this.noteIndex == this.notes.length) {
return;
}
requestAnimationFrame(() => {
this.play();
});
}
I am testing code with a midi file which contains C major scale. I have tested the midi file using timidity and it is fine.
The code does play the midi file correctly execpet a small problem: I hear some clicking sounds during playback. The clicking increases with increasing tempo but does not completely go away even with tempo as small as 50bpm. Any ideas what could be going wrong?
Full code can be viewed at : https://test.meedee.in/
Nothing is "wrong". You are observing a phenomenon intrinsic to the physics of audio.
Chopping audio samples arbitrarily like this creates clicks at the transitions. Any instantaneous change in level is heard as a click. To get rid of the clicks, apply an envelope to the sample, blend adjacent notes, or apply a low-pass filter.

Knowing when I can skip to any point in an audio file without buffering / delay in playback

I'm loading an MP3 on my webpage using audio = new Audio(). But I'd like to know that when setting audio.currentTime, the audio can skip to any point in the file -near the end or wherever -without any delay in playback. Ie I want to know when the MP3 has downloaded in its entirety.
Can I use the Audio object/element for this, or must I use an AudioContext as shown here?
Every AudioElement is exposing its buffered data as a TimeRanges object. TimeRanges is an object which tells you how many continuous parts aka ranges are already buffered. It does also have getters which return the respective start and end of each range in seconds.
In case your AudioElement is named audio the following code snippet will log the buffered time ranges at a given point in time.
const numberOfRanges = audio.buffered.length;
for (let i = 0; i < numberOfRanges; i += 1) {
console.log(
audio.buffered.start(i),
audio.buffered.end(i)
);
}
If you want to detect the point in time at which all data is buffered you could use a check similar to this one:
const isBufferedCompletely = (audio.buffered.length === 1
&& audio.buffered.start(0) === 0
&& audio.buffered.end(0) === audio.duration);
I used the Gist referenced in the comments below to construct an example. The following snippet will periodically check if the file is already buffered. It will log a message to the console once that is the case. I tested it on Chrome (v74) and Firefox (v66) on OS X. Please note that the file can't be played at the same time as the script will set the currentTime of the Audio Element.
const audio = new Audio('http://www.obamadownloads.com/mp3s/charleston-eulogy-speech.mp3');
audio.preload = 'auto';
function detectBuffered(duration) {
// Stick with the duration once it is known because it might get updated
// when reaching the end of the file.
if (duration === undefined && !isNaN(audio.duration)) {
duration = audio.duration;
}
const isBufferedCompletely = (audio.buffered.length === 1
&& audio.buffered.start(0) === 0
&& audio.buffered.end(0) === duration);
if (isBufferedCompletely) {
const seconds = Math.round(duration);
console.log('The complete file is buffered.');
console.log(`It is about ${ seconds } seconds long.`);
} else {
// Move the playhead of the audio element to get the browser to load
// the complete file.
if (audio.buffered.length > 0) {
audio.currentTime = Math.max(0, audio.buffered.end(0) - 1);
}
setTimeout(detectBuffered, 100, duration);
}
}
detectBuffered();

How can I loop AudioBufferSourceNode with overlapping (using the same AudioBuffer)

I need to loop my source with some cross parametr (in sec). It will be great to listen looping without interrupting on the sample border.AudioBufferSourceNode is audioNode in my code.
I faced with the problem of inability to reuse the buffer, is it possible to get around this?
playNoteOn: function(indexNote){
var attack = this.get('attack'),
release = this.get('release'),
volume = 1 + this.get('volume') / 100,
reverb = _.clone(this.get('reverb')),
loop = this.get('loop'), cross;
//peace for Loop process
if (loop) {
//milli sec
attack = this.get('startLoop')*1000;
release = this.get('endLoop')*1000;
//sec
cross = this.get('crossLoop');
}
//peace for ADSR process
var t0 = this.get('audioNode').context.currentTime,
spread = attack/1000 + release/1000,
attackSpread = t0 + attack/1000;
[this.get('schema').leftGain, this.get('schema').rightGain].forEach(function(gain, index){
gain.gain.cancelScheduledValues(0);
gain.gain.setValueAtTime(0, t0);
gain.gain.linearRampToValueAtTime(volume, attackSpread);
// gain.gain.setValueAtTime(volume, decaySpread);
// gain.gain.linearRampToValueAtTime(0, releaseSpread);
});
this.get('audioNode').connect(this.get('schema').splitter, 0, 0);
this.get('audioNode').connect(this.get('schema').leftGain);
this.get('audioNode').connect(this.get('schema').rightGain);
this.get('audioNode').connect(this.get('schema').reverb);
this.get('audioNode').connect(APP.Models.Synth.get('schema').reverb);
APP.Models.Synth.get('effects').where({active: false}).forEach(function(effect){
effect.get('node').disconnect();
});
APP.Models.Synth.get('effects').where({active: true}).forEach(function(effect){
effect.get('node').disconnect();
effect.get('node').setParams(effect.toJSON()).getNode(this.get('audioNode'), [this.get('schema').leftGain, this.get('schema').rightGain]);
}, this);
if(loop){
this.get('audioNode').loop = true;
this.get('audioNode').loopEnd = this.get('audioNode').buffer.duration - cross;
}
this.get('audioNode').start(t0);
},
You cannot reuse a buffer. Once stopped the sourcebuffer is gone forever. Where is your audio node object anyway? But that's no problem. When you decoded from a sound file you can use the buffer from the decoding again. Just create more buffer sources. You can create as much as you like. What language are you using anyway? Say Some backgrounds to what you are doing and which frameworks you use.
Beware the difference between buffer from decoding and buffer source. Your audionode is a buffer source which can be feeded by a buffer. You can reuse the buffer but not the buffer source. So create the buffer source in your playnote code.

Packing Speex with Ogg on iOS

I'm using libogg and libogg, I've succeeded to add those libraries to my iPhone xCode project and encode my voice with Speex. The problem is that I cannot figure out how to pack those audio packet with ogg. Does someone know how a packet of that kind should look like or have a reference code I can use.
I know in Java it's pretty simple (you have a dedicated function for that) but not on iOS. Please help.
UPD 10.09.2013: Please, see the demo project, which basically takes pcm audiodata from wave container, encodes it with speex codec and pack everything into ogg container. Maybe later I'll create a full-fledged library/framework for all that speex routines on IOS.
UPD 16.02.2015: The demo project is republished on GitHub.
I also have been experimenting with Speex on iOS recently, with varied success, but here is something I found. Basically, if you want to pack some speex-encoded voice into an ogg file, you need to do follow three steps (assuming libogg and libspeex are already compiled and added to the project).
1) Add the first ogg page with Speex header; libspeex provides built-in tools for that (the code below is from my project, not optimal, just for the sake of example):
// create speex header
SpeexHeader spxHeader;
SpeexMode spxMode = speex_wb_mode;
int spxRate = 16000;
int spxNumberOfChannels = 1;
speex_init_header(&spxHeader, spxRate, spxNumberOfChannels, &spxMode);
// set audio and ogg packing parameters
spxHeader.vbr = 0;
spxHeader.bitrate = 16;
spxHeader.frame_size = 320;
spxHeader.frames_per_packet = 1;
// wrap speex header in ogg packet
int oggPacketSize;
_oggPacket.packet = (unsigned char *)speex_header_to_packet(&spxHeader, &oggPacketSize);
_oggPacket.bytes = oggPacketSize;
_oggPacket.b_o_s = 1;
_oggPacket.e_o_s = 0;
_oggPacket.granulepos = 0;
_oggPacket.packetno = 0;
// submit the packet to the ogg streaming layer
ogg_stream_packetin(&_oggStreamState, &_oggPacket);
free(_oggPacket.packet);
// form an ogg page
ogg_stream_flush(&_oggStreamState, &_oggPage);
// write the page to file
[_oggFile appendBytes:&_oggStreamState.header length:_oggStreamState.header_fill];
[_oggFile appendBytes:_oggStreamState.body_data length:_oggStreamState.body_fill];
2) Add the second ogg page with Vorbis comment:
// form any comment you like (I use custom struct with all fields)
vorbisCommentStruct *vorbisComment = calloc(sizeof(vorbisCommentStruct), sizeof(char));
...
// wrap Vorbis comment in ogg packet
_oggPacket.packet = (unsigned char *)vorbisComment;
_oggPacket.bytes = vorbisCommentLength;
_oggPacket.b_o_s = 0;
_oggPacket.e_o_s = 0;
_oggPacket.granulepos = 0;
_oggPacket.packetno = _oggStreamState.packetno;
// the rest should be same as in previous step
...
3) Add subsequent ogg pages with your speex-encoded audio in the similar manner.
First of all decide how many frames with audio data you want to have on every ogg page (0-255; I choose 79 quite arbitrarily):
_framesPerOggPage = 79;
Then for each frame:
// calculate current granule position of audio data within ogg file
int curGranulePos = _spxSamplesPerFrame * _oggTotalFramesCount;
// wrap audio data in ogg packet
oggPacket.packet = (unsigned char *)spxFrame;
oggPacket.bytes = spxFrameLength;
oggPacket.granulepos = curGranulePos;
oggPacket.packetno = _oggStreamState.packetno;
oggPacket.b_o_s = 0;
oggPacket.e_o_s = 0;
// submit packets to streaming layer until their number reaches _framesPerOggPage
...
// if we've reached this limit, we're ready to create another ogg page
ogg_stream_flush(&_oggStreamState, &_oggPage);
[_oggFile appendBytes:&_oggStreamState.header length:_oggStreamState.header_fill];
[_oggFile appendBytes:_oggStreamState.body_data length:_oggStreamState.body_fill];
// finally, if this is the last frame, flush all remaining packets,
// which have been created but not packed into a page, to the last page
// (don't forget to set oggPacket.e_o_s to 1 for this frame)
That's it. Hope it will help. Any corrections or questions are welcome.

OpenSL ES can not play audio on Android emulator

I decode amrnb to PCM, then put right pcm buffer to Enqueue buffer (I'm sure PCM data is right), but no sound is heard. And when feeding buffer, log outputs:
/AudioTrack(14857): obtainBuffer timed out (is the CPU pegged?)
My code is below, and my questions are:
Is there something wrong when I use the OpenSL ES?
Is it true that OpenSL ES only works on the real device?
Sample code:
void AudioTest()
{
StartAudioPlay();
while(1)
{
//decode AMR to PCM
/* Convert to little endian and write to wav */
//write buffer to buffer queue
AudioBufferWrite(littleendian, 320);
}
}
void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void *context)
{
//do nothing
}
void AudioBufferWrite(const void* buffer, int size)
{
(*gBQBufferQueue)->Enqueue(gBQBufferQueue, buffer, size );
}
// create buffer queue audio player
void SlesCreateBQPlayer(/*AudioCallBackSL funCallback, void *soundMix,*/ int rate, int nChannel, int bitsPerSample )
{
SLresult result;
// configure audio source
SLDataLocator_AndroidSimpleBufferQueue loc_bufq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2};
SLDataFormat_PCM format_pcm = {SL_DATAFORMAT_PCM, 1, SL_SAMPLINGRATE_8,
SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16,
SL_SPEAKER_FRONT_CENTER, SL_BYTEORDER_LITTLEENDIAN};
SLDataSource audioSrc = {&loc_bufq, &format_pcm};
// configure audio sink
SLDataLocator_OutputMix loc_outmix = {SL_DATALOCATOR_OUTPUTMIX, gOutputMixObject};
SLDataSink audioSnk = {&loc_outmix, NULL};
// create audio player
const SLInterfaceID ids[3] = {SL_IID_BUFFERQUEUE, SL_IID_EFFECTSEND, SL_IID_VOLUME};
const SLboolean req[3] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE};
result = (*gEngineEngine)->CreateAudioPlayer(gEngineEngine, &gBQObject, &audioSrc, &audioSnk,
3, ids, req);
// realize the player
result = (*gBQObject)->Realize(gBQObject, SL_BOOLEAN_FALSE);
// get the play interface
result = (*gBQObject)->GetInterface(gBQObject, SL_IID_PLAY, &gBQPlay);
// get the buffer queue interface
result = (*gBQObject)->GetInterface(gBQObject, SL_IID_BUFFERQUEUE,
&gBQBufferQueue);
// register callback on the buffer queue
result = (*gBQBufferQueue)->RegisterCallback(gBQBufferQueue, bqPlayerCallback, NULL/*soundMix*/);
// get the effect send interface
result = (*gBQObject)->GetInterface(gBQObject, SL_IID_EFFECTSEND,
&gBQEffectSend);
// set the player's state to playing
result = (*gBQPlay)->SetPlayState(gBQPlay, SL_PLAYSTATE_PLAYING );
}
I'm not entirely sure, but I think you're correct in that the emulator's OpenSL ES support doesn't actually work. I've never gotten it to work in practice, while it works on any device I've tried.
In my application I have to support Android 2.2 as well, so I have a fallback to use JNI to access the Java AudioTrack APIs. I added a special case to my app to always use the AudioTrack interface when the emulator is detected.