{"spec_id":"spectrogram-mel","library":"makie","language":"julia","code":"# anyplot.ai\n# spectrogram-mel: Mel-Spectrogram for Audio Analysis\n# Library: makie 0.22.10 | Julia 1.11.9\n# Quality: 88/100 | Created: 2026-06-03\n\nusing CairoMakie\nusing Colors\nusing Random\n\nRandom.seed!(42)\n\n# Theme tokens\nconst THEME       = get(ENV, \"ANYPLOT_THEME\", \"light\")\nconst PAGE_BG     = THEME == \"light\" ? colorant\"#FAF8F1\" : colorant\"#1A1A17\"\nconst INK         = THEME == \"light\" ? colorant\"#1A1A17\" : colorant\"#F0EFE8\"\nconst INK_SOFT    = THEME == \"light\" ? colorant\"#4A4A44\" : colorant\"#B8B7B0\"\n\n# Imprint sequential colormap — canonical imprint_seq direction: green (low) → blue (high)\nconst ANYPLOT_SEQ = cgrad([colorant\"#009E73\", colorant\"#4467A3\"])\n\n# Mel scale helpers\nhz_to_mel(hz) = 2595.0 * log10(1.0 + hz / 700.0)\nmel_to_hz(mel) = 700.0 * (10.0^(mel / 2595.0) - 1.0)\n\n# Audio / spectrogram parameters\nconst SR       = 22050\nconst N_MELS   = 128\nconst HOP      = 512\nconst DURATION = 4.0\n\n# Mel filterbank: evenly spaced in mel, converted back to Hz\nconst MEL_MIN   = hz_to_mel(20.0)\nconst MEL_MAX   = hz_to_mel(SR / 2.0)\nconst MEL_STEP  = (MEL_MAX - MEL_MIN) / (N_MELS - 1)\nconst MEL_FREQS = [mel_to_hz(MEL_MIN + (m - 1) * MEL_STEP) for m in 1:N_MELS]\n\n# Time axis\nconst N_FRAMES  = round(Int, DURATION * SR / HOP)\nconst T_FRAMES  = collect(range(0.0, DURATION, length=N_FRAMES))\n\n# Precompute mel-band centres for Gaussian response\nconst CENTER_MELS = [MEL_MIN + (m - 1) * MEL_STEP for m in 1:N_MELS]\nconst BW_MEL      = 1.2 * MEL_STEP   # filter bandwidth — narrow for sharp harmonic bands\n\n# Synthesize mel-spectrogram: C major scale up and back down\n# C4 D4 E4 G4 A4 G4 E4 C4\nnote_freqs = [261.63, 293.66, 329.63, 392.00, 440.00, 392.00, 329.63, 261.63]\nnote_dur   = DURATION / length(note_freqs)\nn_harm     = 14   # harmonics in the overtone series\n\nmel_spec = zeros(N_MELS, N_FRAMES)\n\nfor (ni, f0) in enumerate(note_freqs)\n    t_start = (ni - 1) * note_dur\n    t_end   = ni * note_dur\n    active  = findall(t -> t_start <= t < t_end, T_FRAMES)\n    isempty(active) && continue\n\n    for h in 1:n_harm\n        f = f0 * h\n        f >= SR / 2 && break\n\n        amp   = (1.0 / h)^2.0          # steeper harmonic decay for clear band contrast\n        f_mel = hz_to_mel(f)\n        resp  = amp .* exp.(-0.5 .* ((f_mel .- CENTER_MELS) ./ BW_MEL).^2)\n\n        for fi in active\n            rel_t = (T_FRAMES[fi] - t_start) / note_dur\n            # ADSR envelope: fast attack, slight decay, sustain, release\n            env = rel_t < 0.06 ? rel_t / 0.06 :\n                  rel_t < 0.18 ? 1.0 - 0.2 * (rel_t - 0.06) / 0.12 :\n                  rel_t < 0.85 ? 0.8 :\n                  0.8 * (1.0 - (rel_t - 0.85) / 0.15)\n            mel_spec[:, fi] .+= resp .* env\n        end\n    end\nend\n\n# Add a very low noise floor for realistic background texture\nmel_spec .+= 0.001 .* rand(N_MELS, N_FRAMES)\n\n# Convert to dB scale and normalise so the peak is 0 dB\nmel_spec_db = 20.0 .* log10.(mel_spec .+ 1e-6)\nmel_spec_db .-= maximum(mel_spec_db)\nclamp!(mel_spec_db, -80.0, 0.0)\n\n# Plot\nfig = Figure(\n    size            = (1600, 900),\n    figure_padding  = (10, 10, 10, 25),  # extra top padding for title breathing room\n    fontsize        = 14,\n    backgroundcolor = PAGE_BG,\n)\n\ntitle_str = \"C Major Scale · spectrogram-mel · julia · makie · anyplot.ai\"\nax = Axis(\n    fig[1, 1];\n    title            = title_str,\n    titlesize        = 20,\n    titlecolor       = INK,\n    xlabel           = \"Time (s)\",\n    xlabelsize       = 14,\n    xlabelcolor      = INK,\n    ylabel           = \"Frequency (Hz)\",\n    ylabelsize       = 14,\n    ylabelcolor      = INK,\n    xticklabelsize   = 12,\n    yticklabelsize   = 12,\n    xticklabelcolor  = INK_SOFT,\n    yticklabelcolor  = INK_SOFT,\n    xtickcolor       = INK_SOFT,\n    ytickcolor       = INK_SOFT,\n    backgroundcolor  = PAGE_BG,\n    leftspinecolor   = INK_SOFT,\n    bottomspinecolor = INK_SOFT,\n    topspinecolor    = INK_SOFT,\n    rightspinecolor  = INK_SOFT,\n    xgridvisible     = false,\n    ygridvisible     = false,\n)\n\n# mel_spec_db is (N_MELS, N_FRAMES); heatmap expects (N_FRAMES, N_MELS)\n# highclip/lowclip set here so the Colorbar inherits them (Makie requirement)\nhm = heatmap!(ax, T_FRAMES, 1:N_MELS, mel_spec_db';\n    colormap   = ANYPLOT_SEQ,\n    colorrange = (-80.0, 0.0),\n    highclip   = colorant\"#4467A3\",\n    lowclip    = colorant\"#009E73\",\n)\n\n# Y-axis: show Hz labels at perceptually meaningful frequency landmarks\ntick_hz  = [50, 100, 200, 500, 1000, 2000, 4000, 8000]\ntick_idx = Float64.([argmin(abs.(MEL_FREQS .- hz)) for hz in tick_hz])\ntick_lbl = [hz >= 1000 ? \"$(hz ÷ 1000)k\" : \"$hz\" for hz in tick_hz]\nax.yticks = (tick_idx, tick_lbl)\n\nColorbar(fig[1, 2], hm;\n    label          = \"Power (dB)\",\n    labelsize      = 14,\n    labelcolor     = INK,\n    ticklabelsize  = 12,\n    ticklabelcolor = INK_SOFT,\n    tickcolor      = INK_SOFT,\n    width          = 28,\n    ticks          = WilkinsonTicks(6),\n)\n\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}