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