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