【问题标题】:Reducing sample rate of a Web Audio spectrum analyzer using mic input使用麦克风输入降低 Web 音频频谱分析仪的采样率
【发布时间】:2020-11-03 21:30:50
【问题描述】:

我正在使用 Web Audio API 创建一个简单的频谱分析仪,使用计算机麦克风作为输入信号。我当前实现的基本功能工作正常,使用默认采样率(通常为 48KHz,但可能为 44.1KHz,具体取决于浏览器)。

对于某些应用,我想为 FFT 使用较低的采样率 (~8KHz)。

Web Audio API 似乎正在增加对自定义采样率的支持,目前仅在 FireFox (https://developer.mozilla.org/en-US/docs/Web/API/AudioContextOptions/sampleRate) 上可用。

向上下文构造函数添加采样率:

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

控制台输出“8000”(在 FireFox 中),所以它似乎一直在工作。

用户使用下拉菜单打开麦克风。这是为下拉服务提供服务的函数:

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();
  }
}

在 FireFox 中,使用下拉菜单激活麦克风会显示一个请求访问麦克风的弹出窗口(如通常预期的那样)。点击允许麦克风后,控制台显示:

“当前不支持从具有不同采样率的 AudioContexts 连接 AudioNodes”。

频谱分析仪的显示屏保持空白。

任何想法如何克服这个错误?如果我们能解决这个问题,在用户声卡采样率未知时如何指定 sampleRate 的任何指导?

【问题讨论】:

  • 我很好奇是什么原因。您可以简单地丢弃输出中高于 4 kHz 的所有内容。

标签: javascript web-audio-api


【解决方案1】:

解决此问题的一种方法是通过脚本处理器节点将从麦克风捕获的音频数据包传递到分析器节点,该脚本处理器节点对通过它的音频数据包进行重新采样。

脚本处理器节点概述

  • 每个脚本处理器节点都有一个输入缓冲区和一个输出缓冲区。
  • 当音频进入输入缓冲区时,脚本处理器节点触发 onaudioprocess 事件。
  • 脚本处理器节点的输出缓冲区中的任何内容都将成为其输出。
  • 详细规格请参考:Script processor node

这是伪代码:

  1. 创建直播媒体源、脚本处理器节点和分析器节点

  2. 通过脚本处理器将实时媒体源连接到分析器节点 节点

  3. 每当音频包进入脚本处理器时 节点,onaudioprocess 事件被触发

  4. 当 onaudioprocess 事件被触发时:

    4.1) 从输入缓冲区中提取音频数据

    4.2) 重新采样音频数据

    4.3) 将重新采样的数据放入输出缓冲区

下面的代码sn-p实现了上面的伪代码:

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;
}

更新 - 2020 年 11 月 3 日

脚本处理器节点已被弃用并替换为AudioWorklets。

更改采样率的方法保持不变。

【讨论】:

  • 非常感谢您的时间和考虑!
  • 非常感谢您的时间和考虑!顺便说一句,for (var sample = 0; sample &lt; outputbuffer.length; sample++) {,应该有outputBuffer。我想我现在遇到的问题是我使用analyserNode.getByteFrequencyData(dataArray);。为了性能,我想保留它 Uint8Array。有没有办法可以将您的代码/缓冲区从 Float32Array 更改为 Uint8Array?
  • @eadgbe 是的,这是可能的,这里有一个小功能来实现这一点:const convertFloat32ArrayToInt16Array = (incomingData) =&gt; { let l = incomingData.length; var buf = new Int16Array(l); while(l--){ var n = incomingData[l]; var v = n &lt; 0 ? n * 32768 : n * 32767; buf[l] = Math.max(-32768, Math.min(32768, v)); } return buf; }
  • 现在,我决定使用getFloatFrequencyData 更改我的代码以与您的原始答案兼容。我已经使用 Float 让 48KHz 的基本频谱分析仪 (SA) 再次工作。然后,当我重新插入您的onaudioprocess 和downsample 时,SA 什么也不显示。 dataArray 的 dataArray 输出中的 console.log 显示 NaN。泰勒,你能通过我个人资料中的电子邮件与我联系吗?
  • @sparkplug 当 AudioWorklets 用于重采样时,我没有观察到任何明显的延迟(使用 ScriptProcessorNode 时有很多问题)。但是,拥有 C/C++ 后端肯定会表现得更好。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-02-10
  • 2018-02-05
  • 1970-01-01
  • 1970-01-01
  • 2019-04-01
  • 2018-12-12
相关资源
最近更新 更多