Audio Waveform Plot — D3.js

A time-domain visualization of audio amplitude that displays the raw waveform shape as seen in digital audio workstations (DAWs). The plot shows positive and negative amplitude symmetrically around a zero baseline, with time on the x-axis and normalized amplitude (-1 to +1) on the y-axis. This is the fundamental representation for inspecting audio signals, revealing dynamics, clipping, silence, and transient characteristics at a glance.

Audio Waveform Plot rendered with D3.js

Renders

JavaScript source (D3.js)

// anyplot.ai
// waveform-audio: Audio Waveform Plot
// Library: d3 7.9.0 | JavaScript 22.22.3
// Quality: 88/100 | Created: 2026-06-03

const t = window.ANYPLOT_TOKENS;
const { width, height } = window.ANYPLOT_SIZE;
const margin = { top: 110, right: 50, bottom: 90, left: 100 };
const iw = width - margin.left - margin.right;
const ih = height - margin.top - margin.bottom;

const SR = 8000;
const DUR = 1.7;
const N = Math.ceil(SR * DUR);

const SYLS = [
  { start: 0.10, end: 0.55, f0: 130, atk: 0.04, rel: 0.12, label: 'Syl. 1' },
  { start: 0.65, end: 1.10, f0: 155, atk: 0.04, rel: 0.12, label: 'Syl. 2' },
  { start: 1.20, end: 1.60, f0: 145, atk: 0.04, rel: 0.10, label: 'Syl. 3' },
];

// 4-harmonic vocal synthesis with per-syllable trapezoidal amplitude envelope
const rawAmp = new Float32Array(N);
for (let i = 0; i < N; i++) {
  const time = i / SR;
  let s = 0;
  for (const syl of SYLS) {
    const tl = time - syl.start;
    const dur = syl.end - syl.start;
    if (tl < 0 || tl >= dur) continue;
    let env;
    if (tl < syl.atk) env = tl / syl.atk;
    else if (dur - tl < syl.rel) env = (dur - tl) / syl.rel;
    else env = 1;
    const tau = 2 * Math.PI * time;
    s += env * (
      0.45 * Math.sin(syl.f0 * tau) +
      0.28 * Math.sin(2 * syl.f0 * tau) +
      0.16 * Math.sin(3 * syl.f0 * tau) +
      0.11 * Math.sin(4 * syl.f0 * tau)
    );
  }
  rawAmp[i] = s;
}

// Downsample to display-width bins: min/max envelope per pixel column
const nBins = Math.round(iw);
const bSz = N / nBins;
const waveData = Array.from({ length: nBins }, (_, b) => {
  const s0 = Math.floor(b * bSz);
  const e0 = Math.min(Math.ceil((b + 1) * bSz), N);
  let lo = Infinity, hi = -Infinity;
  for (let j = s0; j < e0; j++) {
    if (rawAmp[j] < lo) lo = rawAmp[j];
    if (rawAmp[j] > hi) hi = rawAmp[j];
  }
  return { tPos: s0 / SR, lo, hi };
});

// Smooth amplitude envelope (200 pts) for overlay curves
const envCurve = Array.from({ length: 200 }, (_, i) => {
  const tv = i / 199 * DUR;
  let peak = 0;
  for (const syl of SYLS) {
    const tl = tv - syl.start;
    const dur = syl.end - syl.start;
    if (tl < 0 || tl >= dur) continue;
    let env;
    if (tl < syl.atk) env = tl / syl.atk;
    else if (dur - tl < syl.rel) env = (dur - tl) / syl.rel;
    else env = 1;
    peak = Math.max(peak, env);
  }
  return { t: tv, env: peak };
});

// --- SVG mount ---
const svg = d3.select("#container").append("svg")
  .attr("width", width).attr("height", height);
