diff --git a/plots/spectrogram-mel/implementations/r/ggplot2.R b/plots/spectrogram-mel/implementations/r/ggplot2.R new file mode 100644 index 0000000000..15cd11a25d --- /dev/null +++ b/plots/spectrogram-mel/implementations/r/ggplot2.R @@ -0,0 +1,192 @@ +#' anyplot.ai +#' spectrogram-mel: Mel-Spectrogram for Audio Analysis +#' Library: ggplot2 3.5.1 | R 4.4.1 +#' Quality: 88/100 | Created: 2026-06-03 + +library(ggplot2) +library(scales) +library(ragg) + +set.seed(42) + +# --- Theme tokens --- +THEME <- Sys.getenv("ANYPLOT_THEME", "light") +PAGE_BG <- if (THEME == "light") "#FAF8F1" else "#1A1A17" +ELEVATED_BG <- if (THEME == "light") "#FFFDF6" else "#242420" +INK <- if (THEME == "light") "#1A1A17" else "#F0EFE8" +INK_SOFT <- if (THEME == "light") "#4A4A44" else "#B8B7B0" +INK_MUTED <- if (THEME == "light") "#6B6A63" else "#A8A79F" + +# --- Audio parameters --- +sample_rate <- 22050L +duration <- 4.0 +n_fft <- 2048L +hop_length <- 512L +n_mels <- 128L + +# --- Synthesize audio: C-major arpeggio with 8 harmonics + percussive transients --- +n_samples <- as.integer(sample_rate * duration) +t_vec <- seq(0, duration, length.out = n_samples + 1L)[seq_len(n_samples)] +note_freqs <- c(261.63, 329.63, 392.00, 523.25, 659.25, 523.25, 392.00, 329.63) +note_dur <- duration / length(note_freqs) + +audio <- numeric(n_samples) +harm_amp <- 0.5 * 0.6^(0:7) # 8 harmonics with exponential amplitude decay + +for (i in seq_along(note_freqs)) { + t0 <- (i - 1L) * note_dur + idx <- which(t_vec >= t0 & t_vec < t0 + note_dur) + f <- note_freqs[i] + dt <- t_vec[idx] - t0 + env <- pmax(0, (1 - exp(-300 * dt)) * exp(-4 * dt)) + wave <- Reduce("+", lapply(seq_along(harm_amp), function(h) { + harm_amp[h] * sin(2 * pi * h * f * t_vec[idx]) + })) + # Percussive onset burst: broadband noise decaying over ~25 ms + burst_len <- min(as.integer(0.025 * sample_rate), length(idx)) + burst_env <- c(exp(-150 * dt[seq_len(burst_len)]), numeric(length(idx) - burst_len)) + audio[idx] <- env * wave + 0.18 * burst_env * rnorm(length(idx)) +} +audio <- audio + 0.012 * rnorm(n_samples) + +# --- STFT: Hann-windowed power spectrogram --- +n_fft_half <- n_fft %/% 2L + 1L +hann_win <- 0.5 * (1 - cos(2 * pi * seq(0L, n_fft - 1L) / (n_fft - 1L))) +n_frames <- floor((n_samples - n_fft) / hop_length) + 1L + +stft_power <- matrix(0.0, nrow = n_fft_half, ncol = n_frames) +for (i in seq_len(n_frames)) { + s <- (i - 1L) * hop_length + 1L + stft_power[, i] <- Mod(fft(audio[s:(s + n_fft - 1L)] * hann_win)[seq_len(n_fft_half)])^2 +} + +# --- Mel filterbank --- +hz_to_mel <- function(f) 2595 * log10(1 + f / 700) +mel_to_hz <- function(m) 700 * (10^(m / 2595) - 1) + +f_min <- 80.0 +f_max <- as.numeric(sample_rate) / 2.0 +mel_pts <- seq(hz_to_mel(f_min), hz_to_mel(f_max), length.out = n_mels + 2L) +hz_pts <- mel_to_hz(mel_pts) +fft_freqs <- seq(0, f_max, length.out = n_fft_half) + +mel_fb <- matrix(0.0, nrow = n_mels, ncol = n_fft_half) +for (m in seq_len(n_mels)) { + rising <- (fft_freqs - hz_pts[m]) / (hz_pts[m + 1L] - hz_pts[m]) + falling <- (hz_pts[m + 2L] - fft_freqs) / (hz_pts[m + 2L] - hz_pts[m + 1L]) + mel_fb[m, ] <- pmax(0.0, pmin(rising, falling)) +} + +# --- Mel spectrogram in dB, normalized to 0 dB peak --- +mel_spec <- mel_fb %*% stft_power +mel_spec_db <- 10 * log10(mel_spec + 1e-10) +mel_spec_db <- mel_spec_db - max(mel_spec_db) + +# --- Long-format data frame --- +mel_centers <- mel_to_hz(mel_pts[2:(n_mels + 1L)]) +time_axis <- ((seq_len(n_frames) - 1L) * hop_length + n_fft / 2L) / sample_rate + +grid_idx <- expand.grid(mel_band = seq_len(n_mels), time_idx = seq_len(n_frames)) +df <- data.frame( + time_s = time_axis[grid_idx$time_idx], + mel_band = grid_idx$mel_band, + db = mel_spec_db[cbind(grid_idx$mel_band, grid_idx$time_idx)] +) + +# Y-axis: key frequency labels at representative mel-band positions +key_freqs <- c(100, 250, 500, 1000, 2000, 4000, 8000) +key_bands <- sapply(key_freqs, function(f) which.min(abs(mel_centers - f))) +key_labels <- ifelse(key_freqs >= 1000, paste0(key_freqs / 1000, "k Hz"), paste0(key_freqs, " Hz")) + +# Annotation reference positions +note_onsets <- (seq_along(note_freqs) - 1L) * note_dur +band_1k <- which.min(abs(mel_centers - 1000)) +band_2k <- which.min(abs(mel_centers - 2000)) +t_max <- max(time_axis) + +title_str <- "spectrogram-mel · r · ggplot2 · anyplot.ai" + +# --- Plot --- +p <- ggplot(df, aes(x = time_s, y = mel_band, fill = db)) + + geom_tile() + + # Note onset markers — reveal rhythmic structure of the arpeggio + geom_vline( + xintercept = note_onsets[-1], + color = INK_MUTED, + linewidth = 0.3, + linetype = "dotted" + ) + + # Perceptual boundary: 1 kHz separates fundamental region from overtones + geom_hline( + yintercept = band_1k, + color = INK_SOFT, + linewidth = 0.45, + linetype = "dashed" + ) + + annotate("text", + x = t_max * 0.97, y = band_1k + 2.5, + label = "1 kHz", color = INK_SOFT, + size = 2.3, hjust = 1 + ) + + # Frequency region labels for interpretive guidance + annotate("text", + x = 0.10, y = 5, + label = "Fundamentals", color = INK_MUTED, + size = 2.3, hjust = 0, fontface = "italic" + ) + + annotate("text", + x = 0.10, y = band_2k + 4, + label = "Overtones", color = INK_MUTED, + size = 2.3, hjust = 0, fontface = "italic" + ) + + scale_fill_gradient( + name = "dB", + low = "#009E73", + high = "#4467A3", + limits = c(-80, 0), + oob = scales::squish, + breaks = c(0, -20, -40, -60, -80) + ) + + scale_x_continuous( + name = "Time (s)", + expand = c(0, 0) + ) + + scale_y_continuous( + name = "Frequency (Hz)", + breaks = key_bands, + labels = key_labels, + expand = c(0, 0) + ) + + guides(fill = guide_colorbar(barheight = 7, barwidth = 0.7, ticks = TRUE)) + + labs( + title = title_str, + subtitle = "C-major arpeggio · 8 harmonics · mel scale compresses perceptual distances" + ) + + theme_minimal(base_size = 8) + + theme( + plot.background = element_rect(fill = PAGE_BG, color = PAGE_BG), + panel.background = element_rect(fill = PAGE_BG, color = NA), + panel.grid.major = element_blank(), + panel.grid.minor = element_blank(), + panel.border = element_blank(), + axis.title = element_text(color = INK, size = 10), + axis.text = element_text(color = INK_SOFT, size = 8), + axis.line = element_line(color = INK_SOFT, linewidth = 0.4), + plot.title = element_text(color = INK, size = 12, face = "bold"), + plot.subtitle = element_text(color = INK_SOFT, size = 8), + legend.background = element_rect(fill = ELEVATED_BG, color = INK_SOFT, linewidth = 0.3), + legend.text = element_text(color = INK_SOFT, size = 8), + legend.title = element_text(color = INK, size = 10), + plot.margin = margin(16, 16, 16, 16) + ) + +# --- Save --- +ggsave( + filename = sprintf("plot-%s.png", THEME), + plot = p, + device = ragg::agg_png, + width = 8, + height = 4.5, + units = "in", + dpi = 400 +) diff --git a/plots/spectrogram-mel/metadata/r/ggplot2.yaml b/plots/spectrogram-mel/metadata/r/ggplot2.yaml new file mode 100644 index 0000000000..72f9288d8e --- /dev/null +++ b/plots/spectrogram-mel/metadata/r/ggplot2.yaml @@ -0,0 +1,252 @@ +library: ggplot2 +language: r +specification_id: spectrogram-mel +created: '2026-06-03T18:11:42Z' +updated: '2026-06-03T18:32:41Z' +generated_by: claude-sonnet +workflow_run: 26903575433 +issue: 4672 +language_version: 4.4.1 +library_version: 3.5.1 +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/spectrogram-mel/r/ggplot2/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/spectrogram-mel/r/ggplot2/plot-dark.png +preview_html_light: null +preview_html_dark: null +quality_score: 88 +review: + strengths: + - Full STFT and mel filterbank implemented in pure R without librosa — impressive + DSP fidelity with no external audio dependencies + - 'imprint_seq colormap (#009E73 → #4467A3) correctly applied to continuous intensity + data per style guide rules' + - Complete theme-adaptive chrome with all tokens (INK, INK_SOFT, INK_MUTED, PAGE_BG, + ELEVATED_BG) properly flipped between themes + - Meaningful annotations (1 kHz boundary with region labels Fundamentals/Overtones, + note onset markers) guide interpretation + - 'Perfect spec compliance: dB scale, mel filterbank, colorbar labeled in dB, Hz-labeled + y-axis, time x-axis' + - Clean reproducible code with set.seed(42), no classes or unnecessary wrappers, + modern linewidth= API + weaknesses: + - 'Annotation text at size=2.3 mm is slightly small; ''Fundamentals'' in light render + has marginal contrast against teal tiles (#6B6A63 on #009E73 background) — raise + to size=2.8–3.0 and use INK_SOFT instead of INK_MUTED for annotations overlaying + colored tiles' + - Spectrogram dynamic range dominated by narrow energy band; data storytelling limited + — consider tightening colorbar floor from -80 dB to -60 dB to enhance contrast + in the meaningful energy region, or add a second annotated frequency boundary + image_description: |- + Light render (plot-light.png): + Background: Warm off-white #FAF8F1 — correct, not pure white + Chrome: Title "spectrogram-mel · r · ggplot2 · anyplot.ai" bold dark ink, ~70% plot width. Subtitle readable. Axis labels "Time (s)" and "Frequency (Hz)" in dark ink. Tick labels (100 Hz–8k Hz, 0–3 s) in INK_SOFT (#4A4A44). All readable. + Data: imprint_seq colormap (green #009E73 low → blue #4467A3 high) fills heatmap tiles. Dashed 1 kHz boundary line and dotted onset markers visible. "Fundamentals" and "Overtones" annotations present but subtle (INK_MUTED #6B6A63 on teal tiles). "1 kHz" label near right edge readable. + Legibility verdict: PASS (annotation text slightly small/low-contrast on colored tiles — minor) + + Dark render (plot-dark.png): + Background: Warm near-black #1A1A17 — correct, not pure black + Chrome: Title and all text flip to light ink (#F0EFE8 / #B8B7B0). All labels clearly readable. No dark-on-dark failures. "Fundamentals" and "Overtones" annotations more visible in dark mode (INK_MUTED #A8A79F reads better against dark teal). Legend uses elevated background #242420 with soft border. + Data: Colors are IDENTICAL to light render — imprint_seq green-to-blue gradient unchanged between themes. Only chrome flips. + Legibility verdict: PASS + criteria_checklist: + visual_quality: + score: 29 + max: 30 + items: + - id: VQ-01 + name: Text Legibility + score: 7 + max: 8 + passed: true + comment: Title 12pt, axis labels 10pt, tick labels 8pt all properly sized. + Annotation text size=2.3mm slightly small; 'Fundamentals' has marginal contrast + on teal tiles in light render. + - id: VQ-02 + name: No Overlap + score: 6 + max: 6 + passed: true + comment: No text-text or text-data overlaps in either render. + - id: VQ-03 + name: Element Visibility + score: 6 + max: 6 + passed: true + comment: Heatmap tiles clearly visible; dotted onset lines thin but appropriate + for secondary reference. + - id: VQ-04 + name: Color Accessibility + score: 2 + max: 2 + passed: true + comment: imprint_seq (green→blue) is CVD-safe. + - id: VQ-05 + name: Layout & Canvas + score: 4 + max: 4 + passed: true + comment: Canvas 3200x1800 passes gate. Proportions appropriate for spectrogram. + Colorbar well-positioned. + - id: VQ-06 + name: Axis Labels & Title + score: 2 + max: 2 + passed: true + comment: Time (s), Frequency (Hz), colorbar dB — all labeled with units. + - id: VQ-07 + name: Palette Compliance + score: 2 + max: 2 + passed: true + comment: 'imprint_seq (#009E73 low, #4467A3 high). Backgrounds #FAF8F1 / #1A1A17 + correct. All chrome tokens theme-adaptive.' + design_excellence: + score: 12 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 5 + max: 8 + passed: true + comment: 'Intentional design: custom imprint_seq colormap, annotated 1 kHz + boundary, region labels, percussive synthesis. Clear visual hierarchy.' + - id: DE-02 + name: Visual Refinement + score: 4 + max: 6 + passed: true + comment: Grid fully removed, panel border absent, axis lines styled, legend + with elevated-background border, 16px margins. + - id: DE-03 + name: Data Storytelling + score: 3 + max: 6 + passed: true + comment: 1 kHz boundary with region labels and onset markers provide interpretive + scaffolding. Subtitle gives context. Dynamic range concentration limits + visual impact. + spec_compliance: + score: 15 + max: 15 + items: + - id: SC-01 + name: Plot Type + score: 5 + max: 5 + passed: true + comment: Correct mel spectrogram via geom_tile on mel-scaled frequency axis. + - id: SC-02 + name: Required Features + score: 4 + max: 4 + passed: true + comment: dB conversion, mel filterbank, sequential colormap, colorbar labeled + in dB — all present. + - id: SC-03 + name: Data Mapping + score: 3 + max: 3 + passed: true + comment: X=time(s), Y=mel band with Hz labels, fill=dB values. + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 + passed: true + comment: 'Title: ''spectrogram-mel · r · ggplot2 · anyplot.ai''. Colorbar + labeled ''dB''.' + data_quality: + score: 15 + max: 15 + items: + - id: DQ-01 + name: Feature Coverage + score: 6 + max: 6 + passed: true + comment: Full 128-band mel spectrogram, 8 harmonics per note, percussive onset + bursts, additive noise floor. + - id: DQ-02 + name: Realistic Context + score: 5 + max: 5 + passed: true + comment: C-major arpeggio is neutral, universally recognizable, with realistic + synthesis parameters. + - id: DQ-03 + name: Appropriate Scale + score: 4 + max: 4 + passed: true + comment: 'Standard parameters: 22050 Hz SR, n_fft=2048, hop_length=512, n_mels=128, + -80 to 0 dB range.' + code_quality: + score: 10 + max: 10 + items: + - id: CQ-01 + name: KISS Structure + score: 3 + max: 3 + passed: true + comment: No wrapper functions or classes; hz_to_mel/mel_to_hz helpers necessary + for filterbank. + - id: CQ-02 + name: Reproducibility + score: 2 + max: 2 + passed: true + comment: set.seed(42) at top. + - id: CQ-03 + name: Clean Imports + score: 2 + max: 2 + passed: true + comment: ggplot2, scales, ragg — all used. + - id: CQ-04 + name: Code Elegance + score: 2 + max: 2 + passed: true + comment: DSP complexity inherent to plot type; no fake UI; clean R idioms. + - id: CQ-05 + name: Output & API + score: 1 + max: 1 + passed: true + comment: Saves plot-%s.png with THEME env var; modern linewidth= API used. + library_mastery: + score: 7 + max: 10 + items: + - id: LM-01 + name: Idiomatic Usage + score: 4 + max: 5 + passed: true + comment: geom_tile, scale_fill_gradient with limits/breaks/oob, guide_colorbar + customization, scale_y_continuous with custom breaks/labels, expand=c(0,0). + - id: LM-02 + name: Distinctive Features + score: 3 + max: 5 + passed: true + comment: Full mel filterbank in pure R; custom Hz labels from mel scale inversion; + guide_colorbar barheight/barwidth; annotate() with fontface='italic'. + verdict: APPROVED +impl_tags: + dependencies: + - ragg + techniques: + - colorbar + - annotations + - manual-ticks + - layer-composition + patterns: + - data-generation + - matrix-construction + - iteration-over-groups + dataprep: + - normalization + styling: + - custom-colormap