Skip to main content

Live Evaluation

Live evaluation scores singing in real-time by comparing the singer's pitch to a reference melody.

What is Live Evaluation?

Live evaluation answers: "How accurately is this person singing the reference melody?"

It works by:

  1. Extracting pitch from the reference audio (what they should sing)
  2. Detecting pitch from the student in real-time (what they're singing)
  3. Comparing the two and calculating a score

Use Cases

App TypeHow Live Evaluation Helps
KaraokeShow score as users sing along
Music educationProvide instant feedback on pitch accuracy
Practice appsTrack improvement over multiple attempts
Singing gamesGamify pitch accuracy

Key Concepts

Segments

Songs are divided into segments - typically phrases or sections. Each segment is evaluated independently.

Song Timeline:
[Segment 0: "Happy birthday"] [Segment 1: "to you"] [Segment 2: "Happy birthday"]
↓ ↓ ↓
Score: 85% Score: 92% Score: 78%

Benefits of segmentation:

  • Users can retry individual segments
  • Granular feedback per phrase
  • Easier progress tracking

Practice Modes

Singalong Mode

User sings simultaneously with the reference audio.

Reference: ──────────────────────────>
Student: ──────────────────────────>
^ ^
Start End

Best for: Karaoke-style apps where users sing along with music.

Singafter Mode

User listens first, then sings back the phrase.

Reference: ────────>
Student: ────────>
^ ^ ^
Listen Start End

Best for: Ear training apps where users echo what they hear.

Scores

Live evaluation returns several score types:

ScoreWhat It Measures
Overall ScoreCombined pitch + timing accuracy (0.0 to 1.0)
Pitch AccuracyHow many notes matched the reference pitch
Performance LevelQualitative rating (NEEDS_WORK, FAIR, GOOD, VERY_GOOD, EXCELLENT)

Session Lifecycle

1. Create Session

// Create pitch detector
val detector = CalibraPitch.createDetector()

// Create session with reference material
val session = CalibraLiveEval.create(
reference = lessonMaterial, // Contains audio + segment info
detector = detector
)

2. Prepare (Load Reference)

// Suspending - extracts features from reference audio
session.prepareSession()

3. Practice Segment

// Start practicing segment 0
session.startPracticingSegment(0)

// Feed audio from recorder
recorder.audioBuffers.collect { buffer ->
session.feedAudioSamples(buffer.toFloatArray(), buffer.sampleRate)
}

// End segment and get result
val result = session.finishPracticingSegment()
println("Score: ${result?.score}")

4. Cleanup

session.closeSession()  // Releases detector and resources

API Modes

Low-Level API

Full control over audio flow. You manage player and recorder.

val session = CalibraLiveEval.create(
reference = lessonMaterial,
detector = CalibraPitch.createDetector()
)

session.prepareSession()
session.startPracticingSegment(0)

// You control when audio is fed
myRecorder.audioFlow.collect { buffer ->
session.feedAudioSamples(buffer.samples, buffer.sampleRate)
}

val result = session.finishPracticingSegment()
session.closeSession()

Convenience API

Library coordinates playback, recording, and scoring automatically.

val session = CalibraLiveEval.create(
reference = lessonMaterial,
detector = CalibraPitch.createDetector(),
player = myPlayer, // Library controls playback
recorder = myRecorder // Library controls recording
)

session.prepareSession()

// Register callbacks
session.onSegmentComplete { result ->
showScore(result.score)
}

// Single call handles everything
session.startPracticingSegment(0) // Starts practicing the first segment

Observing State

Use StateFlows to observe session state in your UI:

// Observe session phase
session.state.collect { state ->
when (state.phase) {
SessionPhase.IDLE -> showIdleUI()
SessionPhase.READY -> showReadyUI()
SessionPhase.PRACTICING -> showPracticingUI(state.segmentProgress)
SessionPhase.BETWEEN_SEGMENTS -> showScoreUI()
SessionPhase.COMPLETED -> showCompletionUI()
}
}

// Observe live pitch for visualization
session.livePitchContour.collect { contour ->
drawPitchCurve(contour)
}

// Observe practice phase (singalong/singafter)
session.phase.collect { phase ->
when (phase) {
PracticePhase.IDLE -> // Waiting
PracticePhase.LISTENING -> // Listening to reference
PracticePhase.SINGING -> // User is singing
PracticePhase.EVALUATED -> // Segment finished
}
}

Configuration

Auto-Advance

Automatically move to next segment after scoring:

val session = CalibraLiveEval.create(
reference = lessonMaterial,
session = SessionConfig.Builder()
.autoAdvance(true)
.build(),
detector = detector
)

Score Threshold

Require minimum score before advancing (for practice apps):

val session = CalibraLiveEval.create(
reference = lessonMaterial,
session = SessionConfig.Builder()
.scoreThreshold(0.7f) // Must score 70% to advance
.maxAttempts(3) // Allow 3 retries
.build(),
detector = detector
)

Key Transposition

When student sings in a different key than reference:

// Student sings 2 semitones lower
session.setStudentKeyHz(referenceKeyHz * 0.89f) // 2 semitones down = 0.89x

Segment Result

Each segment returns detailed results:

data class SegmentResult(
val segment: Segment, // Which segment
val score: Float, // Overall score (0.0 to 1.0)
val pitchAccuracy: Float, // Pitch accuracy (0.0 to 1.0)
val level: PerformanceLevel, // NEEDS_WORK, FAIR, GOOD, VERY_GOOD, EXCELLENT
val attemptNumber: Int, // Which attempt this was
val referencePitch: PitchContour, // What they should have sung
val studentPitch: PitchContour // What they actually sung
)

Use pitch contours for visualization:

session.onSegmentComplete { result ->
// Draw reference and student pitch on same graph
drawPitchComparison(
reference = result.referencePitch,
student = result.studentPitch
)

// Show score
showScore(result.score, result.level)
}

Ownership Model

Understanding resource ownership prevents memory leaks:

ResourceOwned ByCleanup
DetectorSessionsession.closeSession() releases it
PlayerCallerYou call player.release()
RecorderCallerYou call recorder.release()
// Correct cleanup order
session.closeSession() // Releases detector
player.release() // You manage player
recorder.release() // You manage recorder

Common Patterns

Retry Segment

session.onSegmentComplete { result ->
if (result.score < 0.7f) {
showRetryButton {
session.retryCurrentSegment()
}
} else {
session.startPracticingSegment(result.segment.index + 1)
}
}

Seek to Segment

// Jump to any segment
segmentButtons.forEachIndexed { index, button ->
button.onClick {
session.startPracticingSegment(index)
}
}

Cancel Mid-Segment

cancelButton.onClick {
session.discardCurrentSegment() // Discards the current attempt
}

Next Steps