You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Web Audio API无法捕获22kHz以上超声波的问题排查求助

Web Audio API无法捕获22kHz以上超声波的问题排查求助

我需要编写一段JavaScript代码,实现连接麦克风(包括外接麦克风)并渲染22kHz到48kHz范围内的超声波频谱图。

但我发现Web Audio API(AudioContext)似乎存在瓶颈——22kHz以上的频率会被截断。

需要说明的几个关键点:

  1. 浏览器已经授予了96kHz的采样率权限;
  2. 无论是使用USB连接的超声波麦克风,还是设置了高质量参数的内置麦克风(我的设备最高支持96kHz),都会出现这个截断问题;
  3. 我尝试用AnalyzerNode API重写代码,问题依然存在。

有没有人知道这是为什么?

以下是我目前的代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <title>Improved Spectrogram with Extended Ultrasound Range</title>
  <style>
    body { font-family: Arial, sans-serif; text-align: center; }
    .controls { margin-bottom: 1em; }
    .container {
      display: flex;
      flex-direction: row;
      justify-content: center;
      align-items: flex-start;
      margin: 0 auto;
      position: relative;
      width: fit-content;
    }
    .axis-container {
      position: relative;
      width: 50px;
      height: 300px;
      margin-right: 10px;
    }
    .tick {
      position: absolute;
      white-space: nowrap;
      transform: translateY(-50%);
      font-size: 12px;
      right: 0;
    }
    canvas { border: 1px solid black; display: block; }
  </style>
