Skip to main content

CalibraVocalRange

Streaming vocal range analyzer and guided session for determining a singer's pitch range. Includes both a low-level analyzer (CalibraVocalRange) and a high-level session (VocalRangeSession) with observable state.

Quick Start

Kotlin

val analyzer = CalibraVocalRange.create()

// Feed audio from recorder
recorder.audioBuffers.collect { buffer ->
analyzer.addAudio(buffer.toFloatArray())
}

// Get detected range
val range = analyzer.getRange()
if (range != null) {
println("Range: ${range.lower.noteLabel} to ${range.upper.noteLabel}")
println("Octaves: ${range.octaves}")
}

analyzer.release()

Swift

let analyzer = CalibraVocalRange.create()

// Feed audio from recorder
for await buffer in recorder.audioBuffersStream() {
analyzer.addAudio(samples: buffer.toFloatArray())
}

// Get detected range
if let range = analyzer.getRange() {
print("Range: \(range.lower.noteLabel) to \(range.upper.noteLabel)")
print("Octaves: \(range.octaves)")
}

analyzer.release()

CalibraVocalRange (Low-Level Analyzer)

Creating an Analyzer

// Default config (ASHA 2018 guidelines)
val analyzer = CalibraVocalRange.create()

// Custom config
val analyzer = CalibraVocalRange.create(VocalRangeConfig(
minNoteDurationSeconds = 0.5f,
minConfidence = 0.7f
))
let analyzer = CalibraVocalRange.create()

let analyzer = CalibraVocalRange.create(config: VocalRangeConfig(
minNoteDurationSeconds: 0.5,
minConfidence: 0.7,
stabilityWindowMs: 50,
maxDeviationSemitones: 1.0,
sampleRate: 16000
))

Swift Config Convenience Methods

let config = VocalRangeConfig.fastDetection()     // 0.5s hold time
let config = VocalRangeConfig.strictDetection() // 1.5s hold, 0.7 confidence
let config = VocalRangeConfig.withMinDuration(2.0) // Custom hold time

Configuration

PropertyTypeDefaultDescription
minNoteDurationSecondsFloat1.0Minimum duration for a note to be included
minConfidenceFloat0.5Minimum pitch detection confidence
stabilityWindowMsFloat50Window for stability checking (ms)
maxDeviationSemitonesFloat1.0Maximum deviation for "stable" pitch
sampleRateInt16000Expected audio sample rate

Methods

MethodDescription
addAudio(samples)Feed audio samples for analysis
getRange()Get complete range (lower/upper/octaves), or null if not enough data
getStableNote()Get the longest stable note detected so far
getLowerLimit()Get 5th percentile pitch (lower bound)
getUpperLimit()Get 95th percentile pitch (upper bound)
getStats()Get detection statistics
reset()Reset for new detection session
release()Release resources

Result Types

VocalRange

PropertyTypeDescription
lowerVocalPitch5th percentile (physiological low)
upperVocalPitch95th percentile (physiological high)
octavesFloatRange in octaves
semitonesIntRange in semitones

VocalPitch

PropertyTypeDescription
frequencyHzFloatFrequency in Hz
midiNoteIntMIDI note number (0–127)
noteLabelStringNote name with octave (e.g., "C4", "F#3")
confidenceFloatDetection confidence (0.0–1.0)

DetectedNote

PropertyTypeDescription
pitchVocalPitchThe detected pitch
durationSecondsFloatHow long this note was held stably
isStableBooleanWhether the note met stability criteria

RangeStats

PropertyTypeDescription
totalPitchesReceivedIntTotal pitch frames processed
validPitchesAfterFilteringIntFrames that passed confidence filter
stableSegmentsDetectedIntNumber of stable segments found
longestStableSegmentSecondsFloatDuration of longest stable segment
hasEnoughDataForRangeBooleanWhether enough data for range calculation

Guided Range Detection

For step-by-step range finding (e.g., "sing your lowest note"):

val analyzer = CalibraVocalRange.create()

// Phase 1: Detect lowest note
showPrompt("Sing your lowest comfortable note")
// ... feed audio via addAudio() ...
val lowestNote = analyzer.getStableNote()
analyzer.reset()

// Phase 2: Detect highest note
showPrompt("Sing your highest comfortable note")
// ... feed audio via addAudio() ...
val highestNote = analyzer.getStableNote()

analyzer.release()

Static Utility Methods

