Gain
Sample Rate
Nyquist Freq.
\reg{}{nyq-freq} kHz
Hover the freq.
spectrum to mark
harmonics
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
color.add("marker","orange","firebrick")
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)
let freqRes, timeRes, frameRate
updateStats()
const oscAx = this.newPlot({
position: [this.left+110,this.height/4-20],
size: [this.width-140,this.height/4],
rangeX: [0,40],
rangeY: [-1,1],
labelX: "Time [ms]",
labelGapX: 20,
tickEndsX:false,
labelY: "Amp [relative]",
tickArrayX: [0,0.5,1,2,3,4,5,6,10,20,30,40,50,60,100,200,300,400,500,600],
tickStepY:0.5,
})
const osc = oscAx.group
let p = []
const oscLine = osc.line(()=>lincount(0,timeRes * 1000,p.length), ()=>p)
const specAx = this.newPlot({
position: [this.left+110, this.bottom+90],
size: [this.width-140, this.height/2 + 20],
rangeX: [reg.minFreq.value,reg.maxFreq.value*1000],
rangeY: [0,1],
labelX: "Frequency [Hz]",
labelY: "Amplitude per Frequency [amp/band]",
labelGapX: 20,
tickArrayX: [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"}],
tickStepY: 0.2,
typeX: "log",
tickEndsX: false,
})
const spec = specAx.group
const specOv = specAx.overlay
let P = []
const specLine = spec.line(()=>lincount(20,audioCtx.sampleRate/2,P.length), ()=>P )
reg.running.onUpdate(v=>v?play():pause())
reg.fftSize.onUpdate(v=>{
clear()
updateStats()
oscAx.x.range = [0, timeRes * 1000]
})
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=>{
specAx.x.range = [reg.minFreq.value, v*1000]
specLine.update()
updateMarkers()
})
reg.minFreq.onUpdate(v=>{
specAx.x.range = [v, reg.maxFreq.value*1000]
specLine.update()
updateMarkers()
})
reg.freqScale.onUpdate(v=>{
specAx.x.type = v?"log":"lin"
updateMarkers()
specLine.update()
})
let timer = null
let running = false
let initMic = false
let data, fft
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)
return
})
initMic = true
}
timer = setInterval(()=>{
data = new Uint8Array(analyser.fftSize)
analyser.getByteTimeDomainData(data)
p = Array.from(data, x => 2 * (x - 128) / 128)
oscLine.update()
fft = new Uint8Array(analyser.frequencyBinCount)
analyser.getByteFrequencyData(fft)
P = Array.from(fft, x => x/255 )
P.shift()
specLine.update()
},1000/frameRate)
}
function pause(){
clearInterval(timer)
running = false
}
function clear(){
p = []
P = []
oscLine.update()
specLine.update()
}
function tooltip(w){
mx = round(w.x)
updateMarkers()
}
const hitbox = specOv.rect(
()=>[specAx.x.range[0],specAx.y.range[0]],
()=>[specAx.x.range[1]-specAx.x.range[0],specAx.y.range[1]-specAx.y.range[0]],
{fill:"transparent"}
)
hitbox.classList.add("responsive")
hitbox.addAction(tooltip)
reg.harmonics.onUpdate(v=>{
m.forEach((t,i)=>t.classList.toggle("hidden",i>v))
l.forEach((t,i)=>t.classList.toggle("hidden",i>v))
updateMarkers()
})
function updateMarkers(){
m.forEach(v=>v.update())
l.forEach(v=>v.update())
}
let mx = 100
const m = []
const l = []
const o = specAx.overlay
for(let i = 1; i <= 8; i++){
m[i-1] = spec.line(
()=>[i*mx,i*mx],
()=>[specAx.y.range[0],specAx.y.range[1]],
{stroke:color.marker,strokeWidth:1.5,opacity:0.5}
)
l[i-1] = o.text(
()=>[specAx.x.toScreen(i*mx),specAx.y.toScreen(specAx.y.range[1])],
()=>{
const x = round(i*mx)
return x>=1e3?round(x/1000,i<3?2:1)+" k ":x
},
{screen:true,cy:1.25,cx:i>6?i>7?0:0.25:0.5,color:color.marker,fontSize:22}
)
}
reg.harmonics.value = 3
document.addEventListener("keydown",e=>{
e.preventDefault()
if(e.code=="Space") reg.running.value = !running
})