Analysis-Resynthesis Workflow: FormantPath + KlattGrid
Source:vignettes/analysis-resynthesis-workflow.Rmd
analysis-resynthesis-workflow.RmdIntroduction
This vignette demonstrates the complete analysis-resynthesis workflow in pladdrr, combining:
- FormantPath - Robust formant extraction with multiple ceiling candidates
- KlattGrid - Parametric speech synthesis from formant specifications
This workflow supports several speech research applications:
- Voice morphing - Transform speaker characteristics
- Perceptual experiments - Manipulate acoustic parameters systematically
- Quality assessment - Verify formant tracking accuracy via resynthesis
- Speech restoration - Clean and regenerate degraded audio
Why Analysis-Resynthesis?
The Perceptual Loop
The analysis-resynthesis cycle closes the loop between acoustic analysis and perceptual reality:
Real Speech → FormantPath Analysis → Formant Parameters
↑ ↓
Perceptual Validation ← KlattGrid Synthesis ← Modified Parameters
Benefits:
- Validation - If resynthesis sounds similar, analysis is accurate
- Manipulation - Systematically modify acoustic parameters
- Understanding - Bridge between measurements and perception
- Control - Create impossible stimuli (e.g., male formants + female pitch)
Workflow 1: Basic Analysis → Synthesis
Step 1: Load and Analyze Audio
# Load audio file
sound_orig <- Sound(system.file("extdata", "test.wav", package = "pladdrr"))
cat("Original audio:\n")
#> Original audio:
cat(" Duration:", sound_orig$get_duration(), "s\n")
#> Duration: 1 s
cat(" Samples:", sound_orig$get_number_of_samples(), "\n")
#> Samples: 44100
# Extract formants with FormantPath (robust multi-ceiling)
fp <- sound_orig$to_formant_path(
time_step = 0.005,
max_num_formants = 5,
formant_ceiling = 5500,
num_steps_up_down = 2L # Test 5 different ceilings
)
cat("\nFormantPath analysis:\n")
#>
#> FormantPath analysis:
cat(" Candidates tested:", fp$get_number_of_candidates(), "\n")
#> Candidates tested: 5
cat(" Ceiling range:",
round(min(fp$get_all_ceiling_frequencies())), "-",
round(max(fp$get_all_ceiling_frequencies())), "Hz\n")
#> Ceiling range: 4977 - 6078 Hz
# Extract optimal formant track
frm_result <- fp$extract_formant()
cat(" Formant frames:", frm_result$get_number_of_frames(), "\n")
#> Formant frames: 190Step 2: Extract Mean Formant Values
# Convert to data frame for easy manipulation
df <- as.data.frame(frm_result)
# Calculate mean F1, F2, F3 across all frames
f1_mean <- mean(df$frequency[df$formant == 1], na.rm = TRUE)
f2_mean <- mean(df$frequency[df$formant == 2], na.rm = TRUE)
f3_mean <- mean(df$frequency[df$formant == 3], na.rm = TRUE)
cat("Mean formant values:\n")
#> Mean formant values:
cat(sprintf(" F1: %.0f Hz\n", f1_mean))
#> F1: 421 Hz
cat(sprintf(" F2: %.0f Hz\n", f2_mean))
#> F2: 465 Hz
cat(sprintf(" F3: %.0f Hz\n", f3_mean))
#> F3: 3080 Hz
# Also extract pitch for realistic resynthesis
pitch <- sound_orig$to_pitch()
f0_mean <- mean(as.data.frame(pitch)$frequency, na.rm = TRUE)
cat(sprintf(" F0 (pitch): %.0f Hz\n", f0_mean))
#> F0 (pitch): 440 HzStep 3: Resynthesize with KlattGrid
# Create KlattGrid with extracted parameters
kg <- klattgrid_create_from_vowel(
duration = min(sound_orig$get_duration(), 1.0), # Limit for demo
f0start = f0_mean,
f1 = f1_mean, b1 = 80, # Use typical bandwidths
f2 = f2_mean, b2 = 120,
f3 = f3_mean, b3 = 150
)
# Synthesize audio
sound_resynth <- kg$to_sound()
cat("Resynthesized audio:\n")
#> Resynthesized audio:
cat(" Duration:", sound_resynth$get_duration(), "s\n")
#> Duration: 1 s
cat(" Samples:", sound_resynth$get_number_of_samples(), "\n")
#> Samples: 44100Step 4: Compare Original vs Resynthesized
# Create spectrograms for comparison
spec_orig <- sound_orig$to_spectrogram(
window_length = 0.005,
max_frequency = 5000
)
spec_resynth <- sound_resynth$to_spectrogram(
window_length = 0.005,
max_frequency = 5000
)
# Extract formants for overlay
formant_orig <- sound_orig$to_formant_burg(max_frequency = 5500)
formant_resynth <- sound_resynth$to_formant_burg(max_frequency = 5500)
df_orig <- as.data.frame(formant_orig)
df_orig <- df_orig[df_orig$formant %in% 1:3, ]
df_orig$source <- "Original"
df_resynth <- as.data.frame(formant_resynth)
df_resynth <- df_resynth[df_resynth$formant %in% 1:3, ]
df_resynth$source <- "Resynthesized"
# Combine for plotting
df_combined <- rbind(df_orig, df_resynth)
df_combined$formant_num <- df_combined$formant
# Plot formant tracks comparison
ggplot(df_combined, aes(time, frequency, color = factor(formant_num))) +
geom_line(alpha = 0.7, linewidth = 0.8) +
facet_wrap(~ source, ncol = 1) +
scale_color_manual(
values = c("1" = "#E41A1C", "2" = "#377EB8", "3" = "#4DAF4A"),
labels = c("F1", "F2", "F3")
) +
labs(
title = "Original vs Resynthesized Formant Tracks",
subtitle = "FormantPath analysis → KlattGrid synthesis",
x = "Time (s)",
y = "Frequency (Hz)",
color = "Formant"
) +
theme_minimal() +
theme(legend.position = "top")Workflow 2: Vowel Round-Trip Validation
Create Synthetic Vowel with Known Parameters
# Define target /a/ vowel parameters
target_f1 <- 730
target_f2 <- 1090
target_f3 <- 2440
cat("Target vowel: /a/\n")
#> Target vowel: /a/
cat(sprintf(" F1: %d Hz\n", target_f1))
#> F1: 730 Hz
cat(sprintf(" F2: %d Hz\n", target_f2))
#> F2: 1090 Hz
cat(sprintf(" F3: %d Hz\n", target_f3))
#> F3: 2440 Hz
# Synthesize
kg_vowel <- klattgrid_create_from_vowel(
duration = 0.5,
f0start = 120,
f1 = target_f1, b1 = 80,
f2 = target_f2, b2 = 120,
f3 = target_f3, b3 = 150
)
sound_vowel <- kg_vowel$to_sound()Analyze Synthetic Vowel
# Analyze with FormantPath
fp_vowel <- sound_vowel$to_formant_path(
time_step = 0.005,
formant_ceiling = 5500,
num_steps_up_down = 2L
)
frm_result <- fp_vowel$extract_formant()
# Extract values at midpoint
mid_time <- sound_vowel$get_duration() / 2
f1_extracted <- frm_result$get_value_at_time(1, mid_time, "hertz")
f2_extracted <- frm_result$get_value_at_time(2, mid_time, "hertz")
f3_extracted <- frm_result$get_value_at_time(3, mid_time, "hertz")
cat("\nExtracted formants (at midpoint):\n")
#>
#> Extracted formants (at midpoint):
cat(sprintf(" F1: %.0f Hz\n", f1_extracted))
#> F1: 730 Hz
cat(sprintf(" F2: %.0f Hz\n", f2_extracted))
#> F2: 1097 Hz
cat(sprintf(" F3: %.0f Hz\n", f3_extracted))
#> F3: 2469 Hz
# Calculate errors
f1_error <- abs(f1_extracted - target_f1) / target_f1 * 100
f2_error <- abs(f2_extracted - target_f2) / target_f2 * 100
f3_error <- abs(f3_extracted - target_f3) / target_f3 * 100
cat("\nExtraction accuracy:\n")
#>
#> Extraction accuracy:
cat(sprintf(" F1 error: %.1f%%\n", f1_error))
#> F1 error: 0.0%
cat(sprintf(" F2 error: %.1f%%\n", f2_error))
#> F2 error: 0.7%
cat(sprintf(" F3 error: %.1f%%\n", f3_error))
#> F3 error: 1.2%Visualize Round-Trip
# Create comparison data frame
df_comparison <- data.frame(
Formant = rep(c("F1", "F2", "F3"), 2),
Frequency = c(target_f1, target_f2, target_f3,
f1_extracted, f2_extracted, f3_extracted),
Type = rep(c("Target", "Extracted"), each = 3)
)
ggplot(df_comparison, aes(Formant, Frequency, fill = Type)) +
geom_bar(stat = "identity", position = position_dodge(width = 0.8), width = 0.7) +
geom_text(aes(label = sprintf("%.0f Hz", Frequency)),
position = position_dodge(width = 0.8),
vjust = -0.5, size = 3) +
scale_fill_manual(values = c("Target" = "#377EB8", "Extracted" = "#E41A1C")) +
labs(
title = "Round-Trip Validation: Target vs Extracted Formants",
subtitle = "Synthesis → Analysis cycle for /a/ vowel",
y = "Frequency (Hz)"
) +
theme_minimal() +
theme(legend.position = "top")Workflow 3: Vowel Space Mapping
Synthesize Vowel Triangle
# Define cardinal vowels
vowels <- list(
i = list(label = "/i/ (beet)", f1 = 280, f2 = 2250, f3 = 2890),
a = list(label = "/a/ (father)", f1 = 730, f2 = 1090, f3 = 2440),
u = list(label = "/u/ (boot)", f1 = 310, f2 = 870, f3 = 2250)
)
# Synthesize each vowel
vowel_sounds <- list()
vowel_results <- list()
for (v_name in names(vowels)) {
v <- vowels[[v_name]]
# Synthesize
kg <- klattgrid_create_from_vowel(
duration = 0.3,
f0start = 150,
f1 = v$f1, b1 = 60,
f2 = v$f2, b2 = 100,
f3 = v$f3, b3 = 140
)
vowel_sounds[[v_name]] <- kg$to_sound()
cat(sprintf("Synthesized %s: F1=%d, F2=%d, F3=%d Hz\n",
v$label, v$f1, v$f2, v$f3))
}
#> Synthesized /i/ (beet): F1=280, F2=2250, F3=2890 Hz
#> Synthesized /a/ (father): F1=730, F2=1090, F3=2440 Hz
#> Synthesized /u/ (boot): F1=310, F2=870, F3=2250 HzAnalyze Vowel Triangle
# Analyze each synthetic vowel
for (v_name in names(vowels)) {
sound <- vowel_sounds[[v_name]]
# Robust formant tracking
fp <- sound$to_formant_path(num_steps_up_down = 1L)
frm_result <- fp$extract_formant()
# Get mean formants
df <- as.data.frame(frm_result)
f1_mean <- mean(df$frequency[df$formant == 1], na.rm = TRUE)
f2_mean <- mean(df$frequency[df$formant == 2], na.rm = TRUE)
vowel_results[[v_name]] <- data.frame(
vowel = v_name,
label = vowels[[v_name]]$label,
target_f1 = vowels[[v_name]]$f1,
target_f2 = vowels[[v_name]]$f2,
extracted_f1 = f1_mean,
extracted_f2 = f2_mean
)
cat(sprintf("Analyzed %s: F1=%.0f, F2=%.0f Hz\n",
vowels[[v_name]]$label, f1_mean, f2_mean))
}
#> Analyzed /i/ (beet): F1=305, F2=2262 Hz
#> Analyzed /a/ (father): F1=750, F2=1088 Hz
#> Analyzed /u/ (boot): F1=308, F2=893 Hz
# Combine results
vowel_space <- do.call(rbind, vowel_results)Plot Vowel Space
# Create vowel space plot (F1 vs F2)
vowel_space_long <- rbind(
data.frame(
vowel = vowel_space$vowel,
label = vowel_space$label,
F1 = vowel_space$target_f1,
F2 = vowel_space$target_f2,
type = "Target"
),
data.frame(
vowel = vowel_space$vowel,
label = vowel_space$label,
F1 = vowel_space$extracted_f1,
F2 = vowel_space$extracted_f2,
type = "Extracted"
)
)
ggplot(vowel_space_long, aes(F2, F1, color = type, shape = vowel)) +
geom_point(size = 5, alpha = 0.8) +
geom_line(aes(group = vowel), color = "gray50", linetype = "dashed") +
geom_text(aes(label = vowel), vjust = -1.5, size = 5, fontface = "bold") +
scale_x_reverse() + # Traditional vowel space orientation
scale_y_reverse() +
scale_color_manual(values = c("Target" = "#377EB8", "Extracted" = "#E41A1C")) +
scale_shape_manual(values = c("i" = 16, "a" = 17, "u" = 15)) +
labs(
title = "Vowel Space: Synthesis-Analysis Round Trip",
subtitle = "Target (blue) vs Extracted (red) formant values",
x = "F2 (Hz)",
y = "F1 (Hz)",
color = "Type",
shape = "Vowel"
) +
theme_minimal() +
theme(
legend.position = "right",
panel.grid.major = element_line(color = "gray90")
)Verify Vowel Space Relationships
# Check phonetic relationships are preserved
cat("Vowel space relationships:\n\n")
#> Vowel space relationships:
# F1: /a/ should be highest (lowest vowel)
cat("F1 ordering (high to low vowel height):\n")
#> F1 ordering (high to low vowel height):
cat(sprintf(" /a/: %.0f Hz (low vowel, high F1)\n",
vowel_space[vowel_space$vowel == "a", "extracted_f1"]))
#> /a/: 750 Hz (low vowel, high F1)
cat(sprintf(" /i/: %.0f Hz (high vowel, low F1)\n",
vowel_space[vowel_space$vowel == "i", "extracted_f1"]))
#> /i/: 305 Hz (high vowel, low F1)
cat(sprintf(" /u/: %.0f Hz (high vowel, low F1)\n",
vowel_space[vowel_space$vowel == "u", "extracted_f1"]))
#> /u/: 308 Hz (high vowel, low F1)
f1_a <- vowel_space[vowel_space$vowel == "a", "extracted_f1"]
f1_i <- vowel_space[vowel_space$vowel == "i", "extracted_f1"]
f1_u <- vowel_space[vowel_space$vowel == "u", "extracted_f1"]
cat(sprintf(" /a/ F1 > /i/ F1: %s\n", f1_a > f1_i))
#> /a/ F1 > /i/ F1: TRUE
cat(sprintf(" /a/ F1 > /u/ F1: %s\n", f1_a > f1_u))
#> /a/ F1 > /u/ F1: TRUE
# F2: /i/ (front) > /a/ (central) > /u/ (back)
cat("\nF2 ordering (front to back):\n")
#>
#> F2 ordering (front to back):
cat(sprintf(" /i/: %.0f Hz (front, high F2)\n",
vowel_space[vowel_space$vowel == "i", "extracted_f2"]))
#> /i/: 2262 Hz (front, high F2)
cat(sprintf(" /a/: %.0f Hz (central, mid F2)\n",
vowel_space[vowel_space$vowel == "a", "extracted_f2"]))
#> /a/: 1088 Hz (central, mid F2)
cat(sprintf(" /u/: %.0f Hz (back, low F2)\n",
vowel_space[vowel_space$vowel == "u", "extracted_f2"]))
#> /u/: 893 Hz (back, low F2)
f2_i <- vowel_space[vowel_space$vowel == "i", "extracted_f2"]
f2_a <- vowel_space[vowel_space$vowel == "a", "extracted_f2"]
f2_u <- vowel_space[vowel_space$vowel == "u", "extracted_f2"]
cat(sprintf(" /i/ F2 > /a/ F2: %s\n", f2_i > f2_a))
#> /i/ F2 > /a/ F2: TRUE
cat(sprintf(" /a/ F2 > /u/ F2: %s\n", f2_a > f2_u))
#> /a/ F2 > /u/ F2: TRUEAdvanced: Voice Morphing
Morph Between Two Speakers
# Define two speaker profiles
speaker_male <- list(
f1 = 730, f2 = 1090, f3 = 2440,
f0 = 100, label = "Male"
)
speaker_female <- list(
f1 = 850, f2 = 1300, f3 = 2800,
f0 = 220, label = "Female"
)
# Create morph continuum (5 steps)
morph_alphas <- seq(0, 1, length.out = 5)
morph_results <- data.frame()
for (i in seq_along(morph_alphas)) {
alpha <- morph_alphas[i]
# Linear interpolation between speakers
f1 <- speaker_male$f1 + alpha * (speaker_female$f1 - speaker_male$f1)
f2 <- speaker_male$f2 + alpha * (speaker_female$f2 - speaker_male$f2)
f3 <- speaker_male$f3 + alpha * (speaker_female$f3 - speaker_male$f3)
f0 <- speaker_male$f0 + alpha * (speaker_female$f0 - speaker_male$f0)
# Synthesize
kg <- klattgrid_create_from_vowel(
duration = 0.4,
f0start = f0,
f1 = f1, b1 = 80,
f2 = f2, b2 = 120,
f3 = f3, b3 = 150
)
sound <- kg$to_sound()
morph_results <- rbind(morph_results, data.frame(
step = i,
alpha = alpha,
f0 = f0,
f1 = f1,
f2 = f2,
f3 = f3,
label = sprintf("%.0f%% Female", alpha * 100)
))
cat(sprintf("Step %d (%.0f%% Female): F0=%.0f, F1=%.0f, F2=%.0f Hz\n",
i, alpha * 100, f0, f1, f2))
}
#> Step 1 (0% Female): F0=100, F1=730, F2=1090 Hz
#> Step 2 (25% Female): F0=130, F1=760, F2=1142 Hz
#> Step 3 (50% Female): F0=160, F1=790, F2=1195 Hz
#> Step 4 (75% Female): F0=190, F1=820, F2=1248 Hz
#> Step 5 (100% Female): F0=220, F1=850, F2=1300 HzVisualize Morph Continuum
# Plot formant trajectories across morph
morph_long <- tidyr::pivot_longer(
morph_results,
cols = c(f0, f1, f2, f3),
names_to = "parameter",
values_to = "frequency"
)
ggplot(morph_long, aes(alpha, frequency, color = parameter)) +
geom_line(linewidth = 1.2) +
geom_point(size = 3) +
scale_color_manual(
values = c("f0" = "#984EA3", "f1" = "#E41A1C",
"f2" = "#377EB8", "f3" = "#4DAF4A"),
labels = c("F0 (pitch)", "F1", "F2", "F3")
) +
labs(
title = "Voice Morphing: Male → Female Continuum",
subtitle = "Linear interpolation of formant and pitch parameters",
x = "Morph Parameter (0 = Male, 1 = Female)",
y = "Frequency (Hz)",
color = "Parameter"
) +
theme_minimal() +
theme(legend.position = "right")Use Cases
1. Formant Manipulation Experiments
Systematically vary one formant while holding others constant:
# Create F1 continuum (F2/F3 constant)
f1_values <- seq(400, 800, by = 50) # Low to high
stimuli <- lapply(f1_values, function(f1) {
kg <- klattgrid_create_from_vowel(
duration = 0.5, f0start = 120,
f1 = f1, b1 = 80, # VARIED
f2 = 1500, b2 = 120, # CONSTANT
f3 = 2500, b3 = 150 # CONSTANT
)
sound <- kg$to_sound()
sound$save(file.path(tempdir(), sprintf("f1_%04d.wav", f1)), "WAV")
sound
})2. Pitch-Formant Decoupling
Test perception with impossible voice combinations:
# Male formants + Female pitch (impossible in nature)
kg_impossible <- klattgrid_create_from_vowel(
duration = 0.5,
f0start = 220, # Female pitch
f1 = 730, b1 = 80, # Male formants
f2 = 1090, b2 = 120,
f3 = 2440, b3 = 150
)
sound_impossible <- kg_impossible$to_sound()3. Quality Assessment
Verify formant tracking accuracy on known stimuli:
# Generate test vowels with known formants
test_vowels <- expand.grid(
f1 = seq(300, 800, by = 100),
f2 = seq(900, 2400, by = 300)
)
accuracy <- apply(test_vowels, 1, function(params) {
# Synthesize
kg <- klattgrid_create_from_vowel(
duration = 0.5, f0start = 120,
f1 = params[1], b1 = 80,
f2 = params[2], b2 = 120,
f3 = 2500, b3 = 150
)
sound <- kg$to_sound()
# Analyze
fp <- sound$to_formant_path(num_steps_up_down = 2L)
frm_result <- fp$extract_formant()
# Compare
df <- as.data.frame(frm_result)
f1_extracted <- mean(df$frequency[df$formant == 1], na.rm = TRUE)
f2_extracted <- mean(df$frequency[df$formant == 2], na.rm = TRUE)
data.frame(
target_f1 = params[1],
target_f2 = params[2],
extracted_f1 = f1_extracted,
extracted_f2 = f2_extracted,
error_f1 = abs(f1_extracted - params[1]),
error_f2 = abs(f2_extracted - params[2])
)
})
# Analyze accuracy across vowel space
accuracy_df <- do.call(rbind, accuracy)
mean(accuracy_df$error_f1, na.rm = TRUE) # Mean F1 error in Hz
#> [1] 8.27986Best Practices
DO
- Use FormantPath for analysis - More robust than single-ceiling methods
-
Extract pitch from original - Use
to_pitch()for realistic F0 - Match bandwidths to formant frequency - Higher formants = wider bandwidths
- Validate on synthetic vowels first - Test round-trip accuracy
- Save intermediate results - Keep formant data and KlattGrid parameters
- Compare spectrograms - Visual inspection reveals synthesis quality
- Document parameters - Record all synthesis settings for reproducibility
DON’T
- Don’t use single ceiling for diverse speakers - Use FormantPath instead
- Don’t ignore bandwidth - Critical for natural synthesis
- Don’t expect perfect reconstruction - Some loss is inherent
- Don’t synthesize consonants - KlattGrid is vowel-focused
- Don’t skip validation - Always check round-trip accuracy
- Don’t use extreme formant values - Stay within natural ranges
Troubleshooting
Poor Resynthesis Quality
Symptoms: Synthetic speech sounds unnatural, buzzy, or robotic
Solutions: - Verify F1 < F2 < F3 relationship - Check bandwidth values (B1 ≈ 80, B2 ≈ 120, B3 ≈ 150 Hz) - Ensure pitch is realistic (80-300 Hz) - Use mean formants rather than single time point
High Round-Trip Error
Symptoms: Extracted formants differ greatly from target (>20% error)
Solutions: - Increase FormantPath candidates
(num_steps_up_down = 3L) - Adjust formant ceiling based on
speaker - Check for formant tracking errors (visual inspection) - Use
longer vowel duration (>300 ms)
Segfault During Synthesis
Symptoms: R crashes when calling
kg$to_sound()
Solutions: - Always use
klattgrid_create_from_vowel() (not empty constructor) -
Verify all formant values are positive and finite - Check F1 < F2
< F3 constraint
Summary
The analysis-resynthesis workflow enables: - Validation of formant tracking accuracy - Systematic acoustic parameter manipulation - Voice morphing and transformation - Perceptual experiment stimulus generation
Key functions: -
sound$to_formant_path() - Robust analysis -
fp$extract_formant() - Get optimal track -
klattgrid_create_from_vowel() - Safe synthesis -
as.data.frame() - Extract formant values
Typical pipeline: 1. Load audio:
Sound("file.wav") 2. Analyze:
fp <- sound$to_formant_path(num_steps_up_down=2L) 3.
Extract: frm_result <- fp$extract_formant() 4. Get
values: df <- as.data.frame(frm_result) 5. Synthesize:
kg <- klattgrid_create_from_vowel(...) 6. Compare:
Visual/perceptual validation
Further Reading
-
vignette("formantpath-robust-tracking")- FormantPath details -
vignette("speech-synthesis-klattgrid")- KlattGrid synthesis -
vignette("formant-analysis")- Standard formant extraction - Praat manual: Manual index
Session Info
sessionInfo()
#> R version 4.6.1 (2026-06-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 24.04.5 LTS
#>
#> Matrix products: default
#> BLAS: /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so; LAPACK version 3.12.0
#>
#> locale:
#> [1] LC_CTYPE=C.UTF-8 LC_NUMERIC=C LC_TIME=C.UTF-8
#> [4] LC_COLLATE=C.UTF-8 LC_MONETARY=C.UTF-8 LC_MESSAGES=C.UTF-8
#> [7] LC_PAPER=C.UTF-8 LC_NAME=C LC_ADDRESS=C
#> [10] LC_TELEPHONE=C LC_MEASUREMENT=C.UTF-8 LC_IDENTIFICATION=C
#>
#> time zone: UTC
#> tzcode source: system (glibc)
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] ggplot2_4.0.3 pladdrr_5.0.5
#>
#> loaded via a namespace (and not attached):
#> [1] gtable_0.3.6 jsonlite_2.0.0 dplyr_1.2.1
#> [4] compiler_4.6.1 tidyselect_1.2.1 Rcpp_1.1.2
#> [7] jquerylib_0.1.4 systemfonts_1.3.2 scales_1.4.0
#> [10] textshaping_1.0.5 yaml_2.3.12 fastmap_1.2.0
#> [13] R6_2.6.1 generics_0.1.4 knitr_1.52
#> [16] tibble_3.3.1 desc_1.4.3 bslib_0.12.0
#> [19] pillar_1.11.1 RColorBrewer_1.1-3 rlang_1.3.0
#> [22] cachem_1.1.0 xfun_0.60 fs_2.1.0
#> [25] sass_0.4.10 S7_0.2.2 otel_0.2.0
#> [28] cli_3.6.6 pkgdown_2.2.1 withr_3.0.3
#> [31] magrittr_2.0.5 digest_0.6.39 grid_4.6.1
#> [34] lifecycle_1.0.5 vctrs_0.7.3 evaluate_1.0.5
#> [37] glue_1.8.1 data.table_1.18.6.1 farver_2.1.2
#> [40] codetools_0.2-20 ragg_1.5.2 rmarkdown_2.32
#> [43] tools_4.6.1 pkgconfig_2.0.3 htmltools_0.5.9