const g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`);

// --- Scales ---
const xScale = d3.scaleLinear().domain([0, DUR]).range([0, iw]);
const yScale = d3.scaleLinear().domain([-1.05, 1.05]).range([ih, 0]);

// --- Horizontal gridlines ---
g.selectAll(".hg").data([-1, -0.5, 0, 0.5, 1]).join("line")
  .attr("class", "hg")
  .attr("x1", 0).attr("x2", iw)
  .attr("y1", d => yScale(d)).attr("y2", d => yScale(d))
  .attr("stroke", t.grid)
  .attr("stroke-width", 0.5);

// --- Syllable onset markers (dashed verticals at each syllable start) ---
g.selectAll(".sg").data(SYLS).join("line")
  .attr("class", "sg")
  .attr("x1", syl => xScale(syl.start)).attr("x2", syl => xScale(syl.start))
  .attr("y1", 0).attr("y2", ih)
  .attr("stroke", t.grid)
  .attr("stroke-width", 0.75)
  .attr("stroke-dasharray", "5,3");

// --- Waveform filled area (min/max envelope per pixel column) ---
g.append("path")
  .datum(waveData)
  .attr("d", d3.area()
    .x(d => xScale(d.tPos))
    .y0(d => yScale(d.lo))
    .y1(d => yScale(d.hi)))
  .attr("fill", t.palette[0])
  .attr("fill-opacity", 0.72)
  .attr("stroke", t.palette[0])
  .attr("stroke-width", 0.4)
  .attr("stroke-opacity", 0.9);

// --- Amplitude envelope overlay — positive and negative arms ---
for (const sign of [1, -1]) {
  g.append("path")
    .datum(envCurve)
    .attr("d", d3.line()
      .defined(d => d.env > 0.001)
      .x(d => xScale(d.t))
      .y(d => yScale(sign * d.env))
      .curve(d3.curveCatmullRom.alpha(0.5)))
    .attr("fill", "none")
    .attr("stroke", t.palette[0])
    .attr("stroke-width", 2)
    .attr("stroke-opacity", 0.9);
}

// --- Zero reference line ---
g.append("line")
  .attr("x1", 0).attr("x2", iw)
  .attr("y1", yScale(0)).attr("y2", yScale(0))
  .attr("stroke", t.inkSoft)
  .attr("stroke-width", 1);

// --- Axes ---
const xAxis = g.append("g")
  .attr("transform", `translate(0,${ih})`)
  .call(d3.axisBottom(xScale).ticks(9).tickFormat(d => d.toFixed(1)));
const yAxis = g.append("g")
  .call(d3.axisLeft(yScale).tickValues([-1, -0.5, 0, 0.5, 1]));
for (const ax of [xAxis, yAxis]) {
  ax.selectAll("text").attr("fill", t.inkSoft).style("font-size", "16px");
  ax.selectAll("line").attr("stroke", t.inkSoft).attr("stroke-width", 0.5);
  ax.select(".domain").attr("stroke", t.inkSoft);
}

// --- Syllable labels (mid-margin above chart, with small tick connectors) ---
g.selectAll(".syl-lbl").data(SYLS).join("text")
  .attr("class", "syl-lbl")
  .attr("x", syl => xScale((syl.start + syl.end) / 2))
  .attr("y", -26)
  .attr("text-anchor", "middle")
  .attr("fill", t.inkSoft)
  .style("font-size", "14px")
  .text(syl => `${syl.label} · ${syl.f0} Hz`);
g.selectAll(".syl-tick").data(SYLS).join("line")
  .attr("class", "syl-tick")
  .attr("x1", syl => xScale((syl.start + syl.end) / 2))
  .attr("x2", syl => xScale((syl.start + syl.end) / 2))
  .attr("y1", -14).attr("y2", -4)
  .attr("stroke", t.grid)
  .attr("stroke-width", 1);

// --- Axis labels ---
svg.append("text")
  .attr("x", margin.left + iw / 2)
  .attr("y", height - 18)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink)
  .style("font-size", "20px")
  .text("Time (s)");
svg.append("text")
  .attr("transform", `translate(22, ${margin.top + ih / 2}) rotate(-90)`)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink)
  .style("font-size", "20px")
  .text("Amplitude");

// --- Title ---
svg.append("text")
  .attr("x", width / 2).attr("y", 52)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink)
  .style("font-size", "28px")
  .style("font-weight", "600")
  .text("waveform-audio · javascript · d3 · anyplot.ai");

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/waveform-audio/d3/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "waveform-audio",
  "language": "javascript",
  "library": "d3",
  "page": "https://anyplot.ai/waveform-audio/javascript/d3",
  "hub": "https://anyplot.ai/waveform-audio",
  "code_json": "https://api.anyplot.ai/specs/waveform-audio/d3/code",
  "spec_json": "https://api.anyplot.ai/specs/waveform-audio",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/waveform-audio/javascript/d3/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/waveform-audio/javascript/d3/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/waveform-audio/javascript/d3/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/waveform-audio/javascript/d3/plot-dark.html",
  "quality_score": 88.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Audio Waveform Plot on anyplot.ai.

Other implementations