{"spec_id":"spectrogram-mel","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\nspectrogram-mel: Mel-Spectrogram for Audio Analysis\nLibrary: seaborn 0.13.2 | Python 3.13.13\nQuality: 87/100 | Updated: 2026-06-03\n\"\"\"\n\nimport os\nimport sys\n\n\n# Avoid seaborn.py (this file) being imported as the seaborn package itself\n_script_dir = os.path.dirname(os.path.abspath(__file__))\nif _script_dir in sys.path:\n    sys.path.remove(_script_dir)\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\nfrom matplotlib.colors import LinearSegmentedColormap\nfrom scipy.signal import stft\n\n\nsys.path.insert(0, _script_dir)\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\"\nBRAND = \"#009E73\"  # Imprint palette pos 1 — first series\n\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\"]\n\n# Reversed sequential colormap: blue (low energy) → green (high energy = brand color)\nimprint_seq = LinearSegmentedColormap.from_list(\"imprint_seq\", [\"#4467A3\", \"#009E73\"])\n\nsns.set_theme(\n    style=\"ticks\",\n    rc={\n        \"figure.facecolor\": PAGE_BG,\n        \"axes.facecolor\": PAGE_BG,\n        \"axes.edgecolor\": INK_SOFT,\n        \"axes.labelcolor\": INK,\n        \"text.color\": INK,\n        \"xtick.color\": INK_SOFT,\n        \"ytick.color\": INK_SOFT,\n        \"grid.color\": INK,\n        \"grid.alpha\": 0.15,\n        \"legend.facecolor\": ELEVATED_BG,\n        \"legend.edgecolor\": INK_SOFT,\n    },\n)\n\n# Data — synthesized vowel speech sounds with formant resonances (IPA vowel sequence)\nnp.random.seed(42)\nsample_rate = 22050\nduration = 4.0\nn_fft = 2048\nhop_length = 512\nn_mels = 128\nF0 = 130.0  # glottal fundamental frequency (male voice, Hz)\n\nt = np.linspace(0, duration, int(sample_rate * duration), endpoint=False)\n\n# IPA vowels with [F1, F2, F3] formant frequencies — each vowel has a distinct\n# spectral envelope that produces characteristic formant bands in the mel-spectrogram\nvowels = [\n    (\"/a/\", [780, 1200, 2600]),  # \"father\" — open central vowel\n    (\"/e/\", [500, 1750, 2500]),  # \"bed\"    — mid front vowel\n    (\"/i/\", [240, 2600, 3100]),  # \"beet\"   — high front vowel\n    (\"/o/\", [450, 800, 2500]),  # \"coat\"   — mid back vowel\n    (\"/u/\", [310, 870, 2200]),  # \"boot\"   — high back vowel\n    (\"/a/\", [780, 1200, 2600]),  # \"father\" — return (closes the sequence)\n]\nvowel_labels = [v[0] for v in vowels]\nvowel_formants_list = [v[1] for v in vowels]\nn_vowels = len(vowels)\nsegment_len = len(t) // n_vowels\n\naudio = np.zeros_like(t)\nharmonics = F0 * np.arange(1, 50)\nbw = 150.0  # formant bandwidth in Hz\n\nfor i, formants in enumerate(vowel_formants_list):\n    start = i * segment_len\n    end = start + segment_len if i < n_vowels - 1 else len(t)\n    seg_t = t[start:end] - t[start]\n\n    # Formant amplitude envelope: sum of Gaussian peaks at F1, F2, F3\n    amp_env = sum(np.exp(-((harmonics - f) ** 2) / (2 * bw**2)) for f in formants)\n    amp_env /= amp_env.max() + 1e-9\n\n    # Vectorized harmonic synthesis: shape (n_harmonics, segment_len)\n    h_matrix = np.sin(2 * np.pi * harmonics[:, np.newaxis] * seg_t[np.newaxis, :])\n    seg_signal = amp_env @ h_matrix  # weighted sum → shape (segment_len,)\n\n    # Amplitude shaping: smooth decay + onset transient for natural articulation\n    envelope = 0.7 + 0.3 * np.exp(-2.0 * seg_t / (seg_t[-1] + 1e-9))\n    audio[start:end] = seg_signal * envelope + 0.3 * np.exp(-80.0 * seg_t)\n\naudio += 0.01 * np.random.randn(len(audio))\naudio /= np.abs(audio).max() + 1e-9  # normalize to [-1, 1]\n\n# STFT and power spectrum\nfreqs_stft, times_stft, Zxx = stft(audio, fs=sample_rate, nperseg=n_fft, noverlap=n_fft - hop_length)\npower_spectrum = np.abs(Zxx) ** 2\n\n# Mel filterbank — fully vectorized with numpy broadcasting (no nested Python loops)\nmel_min = 2595.0 * np.log10(1.0 + 0.0 / 700.0)\nmel_max = 2595.0 * np.log10(1.0 + (sample_rate / 2.0) / 700.0)\nmel_points = np.linspace(mel_min, mel_max, n_mels + 2)\nhz_points = 700.0 * (10.0 ** (mel_points / 2595.0) - 1.0)\nbin_indices = np.clip(np.floor((n_fft + 1) * hz_points / sample_rate).astype(int), 0, len(freqs_stft) - 1)\n\nbin_range = np.arange(len(freqs_stft))  # shape (n_freqs,)\nf_l = bin_indices[:-2, np.newaxis]  # shape (n_mels, 1)\nf_c = bin_indices[1:-1, np.newaxis]  # shape (n_mels, 1)\nf_r = bin_indices[2:, np.newaxis]  # shape (n_mels, 1)\nrise_denom = np.where(f_c > f_l, f_c - f_l, 1)\nfall_denom = np.where(f_r > f_c, f_r - f_c, 1)\nfilterbank = np.where((bin_range >= f_l) & (bin_range < f_c), (bin_range - f_l) / rise_denom, 0.0) + np.where(\n    (bin_range >= f_c) & (bin_range < f_r), (f_r - bin_range) / fall_denom, 0.0\n)\n\nmel_spec = filterbank @ power_spectrum\nmel_spec_db = 10 * np.log10(np.maximum(mel_spec, 1e-10))\nmel_spec_db -= mel_spec_db.max()  # normalize to 0 dB peak\n\nmel_center_freqs = 700.0 * (10.0 ** (mel_points[1:-1] / 2595.0) - 1.0)\nmel_spec_flipped = mel_spec_db[::-1]  # flip so low frequency sits at bottom\n\ndf_spec = pd.DataFrame(mel_spec_flipped, index=np.arange(n_mels), columns=np.arange(mel_spec_flipped.shape[1]))\nstep = 80\nwave_df = pd.DataFrame({\"Time (s)\": t[::step], \"Amplitude\": audio[::step]})\n\n# Amplitude distribution per vowel for seaborn KDE statistical panel\nkde_records = []\nfor i, (lbl, _) in enumerate(vowels):\n    start = i * segment_len\n    end = start + segment_len if i < n_vowels - 1 else len(t)\n    for amp in audio[start:end:20]:  # downsample for performance\n        kde_records.append({\"Vowel\": lbl, \"Amplitude\": float(amp)})\nkde_df = pd.DataFrame(kde_records)\n\n# Palette mapping for unique vowels (two /a/ segments merge automatically)\nvowel_palette = {lbl: IMPRINT_PALETTE[j] for j, lbl in enumerate([\"/a/\", \"/e/\", \"/i/\", \"/o/\", \"/u/\"])}\n\n# Plot — three-panel layout: waveform / mel-spectrogram / amplitude KDE distributions\nfig, (ax_wave, ax_spec, ax_kde) = plt.subplots(\n    3, 1, figsize=(8, 4.5), dpi=400, height_ratios=[1, 4, 2], gridspec_kw={\"hspace\": 0.38}\n)\nfig.patch.set_facecolor(PAGE_BG)\n\n# Title on waveform panel\ntitle = \"spectrogram-mel · python · seaborn · anyplot.ai\"\ntitle_fs = round(12 * min(1.0, 67 / len(title)))\nax_wave.set_title(title, fontsize=title_fs, fontweight=\"medium\", pad=8, color=INK)\n\n# Top panel: waveform via seaborn lineplot\nsns.lineplot(data=wave_df, x=\"Time (s)\", y=\"Amplitude\", ax=ax_wave, color=BRAND, linewidth=1.2, alpha=0.9)\nax_wave.fill_between(wave_df[\"Time (s)\"], wave_df[\"Amplitude\"], alpha=0.12, color=BRAND)\nax_wave.set_xlim(0, duration)\nax_wave.set_ylabel(\"Amp.\", fontsize=9, labelpad=4, color=INK)\nax_wave.set_xlabel(\"\")\nax_wave.tick_params(axis=\"y\", labelsize=7, length=2, colors=INK_SOFT)\nax_wave.tick_params(axis=\"x\", labelbottom=False, length=0)\n\n# Vowel IPA labels and segment boundaries on waveform\ny_min, y_max = ax_wave.get_ylim()\nfor i in range(n_vowels):\n    mid_time = (i + 0.5) * segment_len / sample_rate\n    ax_wave.text(\n        mid_time, y_max * 0.72, vowel_labels[i], ha=\"center\", va=\"top\", fontsize=7, color=BRAND, fontweight=\"bold\"\n    )\nfor i in range(1, n_vowels):\n    bnd_time = i * segment_len / sample_rate\n    ax_wave.axvline(x=bnd_time, color=INK_SOFT, alpha=0.3, linewidth=0.5, linestyle=\"--\")\n\nsns.despine(ax=ax_wave, bottom=True)\nfor sp in ax_wave.spines.values():\n    sp.set_edgecolor(INK_SOFT)\n    sp.set_linewidth(0.5)\n\n# Middle panel: mel-spectrogram heatmap (seaborn-native)\nsns.heatmap(\n    df_spec,\n    ax=ax_spec,\n    cmap=imprint_seq,\n    vmin=-80,\n    vmax=0,\n    cbar_kws={\"label\": \"Power (dB)\", \"pad\": 0.015, \"aspect\": 30, \"shrink\": 0.92},\n    xticklabels=False,\n    yticklabels=False,\n    rasterized=True,\n)\n\n# Time axis ticks\nx_tick_seconds = np.arange(0, 4.5, 0.5)\nx_tick_positions = [np.argmin(np.abs(times_stft - s)) for s in x_tick_seconds]\nax_spec.set_xticks(x_tick_positions)\nax_spec.set_xticklabels([f\"{s:.1f}\" for s in x_tick_seconds], fontsize=8, color=INK_SOFT)\n\n# Mel frequency axis ticks with Hz labels at perceptually relevant positions\ntick_freqs = [100, 200, 500, 1000, 2000, 4000, 8000]\ny_ticks, y_labels = [], []\nfor freq in tick_freqs:\n    idx = np.argmin(np.abs(mel_center_freqs - freq))\n    y_ticks.append(n_mels - 1 - idx)\n    y_labels.append(f\"{freq // 1000}k\" if freq >= 1000 else str(freq))\n\nax_spec.set_yticks(y_ticks)\nax_spec.set_yticklabels(y_labels, fontsize=8, color=INK_SOFT)\n\n# Colorbar styling\ncbar = ax_spec.collections[0].colorbar\ncbar.ax.tick_params(labelsize=7, colors=INK_SOFT)\ncbar.set_label(\"Power (dB)\", fontsize=8, color=INK)\ncbar.outline.set_edgecolor(INK_SOFT)\ncbar.outline.set_linewidth(0.5)\n\n# Harmonic annotations on the final /a/ segment — show F0 overtone series\nlast_seg_mid = (n_vowels - 0.5) * segment_len / sample_rate\nx_anno = np.argmin(np.abs(times_stft - last_seg_mid))\nfor h, label in [(1, \"F0\"), (2, \"2F0\"), (3, \"3F0\")]:\n    freq_h = F0 * h\n    mel_idx = np.argmin(np.abs(mel_center_freqs - freq_h))\n    y_pos = n_mels - 1 - mel_idx\n    ax_spec.plot(x_anno, y_pos, marker=\"<\", color=BRAND, markersize=4, alpha=0.9)\n    ax_spec.text(\n        x_anno + 2,\n        y_pos,\n        label,\n        fontsize=7,\n        color=BRAND,\n        fontweight=\"bold\",\n        va=\"center\",\n        ha=\"left\",\n        bbox={\"boxstyle\": \"round,pad=0.1\", \"facecolor\": ELEVATED_BG, \"edgecolor\": \"none\", \"alpha\": 0.75},\n    )\n\n# Vowel boundaries on spectrogram\nfor i in range(1, n_vowels):\n    bnd_time = i * segment_len / sample_rate\n    x_pos = np.argmin(np.abs(times_stft - bnd_time))\n    ax_spec.axvline(x=x_pos, color=INK_SOFT, alpha=0.25, linewidth=0.5, linestyle=\"--\")\n\nax_spec.set_xlabel(\"Time (s)\", fontsize=10, labelpad=6, color=INK)\nax_spec.set_ylabel(\"Frequency (Hz)\", fontsize=10, labelpad=6, color=INK)\nax_spec.tick_params(axis=\"both\", length=2, width=0.5)\n\nsns.despine(ax=ax_spec, top=True, right=True)\nfor sp in ax_spec.spines.values():\n    sp.set_edgecolor(INK_SOFT)\n    sp.set_linewidth(0.5)\n\n# Bottom panel: amplitude distribution per vowel — seaborn KDE statistical visualization\nsns.kdeplot(\n    data=kde_df,\n    x=\"Amplitude\",\n    hue=\"Vowel\",\n    ax=ax_kde,\n    fill=True,\n    alpha=0.3,\n    linewidth=1.2,\n    palette=vowel_palette,\n    hue_order=[\"/a/\", \"/e/\", \"/i/\", \"/o/\", \"/u/\"],\n    bw_adjust=0.8,\n)\nax_kde.set_xlabel(\"Amplitude\", fontsize=9, labelpad=4, color=INK)\nax_kde.set_ylabel(\"Density\", fontsize=9, labelpad=4, color=INK)\nax_kde.tick_params(axis=\"both\", labelsize=7, length=2, colors=INK_SOFT)\n\n# Style the KDE legend — placed upper-right to avoid overlap with waveform area\nlegend = ax_kde.get_legend()\nif legend:\n    legend.set_bbox_to_anchor((1.0, 1.0))\n    legend.set_loc(\"upper right\")\n    legend.get_frame().set_facecolor(ELEVATED_BG)\n    legend.get_frame().set_edgecolor(INK_SOFT)\n    legend.get_frame().set_linewidth(0.5)\n    for text in legend.get_texts():\n        text.set_color(INK)\n        text.set_fontsize(7)\n    legend.get_title().set_color(INK_SOFT)\n    legend.get_title().set_fontsize(7)\n\nsns.despine(ax=ax_kde, top=True, right=True)\nfor sp in ax_kde.spines.values():\n    sp.set_edgecolor(INK_SOFT)\n    sp.set_linewidth(0.5)\n\n# Save — bbox_inches must stay default (None) to preserve exact 3200×1800 canvas\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}