val label = CalibraVocalRange.labelForMidi(60)  // "C4"
val midi = CalibraVocalRange.hzToMidi(440f) // 69
val hz = CalibraVocalRange.midiToHz(69) // 440.0
let label = CalibraVocalRange.labelForMidi(60)  // "C4"
let midi = CalibraVocalRange.hzToMidi(440) // 69
let hz = CalibraVocalRange.midiToHz(69) // 440.0

VocalRangeSession (High-Level)

A high-level session with observable state that manages the full detection flow: countdown, detect low note, transition, detect high note, complete.

Creating a Session

val detector = CalibraPitch.createDetector()
val session = VocalRangeSession.create(detector = detector)
let detector = CalibraPitch.createDetector()
let session = VocalRangeSession.create(detector: detector)

Session Configuration

PropertyTypeDefaultDescription
countdownSecondsInt3Countdown before detection starts
maxDetectionTimeSecondsInt10Max time to wait for stable note
minNoteDurationSecondsFloat1.0Minimum stable note duration
minConfidenceFloat0.5Minimum pitch confidence
transitionDelayMsLong500Delay between low and high detection
autoFlowBooleantrueRun automatic flow

Presets

PresetKotlinDescription
DefaultVocalRangeSessionConfig.DEFAULTAuto-flow with 3s countdown
ManualVocalRangeSessionConfig.MANUAL_FLOWManual phase control
val session = VocalRangeSession.create(detector = CalibraPitch.createDetector())

// Observe state in UI
session.state.collect { state ->
when (state.phase) {
VocalRangePhase.COUNTDOWN -> showCountdown(state.countdownSeconds)
VocalRangePhase.DETECTING_LOW -> showLowDetection(state)
VocalRangePhase.DETECTING_HIGH -> showHighDetection(state)
VocalRangePhase.COMPLETE -> showResult(state.result!!)
else -> {}
}
}

// Start detection
session.start()

// Feed audio
recorder.audioBuffers.collect { buffer ->
session.addAudio(buffer.samples, sampleRate = buffer.sampleRate)
}

// User taps "Lock" button
session.confirmNote()

session.release()

Observing State

Kotlin (StateFlow)

session.state.collect { state ->
updatePhaseUI(state.phase)
updatePitchDisplay(state.currentPitch)
updateStabilityBar(state.stabilityProgress)
state.bestLowNote?.let { showBestLow(it) }
state.bestHighNote?.let { showBestHigh(it) }
state.result?.let { showFinalResult(it) }
}

VocalRangeState

PropertyTypeDescription
phaseVocalRangePhaseCurrent detection phase
countdownSecondsIntRemaining countdown seconds
phaseMessageStringUser-facing status message
currentPitchVocalPitch?Real-time detected pitch
currentAmplitudeFloatCurrent audio amplitude
stabilityProgressFloatProgress toward stable note (0.0–1.0)
bestLowNoteDetectedNote?Lowest stable note found so far
bestHighNoteDetectedNote?Highest stable note found so far
lowNoteDetectedNote?Locked low note (confirmed by user)
highNoteDetectedNote?Locked high note (confirmed by user)
resultVocalRangeResult?Final result when complete
errorString?Error message if detection failed

VocalRangePhase

PhaseDescription
IDLESession not started
COUNTDOWNCountdown before detection
DETECTING_LOWDetecting lowest comfortable note
TRANSITIONTransition between low and high
DETECTING_HIGHDetecting highest comfortable note
COMPLETEDetection complete, results available
CANCELLEDDetection cancelled or failed

VocalRangeResult

PropertyTypeDescription
lowVocalPitchLowest detected note
highVocalPitchHighest detected note
octavesFloatRange in octaves
semitonesIntRange in semitones
naturalShrutiVocalPitchCalculated natural key (Sa) for Indian Classical music

Session Methods

MethodDescription
addAudio(samples, sampleRate)Feed audio samples. Resamples to 16kHz internally.
start()Start auto-flow detection
confirmNote()Lock current best note and advance to next phase
cancel()Cancel the current session
reset()Reset to start a new session
release()Release all resources

Manual Flow Control

For custom UIs that don't use the auto-flow:

val config = VocalRangeSessionConfig(autoFlow = false)
val session = VocalRangeSession.create(config, detector = CalibraPitch.createDetector())

session.startPhase(VocalRangePhase.DETECTING_LOW)
// ... feed audio ...
session.advancePhase() // Move to DETECTING_HIGH
// ... feed audio ...
session.complete()
MethodDescription
startPhase(phase)Start a specific detection phase
advancePhase()Advance to the next phase
complete()Finalize and calculate result

Next Steps