()
| 458 | } |
| 459 | |
| 460 | function drawDiagnostics() { |
| 461 | if (!analyser || !diagVisible) { |
| 462 | diagAnimFrame = null; |
| 463 | return; |
| 464 | } |
| 465 | |
| 466 | diagAnimFrame = requestAnimationFrame(drawDiagnostics); |
| 467 | |
| 468 | // ── Waveform ── |
| 469 | const waveCanvas = document.getElementById('waveformCanvas'); |
| 470 | const wCtx = waveCanvas.getContext('2d'); |
| 471 | const timeData = new Float32Array(analyser.fftSize); |
| 472 | analyser.getFloatTimeDomainData(timeData); |
| 473 | |
| 474 | const w = waveCanvas.width; |
| 475 | const h = waveCanvas.height; |
| 476 | wCtx.fillStyle = '#000'; |
| 477 | wCtx.fillRect(0, 0, w, h); |
| 478 | wCtx.strokeStyle = '#0f0'; |
| 479 | wCtx.lineWidth = 1; |
| 480 | wCtx.beginPath(); |
| 481 | const sliceWidth = w / timeData.length; |
| 482 | let x = 0; |
| 483 | for (let i = 0; i < timeData.length; i++) { |
| 484 | const y = (1 - timeData[i]) * h / 2; |
| 485 | if (i === 0) wCtx.moveTo(x, y); |
| 486 | else wCtx.lineTo(x, y); |
| 487 | x += sliceWidth; |
| 488 | } |
| 489 | wCtx.stroke(); |
| 490 | |
| 491 | // Compute RMS |
| 492 | let sumSq = 0; |
| 493 | for (let i = 0; i < timeData.length; i++) sumSq += timeData[i] * timeData[i]; |
| 494 | const rms = Math.sqrt(sumSq / timeData.length); |
| 495 | const rmsDb = rms > 0 ? (20 * Math.log10(rms)).toFixed(1) : '-Inf'; |
| 496 | document.getElementById('statRMS').textContent = rmsDb + ' dBFS'; |
| 497 | |
| 498 | // ── FFT Spectrum ── |
| 499 | const specCanvas = document.getElementById('spectrumCanvas'); |
| 500 | const sCtx = specCanvas.getContext('2d'); |
| 501 | const freqData = new Float32Array(analyser.frequencyBinCount); |
| 502 | analyser.getFloatFrequencyData(freqData); |
| 503 | |
| 504 | const sw = specCanvas.width; |
| 505 | const sh = specCanvas.height; |
| 506 | sCtx.fillStyle = '#000'; |
| 507 | sCtx.fillRect(0, 0, sw, sh); |
| 508 | |
| 509 | // Draw spectrum (0 to 4kHz range for speech/tone analysis) |
| 510 | const sampleRate = audioCtx.sampleRate; |
| 511 | const binHz = sampleRate / analyser.fftSize; |
| 512 | const maxFreqDisplay = 4000; |
| 513 | const maxBin = Math.min(Math.ceil(maxFreqDisplay / binHz), freqData.length); |
| 514 | const barWidth = sw / maxBin; |
| 515 | |
| 516 | sCtx.fillStyle = '#0cf'; |
| 517 | let peakBin = 0; |
no test coverage detected