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 scores.

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 extractor = PitchDetection.createContourExtractor(ContourExtractorConfig.SCORING)
val studentContour = extractor.extract(studentAudio, 16000)
extractor.release()
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 extractor = PitchDetection.createContourExtractor(config: .scoring)
let studentContour = extractor.extract(audio: studentAudio, sampleRate: 16000)
extractor.release()
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 PitchDetection

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)
noteStartsMsList<Int>?nullExplicit note-start offsets (ms) on the take's playback timeline. null lays notes contiguously from t=0; explicit starts express a lead-in and inter-note gaps (see fromNotes)

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
  • noteStartsMs, when provided, must have the same size as noteFrequencies

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
)

fromNotes -- Create from Explicit Note Windows​

Real lessons rarely start at t=0: there is a lead-in before the first note and gaps or rests between notes. fromNotes places each note at its actual window on the playback timeline, and the evaluator scores each note only against the student audio inside that window — audio in the gaps is not evaluated.

// Kotlin — three notes with a 1s lead-in and 250ms gaps
val pattern = ExercisePattern.fromNotes(
freqsHz = listOf(261.63f, 293.66f, 329.63f),
startsMs = listOf(1000, 1750, 2500),
endsMs = listOf(1500, 2250, 3000)
)
// Swift
let pattern = ExercisePattern.fromNotes(
freqsHz: [261.63, 293.66, 329.63],
startsMs: [1000, 1750, 2500],
endsMs: [1500, 2250, 3000]
)

Pass true target frequencies: exercise targets are the recorded artist's (or synthesizer's) actual pitches, which may sit off the equal-tempered grid by design. Do not snap them to MIDI note numbers.

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

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)
isPassingBooleanWhether the note is passing (score >= 0.5)

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
)

val passing = result.noteResults.count { it.isPassing }
println("Overall: ${result.scorePercent}% ($passing/${result.noteCount} passing)")
result.noteResults.forEach { note ->
val status = if (note.isPassing) "PASS" else "FAIL"
println(" Note ${note.noteIndex}: ${note.scorePercent}% $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
)

let passing = result.noteResults.filter { $0.isPassing }.count
print("Overall: \(result.scorePercent)% (\(passing)/\(result.noteCount) passing)")
for note in result.noteResults {
let status = note.isPassing ? "PASS" : "FAIL"
print(" Note \(note.noteIndex): \(note.scorePercent)% \(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 PitchDetection

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​