{"spec_id":"spectrogram-mel","library":"d3","language":"javascript","code":"// anyplot.ai\n// spectrogram-mel: Mel-Spectrogram for Audio Analysis\n// Library: d3 7.9.0 | JavaScript 22.22.3\n// Quality: 88/100 | Created: 2026-06-03\n\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Mel scale helpers ---\nconst hzToMel = hz => 2595 * Math.log10(1 + hz / 700);\nconst melToHz = mel => 700 * (Math.pow(10, mel / 2595) - 1);\n\n// --- Parameters ---\nconst n_mels = 128;\nconst n_frames = 200;\nconst duration = 4.0;\nconst fmin = 40;\nconst fmax = 8000;\nconst mel_min = hzToMel(fmin);\nconst mel_max = hzToMel(fmax);\n\n// Center frequency (Hz) of each mel bin\nconst mel_hz = Array.from({ length: n_mels }, (_, i) =>\n  melToHz(mel_min + (mel_max - mel_min) * (i + 0.5) / n_mels)\n);\n\n// --- Synthesize mel spectrogram: C major scale ascending then descending ---\nconst notes = [\n  261.6, 293.7, 329.6, 349.2, 392.0, 440.0, 493.9, 523.3,\n  493.9, 440.0, 392.0, 349.2, 329.6, 293.7, 261.6, 261.6,\n];\nconst fpn = n_frames / notes.length;\n\nconst spec = Array.from({ length: n_frames }, (_, f) => {\n  const ni = Math.min(Math.floor(f / fpn), notes.length - 1);\n  const fund = notes[ni];\n  const phase = f / fpn - ni;\n  const env = phase < 0.12 ? phase / 0.12 : phase > 0.80 ? (1 - phase) / 0.20 : 1.0;\n  return Array.from({ length: n_mels }, (_, m) => {\n    const hz = mel_hz[m];\n    let power = 2e-8;\n    for (let h = 1; h <= 10; h++) {\n      const hf = fund * h;\n      if (hf > fmax * 1.1) break;\n      const bw = hf * 0.03 + 18;\n      const dn = (hz - hf) / bw;\n      power += env * Math.pow(0.60, h - 1) * Math.exp(-2 * dn * dn);\n    }\n    return Math.max(-80, Math.min(0, 10 * Math.log10(power)));\n  });\n});\n\n// --- Layout ---\nconst margin = { top: 72, right: 152, bottom: 88, left: 100 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- SVG mount ---\nconst svg = d3.select(\"#container\").append(\"svg\")\n  .attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Color scale: gamma-compressed, noise=dark(seq[1]), signal=bright(seq[0] green) ---\n// Gamma > 1 pushes most noise-floor values to the dark end so harmonics stand out.\nconst dbGamma = 2.2;\nconst colorScale = db => {\n  const norm = Math.max(0, Math.min(1, (db + 80) / 80));\n  const tc = Math.pow(norm, dbGamma);\n  return d3.interpolateRgb(t.seq[1], t.seq[0])(tc);\n};\n\n// --- Heatmap cells ---\nconst cw = iw / n_frames;\nconst ch = ih / n_mels;\n\nconst cells = [];\nfor (let f = 0; f < n_frames; f++) {\n  for (let m = 0; m < n_mels; m++) {\n    cells.push({ f, m, db: spec[f][m] });\n  }\n}\n\ng.selectAll(\"rect.cell\").data(cells).join(\"rect\")\n  .attr(\"class\", \"cell\")\n  .attr(\"x\", d => d.f * cw)\n  .attr(\"y\", d => (n_mels - 1 - d.m) * ch)\n  .attr(\"width\", cw + 0.5)\n  .attr(\"height\", ch + 0.5)\n  .attr(\"fill\", d => colorScale(d.db));\n\n// --- Key frequency helper ---\nconst keyHz = [100, 250, 500, 1000, 2000, 4000, 8000];\nconst hzToY = hz => {\n  const frac = (hzToMel(Math.max(fmin, Math.min(fmax, hz))) - mel_min) / (mel_max - mel_min);\n  return ih * (1 - frac);\n};\n\n// --- Subtle horizontal reference lines at key mel-band edges ---\nkeyHz.forEach(hz => {\n  const y = hzToY(hz);\n  if (y < 0 || y > ih) return;\n  g.append(\"line\")\n    .attr(\"x1\", 0).attr(\"y1\", y).attr(\"x2\", iw).attr(\"y2\", y)\n    .attr(\"stroke\", t.grid).attr(\"stroke-width\", 0.8)\n    .attr(\"stroke-dasharray\", \"3,6\");\n});\n\n// --- X axis: time ---\nconst xScale = d3.scaleLinear().domain([0, duration]).range([0, iw]);\nconst xAxis = g.append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(xScale).ticks(8).tickFormat(d => `${d.toFixed(1)}s`));\nxAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"15px\");\nxAxis.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\nxAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\n// --- Y axis: mel-scaled, custom Hz ticks ---\nconst yg = g.append(\"g\");\nkeyHz.forEach(hz => {\n  const y = hzToY(hz);\n  if (y < 0 || y > ih) return;\n  yg.append(\"line\")\n    .attr(\"x1\", 0).attr(\"y1\", y).attr(\"x2\", -7).attr(\"y2\", y)\n    .attr(\"stroke\", t.inkSoft).attr(\"stroke-width\", 1.5);\n  yg.append(\"text\")\n    .attr(\"x\", -12).attr(\"y\", y).attr(\"dy\", \"0.35em\")\n    .attr(\"text-anchor\", \"end\")\n    .attr(\"fill\", t.inkSoft).style(\"font-size\", \"15px\")\n    .text(hz >= 1000 ? `${hz / 1000}kHz` : `${hz}Hz`);\n});\nyg.append(\"line\")\n  .attr(\"x1\", 0).attr(\"y1\", 0).attr(\"x2\", 0).attr(\"y2\", ih)\n  .attr(\"stroke\", t.inkSoft).attr(\"stroke-width\", 1.5);\n\n// --- Axis labels ---\ng.append(\"text\")\n  .attr(\"x\", iw / 2).attr(\"y\", ih + 68)\n  .attr(\"text-anchor\", \"middle\").attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"18px\")\n  .text(\"Time (s)\");\n\nsvg.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -(margin.top + ih / 2)).attr(\"y\", 22)\n  .attr(\"text-anchor\", \"middle\").attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"18px\")\n  .text(\"Frequency (Hz)\");\n\n// --- Colorbar: bottom = -80 dB (dark), top = 0 dB (bright green) ---\nconst cbX = margin.left + iw + 28;\nconst cbY = margin.top;\nconst cbW = 22;\nconst cbH = ih;\n\nconst defs = svg.append(\"defs\");\nconst grad = defs.append(\"linearGradient\").attr(\"id\", \"cb-grad\")\n  .attr(\"x1\", \"0%\").attr(\"y1\", \"100%\").attr(\"x2\", \"0%\").attr(\"y2\", \"0%\");\n[0, 0.2, 0.4, 0.6, 0.8, 1.0].forEach(f => {\n  grad.append(\"stop\")\n    .attr(\"offset\", `${(f * 100).toFixed(0)}%`)\n    .attr(\"stop-color\", colorScale(-80 + 80 * f));\n});\n\nsvg.append(\"rect\")\n  .attr(\"x\", cbX).attr(\"y\", cbY)\n  .attr(\"width\", cbW).attr(\"height\", cbH)\n  .attr(\"fill\", \"url(#cb-grad)\");\n\nconst cbScale = d3.scaleLinear().domain([-80, 0]).range([cbH, 0]);\nconst cbAxis = svg.append(\"g\")\n  .attr(\"transform\", `translate(${cbX + cbW},${cbY})`)\n  .call(d3.axisRight(cbScale).ticks(6).tickFormat(d => `${d}`));\ncbAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\ncbAxis.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\ncbAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\nsvg.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -(cbY + cbH / 2))\n  .attr(\"y\", cbX + cbW + 56)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\")\n  .text(\"Power (dB)\");\n\n// --- Title ---\nsvg.append(\"text\")\n  .attr(\"x\", width / 2).attr(\"y\", 46)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\").style(\"font-weight\", \"600\")\n  .text(\"spectrogram-mel · javascript · d3 · anyplot.ai\");\n"}