Skip to main content

Utilities & Shared Types

Shared model types, error types, and time utilities used across Calibra APIs.

LessonMaterial

Reference material for singing evaluation. Contains the reference audio, segment boundaries, and musical key.

Creating LessonMaterial

From File

val material = LessonMaterial.fromFile(
audioPath = "/path/to/reference.mp3",
segments = segments,
keyHz = 261.63f // Middle C
)
let material = LessonMaterial.fromFile(
url: URL(fileURLWithPath: "/path/to/reference.mp3"),
segments: segments,
keyHz: 261.63
)

From Audio Samples

val material = LessonMaterial.fromAudio(
samples = audioSamples,
sampleRate = 16000,
segments = segments,
keyHz = 196.0f,
pitchContour = precomputedContour, // Optional: enables fast path
hpcpFrames = precomputedHpcp // Optional: for DTW alignment
)
let material = LessonMaterial.fromAudio(
samples: audioSamples,
sampleRate: 16000,
segments: segments,
keyHz: 196.0,
pitchContour: precomputedContour,
hpcpFrames: precomputedHpcp
)

Properties

PropertyTypeDescription
audioSourceAudioSourceSource of the reference audio
segmentsList<Segment>List of segments with timing and lyrics
keyHzFloatMusical key frequency in Hz (e.g., 261.63 for middle C)
pitchContourPitchContour?Pre-computed pitch contour (enables fast path, skipping YIN extraction)
hpcpFramesList<FloatArray>?Pre-computed HPCP frames for DTW alignment (each frame is 12 floats)
durationFloatTotal duration based on the last segment's end time
segmentCountIntNumber of segments

AudioSource

Represents the source of audio data for evaluation. A sealed class with three variants.

VariantPropertiesDescription
AudioSource.Filepath: StringAudio from a local file path
AudioSource.Urlurl: StringAudio from a URL (for future streaming support)
AudioSource.Samplessamples: FloatArray, sampleRate: IntRaw audio samples already in memory (default 16000 Hz)

Segment

A segment of a song or exercise with timing and optional lyrics. Supports both singalong (student sings with reference) and singafter (student sings after reference) modes.

Creating Segments

Kotlin

// Single segment
val segment = Segment(
index = 0,
startSeconds = 0.0f,
endSeconds = 5.0f,
lyrics = "Sa Re Ga Ma"
)

// Singafter segment (student sings after reference)
val segment = Segment(
index = 0,
startSeconds = 0.0f,
endSeconds = 5.0f,
lyrics = "Sa Re Ga Ma",
studentStartSeconds = 2.5f,
studentEndSeconds = 5.0f
)

// From parallel arrays
val segments = Segment.fromArrays(
starts = floatArrayOf(0f, 5f, 10f),
ends = floatArrayOf(5f, 10f, 15f),
lyrics = listOf("Line 1", "Line 2", "Line 3")
)

Swift

// Single segment
let segment = Segment.create(
index: 0,
startSeconds: 0.0,
endSeconds: 5.0,
lyrics: "Sa Re Ga Ma"
)

// Singafter segment
let segment = Segment.create(
index: 0,
startSeconds: 0.0,
endSeconds: 5.0,
lyrics: "Sa Re Ga Ma",
studentStartSeconds: 2.5,
studentEndSeconds: 5.0
)

Properties

PropertyTypeKotlinSwiftDescription
indexIntIntInt (via extension)Zero-based index of the segment
startSecondsFloatFloatDouble (via extension)Reference audio start time in seconds
endSecondsFloatFloatDouble (via extension)Reference audio end time in seconds
lyricsStringStringStringText/lyrics for this segment
studentStartSecondsFloat?Float?Float?When student recording starts (null = same as startSeconds)
studentEndSecondsFloat?Float?Float?When student recording ends (null = same as endSeconds)
durationFloatFloatDouble (via extension)Duration of the segment in seconds
isSingafterBooleanBooleanBoolTrue if student starts after reference
effectiveStudentStartFloatFloatDouble (via extension)Effective student start time (falls back to segment start)
effectiveStudentEndFloatFloatDouble (via extension)Effective student end time (falls back to segment end)
studentDurationFloatFloatDouble (via extension)Duration of the student recording portion

Factory Methods

MethodDescription
Segment.fromArrays(starts, ends, lyrics, studentStarts, studentEnds)Create segments from parallel arrays of start and end times

