Table of Contents

Class KeyProfiler

Namespace
Celeritas.Core.Analysis
Assembly
Celeritas.dll

High-performance key detection using the Krumhansl-Schmuckler algorithm. Optimized with SIMD (AVX-512/AVX2/SSE2) for real-time analysis.

The algorithm correlates pitch-class distributions with psychological key profiles derived from empirical studies of tonal perception.

public static class KeyProfiler
Inheritance
KeyProfiler
Inherited Members

Methods

AnalyzeModulations(NoteBuffer, Rational, Rational)

Analyze key changes over time using a sliding window: the key profile of every window of windowSize whole notes, one window every stepSize, as a key "trajectory" through the piece. The trajectory's Points are those per-window readings; its DetectModulations() says where the key actually changes.

public static KeyTrajectory AnalyzeModulations(NoteBuffer buffer, Rational windowSize, Rational stepSize)

Parameters

buffer NoteBuffer
windowSize Rational
stepSize Rational

Returns

KeyTrajectory

Remarks

This is the statistical road to a piece's keys, and it needs no starting key: every window is profiled from its notes alone, weighed by duration as DetectFromBuffer(NoteBuffer) weighs them, so the points can be plotted and the opening key is read from the music. Analyze(NoteBuffer, KeySignature) is the harmonic road: it starts from a key the caller knows, reads chords and the line between them rather than windows, and tells a tonicization from a modulation, names the type and finds the pivot chord. Both decide where the key changes by the same rules — a key holds for a phrase, a chord is not a key, a key owns its phrase, its chromatic chords and non-harmonic tones are its own, an arpeggiated chord is that chord, a secondary dominant is not a modulation, a key is entered when its own notes return and heard from where its own chords began — so from the same opening key they place the same modulations, each at the positions it reads at (this one at its window positions, the detector at every chord); use this one to see how the key reading moves, and the detector to have the changes classified.

The window is the resolution of the trajectory, not the length a key must hold: DetectModulations() judges a change over a phrase (four whole notes, or the window when that is longer), whatever the window. A two-bar window over block chords holds two chords and reads as the key of the pitch class they share, which is why the modulations are no longer read off the points one window at a time.

Exceptions

ArgumentNullException

buffer is null.

ArgumentOutOfRangeException

windowSize or stepSize is not positive.

ChordKeyFit(ushort, KeySignature)

Compute how well a chord fits in a given key context. Returns dot product of chord mask with key profile (higher = better fit).

public static float ChordKeyFit(ushort chordMask, KeySignature key)

Parameters

chordMask ushort
key KeySignature

Returns

float

DetectFromBuffer(NoteBuffer)

Detect key from a NoteBuffer (extracts pitch class distribution automatically), weighing each note by how long it is held.

public static KeyDetectionResult DetectFromBuffer(NoteBuffer buffer)

Parameters

buffer NoteBuffer

Returns

KeyDetectionResult

Remarks

This is the only reading in the library that weighs duration, and how long a note is held is strong evidence about the tonal centre: a four-bar pedal should not count the same as a passing sixteenth. It therefore disagrees with DetectFromPitches(ReadOnlySpan<NoteEvent>) on the same notes whenever their durations differ — over 600 random mixed-duration passages the two named a different key in 390 — and agrees with it exactly when every note is the same length. Choose by which question you are asking: this one for "what key does this music sound like", that one for "what key do these notes belong to".

Exceptions

ArgumentNullException

buffer is null.

DetectFromPitches(ReadOnlySpan<NoteEvent>)

Detect key from an array of note events, counting each note once however long it is held.

public static KeyDetectionResult DetectFromPitches(ReadOnlySpan<NoteEvent> notes)

Parameters

notes ReadOnlySpan<NoteEvent>

Returns

KeyDetectionResult

Remarks

The durations on the notes are not read — the name is exact, this detects from their pitches — so a note held for four bars counts as much as a passing sixteenth and no more. For the duration-weighted reading use DetectFromBuffer(NoteBuffer), which is the same algorithm over a distribution built from how long each note sounds; the two agree exactly when every note is the same length and disagreed on 390 of 600 random mixed-duration passages when they were not.

An empty span returns the empty-input sentinel: C major with Confidence of 0 and an empty AllCorrelations array. Check the confidence (or that the correlations are non-empty) before treating the key as a real detection.

DetectFromPitches(ReadOnlySpan<int>)

Detect key from an array of MIDI pitches. Pitches outside the MIDI range are folded to their pitch class rather than skipped: the engine produces them itself, and a key is a question about pitch classes.

public static KeyDetectionResult DetectFromPitches(ReadOnlySpan<int> pitches)

Parameters

pitches ReadOnlySpan<int>

Returns

KeyDetectionResult

DetectFromPitches(string)

Detect key from a human-readable notation string, counting each note once however long the notation says it is held. Example: "C4 D4 E4 F4 G4 A4 B4"

public static KeyDetectionResult DetectFromPitches(string notation)

Parameters

notation string

Returns

KeyDetectionResult

Remarks

The durations written in the notation are parsed but not weighed; see DetectFromPitches(ReadOnlySpan<NoteEvent>) for what that means and DetectFromBuffer(NoteBuffer) for the reading that does weigh them. A notation string containing no notes returns the empty-input sentinel: C major with Confidence of 0 and an empty AllCorrelations array. Check the confidence (or that the correlations are non-empty) before treating the key as a real detection.

Exceptions

ArgumentNullException

notation is null.

GetKeyProfile(int, bool)

Get the key profile for visualization or advanced analysis.

public static ReadOnlySpan<float> GetKeyProfile(int root, bool isMajor)

Parameters

root int

Pitch class of the tonic, 0=C .. 11=B. Folded into that range, as in GetScaleMask(int, bool), so -1 is B and 12 is C.

isMajor bool

Whether to return the major or the minor profile.

Returns

ReadOnlySpan<float>