Skip to main content

CalibraNoteEval

Offline note/exercise evaluation for scales, arpeggios, and svara patterns. Scores how accurately a student performs individual notes in a sequence, with per-note feedback and performance level classification.

Quick Start

Kotlin

val pattern = ExercisePattern(
noteFrequencies = listOf(261.63f, 293.66f, 329.63f), // C4, D4, E4
noteDurations = listOf(500, 500, 500), // 500ms each
notesPerLoop = 3
)

val studentContour = pitchExtractor.extract(studentAudio, 16000)
val result = CalibraNoteEval.evaluate(pattern, studentContour, referenceKeyHz = 261.63f)

println("Score: ${result.scorePercent}%")
result.noteResults.forEach { note ->
println("Note ${note.noteIndex}: ${note.scorePercent}%")
}

Swift

let pattern = ExercisePattern.create(
noteFrequencies: [261.63, 293.66, 329.63], // C4, D4, E4
noteDurations: [500, 500, 500],
notesPerLoop: 3
)

let studentContour = pitchExtractor.extract(audio: studentAudio, sampleRate: 16000)
let result = CalibraNoteEval.evaluate(
pattern: pattern,
student: studentContour,
referenceKeyHz: 261.63
)

print("Score: \(result.scorePercent)%")
for note in result.noteResults {
print("Note \(note.noteIndex): \(note.scorePercent)%")
}

When to Use

ScenarioUse This?Why
Evaluate scales/exercisesYesPer-note scoring
Evaluate complete songsNoUse CalibraMelodyEval
Real-time scoringNoUse CalibraLiveEval
Just detect pitchNoUse CalibraPitch

Configuration

Presets

PresetKotlinSwiftAlgorithmBoundary ToleranceDescription
LenientNoteEvalPreset.LENIENT.lenientSimple200msBeginner-friendly, forgiving on timing
BalancedNoteEvalPreset.BALANCED.balancedSimple100msStandard practice scoring
StrictNoteEvalPreset.STRICT.strictWeighted0msAdvanced/performance scoring

Kotlin

// Use a preset (recommended)
val result = CalibraNoteEval.evaluate(
pattern = pattern,
student = studentContour,
referenceKeyHz = 261.63f,
preset = NoteEvalPreset.LENIENT
)

Swift

// Use a preset (recommended - default is .balanced)
let result = CalibraNoteEval.evaluate(
pattern: pattern,
student: studentContour,
referenceKeyHz: 261.63,
preset: .lenient
)

NoteEvalConfig

For fine-grained control, create a NoteEvalConfig directly.

PropertyTypeDefaultDescription
algorithmScoringAlgorithmSIMPLEScoring algorithm (SIMPLE or WEIGHTED)
boundaryToleranceMsInt0Milliseconds to skip at note start/end boundaries

Kotlin

val config = NoteEvalConfig(
algorithm = ScoringAlgorithm.WEIGHTED,
boundaryToleranceMs = 150
)

val result = CalibraNoteEval.evaluate(
pattern = pattern,
student = studentContour,
referenceKeyHz = 261.63f,
config = config
)

Swift

let config = NoteEvalConfig(
algorithm: .weighted,
boundaryToleranceMs: 150
)

let result = CalibraNoteEval.evaluate(
pattern: pattern,
student: studentContour,
referenceKeyHz: 261.63,
config: config
)

ScoringAlgorithm

AlgorithmDescription
SIMPLECounts percentage of pitch samples within 35 cents of target. Good for beginners and practice.
WEIGHTEDDuration-aware scoring with tighter thresholds. Stricter on longer notes, more forgiving on short notes. Good for advanced evaluation.

ExercisePattern

Defines the note sequence for evaluation, including frequencies, durations, and loop structure.

Constructor

Kotlin

val pattern = ExercisePattern(
noteFrequencies = listOf(261.63f, 293.66f, 329.63f, 349.23f, 392.00f),
noteDurations = listOf(500, 500, 500, 500, 500),
notesPerLoop = 5 // defaults to noteFrequencies.size
)

Swift

let pattern = ExercisePattern.create(
noteFrequencies: [261.63, 293.66, 329.63, 349.23, 392.00],
noteDurations: [500, 500, 500, 500, 500],
notesPerLoop: 5 // defaults to noteFrequencies.count
)

Constructor Parameters

ParameterTypeDefaultDescription
noteFrequenciesList<Float>requiredFrequencies in Hz for each note in the pattern
noteDurationsList<Int>requiredDuration in milliseconds for each note
notesPerLoopIntnoteFrequencies.sizeNumber of notes per loop/cycle (for repeating patterns)

Validation rules:

  • noteFrequencies and noteDurations must have the same size
  • Pattern must have at least one note
  • notesPerLoop must be between 1 and pattern length

Properties