SegmentResult

Result of evaluating a single segment.

Properties

PropertyTypeDescription
segmentSegmentThe segment that was evaluated
scoreFloatOverall score for this segment (0.0 - 1.0)
pitchAccuracyFloatPitch accuracy component of the score (0.0 - 1.0)
attemptNumberIntWhich attempt this is (1-based, for retry tracking)
referencePitchPitchContourReference pitch contour for visualization
studentPitchPitchContourStudent pitch contour for visualization
scorePercentIntScore as a percentage (0-100)

Swift Pitch Data Extensions

In Swift, pitch contour data is accessed via tuple extensions:

let result: SegmentResult = ...

// Reference pitch data
let ref = result.referencePitchData
// ref.times: [Float], ref.pitchesHz: [Float], ref.pitchesMidi: [Float]

// Student pitch data
let student = result.studentPitchData
// student.times: [Float], student.pitchesHz: [Float], student.pitchesMidi: [Float]

Factory Method

val result = SegmentResult.create(
segment = segment,
score = 0.85f,
pitchAccuracy = 0.82f,
attemptNumber = 2,
referencePitch = refContour,
studentPitch = studentContour
)

SingingResult

Complete result of a singing evaluation session, aggregating results across all segments.

Properties

PropertyTypeDescription
overallScoreFloatAggregate score across all segments (0.0 - 1.0)
segmentResultsMap<Int, List<SegmentResult>>Map of segment index to list of attempts
aggregationResultAggregationHow the overall score was calculated
overallScorePercentIntOverall score as a percentage (0-100)
segmentCountIntNumber of segments evaluated
totalAttemptsIntNumber of total attempts across all segments

Methods

MethodReturn TypeDescription
latestScorePerSegment()Map<Int, Float>Get the latest score for each segment
bestScorePerSegment()Map<Int, Float>Get the best score for each segment
averageScorePerSegment()Map<Int, Float>Get the average score for each segment
latestResultPerSegment()Map<Int, SegmentResult>Get the latest result for each segment
latestScore(segmentIndex)Float?Latest score for a single segment, or null if not practiced
bestScore(segmentIndex)Float?Best score for a single segment, or null if not practiced

Single-segment accessors

latestScore / bestScore return one segment's score on both platforms. Swift uses the forSegment: label and native Int:

val score = result.latestScore(segmentIndex = 0)   // Float?
val best = result.bestScore(segmentIndex = 0) // Float?
if let score = result.latestScore(forSegment: 0) {
print("Score: \(Int(score * 100))%")
}
if let best = result.bestScore(forSegment: 0) {
print("Best: \(Int(best * 100))%")
}

Static Members

MemberDescription
SingingResult.EMPTYEmpty result constant (score 0, no segments)
SingingResult.calculateOverallScore(segmentResults, aggregation)Calculate overall score from segment results

ResultAggregation

How to aggregate multiple attempts per segment into a final score.

ValueDescription
LATESTUse the most recent attempt's score
BESTUse the highest score across all attempts
AVERAGEUse the average of all attempts

PracticePhase

Practice phase during a CalibraLiveEval session.

Phase Progressions

Singalong: IDLE -> SINGING -> EVALUATED

  • Student sings with the reference audio simultaneously

Singafter: IDLE -> LISTENING -> SINGING -> EVALUATED

  • Student listens to reference first, then sings during their turn
ValueDescription
IDLENot practicing - waiting to start
LISTENINGReference playing, student not recording yet (singafter only)
SINGINGStudent is being recorded and evaluated
EVALUATEDSegment complete, score available

SessionPhase

Current phase of a CalibraLiveEval session.

ValueDescription
IDLESession created but not started
READYReference loaded, ready to begin practicing
PRACTICINGActively capturing and evaluating audio for a segment
BETWEEN_SEGMENTSFinished one segment, waiting before next
COMPLETEDAll segments completed or session manually finished
CANCELLEDSession was cancelled
ERRORAn error occurred

SessionState

Current state of a CalibraLiveEval session. Exposed as a StateFlow from CalibraLiveEval.

Properties

