Table of Contents

Class KeyAnalyzer

Namespace
Celeritas.Core
Assembly
Celeritas.dll

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

ushort

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

ushort

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

notes NoteEvent[]
key KeySignature

Returns

RomanNumeralChord

Exceptions

ArgumentNullException

notes is 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

pitches int[]
key KeySignature

Returns

RomanNumeralChord

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

pitches is null.

DetectKey(NoteBuffer)

Alias for IdentifyKey for more intuitive API.

public static KeySignature DetectKey(NoteBuffer buffer)

Parameters

buffer NoteBuffer

Returns

KeySignature

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

buffer is null.

DetectKey(ReadOnlySpan<NoteEvent>)

Alias for IdentifyKey for more intuitive API.

public static KeySignature DetectKey(ReadOnlySpan<NoteEvent> notes)

Parameters

notes ReadOnlySpan<NoteEvent>

Returns

KeySignature

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

notation string

Returns

KeySignature

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

notation is 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

root int
isMajor bool

Returns

ushort

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

pitches int[]

Returns

KeySignature

Exceptions

ArgumentNullException

pitches is 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

pitches ReadOnlySpan<int>

Returns

KeySignature

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

value ushort
shift int

Returns

ushort

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)

Parameters

value ushort
shift int

Returns

ushort