PropertyTypeDescription
totalDurationMsIntTotal duration of the pattern in milliseconds
noteCountIntNumber of notes in the pattern

Factory Methods

scale -- Create from Uniform Durations

// Kotlin
val pattern = ExercisePattern.scale(
frequencies = listOf(261.63f, 293.66f, 329.63f, 349.23f, 392.00f),
noteDurationMs = 500 // default: 500
)
// Swift
let pattern = ExercisePattern.scale(
frequencies: [261.63, 293.66, 329.63, 349.23, 392.00],
noteDurationMs: 500 // default: 500
)

fromMidiNotes -- Create from MIDI Note Numbers

// Kotlin — MIDI 60 = middle C
val pattern = ExercisePattern.fromMidiNotes(
midiNotes = listOf(60, 62, 64, 65, 67),
noteDurationMs = 500 // default: 500
)
// Swift
let pattern = ExercisePattern.fromMidiNotes(
midiNotes: [60, 62, 64, 65, 67],
noteDurationMs: 500 // default: 500
)

Evaluation

evaluate with Preset

Kotlin

fun evaluate(
pattern: ExercisePattern,
student: PitchContour,
referenceKeyHz: Float,
studentKeyHz: Float = 0f,
preset: NoteEvalPreset
): ExerciseResult

Swift

static func evaluate(
pattern: ExercisePattern,
student: PitchContour,
referenceKeyHz: Float,
studentKeyHz: Float = 0,
preset: NoteEvalPreset = .balanced
) -> ExerciseResult

evaluate with Config

Kotlin

fun evaluate(
pattern: ExercisePattern,
student: PitchContour,
referenceKeyHz: Float,
studentKeyHz: Float = 0f,
config: NoteEvalConfig = NoteEvalConfig.DEFAULT
): ExerciseResult

Swift

static func evaluate(
pattern: ExercisePattern,
student: PitchContour,
referenceKeyHz: Float,
studentKeyHz: Float = 0,
config: NoteEvalConfig
) -> ExerciseResult

evaluate with Raw Parameters

Kotlin

fun evaluate(
pattern: ExercisePattern,
student: PitchContour,
referenceKeyHz: Float,
studentKeyHz: Float = 0f,
scoreType: Int = 0,
leewaySamples: Int = 0
): ExerciseResult

Swift

static func evaluate(
pattern: ExercisePattern,
student: PitchContour,
referenceKeyHz: Float,
studentKeyHz: Float = 0,
scoreType: Int32 = 0,
leewaySamples: Int32 = 0
) -> ExerciseResult

Parameters

ParameterTypeDefaultDescription
patternExercisePatternrequiredExercise pattern with note frequencies and durations
studentPitchContourrequiredPitch contour from the student's performance
referenceKeyHzFloatrequiredReference key/tonic frequency in Hz
studentKeyHzFloat0Student's key frequency in Hz (0 = same as reference)
presetNoteEvalPreset.balanced (Swift)Evaluation preset
configNoteEvalConfigNoteEvalConfig.DEFAULTEvaluation configuration
scoreTypeInt0Scoring algorithm type (0 = simple, 1 = weighted)
leewaySamplesInt0Tolerance at note boundaries in pitch samples

Key Transposition

When a student sings in a different key than the reference, set studentKeyHz so the evaluator adjusts interval expectations accordingly.

Kotlin

val result = CalibraNoteEval.evaluate(
pattern = pattern,
student = studentContour,
referenceKeyHz = 261.63f, // Reference in C4
studentKeyHz = 277.18f, // Student sings in C#4
preset = NoteEvalPreset.BALANCED
)

Swift

let result = CalibraNoteEval.evaluate(
pattern: pattern,
student: studentContour,
referenceKeyHz: 261.63, // Reference in C4
studentKeyHz: 277.18, // Student sings in C#4
preset: .balanced
)

Result Types

ExerciseResult

Overall result of an exercise evaluation.

PropertyTypeDescription
scoreFloatOverall score (0.0 -- 1.0)
scorePercentIntScore as a percentage (0 -- 100)
noteResultsList<NoteResult>Per-note evaluation results
keyHzFloatKey frequency used for evaluation
noteCountIntNumber of notes evaluated
passingNotesIntNumber of notes with score >= 0.5
passingRatioFloatRatio of passing notes to total notes

ExerciseResult.EMPTY provides an empty result constant (score 0, no notes, key 261.63 Hz).

NoteResult

Result for a single note in the exercise.

PropertyTypeDescription
noteIndexIntIndex of the note in the pattern
expectedFrequencyHzFloatExpected frequency in Hz
scoreFloatScore for this note (0.0 -- 1.0)
scorePercentIntScore as a percentage (0 -- 100)
levelPerformanceLevelPerformance level classification
isPassingBooleanWhether the note is passing (score >= 0.5)

PerformanceLevel

Score-based classification for each note result.