</head>
<body>
  <h1>Improved Spectrogram<br>(Microphone Input with Extended Ultrasound Range)</h1>
  
  <div class="controls">
    <button onclick="startTest()">Start Spectrogram</button>
    <label for="scaleMode">Y‑Scale:</label>
    <select id="scaleMode">
      <option value="log" selected>Log</option>
      <option value="linear">Linear</option>
    </select>
  </div>
  
  <p id="info">
    Spectrogram visualizing the microphone audio signal with enhanced dynamic range and color mapping.
  </p>
  
  <div class="container">
    <div class="axis-container" id="axisContainer"></div>
    <canvas id="spectrogram" width="800" height="300"></canvas>
  </div>
  
  <script>
    // --------------------
    // Global Audio & FFT Parameters
    // --------------------
    let sampleRate = 96000;      // Requested sample rate (will be updated from AudioContext)
    const fftSize = 4096;        // FFT size (power of 2)
    const MIN_FREQ = 20;         // Lowest frequency to display (Hz)
    const NUM_TICKS = 8;         // Number of ticks on Y-axis
    let scaleMode = "log";       // "log" or "linear"
    
    let canvas, canvasCtx;
    
    // Microphone buffer & AudioContext variables:
    let micBuffer = new Float32Array(fftSize);
    let micBufferAvailable = false;
    let audioContext, scriptProcessor;
    
    // --------------------
    // FFT Implementation (Cooley-Tukey)
    // --------------------
    function bitReverse(x, bits) {
      let y = 0;
      for (let i = 0; i < bits; i++) {
        y = (y << 1) | (x & 1);
        x >>>= 1;
      }
      return y;
    }
    
    function fft(re, im) {
      const n = re.length;
      const levels = Math.log2(n);
      for (let i = 0; i < n; i++) {
        const j = bitReverse(i, levels);
        if (j > i) {
          [re[i], re[j]] = [re[j], re[i]];
          [im[i], im[j]] = [im[j], im[i]];
        }
      }
      for (let size = 2; size <= n; size *= 2) {
        const halfsize = size / 2;
        const tablestep = n / size;
        for (let i = 0; i < n; i += size) {
          for (let j = 0; j < halfsize; j++) {
            const k = j * tablestep;
            const angle = -2 * Math.PI * k / n;
            const cos = Math.cos(angle);
            const sin = Math.sin(angle);
            const tre = re[i+j+halfsize] * cos - im[i+j+halfsize] * sin;
            const tim = re[i+j+halfsize] * sin + im[i+j+halfsize] * cos;
            re[i+j+halfsize] = re[i+j] - tre;
            im[i+j+halfsize] = im[i+j] - tim;
            re[i+j] += tre;
            im[i+j] += tim;
          }
        }
      }
    }
    
    // --------------------
    // Frequency Mapping & Y-Axis Functions
    // --------------------
    function yToFreq(y, canvasHeight, minF, maxF) {
      const ratio = 1 - (y / canvasHeight);
      if (scaleMode === "linear") {
        return minF + ratio * (maxF - minF);
      } else {
        const minLog = Math.log10(minF);
        const maxLog = Math.log10(maxF);
        const freqLog = minLog + ratio * (maxLog - minLog);
        return Math.pow(10, freqLog);
      }
    }
    
    function freqToIndex(freq, maxFreq, bufferLength) {
      let idx = (freq / maxFreq) * (bufferLength - 1);
      return Math.floor(Math.min(bufferLength - 1, Math.max(0, idx)));
    }
    
    function freqToY(freq, minF, maxF, containerHeight) {
      let ratio;
      if (scaleMode === "linear") {
        ratio = (freq - minF) / (maxF - minF);
      } else {
        const minLog = Math.log10(minF);
        const maxLog = Math.log10(maxF);
        const freqLog = Math.log10(freq);
        ratio = (freqLog - minLog) / (maxLog - minLog);
      }
      ratio = Math.min(1, Math.max(0, ratio));
      return containerHeight * (1 - ratio);
    }
    
    function buildFreqTicks(numTicks, mode, minF, maxF) {
      const ticks = [];
      for (let i = 0; i <= numTicks; i++) {
        const ratio = i / numTicks;
        if (mode === "linear") {
          ticks.push(minF + ratio * (maxF - minF));
        } else {
          ticks.push(minF * Math.pow(maxF / minF, ratio));
        }
      }
      return ticks;
    }
    
    function drawYAxis() {
      const axisContainer = document.getElementById("axisContainer");
      axisContainer.innerHTML = "";
      const maxFreq = sampleRate / 2;
      const containerHeight = axisContainer.offsetHeight;
      const freqTicks = buildFreqTicks(NUM_TICKS, scaleMode, MIN_FREQ, maxFreq);
      freqTicks.forEach(freq => {
        const yPos = freqToY(freq, MIN_FREQ, maxFreq, containerHeight);
        const label = document.createElement("div");
        label.className = "tick";
        label.textContent = `${Math.round(freq)} Hz`;
        label.style.top = `${yPos}px`;
        axisContainer.appendChild(label);
      });
    }
    
    // --------------------
    // HSV to RGB conversion for color mapping.
    // --------------------
    function hsvToRgb(h, s, v) {
      let c = v * s;
      let x = c * (1 - Math.abs((h / 60) % 2 - 1));
      let m = v - c;
      let r, g, b;
      if (h < 60) {
        r = c; g = x; b = 0;
      } else if (h < 120) {
        r = x; g = c; b = 0;
      } else if (h < 180) {
        r = 0; g = c; b = x;
      } else if (h < 240) {
        r = 0; g = x; b = c;
      } else if (h < 300) {
        r = x; g = 0; b = c;
      } else {
        r = c; g = 0; b = x;
      }
      r = Math.floor((r + m) * 255);
      g = Math.floor((g + m) * 255);
      b = Math.floor((b + m) * 255);
      return `rgb(${r}, ${g}, ${b})`;
    }
    
    // Maps intensity (0-255) to a color (from blue to red).
    function intensityToColor(intensity) {
      // Map intensity to a hue between 240 (blue) and 0 (red)
      let hue = 240 * (255 - intensity) / 255;
      return hsvToRgb(hue, 1, 1);
    }
    
    // --------------------
    // Spectrogram Setup
    // --------------------
    function setupSpectrogram() {
      canvas = document.getElementById("spectrogram");
      canvasCtx = canvas.getContext("2d");
      canvasCtx.clearRect(0, 0, canvas.width, canvas.height);
      const scaleSelect = document.getElementById("scaleMode");
      scaleSelect.onchange = () => {
        scaleMode = scaleSelect.value;
        drawYAxis();
      };
      drawYAxis();
    }
    
    // --------------------
    // Start Microphone Capture and Animation
    // --------------------
    async function startMic() {
      try {
        // Request a 96 kHz sample rate (if supported by the browser/hardware)
        audioContext = new (window.AudioContext || window.webkitAudioContext)({sampleRate: 96000});
        console.log("AudioContext sampleRate:", audioContext.sampleRate);
        sampleRate = audioContext.sampleRate;  // update sample rate
        const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
        const source = audioContext.createMediaStreamSource(stream);
        // Create a ScriptProcessorNode with a buffer size equal to fftSize.
        scriptProcessor = audioContext.createScriptProcessor(fftSize, 1, 1);
        scriptProcessor.onaudioprocess = function(audioProcessingEvent) {
          let inputBuffer = audioProcessingEvent.inputBuffer;
          let channelData = inputBuffer.getChannelData(0);
          micBuffer.set(channelData);
          micBufferAvailable = true;
        };
        source.connect(scriptProcessor);
        scriptProcessor.connect(audioContext.destination);
        // Start the animation loop.
        animate();
      } catch (e) {
        alert('Error accessing microphone: ' + e);
      }
    }
    
    function startTest() {
      setupSpectrogram();
      startMic();
    }
  </script>
</body>
</html>

备注:内容来源于stack exchange,提问作者Ermanno

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.13 18:58:07