Audio Waveform Plot — Plotly

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 Plotly

Python source (Plotly)

""" anyplot.ai
waveform-audio: Audio Waveform Plot
Library: plotly 6.7.0 | Python 3.13.13
Quality: 92/100 | Updated: 2026-06-03
"""

import os
import sys


# Prevent self-import: remove this script's own directory from sys.path so that
# "import plotly" resolves to the installed package, not this file.
_here = os.path.dirname(os.path.abspath(__file__))
sys.path = [p for p in sys.path if os.path.abspath(p) != _here]

import numpy as np
import plotly.graph_objects as go


# Theme-adaptive chrome (Imprint palette)
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
GRID = "rgba(26,26,23,0.15)" if THEME == "light" else "rgba(240,239,232,0.15)"

# Imprint palette — first series is always #009E73
BRAND = "#009E73"
ANYPLOT_AMBER = "#DDCC77"  # warning / decay semantic role

# Data
np.random.seed(42)
sample_rate = 22050
duration = 1.5
num_samples = int(sample_rate * duration)
time = np.linspace(0, duration, num_samples)

# Synthesize audio: fundamental tone + harmonics with amplitude envelope
fundamental_freq = 220
signal = (
    0.6 * np.sin(2 * np.pi * fundamental_freq * time)
    + 0.25 * np.sin(2 * np.pi * fundamental_freq * 2 * time)
    + 0.1 * np.sin(2 * np.pi * fundamental_freq * 3 * time)
    + 0.05 * np.sin(2 * np.pi * fundamental_freq * 5 * time)
)

# Amplitude envelope: attack-sustain-decay shape
envelope = np.ones_like(time)
attack_end = int(0.05 * num_samples)
decay_start = int(0.7 * num_samples)
envelope[:attack_end] = np.linspace(0, 1, attack_end)
envelope[decay_start:] = np.linspace(1, 0.15, num_samples - decay_start)

# Slight tremolo and noise for realism
tremolo = 1.0 + 0.08 * np.sin(2 * np.pi * 5.5 * time)
amplitude = signal * envelope * tremolo
amplitude += np.random.normal(0, 0.02, num_samples)
amplitude = np.clip(amplitude, -1.0, 1.0)

# Downsample with larger blocks for cleaner sustain region visibility
block_size = 40
num_blocks = num_samples // block_size
time_blocks = np.array([(time[i * block_size] + time[(i + 1) * block_size - 1]) / 2 for i in range(num_blocks)])
amp_max = np.array([amplitude[i * block_size : (i + 1) * block_size].max() for i in range(num_blocks)])
amp_min = np.array([amplitude[i * block_size : (i + 1) * block_size].min() for i in range(num_blocks)])

# Data-derived inner envelope: 70th percentile of absolute amplitude per block
# Captures the RMS-like energy content rather than an arbitrary fixed scaling factor
amp_p70 = np.array(
    [np.percentile(np.abs(amplitude[i * block_size : (i + 1) * block_size]), 70) for i in range(num_blocks)]
)

# Phase boundaries
attack_time = duration * 0.05
decay_time = duration * 0.7

# Plot
fig = go.Figure()

# Outer waveform envelope (Imprint brand green fill)
fig.add_trace(
    go.Scatter(
        x=np.concatenate([time_blocks, time_blocks[::-1]]),
        y=np.concatenate([amp_max, amp_min[::-1]]),
        fill="toself",
        fillcolor="rgba(0,158,115,0.18)",
        line={"color": "rgba(0,158,115,0.5)", "width": 0.5, "shape": "spline"},
        name="Waveform",
        hovertemplate="Time: %{x:.3f}s<br>Amplitude: %{y:.3f}<extra></extra>",
    )
)

# Inner envelope: data-derived 70th-percentile energy bounds for visual depth
fig.add_trace(
    go.Scatter(
        x=np.concatenate([time_blocks, time_blocks[::-1]]),
        y=np.concatenate([amp_p70, -amp_p70[::-1]]),
        fill="toself",
        fillcolor="rgba(0,158,115,0.32)",
        line={"width": 0, "shape": "spline"},
        showlegend=False,
        hoverinfo="skip",
    )
)

