{"spec_id":"waveform-audio","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nwaveform-audio: Audio Waveform Plot\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 92/100 | Updated: 2026-06-03\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\n\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint palette — semantic anchor for clipping regions\nCLIP_COLOR = \"#AE3030\"  # matte red: error/clipping role\n\n# Data - synthetic audio: 440 Hz tone with harmonics and amplitude envelope\nsample_rate = 22050\nduration = 1.5\nnum_samples = int(sample_rate * duration)\ntime = np.linspace(0, duration, num_samples)\n\nfundamental = 440\nsignal = (\n    0.6 * np.sin(2 * np.pi * fundamental * time)\n    + 0.25 * np.sin(2 * np.pi * 2 * fundamental * time)\n    + 0.15 * np.sin(2 * np.pi * 3 * fundamental * time)\n)\n\n# Attack-sustain-release envelope with amplitude dip and brief clipping boost\nenvelope = np.ones_like(time)\nattack = int(0.05 * sample_rate)\nrelease = int(0.3 * sample_rate)\nenvelope[:attack] = np.linspace(0, 1, attack)\nenvelope[-release:] = np.linspace(1, 0, release)\n\n# Smooth amplitude dip (0.4-0.7 s) via cosine taper — avoids abrupt rectangular step\ndip_start = int(0.4 * sample_rate)\ndip_end = int(0.7 * sample_rate)\ntransition_len = int(0.035 * sample_rate)\nt_fade = np.linspace(0, 1, transition_len)\ndip_mult = np.ones_like(time)\ndip_mult[dip_start : dip_start + transition_len] = 1.0 - 0.25 * (1 - np.cos(np.pi * t_fade))\ndip_mult[dip_start + transition_len : dip_end - transition_len] = 0.5\ndip_mult[dip_end - transition_len : dip_end] = 0.5 + 0.25 * (1 - np.cos(np.pi * t_fade))\nenvelope *= dip_mult\n\nenvelope[int(0.15 * sample_rate) : int(0.25 * sample_rate)] *= 1.35\n\nsignal = signal * envelope\nsignal = np.clip(signal, -1.0, 1.0)\n\n# Min/max envelope binning — 600 bins avoids sub-pixel vertical striping artifacts\nnum_bins = 600\nbin_size = num_samples // num_bins\nusable = num_bins * bin_size\nsignal_trimmed = signal[:usable].reshape(num_bins, bin_size)\ntime_trimmed = time[:usable].reshape(num_bins, bin_size)\n\ndf = pd.DataFrame(\n    {\n        \"time\": time_trimmed[:, bin_size // 2],\n        \"amp_min\": signal_trimmed.min(axis=1),\n        \"amp_max\": signal_trimmed.max(axis=1),\n    }\n)\ndf[\"clipped\"] = (df[\"amp_max\"] >= 0.99) | (df[\"amp_min\"] <= -0.99)\n\n# Shared encodings\nx_enc = alt.X(\"time:Q\", title=\"Time (seconds)\", axis=alt.Axis(format=\".2f\", tickCount=8))\ny_enc = alt.Y(\"amp_min:Q\", title=\"Amplitude\", scale=alt.Scale(domain=[-1.0, 1.0]))\n\n# Nearest-point selection for interactive crosshair\nnearest = alt.selection_point(nearest=True, on=\"pointerover\", fields=[\"time\"], empty=False)\n\n# Main waveform: Imprint brand green (#009E73) vertical gradient\nwaveform_gradient = (\n    alt.Chart(df)\n    .mark_area(\n        interpolate=\"linear\",\n        color=alt.Gradient(\n            gradient=\"linear\",\n            stops=[\n                alt.GradientStop(color=\"rgba(0, 158, 115, 0.28)\", offset=0),\n                alt.GradientStop(color=\"rgba(0, 158, 115, 0.60)\", offset=0.45),\n                alt.GradientStop(color=\"rgba(0, 158, 115, 0.60)\", offset=0.55),\n                alt.GradientStop(color=\"rgba(0, 158, 115, 0.28)\", offset=1),\n            ],\n            x1=0,\n            x2=0,\n            y1=0,\n            y2=1,\n        ),\n        line=False,\n    )\n    .encode(x=x_enc, y=y_enc, y2=\"amp_max:Q\")\n)\n\n# Clipped regions overlay — Imprint matte red (#AE3030) semantic anchor\nclipped_overlay = (\n    alt.Chart(df)\n    .mark_area(interpolate=\"linear\", color=\"rgba(174, 48, 48, 0.50)\", line=False)\n    .encode(x=\"time:Q\", y=y_enc, y2=\"amp_max:Q\")\n    .transform_filter(alt.datum.clipped == True)\n)\n\n# Zero baseline reference line (theme-adaptive)\nzero_line = (\n    alt.Chart(pd.DataFrame({\"y\": [0]}))\n    .mark_rule(strokeWidth=1.5, opacity=0.35, strokeDash=[6, 4])\n    .encode(y=\"y:Q\", color=alt.value(INK_SOFT))\n)\n\n# Clipping threshold lines at ±1.0 (semantic matte red)\nclip_lines = (\n    alt.Chart(pd.DataFrame({\"y\": [-1.0, 1.0]}))\n    .mark_rule(strokeWidth=0.8, opacity=0.3, strokeDash=[3, 5])\n    .encode(y=\"y:Q\", color=alt.value(CLIP_COLOR))\n)\n\n# Interactive crosshair following pointer\ncrosshair_rule = (\n    alt.Chart(df)\n    .mark_rule(strokeWidth=1, opacity=0.5)\n    .encode(x=\"time:Q\", color=alt.value(INK_SOFT))\n    .transform_filter(nearest)\n)\n\n# Invisible selection trigger with tooltips\nselection_layer = (\n    alt.Chart(df)\n    .mark_point(opacity=0)\n    .encode(\n        x=\"time:Q\",\n        y=\"amp_max:Q\",\n        tooltip=[\n            alt.Tooltip(\"time:Q\", title=\"Time (s)\", format=\".3f\"),\n            alt.Tooltip(\"amp_max:Q\", title=\"Peak\", format=\".3f\"),\n            alt.Tooltip(\"amp_min:Q\", title=\"Trough\", format=\".3f\"),\n        ],\n    )\n    .add_params(nearest)\n)\n\n# Compose layers and apply theme-adaptive configuration\nchart = (\n    alt.layer(waveform_gradient, clipped_overlay, zero_line, clip_lines, crosshair_rule, selection_layer)\n    .properties(\n        width=620,\n        height=320,\n        background=PAGE_BG,\n        title=alt.Title(\n            \"waveform-audio · python · altair · anyplot.ai\",\n            fontSize=16,\n            subtitle=\"440 Hz tone with harmonics · attack–sustain–release envelope · clipped region highlighted\",\n            subtitleFontSize=10,\n        ),\n    )\n    .configure_view(fill=PAGE_BG, strokeWidth=0, continuousWidth=620, continuousHeight=320)\n    .configure_axis(\n        labelFontSize=10,\n        titleFontSize=12,\n        gridColor=INK,\n        gridOpacity=0.15,\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n    )\n    .configure_title(color=INK, subtitleColor=INK_MUTED)\n)\n\n# Save PNG and pad to exact 3200×1800 (landscape target)\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\nTW, TH = 3200, 1800\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. \"\n        \"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\n# Save interactive HTML\nchart.interactive().save(f\"plot-{THEME}.html\")\n"}