PropertyTypeDescription
phaseSessionPhaseCurrent phase of the session
activeSegmentIndexInt?Index of segment being practiced, or null if none
activeSegmentSegment?The segment being practiced, or null if none
currentPitchFloatCurrent detected pitch in Hz (-1 for unvoiced)
currentAmplitudeFloatCurrent audio amplitude (0.0 - 1.0)
segmentProgressFloatProgress through current segment (0.0 - 1.0)
completedSegmentsSet<Int>Set of segment indices that have been completed
errorString?Error message if phase is ERROR, null otherwise
isPracticingBooleanTrue if session is actively practicing
canBeginSegmentBooleanTrue if session is ready to start or between segments
isFinishedBooleanTrue if session is finished (completed, cancelled, or error)
completedCountIntNumber of completed segments

Static Members

MemberKotlinSwiftDescription
IdleSessionState.IDLE.idleInitial idle state

ActiveSegmentState

State of the currently active segment during practice.

Properties

PropertyTypeDescription
segmentIndexIntIndex of the segment
segmentSegmentThe segment being practiced
currentPitchFloatCurrent detected pitch in Hz (-1 for unvoiced)
currentAmplitudeFloatCurrent audio amplitude (0.0 - 1.0)
elapsedSecondsFloatTime elapsed since segment started
isCapturingBooleanWhether audio is currently being captured
progressFloatProgress through the segment (0.0 - 1.0)
remainingSecondsFloatTime remaining in seconds
hasVoiceBooleanTrue if detected pitch is valid

SessionConfig

Configuration for a CalibraLiveEval session.

Presets

PresetKotlinSwiftDescription
DefaultSessionConfig.DEFAULT.defaultBalanced, auto-advancing
PracticeSessionConfig.PRACTICE.practiceRepeats until 70% or 3 attempts, best score
KaraokeSessionConfig.KARAOKE.karaokeAlways advances, one attempt
PerformanceSessionConfig.PERFORMANCE.performanceStrict, one attempt, no repetition

Builder

Kotlin

val config = SessionConfig.Builder()
.preset(SessionConfig.PRACTICE)
.scoreThreshold(0.6f)
.maxAttempts(5)
.resultAggregation(ResultAggregation.BEST)
.build()

Swift

let config = SessionConfig.Builder()
.preset(.practice)
.scoreThreshold(0.6)
.maxAttempts(5)
.resultAggregation(.best)
.build()

Config Properties

PropertyTypeDefaultDescription
autoAdvanceBooleantrueAutomatically advance to next segment
scoreThresholdFloat0Min score to auto-advance (0 = disabled)
maxAttemptsInt0Max attempts before forced advance (0 = unlimited)
resultAggregationResultAggregationLATESTHow to aggregate multiple attempts
hopSizeInt320Hop size between frames in samples (320 = 20 ms at 16 kHz, 2 frames per buffer per ADR-020)
autoPhaseTransitionBooleantrueAuto transition LISTENING to SINGING in singafter mode
autoSegmentDetectionBooleantrueAuto detect segment end from player time
playInterSegmentAudioBooleanfalsePlay authored intros/rests/count-ins between the playhead and a starting segment instead of seeking over them
scoreCalibrationScoreCalibrationIDENTITYCurve applied once at the scoring seam to every score (segment results, session score, scoreThreshold); default IDENTITY passes raw scores through

Builder Methods

MethodDescription
preset(config)Start from a preset configuration
autoAdvance(enabled)Enable or disable auto-advance
scoreThreshold(threshold)Set minimum score threshold (0 = disabled)
maxAttempts(max)Set maximum attempts (0 = unlimited)
resultAggregation(agg)Set how to aggregate multiple attempts
hopSize(samples)Set hop size between frames
autoPhaseTransition(enabled)Enable or disable auto phase transition
autoSegmentDetection(enabled)Enable or disable auto segment end detection
playInterSegmentAudio(enabled)Play authored intros/rests/count-ins instead of seeking over them
scoreCalibration(calibration)Set the score-calibration curve (preset or custom); see ScoreCalibration

ScoreCalibration

A curve that maps raw 0..1 evaluation scores to the 0..1 score a session reports. Applied exactly once, at the scoring seam, to every score a CalibraLiveEval session produces: per-segment results, the session score, and the SessionConfig.scoreThreshold the auto-advance policy compares against. Measurements (pitch accuracy, cents, durations) stay raw.

Presets

PresetDescription
ScoreCalibration.IDENTITYRaw scores pass through unchanged. The default.
ScoreCalibration.LENIENTGenerous low-and-mid boost: rough takes get visible credit.
ScoreCalibration.BALANCEDThe field-tested reference curve (preserved exactly for score-history continuity).
ScoreCalibration.STRICTCloser to raw: the middle is not flattered.

