1

I have a software-defined radio playing an audio stream from a WebSocket server, and a client which consumes the data and plays it using an AudioBufferSourceNode.

It mostly works. The only problem is that there are momentary dropouts every few seconds, presumably caused by the overhead involved in creating each successive AudioBufferSourceNode instance. The WebAudio draft spec says that AudioBuffer should be used for playing sounds that are no longer than a minute or so, and that longer sounds should be played using a MediaElementSourceNode. That doesn't work for me, because I need to play audio from a WebSocket source, and there's no way that I know of to make a media element (e.g. HTML5 audio element) work with a WebSocket.

Maybe I'm trying to do something WebAudio can't support by stringing AudioBufferSourceNode instances together and expecting them to play one after another seamlessly. But it seems there should be a way to play WebSocket data through WebAudio, and indeed auroa.js (together with the aurora-websocket.js plugin) seems to do it. I coded up a client using aurora.js, but I ran into other problems, for which I created an auroa.js issue on Github. In the meantime, I'm hoping that I can do in my client what they seem to have done wrt using WebAudio to play data seamlessly from a WebSocket.

Here is an elided view of my code, to show the implementation I'm using.

var context = ...
var gainNode = ...

var playBuffer = function(buf) {
   var source = context.createBufferSource();
   source.buffer = buf;
   source.connect(gainNode);
   source.start();
};

var socket = ...
socket.binaryType = 'arraybuffer';
socket.addBinaryListener(function (data) {
     context.decodeAudioData(data, playBuffer);
});
socket.connect...

I also tried an implementation wherein I keep track of incoming buffers from the WebSocket and play them in the order received, via an AudioBufferSourceNode, after the 'ended' event is received from the previous AudioBufferSourceNode. This has the same dropout problem that the above implementation has.

MidnightJava
  • 1,927
  • 2
  • 18
  • 38

1 Answers1

1

Your stream is really guaranteed to get complete audio files in each network chunk? (decodeAudioData does not work with partial MP3 chunks.)

It seems like (from the code snippet above) you're just relying on network timing to get the stream chunks started at the right time? That's guaranteed not to line up properly; you need to keep a bit of latency in the stream (to handle inconsistent network), and carefully schedule each chunk. (The bit above that makes me cringe is source.start() - with no time param that will keep the chunks scheduled one right after another. i.e.:

var nextStartTime = 0;

function addChunkToQueue( buffer ) {
    if (!nextStartTime) {
        // we've not yet started the queue - just queue this up,
        // leaving a "latency gap" so we're not desperately trying
        // to keep up.  Note if the network is slow, this is going
        // to fail.  Latency gap here is 1 second.
        nextStartTime = audioContext.currentTime + 1; 
    }
    var bsn = audioContext.createBufferSource();
    bsn.buffer = buffer;
    bsn.connect( audioContext.destination );
    bsn.start( nextStartTime );

    // Ensure the next chunk will start at the right time
    nextStartTime += buffer.duration;
}

In addition, depending on how big your chunks are, I'd wonder if garbage collection isn't contributing to the problem. You should check it out in the profiler.

The onended path is not going to work well; it's reliant on JS event handling, and only fires AFTER the audio system is done playing; so there will ALWAYS be a gap.

Finally - this is not going to work well if the sound stream does not match the default audio device's sample rate; there are always going to be clicks, because decodeAudioData will resample to the device rate, which will not have a perfect duration. It will work, but there will likely be artifacts like clicks at the boundaries of chunks. You need a feature that's not yet spec'ed or implemented - selectable AudioContext sample rates - in order to fix this.

cwilso
  • 13,610
  • 1
  • 30
  • 35
  • Thanks. I did have the start time being handled as you show; I had taken out as one of the things I tried in getting it to work, and forgot to put it back in. So I still had the dropouts with the proper timing. I switched from ogg/vorbis to wav/lpcm data, ad everything is fine. It may have been a problem with the vorbis encoder my colleague wrote; but I didn't have the dropouts when using aurora.js and vorbis.js (but vorbis.js would quit with a memory error after 13 min 25 sec). Uncompressed data is fine for our application. – MidnightJava Dec 12 '14 at 22:54
  • As for sample rate, our source is sampling at 32000 and the AudioContext reports a sample rate of 41100. I don't hear any artifacts on the buffer boundaries, but it's not hi fidelity audio. It's a proof of concept software radio using an RTL device, and we're just streaming mono. – MidnightJava Dec 12 '14 at 22:54
  • 1
    Ah. Most lossy encoders pad the data, so maybe you're getting inconsistent lengths. – cwilso Dec 13 '14 at 15:13
  • you might want to do the ogg decoding in a web worker – Scott Stensland Dec 14 '14 at 04:17
  • How would you do the decoding in a web worker? Web worker can't get an Audio context to decodeAudioData with? – Todd Freed Feb 12 '22 at 07:01