Skip to content

Instantly share code, notes, and snippets.

@TF3RDL
Last active June 16, 2023 22:02
Show Gist options
  • Save TF3RDL/0187c076ccae3f57873c5e8cd17917b7 to your computer and use it in GitHub Desktop.
Save TF3RDL/0187c076ccae3f57873c5e8cd17917b7 to your computer and use it in GitHub Desktop.
Constant-Q transform using Goertzel algorithm
/**
* 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