Custom curves

ScoreCalibration.custom(curve, name) builds a calibration from a ScoreCurve ((rawScore: Float) -> Float). The curve is validated at construction: it must be monotonic non-decreasing over 0..1, finite everywhere, and not constant, or custom throws IllegalArgumentException. Outputs are clamped to 0..1 at apply time.

val calibration = ScoreCalibration.custom({ raw -> raw * raw }, name = "squared")
val config = SessionConfig.Builder()
.scoreCalibration(calibration)
.build()

ScoringAlgorithm

Algorithm for computing note accuracy scores.

ValueDescription
SIMPLESimple threshold counting. Counts percentage of pitch samples within 35 cents of target. Good for beginners.
WEIGHTEDWeighted duration-aware scoring. Tighter thresholds, considers note duration. Good for advanced evaluation.

NoteEvalConfig

Configuration for note evaluation scoring.

Properties

PropertyTypeDefaultDescription
algorithmScoringAlgorithmSIMPLEAlgorithm for computing scores
boundaryToleranceMsInt0Milliseconds to skip at note start/end

Presets

PresetKotlinSwiftAlgorithmBoundary ToleranceDescription
LenientNoteEvalPreset.LENIENT.lenientSIMPLE200msBeginner-friendly
BalancedNoteEvalPreset.BALANCED.balancedSIMPLE100msStandard practice
StrictNoteEvalPreset.STRICT.strictWEIGHTED0msAdvanced/performance

For student key transposition, set studentKeyHz on the evaluation call (CalibraNoteEval.evaluate, CalibraMelodyEval via student.keyHz) or CalibraLiveEval.setStudentKeyHz(...) at runtime.

Breath types (moved to tessera)

The legacy calibra BreathMetrics { capacity, control, isValid } and the short-lived 2.0.0 BreathScore were both replaced by the unified tessera.model.BreathMetrics:

Old typeNew type
BreathMetrics { capacity: Float, control: Float, isValid: Boolean } (1.x calibra)BreathMetrics { controlScore, phrases, alignmentScore }
BreathScore { capacity: Float?, controlScore: Float } (2.0.0-only intermediate)BreathMetrics — capacity moved to phrases?.longestDuration
(none)BreathFunction — composable intermediate
(none)PhraseSummary — phrase-level structure
(none)Alignment match score is now BreathMetrics.alignmentScore: Float?

Error Types

Calibra follows the SDK-wide failure-semantics contract (ADR-022) and the exception hierarchy in ADR-011. There is no Calibra-specific exception type.

Failure semantics (ADR-022)

Failure kindHow it surfaces
SDK not initializedThrows VoxaTraceNotInitializedException (every facade calls VT.ensureInitialized() first)
License invalid / revokedThrows VoxaTraceKilledException
Caller bug / invalid input (empty samples, non-16kHz audio, malformed config)Throws IllegalArgumentException (via require())
Domain inconclusive (valid input, no usable result)Encoded in the return value — e.g. CalibraLiveEval.finishPracticingSegment() returns null, CalibraMelodyEval.evaluate(...) returns SingingResult.EMPTY — never thrown

All thrown types extend VoxaTraceException. See Authentication for VoxaTraceNotInitializedException / VoxaTraceKilledException details.

Kotlin

try {
val result = CalibraMelodyEval.evaluate(reference, student, extractor)
// Domain outcome: empty result when nothing could be scored.
if (result == SingingResult.EMPTY) {
println("Nothing to score (silent or unalignable recording)")
}
} catch (e: VoxaTraceNotInitializedException) {
println("Call VT.initialize(...) first")
} catch (e: IllegalArgumentException) {
// Caller bug, e.g. audio was not 16kHz.
println("Invalid input: ${e.message}")
}

Swift

IllegalArgumentException bridges through SKIE as a caller-catchable error (ADR-010 / ADR-022); domain outcomes come back as typed results, not optionals of optionals.

do {
let result = try CalibraMelodyEval.evaluate(
reference: reference,
student: student,
contourExtractor: extractor
)
if result == SingingResult.companion.EMPTY {
print("Nothing to score")
}
} catch {
// IllegalArgumentException (invalid input) or an uninitialized-SDK error.
print("Evaluation failed: \(error.localizedDescription)")
}

Next Steps