Class KeyAnalyzer
Functional harmony analyzer using bitwise operations for performance
public static class KeyAnalyzer
- Inheritance
-
KeyAnalyzer
- Inherited Members
Fields
MajorScaleMask
12-bit pitch-class mask of the C major scale (bits 0,2,4,5,7,9,11).
public const ushort MajorScaleMask = 2741
Field Value
MinorScaleMask
12-bit pitch-class mask of the C natural-minor scale (bits 0,2,3,5,7,8,10).
public const ushort MinorScaleMask = 1453
Field Value
Methods
Analyze(NoteEvent[], KeySignature)
Analyze chord in the context of a key signature (NoteEvent array overload)
public static RomanNumeralChord Analyze(NoteEvent[] notes, KeySignature key)
Parameters
notesNoteEvent[]keyKeySignature
Returns
Exceptions
- ArgumentNullException
notesis null.
Analyze(int[], KeySignature)
Analyze chord in the context of a key signature (array overload). The degrees of a major key are the seven of its scale; the degrees of a minor key are the seven of natural minor with the raised seventh of harmonic minor beside the natural one, so that the leading-tone chord is read as vii°, vii°7 or viiø7 (Dominant function) and the subtonic major triad as VII, both at Vii. Only a diminished-family chord is read on the raised seventh: a major or minor triad there belongs to no form of the minor scale and stays Invalid.
public static RomanNumeralChord Analyze(int[] pitches, KeySignature key)
Parameters
pitchesint[]keyKeySignature
Returns
Remarks
The minor key's degree map was natural minor alone, so the interval of eleven semitones above the tonic mapped to no degree and Bdim7 in C minor — the leading-tone seventh, the second commonest chord in a minor key after its dominant — was reported as Invalid, "?" and "Chromatic (outside the key)" in a progression report whose own highlight called it the harmonic-minor raised seventh, while G7 on the same raised seventh read V7. A chord read at Vii in a minor key spells its root back through GetRootPitchClass(KeySignature) by its quality: the leading tone for the diminished family, the subtonic for the rest.
Exceptions
- ArgumentNullException
pitchesis null.
DetectKey(NoteBuffer)
Alias for IdentifyKey for more intuitive API.
public static KeySignature DetectKey(NoteBuffer buffer)
Parameters
bufferNoteBuffer
Returns
Remarks
Each note in the buffer counts once towards the key, however long it is held; see IdentifyKey(ReadOnlySpan<int>) for the algorithm and its documented answers for undecidable input. For duration-weighted detection use DetectFromBuffer(NoteBuffer).
Exceptions
- ArgumentNullException
bufferis null.
DetectKey(ReadOnlySpan<NoteEvent>)
Alias for IdentifyKey for more intuitive API.
public static KeySignature DetectKey(ReadOnlySpan<NoteEvent> notes)
Parameters
notesReadOnlySpan<NoteEvent>
Returns
Remarks
Each note counts once towards the key, however long it is held; see IdentifyKey(ReadOnlySpan<int>) for the algorithm and its documented answers for undecidable input. For duration-weighted detection use DetectFromBuffer(NoteBuffer), which is the one reading in the library that weighs how long a note is held. DetectFromPitches(ReadOnlySpan<NoteEvent>) is not it: as its name says, it reads the pitches and not the durations, and named here it sent a caller who wanted duration weighting to the algorithm that has none.
DetectKey(string)
Alias for IdentifyKey for more intuitive API.
Example: DetectKey("G4 B4 D5 G5 D5 B4 G4") -> G major.
public static KeySignature DetectKey(string notation)
Parameters
notationstring
Returns
Remarks
How often a note appears in notation is evidence and is counted: see
IdentifyKey(ReadOnlySpan<int>) for the algorithm and for what is returned
when the notation cannot decide the key (blank text, a bare scale, a chromatic run).
Note durations written in the notation are ignored; only the notes themselves are
weighed.
Exceptions
- ArgumentNullException
notationis null.
GetScaleMask(int, bool)
Pitch-class mask of the major or natural-minor scale rooted at root (0=C…11=B).
public static ushort GetScaleMask(int root, bool isMajor)
Parameters
Returns
IdentifyKey(int[])
Identify key signature from a collection of pitches (array overload). See IdentifyKey(ReadOnlySpan<int>) for the algorithm and for the conventions used where the input cannot decide the key.
public static KeySignature IdentifyKey(int[] pitches)
Parameters
pitchesint[]
Returns
Exceptions
- ArgumentNullException
pitchesis null.
IdentifyKey(ReadOnlySpan<int>)
Identify the key of a collection of pitches: the 24 major and natural-minor scales are first ranked by how many of the input's pitch classes they contain, and the candidates that tie for that best overlap are then separated by how heavily the input weights each of their scale degrees.
public static KeySignature IdentifyKey(ReadOnlySpan<int> pitches)
Parameters
pitchesReadOnlySpan<int>
Returns
Remarks
Scale overlap alone cannot answer this question. A key and its relative (G major and E minor) have identical pitch-class sets, so they always tie; so does any key whose scale merely happens to contain the notes played (a melody on G B D A C sits inside C major, G major, A minor and E minor alike). Which of those a listener hears is decided by emphasis — how often the tonic and the other structural degrees actually sound — and that is why this overload reads a multiset of pitches rather than a set. Repeating a note is evidence, and it is counted.
The tie-break scores each surviving candidate by correlating the input's pitch-class counts against that key's Krumhansl-Kessler profile, the same weights KeyProfiler uses, read from GetKeyProfile(int, bool) so the two analyzers cannot drift apart. Keeping the overlap prefilter in front of it preserves a guarantee KeyProfiler does not make: where some scale contains every pitch class sounded, the key returned is one of those scales — KeyProfiler may name a key whose scale omits a note that is plainly sounding. The two therefore agree on material that decides the key, but can differ on material that does not: this method also divides out the major bias documented in KeyProfiler, which is enough to tip a near-tie between relatives.
Note counts are the weighting; note durations are not. A caller who wants a held whole note to outweigh a passing sixteenth wants DetectFromBuffer(NoteBuffer), which weights by duration.
Documented conventions where the input cannot decide. These are fixed answers, not artifacts of iteration order:
- Empty input returns
C major. - A bare scale — every pitch class of one diatonic set sounded equally often — returns the relative major (a plain G-major scale is G major, not E minor). The Krumhansl-Kessler weights lean that way for an evenly-weighted diatonic set; the margin is thin, and callers who need the distinction must supply material that emphasizes a tonic.
- An input that leaves several candidates scoring identically — a
diminished seventh, an augmented triad, all twelve pitch classes sounded equally often —
is settled by the bass: the tied key rooted nearest above the lowest sounding note wins,
major before minor at equal distance. That keeps the answer equivariant, so
transposing the passage transposes the answer with it. An earlier rule took the lowest
root, which made the answer depend on absolute position: a diminished seventh moved up a
semitone reported a key a fourth away. A chromatic run beginning on C still returns
C major.
This method returns a bare KeySignature and so cannot report how thin the winning margin was, and it will answer a genuinely undecided input as confidently as a decided one. Callers who slide a window across music and must not mistake an ambiguous window for a key change — modulation detection above all — need the margin as well as the answer: use DetectFromPitches(ReadOnlySpan<int>) and gate on its Confidence, as DetectModulations() does.
RotateLeft(ushort, int)
Cyclic left rotation for 12-bit mask. Moves bit k to bit (k+shift) mod 12,
i.e. transposes a pitch-class mask UP by shift semitones.
A negative shift rotates the other way, so
RotateLeft(v, -1) == RotateRight(v, 1).
public static ushort RotateLeft(ushort value, int shift)
Parameters
Returns
RotateRight(ushort, int)
Cyclic right rotation (ROR) for 12-bit mask. Moves bit k to bit (k-shift) mod 12,
i.e. transposes a pitch-class mask DOWN by shift semitones.
A negative shift rotates the other way, so
RotateRight(v, -1) == RotateLeft(v, 1).
To transpose a scale to a root, use GetScaleMask(int, bool).
public static ushort RotateRight(ushort value, int shift)