{"spec_id":"waveform-audio","library":"d3","language":"javascript","code":"// anyplot.ai\n// waveform-audio: Audio Waveform Plot\n// Library: d3 7.9.0 | JavaScript 22.22.3\n// Quality: 88/100 | Created: 2026-06-03\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 110, right: 50, bottom: 90, left: 100 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\nconst SR = 8000;\nconst DUR = 1.7;\nconst N = Math.ceil(SR * DUR);\n\nconst SYLS = [\n  { start: 0.10, end: 0.55, f0: 130, atk: 0.04, rel: 0.12, label: 'Syl. 1' },\n  { start: 0.65, end: 1.10, f0: 155, atk: 0.04, rel: 0.12, label: 'Syl. 2' },\n  { start: 1.20, end: 1.60, f0: 145, atk: 0.04, rel: 0.10, label: 'Syl. 3' },\n];\n\n// 4-harmonic vocal synthesis with per-syllable trapezoidal amplitude envelope\nconst rawAmp = new Float32Array(N);\nfor (let i = 0; i < N; i++) {\n  const time = i / SR;\n  let s = 0;\n  for (const syl of SYLS) {\n    const tl = time - syl.start;\n    const dur = syl.end - syl.start;\n    if (tl < 0 || tl >= dur) continue;\n    let env;\n    if (tl < syl.atk) env = tl / syl.atk;\n    else if (dur - tl < syl.rel) env = (dur - tl) / syl.rel;\n    else env = 1;\n    const tau = 2 * Math.PI * time;\n    s += env * (\n      0.45 * Math.sin(syl.f0 * tau) +\n      0.28 * Math.sin(2 * syl.f0 * tau) +\n      0.16 * Math.sin(3 * syl.f0 * tau) +\n      0.11 * Math.sin(4 * syl.f0 * tau)\n    );\n  }\n  rawAmp[i] = s;\n}\n\n// Downsample to display-width bins: min/max envelope per pixel column\nconst nBins = Math.round(iw);\nconst bSz = N / nBins;\nconst waveData = Array.from({ length: nBins }, (_, b) => {\n  const s0 = Math.floor(b * bSz);\n  const e0 = Math.min(Math.ceil((b + 1) * bSz), N);\n  let lo = Infinity, hi = -Infinity;\n  for (let j = s0; j < e0; j++) {\n    if (rawAmp[j] < lo) lo = rawAmp[j];\n    if (rawAmp[j] > hi) hi = rawAmp[j];\n  }\n  return { tPos: s0 / SR, lo, hi };\n});\n\n// Smooth amplitude envelope (200 pts) for overlay curves\nconst envCurve = Array.from({ length: 200 }, (_, i) => {\n  const tv = i / 199 * DUR;\n  let peak = 0;\n  for (const syl of SYLS) {\n    const tl = tv - syl.start;\n    const dur = syl.end - syl.start;\n    if (tl < 0 || tl >= dur) continue;\n    let env;\n    if (tl < syl.atk) env = tl / syl.atk;\n    else if (dur - tl < syl.rel) env = (dur - tl) / syl.rel;\n    else env = 1;\n    peak = Math.max(peak, env);\n  }\n  return { t: tv, env: peak };\n});\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// --- Scales ---\nconst xScale = d3.scaleLinear().domain([0, DUR]).range([0, iw]);\nconst yScale = d3.scaleLinear().domain([-1.05, 1.05]).range([ih, 0]);\n\n// --- Horizontal gridlines ---\ng.selectAll(\".hg\").data([-1, -0.5, 0, 0.5, 1]).join(\"line\")\n  .attr(\"class\", \"hg\")\n  .attr(\"x1\", 0).attr(\"x2\", iw)\n  .attr(\"y1\", d => yScale(d)).attr(\"y2\", d => yScale(d))\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 0.5);\n\n// --- Syllable onset markers (dashed verticals at each syllable start) ---\ng.selectAll(\".sg\").data(SYLS).join(\"line\")\n  .attr(\"class\", \"sg\")\n  .attr(\"x1\", syl => xScale(syl.start)).attr(\"x2\", syl => xScale(syl.start))\n  .attr(\"y1\", 0).attr(\"y2\", ih)\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 0.75)\n  .attr(\"stroke-dasharray\", \"5,3\");\n\n// --- Waveform filled area (min/max envelope per pixel column) ---\ng.append(\"path\")\n  .datum(waveData)\n  .attr(\"d\", d3.area()\n    .x(d => xScale(d.tPos))\n    .y0(d => yScale(d.lo))\n    .y1(d => yScale(d.hi)))\n  .attr(\"fill\", t.palette[0])\n  .attr(\"fill-opacity\", 0.72)\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", 0.4)\n  .attr(\"stroke-opacity\", 0.9);\n\n// --- Amplitude envelope overlay — positive and negative arms ---\nfor (const sign of [1, -1]) {\n  g.append(\"path\")\n    .datum(envCurve)\n    .attr(\"d\", d3.line()\n      .defined(d => d.env > 0.001)\n      .x(d => xScale(d.t))\n      .y(d => yScale(sign * d.env))\n      .curve(d3.curveCatmullRom.alpha(0.5)))\n    .attr(\"fill\", \"none\")\n    .attr(\"stroke\", t.palette[0])\n    .attr(\"stroke-width\", 2)\n    .attr(\"stroke-opacity\", 0.9);\n}\n\n// --- Zero reference line ---\ng.append(\"line\")\n  .attr(\"x1\", 0).attr(\"x2\", iw)\n  .attr(\"y1\", yScale(0)).attr(\"y2\", yScale(0))\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1);\n\n// --- Axes ---\nconst xAxis = g.append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(xScale).ticks(9).tickFormat(d => d.toFixed(1)));\nconst yAxis = g.append(\"g\")\n  .call(d3.axisLeft(yScale).tickValues([-1, -0.5, 0, 0.5, 1]));\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"16px\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.inkSoft).attr(\"stroke-width\", 0.5);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\n// --- Syllable labels (mid-margin above chart, with small tick connectors) ---\ng.selectAll(\".syl-lbl\").data(SYLS).join(\"text\")\n  .attr(\"class\", \"syl-lbl\")\n  .attr(\"x\", syl => xScale((syl.start + syl.end) / 2))\n  .attr(\"y\", -26)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(syl => `${syl.label} · ${syl.f0} Hz`);\ng.selectAll(\".syl-tick\").data(SYLS).join(\"line\")\n  .attr(\"class\", \"syl-tick\")\n  .attr(\"x1\", syl => xScale((syl.start + syl.end) / 2))\n  .attr(\"x2\", syl => xScale((syl.start + syl.end) / 2))\n  .attr(\"y1\", -14).attr(\"y2\", -4)\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\n// --- Axis labels ---\nsvg.append(\"text\")\n  .attr(\"x\", margin.left + iw / 2)\n  .attr(\"y\", height - 18)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"20px\")\n  .text(\"Time (s)\");\nsvg.append(\"text\")\n  .attr(\"transform\", `translate(22, ${margin.top + ih / 2}) rotate(-90)`)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"20px\")\n  .text(\"Amplitude\");\n\n// --- Title ---\nsvg.append(\"text\")\n  .attr(\"x\", width / 2).attr(\"y\", 52)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"28px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"waveform-audio · javascript · d3 · anyplot.ai\");\n"}