{"spec_id":"spectrogram-mel","library":"muix","language":"javascript","code":"// anyplot.ai\n// spectrogram-mel: Mel-Spectrogram for Audio Analysis\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-08-24\n\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { useDrawingArea, useXScale, useYScale } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Audio synthesis (deterministic, in-memory) ------------------------------\n// A short sung melody (four notes with decaying harmonics + vibrato) plus a\n// breathy broadband onset — the kind of signal an ASR / vocal-quality pipeline\n// would feed into a mel-spectrogram front end.\nconst SAMPLE_RATE = 16000;\nconst DURATION_S = 2.4;\nconst N_SAMPLES = Math.round(SAMPLE_RATE * DURATION_S);\nconst NOTES = [261.63, 329.63, 392.0, 523.25]; // C4, E4, G4, C5\nconst NOTE_DURATION = DURATION_S / NOTES.length;\nconst HARMONIC_AMPS = [1, 0.5, 0.22];\n\n// Tiny LCG for reproducible noise — the browser has no seeded RNG.\nfunction makeLcg(seed: number) {\n  let s = seed >>> 0;\n  return () => {\n    s = (Math.imul(s, 1664525) + 1013904223) >>> 0;\n    return s / 0x100000000;\n  };\n}\nconst rand = makeLcg(42);\n\nconst audioSignal = new Float64Array(N_SAMPLES);\nfor (let i = 0; i < N_SAMPLES; i++) {\n  const time = i / SAMPLE_RATE;\n  const noteIndex = Math.min(NOTES.length - 1, Math.floor(time / NOTE_DURATION));\n  const localT = time - noteIndex * NOTE_DURATION;\n  const envelope = Math.sin(Math.PI * Math.min(1, localT / NOTE_DURATION));\n  const vibrato = 1 + 0.006 * Math.sin(2 * Math.PI * 5 * time);\n  const fundamental = NOTES[noteIndex];\n\n  let tone = 0;\n  for (let h = 0; h < HARMONIC_AMPS.length; h++) {\n    tone += HARMONIC_AMPS[h] * Math.sin(2 * Math.PI * fundamental * (h + 1) * vibrato * time);\n  }\n  const breathyOnset = Math.exp(-time / 0.03) * 0.16 * (2 * rand() - 1);\n  const noiseFloor = 0.006 * (2 * rand() - 1);\n  audioSignal[i] = 0.6 * envelope * tone + breathyOnset + noiseFloor;\n}\n\n// --- STFT: iterative radix-2 FFT ---------------------------------------------\nconst N_FFT = 1024;\nconst HOP = 400;\nconst N_FREQ = N_FFT / 2 + 1;\nconst N_FRAMES = Math.floor((N_SAMPLES - N_FFT) / HOP) + 1;\n\nfunction fft(re: Float64Array, im: Float64Array) {\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      const tr = re[i]; re[i] = re[j]; re[j] = tr;\n      const ti = im[i]; im[i] = im[j]; im[j] = ti;\n    }\n  }\n  for (let len = 2; len <= n; len <<= 1) {\n    const ang = (-2 * Math.PI) / len;\n    const wr = Math.cos(ang);\n    const wi = Math.sin(ang);\n    for (let i = 0; i < n; i += len) {\n      let curWr = 1;\n      let curWi = 0;\n      for (let k = 0; k < len / 2; k++) {\n        const ur = re[i + k];\n        const ui = im[i + k];\n        const vr = re[i + k + len / 2] * curWr - im[i + k + len / 2] * curWi;\n        const vi = re[i + k + len / 2] * curWi + im[i + k + len / 2] * curWr;\n        re[i + k] = ur + vr;\n        im[i + k] = ui + vi;\n        re[i + k + len / 2] = ur - vr;\n        im[i + k + len / 2] = ui - vi;\n        const nextWr = curWr * wr - curWi * wi;\n        const nextWi = curWr * wi + curWi * wr;\n        curWr = nextWr;\n        curWi = nextWi;\n      }\n    }\n  }\n}\n\nconst hannWindow = new Float64Array(N_FFT);\nfor (let n = 0; n < N_FFT; n++) hannWindow[n] = 0.5 - 0.5 * Math.cos((2 * Math.PI * n) / (N_FFT - 1));\n\n// --- Mel filterbank (HTK formula) --------------------------------------------\nconst N_MELS = 64;\nconst F_MAX = SAMPLE_RATE / 2;\nfunction hzToMel(hz: number): number {\n  return 2595 * Math.log10(1 + hz / 700);\n}\nfunction melToHz(mel: number): number {\n  return 700 * (10 ** (mel / 2595) - 1);\n}\nconst MEL_MAX = hzToMel(F_MAX);\nconst melEdges = Array.from({ length: N_MELS + 2 }, (_, i) => (i * MEL_MAX) / (N_MELS + 1));\nconst hzEdges = melEdges.map(melToHz);\nconst binEdges = hzEdges.map((hz) => Math.floor(((N_FFT + 1) * hz) / SAMPLE_RATE));\n\n// Slaney-style area normalization — without it, wideband noise sums to a\n// larger response in the wider high-frequency filters, washing out contrast.\nconst melFilters: Float64Array[] = Array.from({ length: N_MELS }, () => new Float64Array(N_FREQ));\nfor (let m = 1; m <= N_MELS; m++) {\n  const left = binEdges[m - 1];\n  const center = binEdges[m];\n  const right = binEdges[m + 1];\n  const filt = melFilters[m - 1];\n  const norm = 2 / (hzEdges[m + 1] - hzEdges[m - 1] || 1);\n  for (let k = left; k < center && k < N_FREQ; k++) if (k >= 0) filt[k] = ((k - left) / (center - left || 1)) * norm;\n  for (let k = center; k < right && k < N_FREQ; k++) if (k >= 0) filt[k] = ((right - k) / (right - center || 1)) * norm;\n}\n\n// --- STFT -> mel power -> dB (relative to peak, floored at -80 dB) ----------\nconst DB_FLOOR = -80;\nlet peakPower = 1e-10;\nconst melPower: Float64Array[] = [];\nfor (let f = 0; f < N_FRAMES; f++) {\n  const start = f * HOP;\n  const re = new Float64Array(N_FFT);\n  const im = new Float64Array(N_FFT);\n  for (let n = 0; n < N_FFT; n++) re[n] = audioSignal[start + n] * hannWindow[n];\n  fft(re, im);\n\n  const power = new Float64Array(N_FREQ);\n  for (let k = 0; k < N_FREQ; k++) power[k] = (re[k] * re[k] + im[k] * im[k]) / N_FFT;\n\n  const melRow = new Float64Array(N_MELS);\n  for (let m = 0; m < N_MELS; m++) {\n    let sum = 0;\n    const filt = melFilters[m];\n    for (let k = 0; k < N_FREQ; k++) sum += filt[k] * power[k];\n    melRow[m] = sum;\n    if (sum > peakPower) peakPower = sum;\n  }\n  melPower.push(melRow);\n}\nconst melDb: number[][] = melPower.map((row) =>\n  Array.from(row).map((v) => Math.max(DB_FLOOR, 10 * Math.log10(Math.max(v, 1e-10) / peakPower)))\n);\n\nconst TIME_MAX = (N_FRAMES * HOP) / SAMPLE_RATE;\nconst Y_TICKS = Array.from({ length: 9 }, (_, i) => Math.round((i * N_MELS) / 8));\n\nfunction formatHz(hz: number): string {\n  if (hz >= 1000) return `${(hz / 1000).toFixed(1).replace(/\\.0$/, \"\")}k Hz`;\n  return `${Math.round(hz)} Hz`;\n}\n\n// Imprint sequential colormap: seq[0]=#009E73 (low power) -> seq[1]=#4467A3 (high power)\nfunction hexRgb(hex: string): [number, number, number] {\n  return [parseInt(hex.slice(1, 3), 16), parseInt(hex.slice(3, 5), 16), parseInt(hex.slice(5, 7), 16)];\n}\nfunction seqColor(frac: number): string {\n  const [r1, g1, b1] = hexRgb(t.seq[0]);\n  const [r2, g2, b2] = hexRgb(t.seq[1]);\n  return `rgb(${Math.round(r1 + (r2 - r1) * frac)},${Math.round(g1 + (g2 - g1) * frac)},${Math.round(b1 + (b2 - b1) * frac)})`;\n}\n// Gamma < 1 stretches the low end of the dB range across more of the color\n// ramp, so quiet early note onsets read against the background instead of\n// blending into a flat mid-tone field.\nconst COLOR_GAMMA = 0.6;\nfunction gammaFrac(frac: number): number {\n  return Math.pow(Math.max(0, frac), COLOR_GAMMA);\n}\n\n// Mel-band cells drawn directly at their frame/bin boundaries via the MUI X scale hooks\nfunction SpectrogramCells() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n\n  return (\n    <>\n      {melDb.flatMap((row, f) => {\n        const x0 = xScale((f * HOP) / SAMPLE_RATE);\n        const x1 = xScale(((f + 1) * HOP) / SAMPLE_RATE);\n        return row.map((db, m) => {\n          const frac = (db - DB_FLOOR) / -DB_FLOOR;\n          const yTop = yScale(m + 1);\n          const yBottom = yScale(m);\n          return (\n            <rect\n              key={`${f}-${m}`}\n              x={x0}\n              y={yTop}\n              width={x1 - x0 + 0.5}\n              height={yBottom - yTop + 0.5}\n              fill={seqColor(gammaFrac(frac))}\n            />\n          );\n        });\n      })}\n    </>\n  );\n}\n\n// Colorbar gradient positioned from the MUI X drawing-area context\nfunction Colorbar() {\n  const { left, top, width: gW, height: gH } = useDrawingArea();\n  const cbX = left + gW + 26;\n  const cbW = 20;\n\n  return (\n    <>\n      <defs>\n        <linearGradient id=\"melCbGrad\" x1=\"0\" y1=\"1\" x2=\"0\" y2=\"0\">\n          {[0, 0.25, 0.5, 0.75, 1].map((stop) => (\n            <stop key={stop} offset={`${stop * 100}%`} stopColor={seqColor(gammaFrac(stop))} />\n          ))}\n        </linearGradient>\n      </defs>\n      <rect x={cbX} y={top} width={cbW} height={gH} fill=\"url(#melCbGrad)\" />\n      <text x={cbX + cbW / 2} y={top - 10} textAnchor=\"middle\" fontSize={13} fill={t.inkSoft} fontFamily=\"Inter, system-ui, sans-serif\">\n        0 dB\n      </text>\n      <text x={cbX + cbW / 2} y={top + gH + 18} textAnchor=\"middle\" fontSize={13} fill={t.inkSoft} fontFamily=\"Inter, system-ui, sans-serif\">\n        -80 dB\n      </text>\n      <text\n        x={cbX + cbW + 20}\n        y={top + gH / 2}\n        textAnchor=\"middle\"\n        fontSize={14}\n        fill={t.inkSoft}\n        fontFamily=\"Inter, system-ui, sans-serif\"\n        transform={`rotate(90, ${cbX + cbW + 20}, ${top + gH / 2})`}\n      >\n        Power (dB)\n      </text>\n    </>\n  );\n}\n\nfunction ChartTitle() {\n  const { top } = useDrawingArea();\n  return (\n    <text x={width / 2} y={top - 46} textAnchor=\"middle\" fontSize={22} fontWeight={500} fill={t.ink} fontFamily=\"Inter, system-ui, sans-serif\">\n      spectrogram-mel · javascript · muix · anyplot.ai\n    </text>\n  );\n}\n\nexport default function Chart() {\n  return (\n    <ChartContainer\n      width={width}\n      height={height}\n      series={[]}\n      skipAnimation\n      xAxis={[{\n        scaleType: \"linear\",\n        min: 0,\n        max: TIME_MAX,\n        label: \"Time (s)\",\n        valueFormatter: (v: number) => `${v.toFixed(1)}s`,\n      }]}\n      yAxis={[{\n        scaleType: \"linear\",\n        min: 0,\n        max: N_MELS,\n        label: \"Frequency (mel-scaled)\",\n        tickInterval: Y_TICKS,\n        valueFormatter: (v: number) => formatHz(melToHz((v * MEL_MAX) / N_MELS)),\n        // The Hz tick labels (\"8k Hz\") are much wider than the default tick-font-based\n        // label offset assumes — inflate tickFontSize to push the axis label clear,\n        // then restore the real rendered size via tickLabelStyle.\n        tickFontSize: 70,\n        tickLabelStyle: { fontSize: 13 },\n      }]}\n      margin={{ left: 130, right: 140, top: 100, bottom: 80 }}\n    >\n      <ChartTitle />\n      <SpectrogramCells />\n      <Colorbar />\n      <ChartsXAxis />\n      <ChartsYAxis />\n    </ChartContainer>\n  );\n}\n"}