Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Reducing sample rate of a Web Audio spectrum analyzer using mic input

I'm using the Web Audio API to create a simple spectrum analyzer using the computer microphone as the input signal. The basic functionality of my current implementation works fine, using the default sampling rate (usually 48KHz, but could be 44.1KHz depending on the browser).

For some applications, I would like to use a lower sampling rate (~8KHz) for the FFT.

It looks like the Web Audio API is adding support to customize the sample rate, currently only available on FireFox (https://developer.mozilla.org/en-US/docs/Web/API/AudioContextOptions/sampleRate).

Adding sample rate to the context constructor:

// create AudioContext object named 'audioCtx'
var audioCtx = new (AudioContext || webkitAudioContext)({sampleRate: 8000,});
console.log(audioCtx.sampleRate)

The console outputs '8000' (in FireFox), so it appears to be working up to this point.

The microphone is turned on by the user using a pull-down menu. This is the function servicing that pull-down:

var microphone;
function getMicInputState()
{
  let selectedValue = document.getElementById("micOffOn").value;
  if (selectedValue === "on") {
    navigator.mediaDevices.getUserMedia({audio: true})
      .then(stream => {
        microphone = audioCtx.createMediaStreamSource(stream);
        microphone.connect(analyserNode);
      })
      .catch(err => { alert("Microphone is required."); });
  } else {
    microphone.disconnect();
  }
}

In FireFox, using the pulldown to activate the microphone displays a popup requesting access to the microphone (as normally expected). After clicking to allow the microphone, the console displays:

"Connecting AudioNodes from AudioContexts with different sample-rate is currently not supported".

The display of the spectrum analyzer remains blank.

Any ideas how to overcome this error? If we can get past this, any guidance on how to specify sampleRate when the user's soundcard sampling rate is unknown?

like image 931
eadgbe Avatar asked Oct 12 '18 22:10

eadgbe


1 Answers

One approach to overcome this is passing audio packets captured from microphone to analyzer node via a script processor node that re-samples the audio packets passing through it.

Brief overview of script processor node

  • Every script processor node has an input buffer and an output buffer.
  • When audio enters the input buffer, the script processor node fires onaudioprocess event.
  • Whatever is placed in the output buffer of script processor node becomes its output.
  • For detailed specs, refer : Script processor node

Here is the pseudo-code:

  1. Create live media source, script processor node and analyzer node

  2. Connect live media source to analyzer node via script processor node

  3. Whenever an audio packet enters the script processor node, onaudioprocess event is fired

  4. When onaudioprocess event is fired :

    4.1) Extract audio data from input buffer

    4.2) Re-sample audio data

    4.3) Place re-sampled data in output buffer

The following code snippet implements the above pseudocode:

var microphone;
// *** 1) create a script processor node
var scriptProcessorNode = audioCtx.createScriptProcessor(4096, 1, 1);

function getMicInputState()
{
 let selectedValue = document.getElementById("micOffOn").value;
 if (selectedValue === "on") {
   navigator.mediaDevices.getUserMedia({audio: true})
     .then(stream => {
       microphone = audioCtx.createMediaStreamSource(stream);
       // *** 2) connect live media source to analyserNode via script processor node
       microphone.connect(scriptProcessorNode); 
       scriptProcessorNode.connect(analyserNode);
     })
     .catch(err => { alert("Microphone is required."); });
 } else {
   microphone.disconnect();
 }
}

// *** 3) Whenever an audio packet passes through script processor node, resample it
scriptProcessorNode.onaudioprocess = function(event){
   var inputBuffer = event.inputBuffer;
   var outputBuffer = event.outputBuffer;
   for(var channel = 0; channel < outputBuffer.numberOfChannels; channel++){
     var inputData = inputBuffer.getChannelData(channel);
     var outputData = outputBuffer.getChannelData(channel);
     
     // *** 3.1) Resample inputData
     var fromSampleRate = audioCtx.sampleRate;
     var toSampleRate = 8000;
     var resampledAudio = downsample(inputData, fromSampleRate, toSampleRate);
     
     // *** 3.2) make output equal to the resampled audio
     for (var sample = 0; sample < outputData.length; sample++) {
       outputData[sample] = resampledAudio[sample];      
     }
   }
}

function downsample(buffer, fromSampleRate, toSampleRate) {
   // buffer is a Float32Array
   var sampleRateRatio = Math.round(fromSampleRate / toSampleRate);
   var newLength = Math.round(buffer.length / sampleRateRatio);

   var result = new Float32Array(newLength);
   var offsetResult = 0;
   var offsetBuffer = 0;
   while (offsetResult < result.length) {
       var nextOffsetBuffer = Math.round((offsetResult + 1) * sampleRateRatio);
       var accum = 0, count = 0;
       for (var i = offsetBuffer; i < nextOffsetBuffer && i < buffer.length; i++) {
           accum += buffer[i];
           count++;
       }
       result[offsetResult] = accum / count;
       offsetResult++;
       offsetBuffer = nextOffsetBuffer;
   }
   return result;
}

Update - 03 Nov, 2020

Script Processor Node is being deprecated and replaced with AudioWorklets.

The approach to changing the sample rate remains the same.

like image 121
Tyler Durden Avatar answered Nov 04 '22 06:11

Tyler Durden