Web Audio API无法捕获22kHz以上超声波的问题排查求助
Web Audio API无法捕获22kHz以上超声波的问题排查求助
我需要编写一段JavaScript代码,实现连接麦克风(包括外接麦克风)并渲染22kHz到48kHz范围内的超声波频谱图。
但我发现Web Audio API(AudioContext)似乎存在瓶颈——22kHz以上的频率会被截断。
需要说明的几个关键点:
- 浏览器已经授予了96kHz的采样率权限;
- 无论是使用USB连接的超声波麦克风,还是设置了高质量参数的内置麦克风(我的设备最高支持96kHz),都会出现这个截断问题;
- 我尝试用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
相关产品推荐
相关产品推荐

