PitchProcessing

Public facade for batch pitch post-processing — cleanup, correction, smoothing, and segmentation (ADR-001).

What is Pitch Processing?

Raw pitch contours from PitchDetection contain octave errors, short noise bursts (blips), and jitter. This facade provides a config-driven pipeline that cleans them up, plus individual operations for building custom pipelines.

Pipeline order (each stage skipped if disabled in config): octave correction → blip removal → smoothing.

When to Use

ScenarioUseWhy
Clean a contour for scoring / melody evalprocess(contour, .SCORING)Octave fix + blip removal, no smoothing
Clean a contour for visualizationprocess(contour, .DISPLAY)All stages, shorter min-duration
Get raw pitch unchangedprocess(contour, .RAW)No-op passthrough
Build a custom cleanup chainIndividual ops (correctOctaveErrors, smooth, medianFilter, ...)Mix and match
Realtime per-frame processingNot this facade — realtime smoothing/correction is handled inside PitchDetector when enabled via PitchDetectorConfig.Builder.enableProcessing()Different budget (ADR-020)

Usage Tiers

// Tier 1: Preset (80% of users)
val cleaned = PitchProcessing.process(contour, PitchProcessingConfig.SCORING)

// Tier 2: Builder (15%)
val config = PitchProcessingConfig.Builder()
.fixOctaveErrors(true)
.removeSpuriousJumps(false)
.smoothingWindowSize(9)
.build()
val cleaned = PitchProcessing.process(contour, config)

// Tier 3: .copy() (5%)
val cleaned = PitchProcessing.process(contour, PitchProcessingConfig.DISPLAY.copy(hopMs = 20))

// Custom pipeline (individual operations)
var pitches = contour.toPitchesArray()
pitches = PitchProcessing.correctOctaveErrors(pitches, OctaveCorrectionConfig.BASIC)
pitches = PitchProcessing.iqrFilter(pitches)
pitches = PitchProcessing.smooth(pitches)

iOS (Swift)

// Tier 1: Preset (native [Float] arrays, no KotlinFloatArray needed)
let cleaned = PitchProcessing.process(contour: contour, config: .scoring)

// Tier 2: Builder
let config = PitchProcessingConfig.Builder()
.fixOctaveErrors(true)
.removeSpuriousJumps(false)
.smoothingWindowSize(9)
.build()
let cleaned = PitchProcessing.process(contour: contour, config: config)

// Custom pipeline (individual operations with native [Float])
var pitches = contour.toPitchesArray().toSwiftArray()
pitches = PitchProcessing.correctOctaveErrors(pitchesHz: pitches, config: .basic)
pitches = PitchProcessing.iqrFilter(pitchesHz: pitches)
pitches = PitchProcessing.smooth(pitchesHz: pitches)

Common Pitfalls

  1. smooth and medianFilter require odd window/kernel size: Even values throw IllegalArgumentException (ADR-022). Defaults are 7 and 5 respectively.

  2. Unvoiced frames use -1, not 0: All operations preserve the -1 sentinel for unvoiced frames. Don't zero-fill before processing.

  3. hopMs matters for duration-based ops: removeBlips and interpolateSilence convert millisecond thresholds to frame counts via hopMs. Wrong hop → wrong durations.

  4. Batch processing, not realtime: This facade processes entire arrays. For per-frame realtime processing, use PitchDetector with enableProcessing() in PitchDetectorConfig.Builder.

See also

For creating detectors and extractors that produce contours

For histogram, quantization, and transcription on processed contours

The config class with presets (RAW, SCORING, DISPLAY)

Standalone config for the octave correction stage

Functions

Link copied to clipboard
fun correctOctaveErrors(pitchesHz: FloatArray, config: OctaveCorrectionConfig = OctaveCorrectionConfig.FULL): FloatArray

Apply config-driven octave correction.

Link copied to clipboard
fun dbscanFilter(pitchesHz: FloatArray, epsCents: Float = 100.0f, minSamples: Int = 200): FloatArray

Apply DBSCAN-based outlier filter.

Link copied to clipboard
fun findSegments(pitchesHz: FloatArray, mask: BooleanArray, minGapMs: Float = 0.0f, hopMs: Int = 10): List<IntRange>

Find contiguous segments where a mask is true.

Link copied to clipboard
fun getDurationMask(pitchesHz: FloatArray, hopMs: Int = 10, minDurationMs: Float = 80.0f): BooleanArray

Get mask filtering short voiced segments below minimum duration.

Link copied to clipboard
fun getStableSlopeMask(pitchesHz: FloatArray, hopMs: Int = 10, maxSlopeCentsPerSec: Float = 500.0f): BooleanArray

Get mask of frames with stable pitch slope.

Link copied to clipboard

Get mask of valid (voiced) pitch values.

Link copied to clipboard
fun interpolateSilence(pitchesHz: FloatArray, hopMs: Int = 10, method: InterpolationMethod = InterpolationMethod.LINEAR, maxGapMs: Float = 250.0f, forceRemaining: Boolean = false): FloatArray

Interpolate over silence gaps in a pitch contour.

Link copied to clipboard
fun iqrFilter(pitchesHz: FloatArray, multiplier: Float = 2.5f): FloatArray

Apply IQR-based outlier filter.

Link copied to clipboard
fun medianFilter(pitchesHz: FloatArray, kernelSize: Int = 5): FloatArray

Apply median filter for spike removal.

Link copied to clipboard
fun process(contour: PitchContour, config: PitchProcessingConfig = PitchProcessingConfig.DISPLAY): PitchContour

Process a pitch contour with preset or custom config.

fun process(pitchesHz: FloatArray, config: PitchProcessingConfig = PitchProcessingConfig.DISPLAY): FloatArray

Process a pitch array with preset or custom config.

Link copied to clipboard
fun rangeFilter(pitchesHz: FloatArray, minHz: Float, maxHz: Float): FloatArray

Apply frequency range filter.

Link copied to clipboard
fun removeBlips(pitchesHz: FloatArray, hopMs: Int = 10, minDurationMs: Float = 80.0f): FloatArray

Remove short voiced runs (blips) below minimum duration.

Link copied to clipboard
fun resample(pitchesHz: FloatArray, confidences: FloatArray, sourceHopMs: Float, targetHopMs: Float): FloatArray

Resample a pitch sequence to a different hop rate.

Link copied to clipboard
fun smooth(pitchesHz: FloatArray, windowSize: Int = 7): FloatArray

Apply weighted average smoothing in cents space.

Link copied to clipboard
fun smoothGaussian(pitchesHz: FloatArray, sigma: Float = 3.0f): FloatArray

Apply Gaussian smoothing with NaN-aware gap interpolation.