Play PCM with javascript

2019-04-01 00:21发布

问题:

I got some problems playing PCM Audio on the browser. The PCM audio comes from an android device with udp-protocol and is saved on the server as *.raw

I unsuccessfully trying to play this saved file with the help of webaudioapi. Using following code, plays me some creepy sound with white noise:

var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
audioCtx.sampleRate = 16000;


// Stereo
var channels = 1;
// Create an empty two second stereo buffer at the
// sample rate of the AudioContext
var frameCount = audioCtx.sampleRate * 10.0;

var myAudioBuffer = audioCtx.createBuffer(channels, frameCount, audioCtx.sampleRate);


var req = new XMLHttpRequest();
req.open('GET', "example.raw", false);
req.overrideMimeType('text\/plain; charset=x-user-defined');
req.send(null);

function play(){
    for (var channel = 0; channel < channels; channel++) {

        var nowBuffering = myAudioBuffer.getChannelData(channel,16,16000);
        for (var i = 0; i < frameCount; i++) {
            // audio needs to be in [-1.0; 1.0]
            // for this reason I also tried to divide it by 32767
            // as my pcm sample is in 16-Bit. It plays still the
            // same creepy sound less noisy.
            nowBuffering[i] = (req.responseText.charCodeAt(i) & 0xff;

        }
    }
    // Get an AudioBufferSourceNode.
    // This is the AudioNode to use when we want to play an AudioBuffer
    var source = audioCtx.createBufferSource();
    // set the buffer in the AudioBufferSourceNode
    source.buffer = myAudioBuffer;
    // connect the AudioBufferSourceNode to the
    // destination so we can hear the sound
    source.connect(audioCtx.destination);
    // start the source playing
    source.start();
}

It's playing such an unidentifiable sound that I'm not sure if it's playing the pcm file which I supposed it has to do.

I'm supposing it has to do something with the pcm file. The PCM file has 16 kHz sample rate, 16 bits per sample and only one channel or rather mono-channel.

Anybody with the same problem here or did anybody have suggestions to fix my problem?

I am looking since some days for a solution and appreciate any help.

回答1:

First of all:

audioCtx.sampleRate = 16000; doesn't work. You can't modify audioCtx.sampleRate. Instead, you needed to do the following:

var frameCount = req.responseText.length / 2;
var myAudioBuffer = audioCtx.createBuffer(channels, frameCount, 16000);

Because your file is 16-bit, its length in bytes is twice the number of frames you need.

(req.responseText.charCodeAt(i) & 0xff) will yield a value between 0 and 255, representing a single 8-bit byte. You need 16 bits.

You need to know the byte order of your sample, and process two bytes each time

For little endian (LSB first):

var word = (req.responseText.charCodeAt(i * 2) & 0xff) + ((req.responseText.charCodeAt(i * 2 + 1) & 0xff) << 8);

For big endian (MSB first):

var unsignedWord = ((req.responseText.charCodeAt(i * 2) & 0xff) << 8) + (req.responseText.charCodeAt(i * 2 + 1) & 0xff);

That will yield a number between 0 and 65535, representing an unsigned 16-bit integer. In order to convert to signed integer, you need to do the following (replace X with the above code)

var signedWord = (unsignedWord + 32768) % 65536 - 32768;

This will yield a number between -32768 and 32767, which you can then divide by 32768.0 in order to obtain your desired result.

nowBuffering[i] = signedWord / 32768.0;

Edit: Working example https://o.lgm.cl/example.html (16-bit LSB)



回答2:

@Locolois

I tried your suggestion/solution and get some clear sounds, which unfortunately still didn't sound like the original. It has also white noise every second, which was less creepy than my solution :D but I still didn't hear my voice which I recorded. I'm not sure if the pcm, which android.audiorecord exports has big or little endian, so I tried both ways. But the sound which I heard by using the suggestion you made for big endian, sounded more correct for me than the little endian version. The little endian version was also fully with white noise.

Is it the right implementation, of your explanation?:

for (var i = 0; i < frameCount; i+=2) {     
    var msbFirst = (req.responseText.charCodeAt(i) & 0xff) + (req.responseText.charCodeAt(i + 1) & 0xff) << 8;
    var msbSigned = (msbFirst + 32768) % 65536 - 32768;
    nowBuffering[i] =  msbSigned / 65536.0;
}