Ragy Morkos
Ragy Morkos

Reputation: 83

Obtaining microphone PCM data from getChannelData method using WebAudio API doesn't work

I'm trying to obtain raw PCM samples from the microphone using the Web Audio API. After some research, it seems that I should be getting "a Float32Array containing the PCM data associated with the channel". The flow works for me, but when I try to put the PCM samples in a WAV file, I get random noise and not what I actually spoke into the microphone while it was recording. Here's a code snippet for obtaining the PCM data using AudioBuffer.getChannelData() that you can paste directly into the browser console:

let recLength = 0;
let recBuffers = [];

let bufferLen = 4096;
let numChannels = 1;
let mimeType = 'audio/wav';

window.AudioContext = window.AudioContext || window.webkitAudioContext;
let audio_context = new AudioContext();

let audio_stream;

navigator.mediaDevices.getUserMedia({audio: true}).then(function (stream)
{
    audio_stream = stream;
});

// wait a sec or two

let source = audio_context.createMediaStreamSource(audio_stream);
let context = source.context;
let sampleRate = context.sampleRate; // you will need to know this number for creating a WAV file.

let node = (context.createScriptProcessor || context.createJavaScriptNode).call(context, bufferLen, numChannels, numChannels);

node.onaudioprocess = function(e)
{
    const inputBuffer = e.inputBuffer.getChannelData(0);

    recBuffers.push(inputBuffer);
    recLength += inputBuffer.length;
};

source.connect(node);
node.connect(context.destination);

// wait 10 seconds or so, while speaking into microphone

node.disconnect();

function mergeBuffers()
{
    let result = new Float32Array(recLength);
    let offset = 0;
    for (let i = 0; i < recBuffers.length; i++) {
        result.set(recBuffers[i], offset);
        offset += recBuffers[i].length;
    }
    return result;
}

let mergedBuffer = mergeBuffers();

let normalBuffer = [];
for (let i = 0; i < mergedBuffer.length; i++)
{
    normalBuffer.push(mergedBuffer[i]);
}

JSON.stringify(normalBuffer);

Now, if I copy the string output of the last line, and pass it onto any library that produces WAV files, the output WAV file is just random noise. If you want to replicate this yourself, I am using this NodeJS library for writing the WAV file:

let arr = [1,2,3]; // replace this array with the string output of normalBuffer from above

samples = [[]];
for (let i = 0; i < arr.length; i++)
{
    samples[0].push(arr[i]);
}

const fs = require("fs");
const WaveFile = require("wavefile");

let wav = new WaveFile();
wav.fromScratch(1, 44100, "32f", samples); // the sampling rate (second parameter) could be different on your machine, but you can print it out in the code snippet above to find out
fs.writeFileSync("test.wav", wav.toBuffer());

I've also tried converting the samples to unsigned 16-bit Ints, but I still have the same issue, and I've tried multiplying the samples by some constant in case the recording was too low volume-wise, but also to no avail.

Upvotes: 3

Views: 2594

Answers (1)

chrisguttandin
chrisguttandin

Reputation: 9066

The problem (at least in Chrome) is that the ScriptProcessorNode keeps reusing the same underlying audio buffer. That means each Float32Array inside your recBuffers array points to the same memory. You can avoid that by copying the data.

The line ...

const inputBuffer = e.inputBuffer.getChannelData(0);

... turns into ...

const inputBuffer = new Float32Array(bufferLen);

e.inputBuffer.copyFromChannel(inputBuffer, 0);

Please keep in mind that this will not work in Safari as it doesn't have a copyFromChannel method yet. The buffer would need to be copied manually instead.

In case you only want to record wav files it might be easier to reuse an existing library like extendable-media-recorder. But I guess you only created a wav file to debug the problem.

Upvotes: 2

Related Questions