{"spec_id":"spectrogram-mel","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\nspectrogram-mel: Mel-Spectrogram for Audio Analysis\nLibrary: plotnine 0.15.5 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-06-03\n\"\"\"\n\nimport os\nimport sys\n\nimport numpy as np\nimport pandas as pd\n\n\n# Work around naming conflict with plotnine.py script and plotnine package\nscript_dir = os.path.dirname(os.path.abspath(__file__))\nif script_dir in sys.path:\n    sys.path.remove(script_dir)\nif \"\" in sys.path:\n    sys.path.remove(\"\")\nif \".\" in sys.path:\n    sys.path.remove(\".\")\n\nfrom plotnine import (\n    aes,\n    coord_cartesian,\n    element_blank,\n    element_rect,\n    element_text,\n    geom_line,\n    geom_raster,\n    geom_segment,\n    geom_text,\n    ggplot,\n    guide_colorbar,\n    guides,\n    labs,\n    scale_fill_gradientn,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n    theme_minimal,\n)\nfrom scipy.signal import stft\n\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\"\n\n# Imprint categorical palette — 8 hues, theme-independent\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\n# Data — synthesize 3s audio with 330 Hz fundamental, harmonics, and rhythmic noise bursts\nnp.random.seed(42)\nsample_rate = 22050\nduration = 3.0\nn_samples = int(sample_rate * duration)\nt = np.linspace(0, duration, n_samples, endpoint=False)\n\nfundamental = 330  # E4 — distinct from sibling implementations using 220 Hz\nsignal = (\n    0.6 * np.sin(2 * np.pi * fundamental * t) * np.exp(-0.35 * t)\n    + 0.45 * np.sin(2 * np.pi * 660 * t) * (0.5 + 0.5 * np.sin(2 * np.pi * 2.0 * t))\n    + 0.3 * np.sin(2 * np.pi * 990 * t) * np.exp(-0.55 * t)\n    + 0.2 * np.sin(2 * np.pi * 1650 * t) * np.exp(-0.8 * t)\n    + 0.12 * np.sin(2 * np.pi * 3300 * t) * np.exp(-1.2 * t)\n    + 0.08 * np.random.randn(n_samples) * 0.5\n)\n\n# Rhythmic noise bursts simulating percussion attacks every ~500 ms\nburst_times = [0.3, 0.8, 1.3, 1.8, 2.3, 2.8]\nburst_len = int(0.07 * sample_rate)\nfor bt in burst_times:\n    idx = int(bt * sample_rate)\n    if idx + burst_len < n_samples:\n        burst = np.random.randn(burst_len) * np.exp(-np.linspace(0, 5, burst_len))\n        signal[idx : idx + burst_len] += 0.45 * burst\n\n# STFT\nn_fft = 2048\nhop_length = 512\n_, time_bins, Zxx = stft(signal, fs=sample_rate, nperseg=n_fft, noverlap=n_fft - hop_length)\npower_spec = np.abs(Zxx) ** 2\n\n# Mel filterbank (vectorized — no librosa dependency)\nn_mels = 128\nfreq_bins = np.linspace(0, sample_rate / 2, power_spec.shape[0])\n\nmel_low = 2595.0 * np.log10(1.0 + 0 / 700.0)\nmel_high = 2595.0 * np.log10(1.0 + (sample_rate / 2) / 700.0)\nmel_points = np.linspace(mel_low, mel_high, n_mels + 2)\nhz_points = 700.0 * (10.0 ** (mel_points / 2595.0) - 1.0)\n\nlower = hz_points[:-2, np.newaxis]\ncenter = hz_points[1:-1, np.newaxis]\nupper = hz_points[2:, np.newaxis]\nfreqs = freq_bins[np.newaxis, :]\n\nrising = np.where((freqs >= lower) & (freqs <= center) & (center != lower), (freqs - lower) / (center - lower), 0.0)\nfalling = np.where((freqs > center) & (freqs <= upper) & (upper != center), (upper - freqs) / (upper - center), 0.0)\nfilterbank = rising + falling\n\n# Apply filterbank and convert to dB\nmel_spec = filterbank @ power_spec\nmel_spec_db = 10 * np.log10(np.maximum(mel_spec, 1e-10))\nmel_spec_db -= mel_spec_db.max()\n\n# Build long-form DataFrame for geom_raster\nmel_center_freqs = 700.0 * (10.0 ** (mel_points[1:-1] / 2595.0) - 1.0)\ntime_grid, mel_idx_grid = np.meshgrid(time_bins, np.arange(n_mels))\ndf = pd.DataFrame({\"Time (s)\": time_grid.ravel(), \"mel_band\": mel_idx_grid.ravel(), \"Power (dB)\": mel_spec_db.ravel()})\n\n# Y-axis ticks: map Hz reference values to mel band indices\ny_ticks_hz = [128, 256, 512, 1024, 2048, 4096, 8000]\ny_ticks_hz = [f for f in y_ticks_hz if f <= sample_rate / 2]\ny_ticks_band = np.interp(y_ticks_hz, mel_center_freqs, np.arange(n_mels))\n\n# Spectral peak trajectory — smoothed argmax per time frame (second data layer)\npeak_raw = np.argmax(mel_spec_db, axis=0).astype(float)\npeak_smooth = pd.Series(peak_raw).rolling(window=7, center=True, min_periods=1).median().values\ndf_peak = pd.DataFrame({\"time\": time_bins, \"mel_band\": peak_smooth})\n\n# Fundamental frequency reference line and label\nf0_band = float(np.interp(fundamental, mel_center_freqs, np.arange(n_mels)))\ndf_refline = pd.DataFrame({\"x\": [0.0], \"xend\": [duration], \"y\": [f0_band], \"yend\": [f0_band]})\ndf_label = pd.DataFrame({\"x\": [0.12], \"y\": [f0_band + 4.5], \"label\": [\"F₀ = 330 Hz\"]})\n\n# Imprint sequential colormap: PAGE_BG (silence) → brand green → blue (energy)\nspectrogram_colors = [PAGE_BG, \"#009E73\", \"#4467A3\"]\n\n# Plot\ntitle = \"spectrogram-mel · python · plotnine · anyplot.ai\"\nplot = (\n    ggplot(df, aes(x=\"Time (s)\", y=\"mel_band\", fill=\"Power (dB)\"))\n    + geom_raster(interpolate=True)\n    + scale_fill_gradientn(colors=spectrogram_colors, name=\"Power (dB)\")\n    + guides(fill=guide_colorbar(nbin=256))\n    # Spectral peak trajectory — grammar-of-graphics second data layer\n    + geom_line(\n        aes(x=\"time\", y=\"mel_band\"),\n        data=df_peak,\n        inherit_aes=False,\n        color=IMPRINT_PALETTE[3],  # ochre — contrasts with green→blue colormap\n        size=0.9,\n        alpha=0.65,\n    )\n    # F0 reference line — dashed, clearly visible\n    + geom_segment(\n        aes(x=\"x\", xend=\"xend\", y=\"y\", yend=\"yend\"),\n        data=df_refline,\n        inherit_aes=False,\n        color=IMPRINT_PALETTE[5],  # cyan — distinct from green heatmap, CVD-accessible\n        alpha=0.75,\n        size=0.7,\n        linetype=\"dashed\",\n    )\n    # F0 label\n    + geom_text(aes(x=\"x\", y=\"y\", label=\"label\"), data=df_label, inherit_aes=False, color=INK_SOFT, size=3.5, ha=\"left\")\n    + scale_x_continuous(expand=(0, 0))\n    + scale_y_continuous(breaks=y_ticks_band.tolist(), labels=[str(f) for f in y_ticks_hz], expand=(0, 0))\n    + coord_cartesian(ylim=(0, n_mels - 1))\n    + labs(x=\"Time (s)\", y=\"Frequency (Hz)\", title=title)\n    + theme_minimal()\n    + theme(\n        figure_size=(8, 4.5),\n        text=element_text(family=\"sans-serif\"),\n        plot_title=element_text(size=12, ha=\"center\", color=INK, weight=\"bold\"),\n        axis_title_x=element_text(size=10, color=INK),\n        axis_title_y=element_text(size=10, color=INK),\n        axis_text_x=element_text(size=8, color=INK_SOFT),\n        axis_text_y=element_text(size=8, color=INK_SOFT),\n        legend_title=element_text(size=8, color=INK),\n        legend_text=element_text(size=8, color=INK_SOFT),\n        legend_position=\"right\",\n        legend_key_height=30,\n        legend_key_width=8,\n        panel_grid_major=element_blank(),\n        panel_grid_minor=element_blank(),\n        panel_background=element_rect(fill=PAGE_BG, color=\"none\"),\n        plot_background=element_rect(fill=PAGE_BG, color=\"none\"),\n        plot_margin=0.02,\n    )\n)\n\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=8, height=4.5, units=\"in\", verbose=False)\n"}