Select a vowel below to see its idealized power spectrum. The demo walks through the MFCC pipeline one step at a time: warp the frequency axis onto the mel scale, apply triangular filterbank bins, take the log, and finally a Discrete Cosine Transform to decorrelate the channels into the final coefficients.
<!-- MFCC pipeline demo: power spectrum -> mel filterbank -> log -> DCT -> 13 coefficients -->
<div class="controls">
<label for="vowel-sel">Vowel:</label>
<select id="vowel-sel">
<option value="a">/a/ (father)</option>
<option value="e">/e/ (bed)</option>
<option value="i">/i/ (see)</option>
<option value="o">/o/ (go)</option>
<option value="u">/u/ (too)</option>
</select>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div class="steps">
<button id="btn-prev" type="button" class="nav" disabled>←</button>
<span id="step-label" class="step-label"></span>
<button id="btn-next" type="button" class="nav">→</button>
</div>
<div class="desc-box" id="step-desc"></div>
<canvas id="chart" width="560" height="200"></canvas>
<div class="mfcc-row" id="mfcc-row" style="display:none">
<div class="mfcc-label">MFCC vector (13 coefficients):</div>
<div id="mfcc-bars" class="mfcc-bars"></div>
</div>
/* Demo layout: controls on top, step navigator, description, canvas chart, MFCC bars */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; background: #fff; }
.controls { display: flex; align-items: center; gap: .6rem; margin-bottom: .6rem; flex-wrap: wrap; }
label { font-size: .9rem; font-weight: 600; }
select { font-size: .9rem; padding: .3rem .5rem; border: 1px solid #bbb; border-radius: 6px; }
button { font: 600 13px system-ui; padding: .35rem .75rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 6px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .35; cursor: default; }
.steps { display: flex; align-items: center; gap: .5rem; margin-bottom: .4rem; }
.step-label { font-size: .92rem; font-weight: 700; flex: 1; text-align: center; }
.nav { padding: .3rem .65rem; font-size: 1rem; min-width: 36px; }
.desc-box { font-size: .84rem; color: #444; line-height: 1.5; margin-bottom: .5rem;
min-height: 2.6rem; padding: .35rem .6rem; background: #f4f7fa;
border-left: 3px solid #1d3557; border-radius: 4px; }
canvas { width: 100%; max-width: 560px; display: block; border: 1px solid #e0e5ea; border-radius: 8px; background: #fafbfc; }
/* Compact MFCC coefficient bar strip shown at step 4 */
.mfcc-row { margin-top: .6rem; }
.mfcc-label { font-size: .8rem; font-weight: 700; color: #555; margin-bottom: .25rem; }
.mfcc-bars { display: flex; gap: 3px; align-items: flex-end; height: 54px; }
.mfcc-bar { flex: 1; border-radius: 3px 3px 0 0; min-height: 4px; transition: height .35s; }
/* MFCC pipeline: power spectrum -> mel filterbank -> log energy -> DCT coefficients */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Approximate formant frequencies (F1, F2, F3) in Hz for five English vowels */
const VOWELS = {
a: { f1: 730, f2: 1090, f3: 2440 },
e: { f1: 530, f2: 1840, f3: 2480 },
i: { f1: 270, f2: 2290, f3: 3010 },
o: { f1: 570, f2: 840, f3: 2410 },
u: { f1: 300, f2: 870, f3: 2240 }
};
/* Standard mel-scale conversion: mel = 2595 * log10(1 + f/700) */
const hz2mel = f => 2595 * Math.log10(1 + f / 700);
const mel2hz = m => 700 * (Math.pow(10, m / 2595) - 1);
const N_FFT = 128; // number of simulated FFT power spectrum bins
const N_MELS = 20; // number of triangular mel filterbank channels
const N_MFCC = 13; // number of MFCC coefficients to keep
const F_MAX = 8000; // maximum frequency in Hz
/* Build an idealized power spectrum for the given vowel using Gaussian formant peaks */
function makePowerSpectrum(vowel) {
const { f1, f2, f3 } = VOWELS[vowel];
const spec = new Float32Array(N_FFT);
const fStep = F_MAX / N_FFT;
for (let k = 0; k < N_FFT; k++) {
const f = (k + 0.5) * fStep;
// three formant peaks modeled as Gaussians — three formant peaks modeled as Gaussians
const g1 = Math.exp(-Math.pow((f - f1) / 150, 2));
const g2 = Math.exp(-Math.pow((f - f2) / 250, 2)) * 0.7;
const g3 = Math.exp(-Math.pow((f - f3) / 350, 2)) * 0.4;
spec[k] = Math.max(0.01, g1 + g2 + g3);
}
return spec;
}
/* Build a bank of N_MELS triangular filters equally spaced on the mel scale */
function buildFilterbank() {
const melMin = hz2mel(0);
const melMax = hz2mel(F_MAX);
const melPoints = [];
for (let m = 0; m <= N_MELS + 1; m++)
melPoints.push(mel2hz(melMin + (m / (N_MELS + 1)) * (melMax - melMin)));
const fb = [];
const fStep = F_MAX / N_FFT;
for (let m = 1; m <= N_MELS; m++) {
const row = new Float32Array(N_FFT);
for (let k = 0; k < N_FFT; k++) {
const f = (k + 0.5) * fStep;
const lo = melPoints[m - 1], cen = melPoints[m], hi = melPoints[m + 1];
if (f >= lo && f <= cen) row[k] = (f - lo) / (cen - lo);
else if (f > cen && f <= hi) row[k] = (hi - f) / (hi - cen);
}
fb.push(row);
}
return fb;
}
/* Multiply the power spectrum by each filter row and sum (dot product per channel) */
function applyFilterbank(spec, fb) {
return fb.map(row => {
let s = 0;
for (let k = 0; k < N_FFT; k++) s += row[k] * spec[k];
return s;
});
}
/* Type-II DCT: decorrelates the log filterbank energies into cepstral coefficients */
function dct(x, C) {
const out = [];
for (let n = 0; n < C; n++) {
let s = 0;
for (let k = 0; k < x.length; k++)
s += x[k] * Math.cos(Math.PI * n * (k + 0.5) / x.length);
out.push(s);
}
return out;
}
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const FB = buildFilterbank();
let currentVowel = 'a';
let currentStep = 0; // 0=power spectrum, 1=mel filterbank, 2=log energy, 3=MFCC coefficients
/* Keys matching step index to i18n description keys */
const STEP_KEYS = ['step_power', 'step_mel', 'step_log', 'step_mfcc'];
function getStepDesc(key) {
return {
step_power: 'The raw power spectrum <b>|X(f)|²</b> shows energy at each frequency bin. Vowels have distinct peaks called <b>formants</b> (F1, F2, F3) that define their sound. The x-axis spans 0–8 kHz; high-frequency detail is perceptually less important.',
step_mel: 'A bank of <b>20 triangular filters</b> is applied, spaced evenly on the <b>mel scale</b>. Bins are narrow at low frequencies (where the ear is most sensitive) and wide at high frequencies. Each bar is the total energy captured by one filter.',
step_log: 'The <b>logarithm</b> of each filterbank energy compresses the dynamic range to match how loudness is perceived. It also turns the vocal-tract convolution into a sum — separating the speaker’s voice from the message content.',
step_mfcc: 'A <b>Discrete Cosine Transform</b> decorrelates the 20 log energies into 13 MFCC coefficients. Adjacent mel channels are correlated; the DCT removes that redundancy. The first few coefficients capture the broad spectral envelope; later ones encode finer detail.'
}[key] || '';
}
function computeSteps(vowel) {
const spec = makePowerSpectrum(vowel);
const melEnergy = applyFilterbank(spec, FB);
const logEnergy = melEnergy.map(v => Math.log(v + 1e-8));
const mfcc = dct(logEnergy, N_MFCC);
return { spec, melEnergy, logEnergy, mfcc };
}
/* Generic bar chart renderer: values array, fill color, axis labels */
function drawBars(values, color, xLabel, yLabel) {
const W = canvas.width, H = canvas.height;
const pad = { left: 42, right: 10, top: 18, bottom: 32 };
const plotW = W - pad.left - pad.right;
const plotH = H - pad.top - pad.bottom;
ctx.clearRect(0, 0, W, H);
const minV = Math.min(...values);
const maxV = Math.max(...values);
const range = maxV - minV || 1;
ctx.fillStyle = '#e8eef3';
ctx.fillRect(pad.left, pad.top, plotW, plotH);
const barW = plotW / values.length;
values.forEach((v, i) => {
const normH = ((v - minV) / range) * plotH;
ctx.fillStyle = color;
ctx.fillRect(pad.left + i * barW + 1, pad.top + plotH - normH, barW - 2, normH);
});
// Draw x-axis and y-axis labels
ctx.fillStyle = '#555';
ctx.font = '11px system-ui';
ctx.textAlign = 'center';
ctx.fillText(xLabel, pad.left + plotW / 2, H - 4);
ctx.save();
ctx.translate(12, pad.top + plotH / 2);
ctx.rotate(-Math.PI / 2);
ctx.fillText(yLabel, 0, 0);
ctx.restore();
}
function update() {
const data = computeSteps(currentVowel);
const stepKey = STEP_KEYS[currentStep];
document.getElementById('step-label').textContent =
{ step_power: 'Step 1 — Power Spectrum', step_mel: 'Step 2 — Mel Filterbank',
step_log: 'Step 3 — Log Energy', step_mfcc: 'Step 4 — MFCC Coefficients' }[stepKey] || '';
document.getElementById('step-desc').innerHTML = getStepDesc(stepKey);
const mfccRow = document.getElementById('mfcc-row');
mfccRow.style.display = currentStep === 3 ? 'block' : 'none';
if (currentStep === 0) {
drawBars(Array.from(data.spec), '#4a7faa', 'Frequency (Hz)', 'Power');
} else if (currentStep === 1) {
drawBars(data.melEnergy, '#2a9d5c', 'Mel filterbank channel', 'Energy');
} else if (currentStep === 2) {
drawBars(data.logEnergy, '#e07a1a', 'Mel filterbank channel', 'Log energy');
} else {
drawBars(data.mfcc, '#6a4cbf', 'MFCC coefficient index', 'Amplitude');
renderMfccBars(data.mfcc);
}
document.getElementById('btn-prev').disabled = currentStep === 0;
document.getElementById('btn-next').disabled = currentStep === STEP_KEYS.length - 1;
}
/* Render the compact colored MFCC coefficient strip below the chart */
function renderMfccBars(mfcc) {
const container = document.getElementById('mfcc-bars');
container.innerHTML = '';
const maxAbs = Math.max(...mfcc.map(Math.abs), 1);
const COLORS = ['#1d3557','#457b9d','#2a9d5c','#e07a1a','#6a4cbf',
'#c62828','#00838f','#558b2f','#f57f17','#4527a0',
'#00695c','#283593','#6d4c41'];
mfcc.forEach((v, i) => {
const bar = document.createElement('div');
bar.className = 'mfcc-bar';
bar.title = i18nFmt('c%n%: %v%', { n: i + 1, v: v.toFixed(2) });
const pct = Math.abs(v) / maxAbs;
bar.style.height = (pct * 48 + 4) + 'px';
bar.style.background = COLORS[i % COLORS.length];
container.appendChild(bar);
});
}
document.getElementById('vowel-sel').addEventListener('change', e => {
currentVowel = e.target.value;
update();
});
document.getElementById('btn-prev').addEventListener('click', () => {
if (currentStep > 0) { currentStep--; update(); }
});
document.getElementById('btn-next').addEventListener('click', () => {
if (currentStep < STEP_KEYS.length - 1) { currentStep++; update(); }
});
document.getElementById('btn-reset').addEventListener('click', () => {
currentStep = 0;
document.getElementById('vowel-sel').value = 'a';
currentVowel = 'a';
update();
});
update();
<!-- MFCC pipeline demo: power spectrum -> mel filterbank -> log -> DCT -> 13 coefficients -->
<div class="controls">
<label for="vowel-sel">Vowel:</label>
<select id="vowel-sel">
<option value="a">/a/ (father)</option>
<option value="e">/e/ (bed)</option>
<option value="i">/i/ (see)</option>
<option value="o">/o/ (go)</option>
<option value="u">/u/ (too)</option>
</select>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div class="steps">
<button id="btn-prev" type="button" class="nav" disabled>←</button>
<span id="step-label" class="step-label"></span>
<button id="btn-next" type="button" class="nav">→</button>
</div>
<div class="desc-box" id="step-desc"></div>
<canvas id="chart" width="560" height="200"></canvas>
<div class="mfcc-row" id="mfcc-row" style="display:none">
<div class="mfcc-label">MFCC vector (13 coefficients):</div>
<div id="mfcc-bars" class="mfcc-bars"></div>
</div>
/* Demo layout: controls on top, step navigator, description, canvas chart, MFCC bars */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; background: #fff; }
.controls { display: flex; align-items: center; gap: .6rem; margin-bottom: .6rem; flex-wrap: wrap; }
label { font-size: .9rem; font-weight: 600; }
select { font-size: .9rem; padding: .3rem .5rem; border: 1px solid #bbb; border-radius: 6px; }
button { font: 600 13px system-ui; padding: .35rem .75rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 6px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .35; cursor: default; }
.steps { display: flex; align-items: center; gap: .5rem; margin-bottom: .4rem; }
.step-label { font-size: .92rem; font-weight: 700; flex: 1; text-align: center; }
.nav { padding: .3rem .65rem; font-size: 1rem; min-width: 36px; }
.desc-box { font-size: .84rem; color: #444; line-height: 1.5; margin-bottom: .5rem;
min-height: 2.6rem; padding: .35rem .6rem; background: #f4f7fa;
border-left: 3px solid #1d3557; border-radius: 4px; }
canvas { width: 100%; max-width: 560px; display: block; border: 1px solid #e0e5ea; border-radius: 8px; background: #fafbfc; }
/* Compact MFCC coefficient bar strip shown at step 4 */
.mfcc-row { margin-top: .6rem; }
.mfcc-label { font-size: .8rem; font-weight: 700; color: #555; margin-bottom: .25rem; }
.mfcc-bars { display: flex; gap: 3px; align-items: flex-end; height: 54px; }
.mfcc-bar { flex: 1; border-radius: 3px 3px 0 0; min-height: 4px; transition: height .35s; }
/* MFCC pipeline: power spectrum -> mel filterbank -> log energy -> DCT coefficients */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Approximate formant frequencies (F1, F2, F3) in Hz for five English vowels */
const VOWELS = {
a: { f1: 730, f2: 1090, f3: 2440 },
e: { f1: 530, f2: 1840, f3: 2480 },
i: { f1: 270, f2: 2290, f3: 3010 },
o: { f1: 570, f2: 840, f3: 2410 },
u: { f1: 300, f2: 870, f3: 2240 }
};
/* Standard mel-scale conversion: mel = 2595 * log10(1 + f/700) */
const hz2mel = f => 2595 * Math.log10(1 + f / 700);
const mel2hz = m => 700 * (Math.pow(10, m / 2595) - 1);
const N_FFT = 128; // number of simulated FFT power spectrum bins
const N_MELS = 20; // number of triangular mel filterbank channels
const N_MFCC = 13; // number of MFCC coefficients to keep
const F_MAX = 8000; // maximum frequency in Hz
/* Build an idealized power spectrum for the given vowel using Gaussian formant peaks */
function makePowerSpectrum(vowel) {
const { f1, f2, f3 } = VOWELS[vowel];
const spec = new Float32Array(N_FFT);
const fStep = F_MAX / N_FFT;
for (let k = 0; k < N_FFT; k++) {
const f = (k + 0.5) * fStep;
// three formant peaks modeled as Gaussians — three formant peaks modeled as Gaussians
const g1 = Math.exp(-Math.pow((f - f1) / 150, 2));
const g2 = Math.exp(-Math.pow((f - f2) / 250, 2)) * 0.7;
const g3 = Math.exp(-Math.pow((f - f3) / 350, 2)) * 0.4;
spec[k] = Math.max(0.01, g1 + g2 + g3);
}
return spec;
}
/* Build a bank of N_MELS triangular filters equally spaced on the mel scale */
function buildFilterbank() {
const melMin = hz2mel(0);
const melMax = hz2mel(F_MAX);
const melPoints = [];
for (let m = 0; m <= N_MELS + 1; m++)
melPoints.push(mel2hz(melMin + (m / (N_MELS + 1)) * (melMax - melMin)));
const fb = [];
const fStep = F_MAX / N_FFT;
for (let m = 1; m <= N_MELS; m++) {
const row = new Float32Array(N_FFT);
for (let k = 0; k < N_FFT; k++) {
const f = (k + 0.5) * fStep;
const lo = melPoints[m - 1], cen = melPoints[m], hi = melPoints[m + 1];
if (f >= lo && f <= cen) row[k] = (f - lo) / (cen - lo);
else if (f > cen && f <= hi) row[k] = (hi - f) / (hi - cen);
}
fb.push(row);
}
return fb;
}
/* Multiply the power spectrum by each filter row and sum (dot product per channel) */
function applyFilterbank(spec, fb) {
return fb.map(row => {
let s = 0;
for (let k = 0; k < N_FFT; k++) s += row[k] * spec[k];
return s;
});
}
/* Type-II DCT: decorrelates the log filterbank energies into cepstral coefficients */
function dct(x, C) {
const out = [];
for (let n = 0; n < C; n++) {
let s = 0;
for (let k = 0; k < x.length; k++)
s += x[k] * Math.cos(Math.PI * n * (k + 0.5) / x.length);
out.push(s);
}
return out;
}
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const FB = buildFilterbank();
let currentVowel = 'a';
let currentStep = 0; // 0=power spectrum, 1=mel filterbank, 2=log energy, 3=MFCC coefficients
/* Keys matching step index to i18n description keys */
const STEP_KEYS = ['step_power', 'step_mel', 'step_log', 'step_mfcc'];
function getStepDesc(key) {
return {
step_power: 'The raw power spectrum <b>|X(f)|²</b> shows energy at each frequency bin. Vowels have distinct peaks called <b>formants</b> (F1, F2, F3) that define their sound. The x-axis spans 0–8 kHz; high-frequency detail is perceptually less important.',
step_mel: 'A bank of <b>20 triangular filters</b> is applied, spaced evenly on the <b>mel scale</b>. Bins are narrow at low frequencies (where the ear is most sensitive) and wide at high frequencies. Each bar is the total energy captured by one filter.',
step_log: 'The <b>logarithm</b> of each filterbank energy compresses the dynamic range to match how loudness is perceived. It also turns the vocal-tract convolution into a sum — separating the speaker’s voice from the message content.',
step_mfcc: 'A <b>Discrete Cosine Transform</b> decorrelates the 20 log energies into 13 MFCC coefficients. Adjacent mel channels are correlated; the DCT removes that redundancy. The first few coefficients capture the broad spectral envelope; later ones encode finer detail.'
}[key] || '';
}
function computeSteps(vowel) {
const spec = makePowerSpectrum(vowel);
const melEnergy = applyFilterbank(spec, FB);
const logEnergy = melEnergy.map(v => Math.log(v + 1e-8));
const mfcc = dct(logEnergy, N_MFCC);
return { spec, melEnergy, logEnergy, mfcc };
}
/* Generic bar chart renderer: values array, fill color, axis labels */
function drawBars(values, color, xLabel, yLabel) {
const W = canvas.width, H = canvas.height;
const pad = { left: 42, right: 10, top: 18, bottom: 32 };
const plotW = W - pad.left - pad.right;
const plotH = H - pad.top - pad.bottom;
ctx.clearRect(0, 0, W, H);
const minV = Math.min(...values);
const maxV = Math.max(...values);
const range = maxV - minV || 1;
ctx.fillStyle = '#e8eef3';
ctx.fillRect(pad.left, pad.top, plotW, plotH);
const barW = plotW / values.length;
values.forEach((v, i) => {
const normH = ((v - minV) / range) * plotH;
ctx.fillStyle = color;
ctx.fillRect(pad.left + i * barW + 1, pad.top + plotH - normH, barW - 2, normH);
});
// Draw x-axis and y-axis labels
ctx.fillStyle = '#555';
ctx.font = '11px system-ui';
ctx.textAlign = 'center';
ctx.fillText(xLabel, pad.left + plotW / 2, H - 4);
ctx.save();
ctx.translate(12, pad.top + plotH / 2);
ctx.rotate(-Math.PI / 2);
ctx.fillText(yLabel, 0, 0);
ctx.restore();
}
function update() {
const data = computeSteps(currentVowel);
const stepKey = STEP_KEYS[currentStep];
document.getElementById('step-label').textContent =
{ step_power: 'Step 1 — Power Spectrum', step_mel: 'Step 2 — Mel Filterbank',
step_log: 'Step 3 — Log Energy', step_mfcc: 'Step 4 — MFCC Coefficients' }[stepKey] || '';
document.getElementById('step-desc').innerHTML = getStepDesc(stepKey);
const mfccRow = document.getElementById('mfcc-row');
mfccRow.style.display = currentStep === 3 ? 'block' : 'none';
if (currentStep === 0) {
drawBars(Array.from(data.spec), '#4a7faa', 'Frequency (Hz)', 'Power');
} else if (currentStep === 1) {
drawBars(data.melEnergy, '#2a9d5c', 'Mel filterbank channel', 'Energy');
} else if (currentStep === 2) {
drawBars(data.logEnergy, '#e07a1a', 'Mel filterbank channel', 'Log energy');
} else {
drawBars(data.mfcc, '#6a4cbf', 'MFCC coefficient index', 'Amplitude');
renderMfccBars(data.mfcc);
}
document.getElementById('btn-prev').disabled = currentStep === 0;
document.getElementById('btn-next').disabled = currentStep === STEP_KEYS.length - 1;
}
/* Render the compact colored MFCC coefficient strip below the chart */
function renderMfccBars(mfcc) {
const container = document.getElementById('mfcc-bars');
container.innerHTML = '';
const maxAbs = Math.max(...mfcc.map(Math.abs), 1);
const COLORS = ['#1d3557','#457b9d','#2a9d5c','#e07a1a','#6a4cbf',
'#c62828','#00838f','#558b2f','#f57f17','#4527a0',
'#00695c','#283593','#6d4c41'];
mfcc.forEach((v, i) => {
const bar = document.createElement('div');
bar.className = 'mfcc-bar';
bar.title = i18nFmt('c%n%: %v%', { n: i + 1, v: v.toFixed(2) });
const pct = Math.abs(v) / maxAbs;
bar.style.height = (pct * 48 + 4) + 'px';
bar.style.background = COLORS[i % COLORS.length];
container.appendChild(bar);
});
}
document.getElementById('vowel-sel').addEventListener('change', e => {
currentVowel = e.target.value;
update();
});
document.getElementById('btn-prev').addEventListener('click', () => {
if (currentStep > 0) { currentStep--; update(); }
});
document.getElementById('btn-next').addEventListener('click', () => {
if (currentStep < STEP_KEYS.length - 1) { currentStep++; update(); }
});
document.getElementById('btn-reset').addEventListener('click', () => {
currentStep = 0;
document.getElementById('vowel-sel').value = 'a';
currentVowel = 'a';
update();
});
update();
Comments
Loading comments...