Live Evaluation
A complete guide to real-time singing evaluation with CalibraLiveEval.
What You'll Learn
- Score singing against a reference melody in real-time
- Configure singalong and singafter modes
- Handle segment-based practice with auto-advance
- Display pitch visualization with reference comparison
Prerequisites
- VoxaTrace installed
- Reference audio with segment annotations (LessonMaterial)
Quick Start
Kotlin
// 1. Create detector and session
val detector = CalibraPitch.createDetector()
val session = CalibraLiveEval.create(
reference = lessonMaterial,
detector = detector
)
// 2. Prepare (loads reference features)
session.prepareSession()
// 3. Start segment
session.startPracticingSegment(0)
// 4. Feed audio
recorder.audioBuffers.collect { buffer ->
session.feedAudioSamples(buffer.toFloatArray(), buffer.sampleRate)
}
// 5. Get result
val result = session.finishPracticingSegment()
println("Score: ${result?.score}")
// 6. Cleanup
session.closeSession()
Swift
// 1. Create detector and session
let detector = CalibraPitch.createDetector()
let session = CalibraLiveEval.create(
reference: lessonMaterial,
detector: detector
)
// 2. Prepare
try await session.prepareSession()
// 3. Start segment
session.startPracticingSegment(index: 0)
// 4. Feed audio
for await buffer in recorder.audioBuffersStream() {
session.feedAudioSamples(buffer.samples, sampleRate: Int(buffer.sampleRate))
}
// 5. Get result
if let result = session.finishPracticingSegment() {
print("Score: \(result.score)")
}
// 6. Cleanup
session.closeSession()
Low-Level vs Convenience API
Low-Level API
Full control over audio flow:
val session = CalibraLiveEval.create(
reference = lessonMaterial,
detector = detector
)
session.prepareSession()
session.startPracticingSegment(0)
// You manage recorder
recorder.start()
recorder.audioBuffers.collect { buffer ->
session.feedAudioSamples(buffer.toFloatArray(), buffer.sampleRate)
}
recorder.stop()
val result = session.finishPracticingSegment()
Convenience API
Library coordinates playback and recording:
val session = CalibraLiveEval.create(
reference = lessonMaterial,
detector = detector,
player = player, // Library controls
recorder = recorder // Library controls
)
session.prepareSession()
// Register callbacks
session.onSegmentComplete { result ->
showScore(result)
}
// Single call handles everything
session.startPracticingSegment(0) // Seeks, plays, records, scores
Configuration
Auto-Advance
Automatically move to next segment:
val session = CalibraLiveEval.create(
reference = lessonMaterial,
session = SessionConfig.Builder()
.autoAdvance(true)
.build(),
detector = detector
)
Score Threshold
Require minimum score before advancing:
val session = CalibraLiveEval.create(
reference = lessonMaterial,
session = SessionConfig.Builder()
.scoreThreshold(0.7f) // Must score 70%
.maxAttempts(3) // Up to 3 retries
.autoAdvance(true)
.build(),
detector = detector
)
Key Transposition
When student sings in a different key:
session.setStudentKeyHz(referenceKeyHz * 0.89f) // 2 semitones down
Observing State
// Session state
session.state.collect { state ->
when (state.phase) {
SessionPhase.IDLE -> showIdleUI()
SessionPhase.READY -> showReadyUI()
SessionPhase.PRACTICING -> {
updateProgress(state.segmentProgress)
showPitch(state.currentPitch)
}
SessionPhase.BETWEEN_SEGMENTS -> showScoreUI()
SessionPhase.COMPLETED -> showCompletionUI()
}
}
// Practice phase (singalong/singafter)
session.phase.collect { phase ->
when (phase) {
PracticePhase.LISTENING -> showListeningIndicator()
PracticePhase.SINGING -> showSingingIndicator()
PracticePhase.EVALUATED -> hideIndicators()
}
}
// Live pitch for visualization
session.livePitchContour.collect { contour ->
drawPitchCurve(contour)
}
Segment Control
// Jump to specific segment
session.startPracticingSegment(2)
// Retry current segment
session.retryCurrentSegment()
// Cancel without scoring
session.discardCurrentSegment()
// End early and get result
val result = session.finishPracticingSegment()
// Navigate between segments
segmentButtons.forEachIndexed { index, button ->
button.onClick { session.startPracticingSegment(index) }
}
Working with Results
session.onSegmentComplete { result ->
// Show score
scoreLabel.text = "${(result.score * 100).toInt()}%"
// Show performance level
levelLabel.text = result.level.name // NEEDS_WORK, FAIR, GOOD, VERY_GOOD, EXCELLENT
// Draw pitch comparison
drawPitchComparison(
reference = result.referencePitch,
student = result.studentPitch
)
// Handle based on score
when {
result.score >= 0.9f -> showCelebration()
result.score >= 0.7f -> showEncouragement()
else -> offerRetry()
}
}
Complete Example
class PracticeViewModel : ViewModel() {
private var session: CalibraLiveEval? = null
val state = MutableStateFlow<SessionState?>(null)
val currentResult = MutableStateFlow<SegmentResult?>(null)
suspend fun startSession(lesson: LessonMaterial, player: SonixPlayer, recorder: SonixRecorder) {
val detector = CalibraPitch.createDetector()
session = CalibraLiveEval.create(
reference = lesson,
session = SessionConfig.Builder()
.preset(SessionConfig.PRACTICE)
.autoAdvance(true)
.scoreThreshold(0.6f)
.build(),
detector = detector,
player = player,
recorder = recorder
)
session?.prepareSession()
// Observe state
viewModelScope.launch {
session?.state?.collect { state.value = it }
}
// Handle segment completion
session?.onSegmentComplete { result ->
currentResult.value = result
}
// Handle session completion
session?.onSessionComplete { result ->
showFinalScore(result.overallScore)
}
}
fun startPracticingSegment(index: Int) {
session?.startPracticingSegment(index)
}
fun retryCurrentSegment() {
session?.retryCurrentSegment()
}
fun cleanup() {
session?.closeSession()
session = null
}
}
Next Steps
- Live Evaluation Concepts - Theory and background
- Karaoke App Recipe - Complete example