{"spec_id":"waveform-audio","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nwaveform-audio: Audio Waveform Plot\nLibrary: letsplot 4.10.1 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-06-03\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import *\nfrom lets_plot.export import ggsave\n\n\nLetsPlot.setup_html()\n\n# Theme tokens — Imprint palette, theme-adaptive chrome\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\"\nGRID_COLOR = \"rgba(26,26,23,0.15)\" if THEME == \"light\" else \"rgba(240,239,232,0.15)\"\n\n# Data — synthetic audio waveform: 220 Hz tone with harmonics and ASR amplitude envelope\nnp.random.seed(42)\nsample_rate = 22050\nduration = 1.5\nn_samples = int(sample_rate * duration)\ntime = np.linspace(0, duration, n_samples)\n\nfundamental = 220\nsignal = (\n    0.6 * np.sin(2 * np.pi * fundamental * time)\n    + 0.25 * np.sin(2 * np.pi * fundamental * 2 * time)\n    + 0.1 * np.sin(2 * np.pi * fundamental * 3 * time)\n    + 0.05 * np.sin(2 * np.pi * fundamental * 5 * time)\n)\n\n# Attack-sustain-release amplitude envelope\nenvelope = np.ones(n_samples)\nattack_samples = int(0.05 * sample_rate)\nrelease_samples = int(0.3 * sample_rate)\nenvelope[:attack_samples] = np.linspace(0, 1, attack_samples)\nenvelope[-release_samples:] = np.linspace(1, 0, release_samples)\nenvelope[int(0.4 * sample_rate) : int(0.7 * sample_rate)] *= 0.5\n\nsignal = signal * envelope\nsignal = signal / np.max(np.abs(signal))\n\n# Downsample via min/max envelope binning to avoid aliasing at display resolution\nn_bins = 800\nbin_edges = np.linspace(0, n_samples, n_bins + 1, dtype=int)\ntime_env = np.array([time[(bin_edges[i] + bin_edges[i + 1]) // 2] for i in range(n_bins)])\namp_max = np.array([signal[bin_edges[i] : bin_edges[i + 1]].max() for i in range(n_bins)])\namp_min = np.array([signal[bin_edges[i] : bin_edges[i + 1]].min() for i in range(n_bins)])\namp_range = amp_max - amp_min\n\ndf = pd.DataFrame({\"time\": time_env, \"ymin\": amp_min, \"ymax\": amp_max, \"intensity\": amp_range})\n\nann_data = pd.DataFrame(\n    {\n        \"time\": [0.025, 0.225, 0.55, 0.95, 1.35],\n        \"y\": [1.07, 1.07, 1.07, 1.07, 1.07],\n        \"label\": [\"Attack\", \"Sustain\", \"Dip\", \"Sustain\", \"Release\"],\n    }\n)\n\nsection_df = pd.DataFrame({\"x\": [0.05, 0.4, 0.7, 1.2]})\n\nsubtitle = \"220 Hz fundamental + harmonics · ASR envelope with amplitude dip at 0.4–0.7 s\"\n\n# Theme-adaptive chrome applied after theme_minimal()\nanyplot_chrome = theme(\n    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_background=element_rect(fill=PAGE_BG),\n    panel_grid_major_y=element_line(color=GRID_COLOR, size=0.3),\n    panel_grid_major_x=element_blank(),\n    panel_grid_minor=element_blank(),\n    axis_title=element_text(color=INK, size=12),\n    axis_text=element_text(color=INK_SOFT, size=10),\n    axis_line=element_line(color=INK_SOFT),\n    plot_title=element_text(color=INK, size=16),\n    plot_subtitle=element_text(color=INK_SOFT, size=10),\n    legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n    legend_text=element_text(color=INK_SOFT, size=10),\n    legend_title=element_text(color=INK, size=12),\n    legend_position=\"right\",\n    plot_margin=[40, 20, 20, 20],\n)\n\nplot = (\n    ggplot(df)\n    # DAW-style vertical bars coloured by local amplitude range (Imprint sequential cmap)\n    + geom_segment(\n        mapping=aes(x=\"time\", y=\"ymin\", xend=\"time\", yend=\"ymax\", color=\"intensity\"),\n        size=1.5,\n        alpha=0.85,\n        tooltips=layer_tooltips()\n        .format(\"ymax\", \".2f\")\n        .format(\"ymin\", \".2f\")\n        .format(\"time\", \".3f\")\n        .line(\"Time: @time s\")\n        .line(\"Max: @ymax\")\n        .line(\"Min: @ymin\"),\n    )\n    # Imprint sequential: brand green (#009E73) → blue (#4467A3) for single-polarity data\n    + scale_color_gradient(low=\"#009E73\", high=\"#4467A3\", name=\"Amplitude\\nRange\")\n    # Zero reference line\n    + geom_hline(yintercept=0, color=INK_MUTED, size=0.5, linetype=\"dashed\")\n    # Envelope section boundary markers\n    + geom_vline(\n        data=section_df,\n        mapping=aes(xintercept=\"x\"),\n        color=INK_MUTED,\n        size=0.4,\n        linetype=\"dotted\",\n    )\n    # Section labels — geom_text size is in mm (~2.845 mm = 1 pt)\n    + geom_text(\n        data=ann_data,\n        mapping=aes(x=\"time\", y=\"y\", label=\"label\"),\n        size=4,\n        color=INK,\n        fontface=\"italic\",\n    )\n    + scale_x_continuous(name=\"Time (seconds)\", limits=[0, duration])\n    + scale_y_continuous(\n        name=\"Amplitude\", limits=[-1.15, 1.18], breaks=[-1.0, -0.5, 0.0, 0.5, 1.0]\n    )\n    + labs(title=\"waveform-audio · python · letsplot · anyplot.ai\", subtitle=subtitle)\n    # Canvas: 800×450 × scale=4 → 3200×1800 px (landscape 16:9)\n    + ggsize(800, 450)\n    + theme_minimal()\n    + anyplot_chrome\n)\n\n# Save PNG (3200×1800 px) and interactive HTML — path=\".\" keeps files in the current dir\nggsave(plot, filename=f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, filename=f\"plot-{THEME}.html\", path=\".\")\n"}