# Peak envelope lines for crisp edge definition
fig.add_trace(
    go.Scatter(
        x=time_blocks,
        y=amp_max,
        mode="lines",
        line={"color": BRAND, "width": 1.2, "shape": "spline"},
        showlegend=False,
        hoverinfo="skip",
    )
)
fig.add_trace(
    go.Scatter(
        x=time_blocks,
        y=amp_min,
        mode="lines",
        line={"color": BRAND, "width": 1.2, "shape": "spline"},
        showlegend=False,
        hoverinfo="skip",
    )
)

# Zero reference line
fig.add_hline(y=0, line_dash="solid", line_color=GRID, line_width=1)

# Phase region shading (subtle — semantic color cues, not data)
fig.add_vrect(x0=0, x1=attack_time, fillcolor="rgba(0,158,115,0.06)", line_width=0)
fig.add_vrect(x0=decay_time, x1=duration, fillcolor="rgba(221,204,119,0.08)", line_width=0)

# Phase boundary markers
for t_boundary in [attack_time, decay_time]:
    fig.add_vline(x=t_boundary, line_dash="dot", line_color=INK_MUTED, line_width=0.8)

# Phase annotations — semantic color coding: green=start, muted=stable, amber=fade
fig.add_annotation(
    x=attack_time / 2,
    y=1.04,
    text="ATTACK",
    showarrow=False,
    font={"size": 11, "color": BRAND, "family": "Arial Black, sans-serif"},
    yref="y",
)
fig.add_annotation(
    x=(attack_time + decay_time) / 2,
    y=1.04,
    text="SUSTAIN",
    showarrow=False,
    font={"size": 11, "color": INK_MUTED, "family": "Arial Black, sans-serif"},
    yref="y",
)
fig.add_annotation(
    x=(decay_time + duration) / 2,
    y=1.04,
    text="DECAY",
    showarrow=False,
    font={"size": 11, "color": ANYPLOT_AMBER, "family": "Arial Black, sans-serif"},
    yref="y",
)

# Layout with canonical canvas, theme-adaptive chrome, and correct font sizes
fig.update_layout(
    autosize=False,
    paper_bgcolor=PAGE_BG,
    plot_bgcolor=PAGE_BG,
    font={"color": INK},
    title={
        "text": "waveform-audio · python · plotly · anyplot.ai",
        "font": {"size": 16, "color": INK, "family": "Arial Black, sans-serif"},
        "x": 0.02,
        "xanchor": "left",
        "y": 0.97,
        "yanchor": "top",
    },
    xaxis={
        "title": {"text": "Time (seconds)", "font": {"size": 12, "color": INK}},
        "tickfont": {"size": 10, "color": INK_SOFT},
        "showgrid": False,
        "zeroline": False,
        "linecolor": INK_SOFT,
        "linewidth": 1,
        "ticks": "outside",
        "tickcolor": INK_SOFT,
        "ticklen": 5,
    },
    yaxis={
        "title": {"text": "Amplitude", "font": {"size": 12, "color": INK}},
        "tickfont": {"size": 10, "color": INK_SOFT},
        "range": [-1.12, 1.12],
        "showgrid": True,
        "gridcolor": GRID,
        "gridwidth": 1,
        "zeroline": False,
        "linecolor": INK_SOFT,
        "linewidth": 1,
        "ticks": "outside",
        "tickcolor": INK_SOFT,
        "ticklen": 5,
        "dtick": 0.5,
    },
    showlegend=False,
    margin={"l": 80, "r": 40, "t": 80, "b": 60},
)

# Save — canonical plotly landscape: width=800, height=450, scale=4 → 3200×1800 px
fig.write_image(f"plot-{THEME}.png", width=800, height=450, scale=4)
fig.write_html(f"plot-{THEME}.html", include_plotlyjs="cdn")

Part of Audio Waveform Plot on anyplot.ai.

Other implementations