{"spec_id":"spectrogram-mel","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// spectrogram-mel: Mel-Spectrogram for Audio Analysis\n// Library: chartjs 4.4.7 | JavaScript 22.22.3\n// Quality: 91/100 | Created: 2026-06-03\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Configuration ---------------------------------------------------------\nconst N_MELS = 64;\nconst N_FRAMES = 250;\nconst DURATION = 5.0;\nconst SAMPLE_RATE = 22050;\nconst DB_MIN = -80;\nconst DB_MAX = 0;\n\n// Mel scale constants: filter banks span 20 Hz to Nyquist\nconst MEL_MIN_VAL = 2595 * Math.log10(1 + 20 / 700);\nconst MEL_MAX_VAL = 2595 * Math.log10(1 + (SAMPLE_RATE / 2) / 700);\n\nfunction melToHz(mel) {\n  return 700 * (Math.pow(10, mel / 2595) - 1);\n}\nfunction hzToMelBand(hz) {\n  const mel = 2595 * Math.log10(1 + hz / 700);\n  return ((mel - MEL_MIN_VAL) / (MEL_MAX_VAL - MEL_MIN_VAL)) * (N_MELS - 1);\n}\n\n// --- Deterministic PRNG (LCG) ---------------------------------------------\nfunction makeLcg(seed) {\n  let s = seed >>> 0;\n  return () => {\n    s = (Math.imul(s, 1664525) + 1013904223) >>> 0;\n    return s / 4294967296;\n  };\n}\n\n// --- Synthetic speech-like mel-spectrogram (voiced + unvoiced segments) ---\nfunction generateSpec() {\n  const rand = makeLcg(42);\n  const data = new Float32Array(N_MELS * N_FRAMES);\n\n  for (let f = 0; f < N_FRAMES; f++) {\n    const tNorm = f / N_FRAMES;\n    const isVoiced = Math.sin(tNorm * Math.PI * 7) > 0.1;\n    const formant1 = 15 + 5 * Math.sin(tNorm * 2 * Math.PI * 1.5);\n    const formant2 = 32 + 4 * Math.sin(tNorm * 2 * Math.PI * 2.3);\n\n    for (let m = 0; m < N_MELS; m++) {\n      let db = -68 + rand() * 10;\n\n      if (isVoiced) {\n        // Fundamental frequency harmonics\n        for (let h = 1; h <= 7; h++) {\n          const hBand = 7 * h;\n          if (hBand < N_MELS) {\n            db += (22 - h * 2.5) * Math.exp(-((m - hBand) ** 2) / 8);\n          }\n        }\n        // Formant resonances (F1, F2)\n        db += 20 * Math.exp(-((m - formant1) ** 2) / 18);\n        db += 14 * Math.exp(-((m - formant2) ** 2) / 30);\n      } else {\n        // Unvoiced fricative: broadband energy in upper mel bands\n        db += 10 * Math.exp(-((m - 52) ** 2) / 45) * (0.4 + rand() * 0.6);\n      }\n\n      data[m * N_FRAMES + f] = Math.max(DB_MIN, Math.min(DB_MAX, db));\n    }\n  }\n  return data;\n}\n\nconst specData = generateSpec();\n\n// --- Color mapping: imprint_seq (pageBg → seq[0] → seq[1]) ---------------\n// Pre-parse hex stops once so dbToColor avoids repeated string parsing in the pixel loop\nconst _p = (h) => [parseInt(h.slice(1,3),16), parseInt(h.slice(3,5),16), parseInt(h.slice(5,7),16)];\nconst BG = _p(t.pageBg), C0 = _p(t.seq[0]), C1 = _p(t.seq[1]);\nfunction dbToColor(db) {\n  const n = (db - DB_MIN) / (DB_MAX - DB_MIN);\n  let r, g, b, s;\n  if (n < 0.4) {\n    s = n / 0.4;\n    r = BG[0] + s*(C0[0]-BG[0]); g = BG[1] + s*(C0[1]-BG[1]); b = BG[2] + s*(C0[2]-BG[2]);\n  } else {\n    s = (n - 0.4) / 0.6;\n    r = C0[0] + s*(C1[0]-C0[0]); g = C0[1] + s*(C1[1]-C0[1]); b = C0[2] + s*(C1[2]-C0[2]);\n  }\n  return [Math.round(r), Math.round(g), Math.round(b)];\n}\n\n// --- Custom plugin: spectrogram raster + colorbar -------------------------\nconst spectrogramPlugin = {\n  id: 'spectrogram',\n  afterDraw(chart) {\n    const ctx = chart.ctx;\n    const { left, top, right, bottom } = chart.chartArea;\n    const W = Math.floor(right - left);\n    const H = Math.floor(bottom - top);\n\n    // Render spectrogram pixels to an offscreen canvas\n    const off = document.createElement('canvas');\n    off.width = W;\n    off.height = H;\n    const offCtx = off.getContext('2d');\n    const imgData = offCtx.createImageData(W, H);\n    const px = imgData.data;\n\n    for (let py = 0; py < H; py++) {\n      const mFrac = (1 - py / (H - 1)) * (N_MELS - 1);\n      const m0 = Math.floor(mFrac);\n      const m1 = Math.min(m0 + 1, N_MELS - 1);\n      const dm = mFrac - m0;\n\n      for (let pxX = 0; pxX < W; pxX++) {\n        const fFrac = (pxX / (W - 1)) * (N_FRAMES - 1);\n        const f0 = Math.floor(fFrac);\n        const f1 = Math.min(f0 + 1, N_FRAMES - 1);\n        const df = fFrac - f0;\n\n        // Bilinear interpolation for smooth rendering\n        const db =\n          specData[m0 * N_FRAMES + f0] * (1 - dm) * (1 - df) +\n          specData[m0 * N_FRAMES + f1] * (1 - dm) * df +\n          specData[m1 * N_FRAMES + f0] * dm * (1 - df) +\n          specData[m1 * N_FRAMES + f1] * dm * df;\n\n        const [r, g, b] = dbToColor(db);\n        const i = (py * W + pxX) * 4;\n        px[i] = r; px[i + 1] = g; px[i + 2] = b; px[i + 3] = 255;\n      }\n    }\n    offCtx.putImageData(imgData, 0, 0);\n\n    // Blit spectrogram into chart area with clip guard\n    ctx.save();\n    ctx.beginPath();\n    ctx.rect(left, top, W, H);\n    ctx.clip();\n    ctx.drawImage(off, left, top);\n    ctx.restore();\n\n    // Redraw chart border over spectrogram — thicker for visual weight\n    ctx.strokeStyle = t.ink;\n    ctx.lineWidth = 2;\n    ctx.strokeRect(left, top, W, H);\n\n    // --- Colorbar ---------------------------------------------------------\n    const cbX = right + 20;\n    const cbW = 24;\n    const cbH = H;\n    const fs = Math.max(11, Math.round(H / 36));\n\n    // Elevated background separating the colorbar column from the plot\n    ctx.fillStyle = t.elevatedBg;\n    ctx.fillRect(right + 8, top - 6, 108, cbH + 12);\n\n    const grad = ctx.createLinearGradient(0, top, 0, top + cbH);\n    grad.addColorStop(0, t.seq[1]);\n    grad.addColorStop(0.6, t.seq[0]);\n    grad.addColorStop(1, t.pageBg);\n    ctx.fillStyle = grad;\n    ctx.fillRect(cbX, top, cbW, cbH);\n    ctx.strokeStyle = t.inkSoft;\n    ctx.lineWidth = 1;\n    ctx.strokeRect(cbX, top, cbW, cbH);\n\n    // Colorbar tick marks and dB labels\n    ctx.fillStyle = t.inkSoft;\n    ctx.font = `${fs}px sans-serif`;\n    ctx.textAlign = 'left';\n    const dbLabels = [0, -20, -40, -60, -80];\n    for (const db of dbLabels) {\n      const yPos = top + ((DB_MAX - db) / (DB_MAX - DB_MIN)) * cbH;\n      ctx.beginPath();\n      ctx.moveTo(cbX + cbW, yPos);\n      ctx.lineTo(cbX + cbW + 5, yPos);\n      ctx.strokeStyle = t.inkSoft;\n      ctx.lineWidth = 1;\n      ctx.stroke();\n      ctx.fillText(`${db}`, cbX + cbW + 7, yPos + fs * 0.35);\n    }\n\n    // Rotated \"Power (dB)\" label\n    ctx.fillStyle = t.ink;\n    ctx.font = `bold ${fs}px sans-serif`;\n    ctx.save();\n    ctx.translate(cbX + cbW + fs * 5.5, top + cbH / 2);\n    ctx.rotate(-Math.PI / 2);\n    ctx.textAlign = 'center';\n    ctx.fillText('Power (dB)', 0, 0);\n    ctx.restore();\n  },\n};\n\n// --- Title ----------------------------------------------------------------\nconst titleText = 'spectrogram-mel · javascript · chartjs · anyplot.ai';\nconst titleSize = 26;\n\n// --- Mount ----------------------------------------------------------------\nconst canvas = document.createElement('canvas');\ndocument.getElementById('container').appendChild(canvas);\n\n// --- Chart ----------------------------------------------------------------\nnew Chart(canvas, {\n  type: 'scatter',\n  data: { datasets: [{ data: [] }] },\n  plugins: [spectrogramPlugin],\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: {\n      padding: { right: 110, top: 10, bottom: 10, left: 10 },\n    },\n    plugins: {\n      title: {\n        display: true,\n        text: titleText,\n        color: t.ink,\n        font: { size: titleSize, weight: '500' },\n        padding: { bottom: 14 },\n      },\n      legend: { display: false },\n    },\n    scales: {\n      x: {\n        type: 'linear',\n        min: 0,\n        max: DURATION,\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          callback: (v) => v.toFixed(1) + 's',\n          maxTicksLimit: 7,\n        },\n        grid: { display: false },\n        title: {\n          display: true,\n          text: 'Time (s)',\n          color: t.ink,\n          font: { size: 16 },\n        },\n        border: { color: t.inkSoft },\n      },\n      y: {\n        type: 'linear',\n        min: 0,\n        max: N_MELS - 1,\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          maxTicksLimit: 7,\n          callback: (v) => {\n            const mel = MEL_MIN_VAL + (v / (N_MELS - 1)) * (MEL_MAX_VAL - MEL_MIN_VAL);\n            const hz = Math.round(melToHz(mel));\n            return hz >= 1000 ? (Math.round(hz / 100) / 10) + 'k' : `${hz}`;\n          },\n        },\n        grid: { display: false },\n        title: {\n          display: true,\n          text: 'Frequency (Hz, mel scale)',\n          color: t.ink,\n          font: { size: 16 },\n        },\n        border: { color: t.inkSoft },\n      },\n    },\n  },\n});\n"}