{"spec_id":"spectrogram-mel","library":"echarts","language":"javascript","code":"// anyplot.ai\n// spectrogram-mel: Mel-Spectrogram for Audio Analysis\n// Library: echarts 5.5.1 | JavaScript 22.22.3\n// Quality: 90/100 | Created: 2026-06-03\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Signal parameters -------------------------------------------------------\nconst SR = 22050;           // sample rate Hz\nconst DURATION = 2.0;       // seconds\nconst N_SAMPLES = Math.floor(SR * DURATION);\nconst N_FFT = 512;          // FFT size (power of 2)\nconst HOP = 256;            // hop length\nconst N_MELS = 64;          // mel frequency bins\nconst F_MIN = 20;           // Hz\nconst F_MAX = 8000;         // Hz\nconst DB_MIN = -80;         // dB floor for display\n\n// --- Synthesize C-major ascending scale (C4→A4) with harmonics ---------------\n// Each note is 1/6 of the total duration; each has a fundamental + 4 harmonics\nconst signal = new Float64Array(N_SAMPLES);\nconst notes = [261.63, 293.66, 329.63, 349.23, 392.00, 440.00]; // C4 D4 E4 F4 G4 A4\nconst noteDur = DURATION / notes.length;\nconst harmonicAmp = [0.55, 0.30, 0.17, 0.10, 0.05];\n\nfor (let i = 0; i < N_SAMPLES; i++) {\n  const t_s = i / SR;\n  const f0 = notes[Math.min(Math.floor(t_s / noteDur), notes.length - 1)];\n  let v = 0;\n  for (let h = 0; h < harmonicAmp.length; h++) {\n    v += harmonicAmp[h] * Math.sin(2 * Math.PI * (h + 1) * f0 * t_s);\n  }\n  signal[i] = v;\n}\n\n// --- Hann window -------------------------------------------------------------\nconst hann = new Float64Array(N_FFT);\nfor (let i = 0; i < N_FFT; i++) {\n  hann[i] = 0.5 * (1 - Math.cos(2 * Math.PI * i / (N_FFT - 1)));\n}\n\n// --- Radix-2 Cooley-Tukey FFT (in-place, DIF) --------------------------------\nfunction fft(re, im) {\n  const n = re.length;\n  for (let i = 1, j = 0; i < n; i++) {\n    let bit = n >> 1;\n    for (; j & bit; bit >>= 1) j ^= bit;\n    j ^= bit;\n    if (i < j) {\n      let tmp = re[i]; re[i] = re[j]; re[j] = tmp;\n      tmp = im[i]; im[i] = im[j]; im[j] = tmp;\n    }\n  }\n  for (let len = 2; len <= n; len <<= 1) {\n    const ang = -2 * Math.PI / len;\n    const wc = Math.cos(ang), ws = Math.sin(ang);\n    const half = len >> 1;\n    for (let i = 0; i < n; i += len) {\n      let wr = 1, wi = 0;\n      for (let k = 0; k < half; k++) {\n        const ur = re[i + k], ui = im[i + k];\n        const vr = re[i + k + half] * wr - im[i + k + half] * wi;\n        const vi = re[i + k + half] * wi + im[i + k + half] * wr;\n        re[i + k] = ur + vr; im[i + k] = ui + vi;\n        re[i + k + half] = ur - vr; im[i + k + half] = ui - vi;\n        const nwr = wr * wc - wi * ws;\n        wi = wr * ws + wi * wc;\n        wr = nwr;\n      }\n    }\n  }\n}\n\n// --- Mel filter banks --------------------------------------------------------\nconst hzToMel = hz => 2595 * Math.log10(1 + hz / 700);\nconst melToHz = mel => 700 * (Math.pow(10, mel / 2595) - 1);\n\nconst melMin = hzToMel(F_MIN);\nconst melMax = hzToMel(F_MAX);\nconst N_BINS = Math.floor(N_FFT / 2) + 1;\n\n// N_MELS+2 equally-spaced mel edge points (lower edge, N_MELS peaks, upper edge)\nconst melEdgeHz = Array.from({ length: N_MELS + 2 }, (_, i) =>\n  melToHz(melMin + (melMax - melMin) * i / (N_MELS + 1))\n);\nconst edgeBins = melEdgeHz.map(hz => Math.floor((N_FFT + 1) * hz / SR));\n\n// Sparse triangular filters: only (bin, weight) pairs where weight > 0\nconst filters = Array.from({ length: N_MELS }, (_, m) => {\n  const b0 = edgeBins[m], b1 = edgeBins[m + 1], b2 = edgeBins[m + 2];\n  const entries = [];\n  for (let k = b0; k < b1; k++) if (b1 > b0) entries.push([k, (k - b0) / (b1 - b0)]);\n  for (let k = b1; k <= b2; k++) if (b2 > b1) entries.push([k, (b2 - k) / (b2 - b1)]);\n  return entries;\n});\n\n// Center Hz of each mel bin (for y-axis labels)\nconst melCenterHz = Array.from({ length: N_MELS }, (_, m) => melEdgeHz[m + 1]);\n\n// --- Compute STFT then mel spectrogram ---------------------------------------\nconst n_frames = Math.floor((N_SAMPLES - N_FFT) / HOP) + 1;\nconst re = new Float64Array(N_FFT);\nconst im = new Float64Array(N_FFT);\nconst power = new Float64Array(N_BINS);\nconst melFrame = new Float64Array(N_MELS);\nconst melSpec = [];  // array of Float64Array snapshots\n\nfor (let fr = 0; fr < n_frames; fr++) {\n  const start = fr * HOP;\n  for (let i = 0; i < N_FFT; i++) {\n    re[i] = (start + i < N_SAMPLES ? signal[start + i] : 0) * hann[i];\n    im[i] = 0;\n  }\n  fft(re, im);\n  for (let k = 0; k < N_BINS; k++) power[k] = re[k] * re[k] + im[k] * im[k];\n  for (let m = 0; m < N_MELS; m++) {\n    let s = 0;\n    for (const [k, w] of filters[m]) s += w * power[k];\n    melFrame[m] = Math.max(s, 1e-20);\n  }\n  melSpec.push(melFrame.slice());\n}\n\n// Convert to dB relative to peak power across all frames and bins\nlet maxPow = 0;\nfor (const fr of melSpec) for (const v of fr) if (v > maxPow) maxPow = v;\nconst specDb = melSpec.map(fr =>\n  Array.from(fr, v => Math.max(10 * Math.log10(v / maxPow), DB_MIN))\n);\n\n// --- Build ECharts heatmap data [frame_idx, mel_idx, dB] --------------------\nconst data = [];\nfor (let fr = 0; fr < n_frames; fr++) {\n  for (let m = 0; m < N_MELS; m++) {\n    data.push([fr, m, parseFloat(specDb[fr][m].toFixed(1))]);\n  }\n}\n\n// --- Axis labels -------------------------------------------------------------\n// X-axis: time in seconds (show ~5 labels, ~0.5 s apart)\nconst timeLabels = Array.from({ length: n_frames }, (_, i) => (i * HOP / SR).toFixed(2));\nconst xInterval = Math.round(0.5 * SR / HOP) - 1;  // frames per 0.5 s minus 1\n\n// Y-axis: center frequency of each mel bin in Hz / kHz\nconst freqLabels = melCenterHz.map(hz =>\n  hz < 1000 ? Math.round(hz) + ' Hz' : (hz / 1000).toFixed(1) + ' kHz'\n);\nconst yInterval = Math.ceil(N_MELS / 8) - 1;  // show ~8 frequency labels\n\n// --- Title size (scale for length > 67 chars) --------------------------------\nconst titleText = 'C Major Scale · spectrogram-mel · javascript · echarts · anyplot.ai';\nconst titleSize = Math.max(15, Math.round(22 * Math.min(1, 67 / titleText.length)));\n\n// --- Render ------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById('container'));\n\nchart.setOption({\n  animation: false,\n  backgroundColor: 'transparent',\n\n  title: {\n    text: titleText,\n    left: 'center',\n    top: 18,\n    textStyle: { color: t.ink, fontSize: titleSize, fontWeight: 'bold' },\n  },\n\n  grid: { left: 130, right: 155, top: 90, bottom: 90 },\n\n  xAxis: {\n    type: 'category',\n    data: timeLabels,\n    name: 'Time (s)',\n    nameLocation: 'middle',\n    nameGap: 45,\n    nameTextStyle: { color: t.ink, fontSize: 18 },\n    axisLabel: {\n      color: t.inkSoft,\n      fontSize: 14,\n      interval: xInterval,\n      formatter: v => v + 's',\n    },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { show: false },\n  },\n\n  yAxis: {\n    type: 'category',\n    data: freqLabels,\n    name: 'Frequency (Mel scale)',\n    nameLocation: 'middle',\n    nameGap: 95,\n    nameTextStyle: { color: t.ink, fontSize: 18 },\n    axisLabel: {\n      color: t.inkSoft,\n      fontSize: 13,\n      interval: yInterval,\n    },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { show: false },\n  },\n\n  // Imprint sequential colormap: brand green → blue (low → high energy)\n  visualMap: {\n    min: DB_MIN,\n    max: 0,\n    calculable: false,\n    orient: 'vertical',\n    right: 28,\n    top: 'middle',\n    itemWidth: 22,\n    itemHeight: 320,\n    text: ['0 dB', DB_MIN + ' dB'],\n    textStyle: { color: t.inkSoft, fontSize: 14 },\n    inRange: { color: t.seq },\n    backgroundColor: 'transparent',\n    borderWidth: 0,\n  },\n\n  series: [{\n    type: 'heatmap',\n    data: data,\n    emphasis: { disabled: true },\n    label: { show: false },\n  }],\n\n  tooltip: { show: false },\n});\n"}