Audio Waveform Plot — plotnine

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 plotnine

Python source (plotnine)

""" anyplot.ai
waveform-audio: Audio Waveform Plot
Library: plotnine 0.15.5 | Python 3.13.13
Quality: 86/100 | Updated: 2026-06-03
"""

import os

import numpy as np
import pandas as pd
from plotnine import (
    aes,
    annotate,
    element_blank,
    element_line,
    element_rect,
    element_text,
    geom_hline,
    geom_ribbon,
    geom_vline,
    ggplot,
    labs,
    scale_alpha_manual,
    scale_fill_manual,
    scale_x_continuous,
    scale_y_continuous,
    theme,
    theme_minimal,
)


# Theme tokens
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"

# Imprint palette — positions 1, 2, 3 for Attack, Sustain, Release phases
IMPRINT_PALETTE = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD", "#954477", "#99B314"]

# Data - synthetic audio waveform: tone with harmonics and amplitude envelope
np.random.seed(42)
sample_rate = 22050
duration = 1.5
num_samples = int(sample_rate * duration)
time = np.linspace(0, duration, num_samples)

fundamental = 220
signal = (
    0.6 * np.sin(2 * np.pi * fundamental * time)
    + 0.25 * np.sin(2 * np.pi * fundamental * 2 * time)
    + 0.1 * np.sin(2 * np.pi * fundamental * 3 * time)
    + 0.05 * np.sin(2 * np.pi * fundamental * 5 * time)
)

envelope = np.ones_like(time)
attack_end = int(0.05 * sample_rate)
sustain_end = int(1.1 * sample_rate)
envelope[:attack_end] = np.linspace(0, 1, attack_end)
envelope[sustain_end:] = np.linspace(1, 0, num_samples - sustain_end)

vibrato = 1.0 + 0.03 * np.sin(2 * np.pi * 5 * time)
noise = np.random.normal(0, 0.02, num_samples)
amplitude = np.clip((signal * envelope * vibrato) + noise, -1.0, 1.0)

# Downsample using min/max envelope to avoid aliasing
chunk_size = 64
num_chunks = num_samples // chunk_size
time_chunks = np.array([time[i * chunk_size] for i in range(num_chunks)])
amp_min = np.array([amplitude[i * chunk_size : (i + 1) * chunk_size].min() for i in range(num_chunks)])
amp_max = np.array([amplitude[i * chunk_size : (i + 1) * chunk_size].max() for i in range(num_chunks)])

attack_time = 0.05
sustain_time = 1.1
phase = []
for t in time_chunks:
    if t < attack_time:
        phase.append("Attack")
    elif t < sustain_time:
        phase.append("Sustain")
    else:
        phase.append("Release")

df = pd.DataFrame(
    {
        "time": time_chunks,
        "amp_min": amp_min,
        "amp_max": amp_max,
        "phase": pd.Categorical(phase, categories=["Attack", "Sustain", "Release"], ordered=True),
    }
)

# Imprint palette: Attack=green (pos 1), Sustain=lavender (pos 2), Release=blue (pos 3)
phase_colors = {"Attack": IMPRINT_PALETTE[0], "Sustain": IMPRINT_PALETTE[1], "Release": IMPRINT_PALETTE[2]}
phase_alphas = {"Attack": 0.85, "Sustain": 0.65, "Release": 0.55}

title = "waveform-audio · python · plotnine · anyplot.ai"

# Plot
plot = (
    ggplot(df, aes(x="time"))
    + geom_ribbon(aes(ymin="amp_min", ymax="amp_max", fill="phase", alpha="phase"), show_legend=False)
    + scale_fill_manual(values=phase_colors)
    + scale_alpha_manual(values=phase_alphas)
    + geom_hline(yintercept=0, color=INK_MUTED, size=0.4, linetype="solid")
    + geom_vline(xintercept=attack_time, color=INK_SOFT, size=0.3, linetype="dashed", alpha=0.5)
    + geom_vline(xintercept=sustain_time, color=INK_SOFT, size=0.3, linetype="dashed", alpha=0.5)
    + annotate("text", x=0.025, y=0.90, label="Attack", size=3.5, color=IMPRINT_PALETTE[0], fontstyle="italic")
    + annotate("text", x=0.575, y=0.90, label="Sustain", size=3.5, color=IMPRINT_PALETTE[1], fontstyle="italic")
    + annotate("text", x=1.30, y=0.90, label="Release", size=3.5, color=IMPRINT_PALETTE[2], fontstyle="italic")
    + labs(x="Time (seconds)", y="Amplitude", title=title)
    + scale_x_continuous(
        breaks=np.arange(0, duration + 0.1, 0.25), labels=lambda lst: [f"{v:.2f}" for v in lst], expand=(0.01, 0.01)
    )
    + scale_y_continuous(limits=(-1.0, 1.0), breaks=np.arange(-1.0, 1.1, 0.25))
    + theme_minimal()
    + theme(
        figure_size=(8, 4.5),
        text=element_text(size=7, color=INK),
        axis_title=element_text(size=10, color=INK),
        axis_text=element_text(size=8, color=INK_SOFT),
        plot_title=element_text(size=12, color=INK),
        legend_text=element_text(size=8, color=INK_SOFT),
        panel_background=element_rect(fill=PAGE_BG, color="none"),
        plot_background=element_rect(fill=PAGE_BG, color="none"),
        panel_grid_major_y=element_line(color=INK, size=0.3, alpha=0.15),
        panel_grid_major_x=element_blank(),
        panel_grid_minor=element_blank(),
        axis_line_x=element_line(color=INK_SOFT, size=0.5),
        axis_line_y=element_line(color=INK_SOFT, size=0.5),
        axis_ticks_major_x=element_line(color=INK_SOFT, size=0.4),
        axis_ticks_major_y=element_blank(),
    )
)

plot.save(f"plot-{THEME}.png", dpi=400, width=8, height=4.5, units="in", verbose=False)

Part of Audio Waveform Plot on anyplot.ai.

Other implementations