LevelScore RangeDisplay Name
NEEDS_WORK< 0.3"Needs Work"
FAIR0.3 -- 0.6"Fair"
GOOD0.6 -- 0.8"Good"
VERY_GOOD0.8 -- 0.95"Very Good"
EXCELLENT>= 0.95"Excellent"
NOT_EVALUATEDN/A"Not Evaluated"
NOT_DETECTED< 0"No Voice"
Property / MethodTypeDescription
displayNameStringHuman-readable label for UI display
fromScore(score)PerformanceLevelClassify a score (0.0 -- 1.0) into a level
fromCode(code)PerformanceLevelConvert from integer code (for native interop)

Common Patterns

Scale Practice with Per-Note Feedback

// Kotlin
val cMajorScale = ExercisePattern.scale(
frequencies = listOf(261.63f, 293.66f, 329.63f, 349.23f, 392.00f, 440.00f, 493.88f, 523.25f),
noteDurationMs = 600
)

val result = CalibraNoteEval.evaluate(
pattern = cMajorScale,
student = studentContour,
referenceKeyHz = 261.63f,
preset = NoteEvalPreset.BALANCED
)

println("Overall: ${result.scorePercent}% (${result.passingNotes}/${result.noteCount} passing)")
result.noteResults.forEach { note ->
val status = if (note.isPassing) "PASS" else "FAIL"
println(" Note ${note.noteIndex}: ${note.scorePercent}% [${note.level.displayName}] $status")
}
// Swift
let cMajorScale = ExercisePattern.scale(
frequencies: [261.63, 293.66, 329.63, 349.23, 392.00, 440.00, 493.88, 523.25],
noteDurationMs: 600
)

let result = CalibraNoteEval.evaluate(
pattern: cMajorScale,
student: studentContour,
referenceKeyHz: 261.63,
preset: .balanced
)

print("Overall: \(result.scorePercent)% (\(result.passingNotes)/\(result.noteCount) passing)")
for note in result.noteResults {
let status = note.isPassing ? "PASS" : "FAIL"
print(" Note \(note.noteIndex): \(note.scorePercent)% [\(note.level.displayName)] \(status)")
}

MIDI-Based Arpeggio Exercise

// Kotlin — C major arpeggio: C4, E4, G4, C5
val arpeggio = ExercisePattern.fromMidiNotes(
midiNotes = listOf(60, 64, 67, 72),
noteDurationMs = 700
)

val result = CalibraNoteEval.evaluate(
pattern = arpeggio,
student = studentContour,
referenceKeyHz = 261.63f,
preset = NoteEvalPreset.STRICT
)
// Swift
let arpeggio = ExercisePattern.fromMidiNotes(
midiNotes: [60, 64, 67, 72],
noteDurationMs: 700
)

let result = CalibraNoteEval.evaluate(
pattern: arpeggio,
student: studentContour,
referenceKeyHz: 261.63,
preset: .strict
)

Beginner-Friendly Evaluation

// Kotlin — lenient preset with key transposition
val result = CalibraNoteEval.evaluate(
pattern = pattern,
student = studentContour,
referenceKeyHz = 261.63f,
studentKeyHz = 246.94f, // Student sings in B3
preset = NoteEvalPreset.LENIENT
)

// Show only encouragement for beginners
if (result.scorePercent >= 70) {
println("Great job!")
} else {
val weakNotes = result.noteResults.filter { !it.isPassing }
println("Practice notes: ${weakNotes.map { it.noteIndex }}")
}
// Swift
let result = CalibraNoteEval.evaluate(
pattern: pattern,
student: studentContour,
referenceKeyHz: 261.63,
studentKeyHz: 246.94,
preset: .lenient
)

if result.scorePercent >= 70 {
print("Great job!")
} else {
let weakNotes = result.noteResults.filter { !$0.isPassing }
print("Practice notes: \(weakNotes.map { $0.noteIndex })")
}

Platform Notes

iOS

  • Audio must be 16kHz mono; use SonixDecoder/SonixResampler to convert
  • Swift extensions provide idiomatic static methods on CalibraNoteEval (no .companion, no .shared)
  • Presets use lowercase Swift enum style (.lenient, .balanced, .strict)

Android

  • Audio must be 16kHz mono; use SonixDecoder/SonixResampler to convert
  • Works with any PitchContour from CalibraPitch

Common Pitfalls

  1. Wrong audio sample rate -- Audio must be 16kHz. Use SonixResampler if your source is 44.1kHz or 48kHz.
  2. Mismatched array sizes -- noteFrequencies and noteDurations must have the same length.
  3. Forgetting key transposition -- Set studentKeyHz if the student sings in a different key than the reference.
  4. Using raw parameters when presets suffice -- Prefer NoteEvalPreset or NoteEvalConfig over raw scoreType/leewaySamples for readability.

Next Steps