Gain

clear

Sample Rate

Nyquist Freq.

\reg{}{nyq-freq} kHz

Max Freq. kHz

Min Freq. Hz

Freq. Scale

FFT Rate

\reg{}{fft-rate-sec} Hz 1 FFT every \reg{}{fft-rate-per} ms

FFT Size

2\sup{\reg{}{fft-size-exp}} Samples \reg{}{fft-size-sec} ms

Freq. Resolution

\reg{}{freq-resolution} Hz

Freq. Bins

\reg{}{freq-bins} Bins
const audioCtx = new AudioContext() reg.sampleRate.text = audioCtx.sampleRate/1e3 + " kHz" const analyser = audioCtx.createAnalyser() const gainNode = audioCtx.createGain() reg.gain.onUpdate(v=>gainNode.gain.value = v) const duration = 5 const timeBins = floor(duration / analyser.fftSize * audioCtx.sampleRate) const pAx = this.newPlot({ position: [this.left+100, this.bottom+100], size: [this.width-120, this.height-120], rangeX: [-duration, 0], rangeY: [reg.minFreq.value, reg.maxFreq.value*1e3], labelX: "Time [s]", labelY: "Frequency [Hz]", tickArrayY: [20,40,100,200,400,{value:1e3,label:"1k"},{value:2e3,label:"2k"},{value:4000,label:"4k"},{value:10e3,label:"10k"},{value:20e3,label:"20k"}], typeY: reg.scale.value?"log":"lin", tickEndsY: false, }) const p = pAx.group const canvas = p.canvas({ position: ()=>[ -duration, reg.minFreq.value ], size: ()=>[ duration, reg.maxFreq.value*1e3 ], resolution: [800,400], minMax: [0,255], smooth: false, }) let freqRes, timeRes, frameRate updateStats(reg.fftSize.value) reg.running.onUpdate(v=>v?play():pause()) reg.fftSize.onUpdate(v=>updateStats()) reg.fftRate.onUpdate(v=>{ updateStats() if (running){ pause() play() } }) function updateStats(){ analyser.fftSize = 2**reg.fftSize.value reg.fftSizeExp.textContent = reg.fftSize.value timeRes = analyser.fftSize / audioCtx.sampleRate reg.fftSizeSec.textContent = round( timeRes * 1e3,1) freqRes = audioCtx.sampleRate / analyser.frequencyBinCount / 2 reg.freqResolution.textContent = round(freqRes,1) reg.nyqFreq.textContent = audioCtx.sampleRate/2 *1e-3 reg.freqBins.textContent = analyser.frequencyBinCount frameRate = reg.fftRate.value reg.fftRateSec.textContent = round(frameRate) reg.fftRatePer.textContent = round(1000 / frameRate,1) } reg.maxFreq.onUpdate((v)=>updateAxes()) reg.minFreq.onUpdate((v)=>updateAxes()) reg.scale.onUpdate(v=>updateAxes()) function updateAxes(){ pAx.y.type = reg.scale.value?"log":"lin" pAx.y.range = [reg.minFreq.value, reg.maxFreq.value*1000] canvas.update() } let timer = null let running = false let initMic = false const fft = new Uint8Array(analyser.frequencyBinCount) function play(){ running = true if(!initMic){ navigator.mediaDevices.getUserMedia({audio:{echoCancellation:false, noiseSuppression:false, autoGainControl:false}}) .then((stream)=>{ audioCtx.createMediaStreamSource(stream) .connect(gainNode) .connect(analyser) }) .catch(function (error){ console.error("Failure to connect to mic. Check permissions.",error) }) initMic = true } timer = setInterval(()=>{ analyser.getByteFrequencyData(fft) shiftImg() const start = Math.floor(pAx.y.range[0] / freqRes) const end = Math.ceil(pAx.y.range[1] / freqRes) for (let i = start; i <= end; i++) draw(fft[i], f = i * freqRes) },1000/frameRate) } function pause(){ clearInterval(timer) running = false } function draw(v,f){ const x1 = canvas.toX(-1/frameRate) const x2 = canvas.toX(0) const y1 = canvas.toY(f) const y2 = canvas.toY(f+freqRes) const c = canvas.colorMap.toColor(v) canvas.ctx.fillStyle = `rgba(${c[0]},${c[1]},${c[2]},${c[3]})` canvas.ctx.fillRect(x1,y1,x2-x1,y2-y1) } function shiftImg(){ const w = canvas.width const h = canvas.height const dPx = round(canvas.toX(0) - canvas.toX(-1/frameRate)) canvas.ctx.drawImage(canvas, dPx, 0, w, h, 0, 0, w, h ) } reg.clear.addAction(clear) function clear(){ canvas.ctx.clearRect(0,0,canvas.width, canvas.height) } canvas.addAction(()=>{ reg.running.value = !running },"click") document.addEventListener("keydown",e=>{ e.preventDefault() if(e.code=="Space") reg.running.value = !running })