Last active
June 16, 2023 22:02
-
-
Save TF3RDL/0187c076ccae3f57873c5e8cd17917b7 to your computer and use it in GitHub Desktop.
Constant-Q transform using Goertzel algorithm
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Constant-Q Transform (CQT) calculated using Goertzel algorithm | |
* | |
* This by itself doesn't need Web Audio API in order to work but it is necessary for real-time visualizations | |
* | |
* Real-time usage: | |
* analyserNode.getFloatTimeDomainData(dataArray); | |
* const spectrum = cqt(dataArray, freqBands, audioCtx.sampleRate, bandwidthOffset, windowFunction); | |
* | |
* Note: the implementation of this CQT is slow compared to FFT | |
*/ | |
function cqt(waveform, hzArray = generateOctaveBands(), sampleRate = 44100, bandwidthOffset = 1, windowFunction = applyWindow) { | |
return hzArray.map(x => { | |
const coeff = 2 * Math.cos(2*Math.PI*x.ctr/sampleRate), | |
bandwidth = Math.abs(x.hi - x.lo) + (sampleRate/waveform.length) * bandwidthOffset, | |
tlen = Math.min(1/bandwidth, waveform.length/sampleRate); | |
let f1 = 0, | |
f2 = 0, | |
sine, | |
lowerIdx = Math.trunc(waveform.length-tlen*sampleRate), | |
higherIdx = waveform.length-1, | |
norm = 0; | |
for (let i = lowerIdx; i <= higherIdx; i++) { | |
let posX = (i - lowerIdx) / (higherIdx - lowerIdx) * 2 - 1; | |
let w = windowFunction(posX); | |
norm += w; | |
// Goertzel transform | |
sine = waveform[i]*w + coeff * f1 - f2; | |
f2 = f1; | |
f1 = sine; | |
} | |
return Math.sqrt(f1 ** 2 + f2 ** 2 - coeff * f1 * f2) / norm; | |
}); | |
} | |
// Generate octave bands, default is 1/12 octave spanning 20Hz-20kHz (note index 4 to 123, which corresponds to E0 to D#10 respectively) | |
function generateOctaveBands(bandsPerOctave = 12, lowerNote = 4, higherNote = 123, detune = 0, bandwidth = 0.5) { | |
const root24 = 2 ** ( 1 / 24 ); | |
const c0 = 440 * root24 ** -114; // ~16.35 Hz | |
const groupNotes = 24/bandsPerOctave; | |
let bands = []; | |
for (let i = Math.round(lowerNote*2/groupNotes); i <= Math.round(higherNote*2/groupNotes); i++) { | |
bands.push({ | |
lo: c0 * root24 ** ((i-bandwidth)*groupNotes+detune), | |
ctr: c0 * root24 ** (i*groupNotes+detune), | |
hi: c0 * root24 ** ((i+bandwidth)*groupNotes+detune) | |
}); | |
} | |
return bands; | |
} | |
function applyWindow(x) { | |
return 0.42 + 0.5 * Math.cos(x*Math.PI) + 0.08 * Math.cos(x*Math.PI*2); // Blackman window to match AnalyserNode's windowing function | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment