{"spec_id":"spectrogram-mel","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nspectrogram-mel: Mel-Spectrogram for Audio Analysis\nLibrary: letsplot 4.10.1 | Python 3.13.13\nQuality: 87/100 | Updated: 2026-06-03\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_text,\n    geom_tile,\n    ggplot,\n    ggsave,\n    ggsize,\n    labs,\n    layer_tooltips,\n    scale_fill_gradient,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n)\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\"\n\n# Data — synthesize C major arpeggio (C4, E4, G4, C5) with harmonics and vibrato\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\nmelody_freqs = [261.6, 329.6, 392.0, 523.3]\nnote_names = [\"C4\", \"E4\", \"G4\", \"C5\"]\naudio_signal = np.zeros(n_samples)\nfor i, freq in enumerate(melody_freqs):\n    start = int(i * n_samples / len(melody_freqs))\n    end = int((i + 1) * n_samples / len(melody_freqs))\n    segment_t = t[start:end]\n    envelope = np.sin(np.linspace(0, np.pi, end - start)) ** 1.5\n    vibrato = 1 + 0.005 * np.sin(2 * np.pi * 5.5 * segment_t)\n    audio_signal[start:end] += 0.5 * envelope * np.sin(2 * np.pi * freq * vibrato * segment_t)\n    for harmonic, amplitude in [(2, 0.25), (3, 0.15), (4, 0.08), (5, 0.05)]:\n        audio_signal[start:end] += (\n            (amplitude / harmonic) * envelope * np.sin(2 * np.pi * freq * harmonic * vibrato * segment_t)\n        )\n\naudio_signal += 0.015 * np.random.randn(n_samples)\n\n# STFT via numpy\nn_fft = 2048\nhop_length = 512\nn_mels = 128\n\nwindow = np.hanning(n_fft)\nn_frames = 1 + (n_samples - n_fft) // hop_length\nstft_matrix = np.zeros((n_fft // 2 + 1, n_frames))\nfor frame_idx in range(n_frames):\n    start_sample = frame_idx * hop_length\n    frame = audio_signal[start_sample : start_sample + n_fft] * window\n    spectrum = np.fft.rfft(frame)\n    stft_matrix[:, frame_idx] = np.abs(spectrum) ** 2\n\ntimes = np.arange(n_frames) * hop_length / sample_rate\nfrequencies = np.fft.rfftfreq(n_fft, 1.0 / sample_rate)\n\n# Mel filter bank\nmel_low = 2595 * np.log10(1 + 0 / 700)\nmel_high = 2595 * np.log10(1 + (sample_rate / 2) / 700)\nmel_points = np.linspace(mel_low, mel_high, n_mels + 2)\nhz_points = 700 * (10 ** (mel_points / 2595) - 1)\nfft_bins = np.floor((n_fft + 1) * hz_points / sample_rate).astype(int)\n\nmel_filterbank = np.zeros((n_mels, len(frequencies)))\nfor m in range(1, n_mels + 1):\n    f_left = fft_bins[m - 1]\n    f_center = fft_bins[m]\n    f_right = fft_bins[m + 1]\n    for k in range(f_left, min(f_center, len(frequencies))):\n        if f_center != f_left:\n            mel_filterbank[m - 1, k] = (k - f_left) / (f_center - f_left)\n    for k in range(f_center, min(f_right, len(frequencies))):\n        if f_right != f_center:\n            mel_filterbank[m - 1, k] = (f_right - k) / (f_right - f_center)\n\nmel_spec = mel_filterbank @ stft_matrix\nmel_spec_db = 10 * np.log10(mel_spec + 1e-10)\n\n# Clip dB range to emphasize musical content and reduce noise floor\ndb_min = -10.0\ndb_max = float(np.max(mel_spec_db))\nmel_spec_db = np.clip(mel_spec_db, db_min, db_max)\n\nmel_center_hz = 700 * (10 ** (np.linspace(mel_low, mel_high, n_mels) / 2595) - 1)\n\n# Downsample for tile rendering performance\ntime_step = max(1, len(times) // 300)\nmel_step = max(1, n_mels // 128)\ntimes_ds = times[::time_step]\nmel_indices_ds = np.arange(0, n_mels, mel_step)\nmel_spec_ds = mel_spec_db[::mel_step][:, ::time_step]\nmel_hz_ds = mel_center_hz[mel_indices_ds]\n\ntime_grid, mel_idx_grid = np.meshgrid(times_ds, mel_indices_ds)\nhz_grid = np.broadcast_to(mel_hz_ds[:, None], mel_spec_ds.shape)\ndf = pd.DataFrame(\n    {\n        \"Time (s)\": time_grid.flatten(),\n        \"Mel Band\": mel_idx_grid.flatten(),\n        \"Power (dB)\": mel_spec_ds.flatten(),\n        \"Freq (Hz)\": hz_grid.flatten(),\n    }\n)\n\n# Clamp displayed frequency range to 0–4000 Hz so harmonic content fills the canvas\ndf = df[df[\"Freq (Hz)\"] <= 4000]\n\n# Y-axis breaks: map Hz values to mel band indices\nlabel_hz = [100, 200, 500, 1000, 2000, 4000]\nlabel_mel_vals = [2595 * np.log10(1 + f / 700) for f in label_hz]\nmel_range = np.linspace(mel_low, mel_high, n_mels)\nlabel_indices = [float(np.interp(mv, mel_range, np.arange(n_mels))) for mv in label_mel_vals]\nlabel_strs = [\"100\", \"200\", \"500\", \"1k\", \"2k\", \"4k\"]\n\n# Note annotation positions (fundamental frequency of each arpeggio note)\nnote_annotations = []\nfor i, (freq, name) in enumerate(zip(melody_freqs, note_names, strict=True)):\n    mel_val = 2595 * np.log10(1 + freq / 700)\n    mel_idx = float(np.interp(mel_val, mel_range, np.arange(n_mels)))\n    mid_time = (i + 0.5) * duration / len(melody_freqs)\n    note_annotations.append({\"x\": mid_time, \"y\": mel_idx, \"label\": name})\n\ndf_notes = pd.DataFrame(note_annotations)\n\n# Plot\ntitle = \"spectrogram-mel · python · letsplot · anyplot.ai\"\n\nanyplot_theme = theme(\n    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_background=element_rect(fill=PAGE_BG),\n    panel_grid_major=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    axis_ticks=element_line(color=INK_SOFT, size=0.3),\n    plot_title=element_text(color=INK, size=16),\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=10),\n    plot_margin=[20, 60, 20, 20],\n)\n\nplot = (\n    ggplot(df, aes(x=\"Time (s)\", y=\"Mel Band\", fill=\"Power (dB)\"))\n    + geom_tile(\n        tooltips=layer_tooltips()\n        .title(\"Mel Spectrogram\")\n        .line(\"@{Time (s)}s | @{Freq (Hz)} Hz\")\n        .line(\"Power|@{Power (dB)} dB\")\n        .format(\"Time (s)\", \".2f\")\n        .format(\"Freq (Hz)\", \".0f\")\n        .format(\"Power (dB)\", \".1f\")\n        .min_width(180)\n    )\n    + geom_text(aes(x=\"x\", y=\"y\", label=\"label\"), data=df_notes, color=INK, size=5, fontface=\"bold\", alpha=0.9)\n    + scale_fill_gradient(low=\"#009E73\", high=\"#4467A3\", name=\"Power\\n(dB)\", limits=[db_min, db_max])\n    + scale_y_continuous(breaks=label_indices, labels=label_strs, expand=[0, 0])\n    + scale_x_continuous(expand=[0, 0])\n    + labs(x=\"Time (s)\", y=\"Frequency (Hz)\", title=title)\n    + ggsize(800, 450)\n    + anyplot_theme\n)\n\n# Save\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}