Batch Operations Guide
Source:vignettes/articles/batch-operations-guide.Rmd
batch-operations-guide.RmdBatch Operations Guide
pladdrr v5.0.0
This guide explains how to use batch operations in pladdrr when processing multiple files or querying multiple time points.
Why Use Batch Operations?
Problem: Traditional loops cross the R/C++ boundary repeatedly, adding overhead.
sound <- Sound(system.file("extdata", "test.wav", package = "pladdrr"))
formant <- sound$to_formant()
# 100 R->C crossings for 100 time points
times <- seq(0.1, 0.5, by = 0.004)
f1_values <- numeric(length(times))
for (i in seq_along(times)) {
f1_values[i] <- formant$get_value_at_time(1, times[i], "hertz") # R->C
}Solution: Batch operations process everything in a single C++ call.
# 1 R->C crossing for the whole vector of time points
f1_values <- formant$get_values_at_times(formant_number = 1, times = times)Types of Batch Operations
1. Batch Conversions
Convert multiple sounds to analysis objects in one call.
Functions: -
sound_to_pitch_batch(sounds, ...) - Extract pitch from
multiple sounds - sound_to_pitch_ac_batch(sounds, ...) -
Autocorrelation pitch (batch) -
sound_to_pitch_cc_batch(sounds, ...) - Cross-correlation
pitch (batch) - sound_to_formant_batch(sounds, ...) -
Extract formants (batch) -
sound_to_intensity_batch(sounds, ...) - Extract intensity
(batch)
Example:
# Load multiple sounds (here, the bundled test file repeated;
# point list.files() at your own directory of WAV files in practice)
files <- rep(system.file("extdata", "test.wav", package = "pladdrr"), 3)
sounds <- lapply(files, Sound)
# Loop approach
pitches <- lapply(sounds, function(s) s$to_pitch()) # n R->C crossings
# Batch approach
pitches <- sound_to_pitch_batch(sounds) # 1 R->C crossing2. Extract-and-Analyze Combinations
Extract segments from a sound and analyze them in a single call.
Functions: -
sound_extract_and_pitch(sound, from_times, to_times, ...) -
Extract + pitch -
sound_extract_and_formant(sound, from_times, to_times, ...)
- Extract + formant
Example:
# Analyze multiple intervals from a recording, using its TextGrid annotation
sound <- Sound(system.file("extdata", "test.wav", package = "pladdrr"))
textgrid <- TextGrid(system.file("extdata", "test.TextGrid", package = "pladdrr"))
# Get interval times (non-empty intervals that fall within the sound's duration)
intervals <- textgrid$get_all_intervals(tier = "phones")
intervals <- intervals[nzchar(intervals$text), ]
intervals <- intervals[intervals$end <= sound$get_total_duration(), ]
from_times <- intervals$start
to_times <- intervals$end
# Extract then analyze (2n R->C crossings)
parts <- lapply(seq_along(from_times), function(i) {
sound$extract_part(from_times[i], to_times[i])
})
pitches <- lapply(parts, function(p) p$to_pitch())
# Combined operation (1 R->C crossing)
pitches <- sound_extract_and_pitch(sound, from_times, to_times)3. Vectorized Queries
Extract values at multiple time points in one call.
Functions and methods: -
pitch$get_values_at_times(times, ...) - Pitch at multiple
times (R6 method) -
formant$get_values_at_times(formant_number, times, ...) -
Single formant at multiple times (R6 method) -
get_formants_at_times(formant, times, ...) - All formants
(F1-F4) at multiple times -
intensity$get_values_at_times(times, ...) - Intensity at
multiple times (R6 method) -
get_formant_bandwidths_at_times(formant, times, ...) -
Bandwidths at multiple times -
get_pitch_strengths_at_times(pitch, times, ...) - Pitch
strength at multiple times
Example: Formant Tracking
sound <- Sound(system.file("extdata", "test.wav", package = "pladdrr"))
formant <- sound$to_formant()
# Define time points
times <- seq(0.1, 0.5, by = 0.001) # 400 points
# Loop: 400 R->C crossings
f1_values <- vapply(times, function(t) {
formant$get_value_at_time(1, t, "hertz")
}, numeric(1))
# Vectorized: 1 R->C crossing for the whole vector
f1_values <- formant$get_values_at_times(formant_number = 1, times = times)
# Get all formants at once
all_formants <- get_formants_at_times(formant, times, formant_numbers = 1:4)
f1_values <- all_formants$F1
f2_values <- all_formants$F2
f3_values <- all_formants$F3
f4_values <- all_formants$F44. Batch Aggregation
Get multiple measurements in a single call.
Functions: -
sound_concatenate_all(sounds, ...) - Concatenate multiple
sounds
Example:
# Concatenate multiple recordings
sounds <- lapply(files, Sound)
# Sequential concatenation: O(n) operations
result <- Reduce(function(a, b) a$concatenate_sounds(list(b)), sounds)
# Batch concatenation: O(1) operation
result <- sound_concatenate_all(sounds)Complete Workflow Examples
Example 1: Formant Trajectories from TextGrid Segments
Extracting formant trajectories for a set of labeled segments (e.g. for voice-quality or vowel-space measurements) is a typical use of the extract-and-analyze batch functions.
library(pladdrr)
# Load recording and its TextGrid
sound <- Sound(system.file("extdata", "test.wav", package = "pladdrr"))
tg <- TextGrid(system.file("extdata", "test.TextGrid", package = "pladdrr"))
# Non-empty segments that fall within the sound's duration
segment_intervals <- tg$get_all_intervals(tier = "phones")
segment_intervals <- segment_intervals[nzchar(segment_intervals$text), ]
segment_intervals <- segment_intervals[
segment_intervals$end <= sound$get_total_duration(), ]
# Extract segment portions and analyze in batch
segment_formants <- sound_extract_and_formant(
sound,
segment_intervals$start,
segment_intervals$end,
time_step = 0.005
)
# Get F1-F4 trajectories for all segments
segment_trajectories <- lapply(segment_formants, function(f) {
times <- seq(f$get_start_time(), f$get_end_time(), by = 0.001)
get_formants_at_times(f, times, formant_numbers = 1:4)
})
# Process results
all_f1 <- unlist(lapply(segment_trajectories, function(x) x$F1))
mean_f1 <- mean(all_f1, na.rm = TRUE)Example 2: Tremor Analysis
Analyze voice tremor by tracking pitch and intensity modulation.
sound <- Sound(system.file("extdata", "test.wav", package = "pladdrr"))
# Extract pitch and intensity
pitch <- sound$to_pitch()
intensity <- sound$to_intensity()
# Define high-resolution time grid
times <- seq(pitch$get_start_time(),
pitch$get_end_time(),
by = 0.001) # 1ms resolution
# Get values at all time points (vectorized R6 methods)
f0_values <- pitch$get_values_at_times(times, unit = "hertz")
int_values <- intensity$get_values_at_times(times)
# Analyze modulation
f0_range <- diff(range(f0_values, na.rm = TRUE))
f0_sd <- sd(f0_values, na.rm = TRUE)
# Spectral analysis of modulation
f0_clean <- na.omit(f0_values)
f0_detrended <- f0_clean - mean(f0_clean)
tremor_spectrum <- stats::spectrum(f0_detrended, plot = FALSE)
# spectrum() reports frequency as cycles per sample of the modulation
# series (sampled at 1 ms steps here, i.e. 1000 Hz), not the audio's
# sampling rate
modulation_sampling_rate <- 1 / 0.001
tremor_freq_hz <- tremor_spectrum$freq * modulation_sampling_rate
# Identify tremor frequency (typically 4-7 Hz)
tremor_freq_idx <- which(tremor_freq_hz >= 4 & tremor_freq_hz <= 7)
tremor_power <- max(tremor_spectrum$spec[tremor_freq_idx])Example 3: Large-Scale Corpus Analysis
Process many files efficiently.
# Get all files (here, the package's bundled extdata directory;
# point this at your own corpus directory in practice)
corpus_files <- list.files(system.file("extdata", package = "pladdrr"),
pattern = "\\.wav$", full.names = TRUE)
cat(sprintf("Processing %d files\n", length(corpus_files)))
# Parallel batch processing
library(parallel)
results <- analyze_files_parallel(corpus_files, function(sound) {
# Extract pitch
pitch <- sound$to_pitch(pitch_floor = 75, pitch_ceiling = 300)
# Get comprehensive statistics in one call (Tier 2 Direct API)
pitch_stats <- get_pitch_stats_direct(pitch)
# Extract formants
formant <- sound$to_formant()
# Get formants at 10 evenly-spaced time points
duration <- sound$get_total_duration()
times <- seq(0.1 * duration, 0.9 * duration, length.out = 10)
formant_values <- get_formants_at_times(formant, times, formant_numbers = 1:4)
# Return aggregated results (analysis_func only receives the Sound,
# not its file path, so the file name is attached afterwards)
list(
duration = duration,
pitch_mean = pitch_stats$mean,
pitch_sd = pitch_stats$stdev,
f1_mean = mean(formant_values$F1, na.rm = TRUE),
f2_mean = mean(formant_values$F2, na.rm = TRUE),
f3_mean = mean(formant_values$F3, na.rm = TRUE)
)
}, n_cores = 1)
# Convert to data frame, attaching file names by position
results_df <- do.call(rbind, Map(function(f, r) {
data.frame(file = basename(f), as.data.frame(r))
}, corpus_files, results))
write.csv(results_df, "corpus_analysis_results.csv", row.names = FALSE)Best Practices
1. Always Vectorize Time Queries
# Avoid: one R->C crossing per query
for (t in times) {
val <- pitch$get_value_at_time(t, "hertz")
}
# Prefer: a single vectorized call
values <- pitch$get_values_at_times(times, unit = "hertz")2. Combine Batch Operations
starts <- c(0.1, 0.4, 0.7)
ends <- c(0.3, 0.6, 0.9)
# Separate operations
parts <- sound$extract_parts_batch(starts, ends)
pitches <- lapply(parts, function(p) p$to_pitch())
# Combined operation (one C++ call instead of two)
pitches <- sound_extract_and_pitch(sound, starts, ends)3. Use Batch for TextGrid Workflows
# Extract intervals from TextGrid
tg <- TextGrid(system.file("extdata", "test.TextGrid", package = "pladdrr"))
intervals <- tg$get_all_intervals(tier = "words")
intervals <- intervals[nzchar(intervals$text), ]
# Batch extract and analyze
formants <- sound_extract_and_formant(
sound,
intervals$start,
intervals$end
)4. Leverage Return Types
# Batch functions support return_r6 parameter
# Set to FALSE to get raw pointers instead of wrapped R6 objects
pitch_ptrs <- sound_to_pitch_batch(sounds, return_r6 = FALSE)
# Use with Direct API to avoid re-wrapping each result
stats <- lapply(pitch_ptrs, get_pitch_stats_direct)Function Reference Table
| Function | Input | Output | Use Case |
|---|---|---|---|
sound_to_pitch_batch() |
List of Sounds | List of Pitch | Batch pitch extraction |
sound_to_formant_batch() |
List of Sounds | List of Formant | Batch formant extraction |
sound_extract_and_pitch() |
Sound + times | List of Pitch | Interval analysis |
pitch$get_values_at_times() |
Pitch + times | Numeric vector | F0 tracking (R6 method) |
get_formants_at_times() |
Formant + times | List (F1,F2,F3,F4) | Formant tracking |
formant$get_values_at_times() |
Formant + times | Numeric vector | Single formant tracking (R6 method) |
sound_concatenate_all() |
List of Sounds | Sound | Concatenation |
Troubleshooting
“Could not extract pointer from Sound object” Error
Both Sound$new(path) and Sound(path)
construct equivalent objects and work interchangeably. This error is
raised by batch functions (e.g. sound_to_pitch_batch())
when an element of the input list is neither a Sound object
nor a raw external pointer:
sound <- Sound(system.file("extdata", "test.wav", package = "pladdrr"))
# Fails: batch functions expect a list of Sound objects, not a bare path
# pitches <- sound_to_pitch_batch("test.wav")
# Works
pitches <- sound_to_pitch_batch(list(sound))Memory Issues with Large Batches
Process in chunks if you run out of memory:
# files and analysis_func come from your own workflow, e.g. the
# corpus_files list and analysis function from Example 3 above
files <- corpus_files
analysis_func <- function(sound) list(duration = sound$get_total_duration())
chunk_size <- 100
n_chunks <- ceiling(length(files) / chunk_size)
results <- list()
for (i in seq_len(n_chunks)) {
idx_start <- (i - 1) * chunk_size + 1
idx_end <- min(i * chunk_size, length(files))
chunk_files <- files[idx_start:idx_end]
results[[i]] <- analyze_files_parallel(chunk_files, analysis_func)
}
all_results <- unlist(results, recursive = FALSE)See Also
-
vignette("performance-optimization")- performance guide -
?analyze_files_parallel- parallel processing documentation -
?get_pitch_stats_direct- Direct API reference