Immutable, validated musical value types plus a harmonic and structural analysis layer, all pure logic with no I/O.
ktsu.Semantics.Music is one package in the ktsu.Semantics family. Start at the root README for the family overview.
ktsu.Semantics.Music models music theory as immutable value types. Pitches, intervals, scales, modes, chords, keys, rational durations, and time signatures are all first-class, validate on creation, and return new instances on every operation. The pitch convention is MIDI 60 = C4, and equal temperament is anchored at A4 = 440 Hz.
Above the single-event types sits an analysis layer that models harmony nested inside structure. A Progression is an ordered chord sequence with bar-based harmonic rhythm that computes roman numerals, functional roles, cadences, an inferred key, and chromatic classifications. Sections group progressions, an Arrangement orders them into a piece, and Form names the structural pattern.
- Pitch and interval types:
PitchClass,Pitch(MIDI, with name and frequency conversion),Interval(signed semitones, cents, folding). - Scales and modes:
Modewith roughly 29 presets (diatonic, jazz, symmetric, pentatonic, blues),Scalerooting a mode at a pitch class, withContainsandDegreeOf. - Chord-symbol parsing:
Chord.Parsehandles triads, sixths, sevenths (includingm7b5, the half-diminishedø7,mmaj7, and a flat fifth over a major third such as7b5andmaj7b5), extensions and altered tensions (9/11/13,b9/#9/#11/b13), suspensions, power chords, omissions (no3/no5), the six-nine idiom (C6/9), and slash bass. - Chord realization:
ChordTones()andVoice(octave)/Voice(octave, inversion), plusTranspose. - Roman-numeral analysis both directions:
Key.RomanNumeralOf(chord)andKey.ChordFromRomanNumeral(numeral). A numeral carries the chord's whole body, sixths, tensions and suspensions included (I6,V13,V7b9,Imaj7sus4), so the two are inverses for any chord without a slash bass. The bass is dropped, becauseI6already means an added sixth. - Rhythm and real time: rational
Duration,TimeSignature,Tempo, andNote/Rest/ChordEventevents that convert to seconds. - Analysis layer:
Progression(roman numerals, functions, cadences, key inference, chromatic chords),Section,Arrangement, andFormwith named-form recognition.
Install-Package ktsu.Semantics.Musicdotnet add package ktsu.Semantics.Music<PackageReference Include="ktsu.Semantics.Music" Version="x.y.z" />using ktsu.Semantics.Music;
Pitch middleC = Pitch.Parse("C4"); // MIDI 60
Pitch cSharp4 = Pitch.Create(NoteLetter.C, Accidental.Sharp, octave: 4); // type-safe, no parsing
Pitch g4 = middleC.Transpose(7); // a perfect fifth up
Interval fifth = Interval.Between(middleC, g4); // +7 semitones
double hz = Pitch.Parse("A4").FrequencyHz; // 440.0
Scale dDorian = Scale.Create(PitchClass.Create(2), Mode.Dorian);
bool hasF = dDorian.Contains(PitchClass.Create(5)); // trueusing ktsu.Semantics.Music;
Chord cmaj7 = Chord.Parse("Cmaj7");
IReadOnlyList<Pitch> voicing = cmaj7.Voice(octave: 4); // C4, E4, G4, B4
IReadOnlyList<int> tones = Chord.Parse("Cmaj7#11").ChordTones();
Chord up = cmaj7.Transpose(2); // Dmaj7
Tempo tempo = Tempo.Create(120.0); // quarter = 120 bpm
double halfNoteSeconds = tempo.Seconds(Duration.Half); // 1.0 s
Note a4 = Note.Create(Pitch.Parse("A4"), Duration.Quarter, Velocity.Forte);
double noteSeconds = a4.Seconds(tempo); // 0.5 sChord symbols use one grammar for parsing and canonical formatting:
symbol = root body [ "/" bass ] EOF
root, bass = letter { accidental }
accidental = "#" | "b" | "♯" | "♭"
body = [ quality ] [ extension ] [ suspension ] { modifier }
quality = minor [ "aug" | "+" ] | "dim" | "°" | "ø" | "aug" | "+"
| majorWord
minor = "m" | "min" | "-"
majorWord = "maj" | "Maj" | "M" | "Δ"
extension = [ majorWord ] number | number "M" | "Δ"
| "6" [ "/9" | "9" ] | "b6" | "♭6" | "5"
number = "7" | "9" | "11" | "13"
suspension = "sus2" | "sus4" | "sus"
modifier = alteration | add | omit | "b6" | "♭6"
| "(" grouped { [ "," ] grouped } ")"
grouped = modifier | "9" | "11" | "13" | majorWord "7"
alteration = ("b" | "♭" | "-") ( "5" | "9" | "13" )
| ("#" | "♯" | "+") ( "5" | "9" | "11" )
add = "add" ( "9" | "11" | "13" | "6" | "b6" )
omit = "no3" | "no5"
The grammar consumes the complete symbol and does not permit whitespace. A numbered extension
implies a seventh and its natural lower stack tones unless an alteration replaces that degree;
addN adds only that tone. M/maj before an extension or an M suffix on its number marks a
major seventh. A small compatibility reader also accepts previously printed forms
such as C79, Cmaj79, Csus47, and C6add9; a bare + after a numbered chord remains an
augmentation mark. A + at the start of the body is the augmented mark before a number too, so
C+7 is Caug7 and Cm+7 is Cm7#5; only C+5 reads as a raised fifth. A suspension can follow
a major seventh (Cmaj7sus4, Cmaj9sus4). Inside parentheses, a bare 9, 11 or 13 is an added
tone as on a lead sheet (C7(13) is C7add13), and a major-seventh mark supplies the seventh a body
without a number lacks (Cm(maj7) is Cmmaj7). ToString() emits the canonical form, for example C13b9, C7sus4, C6/9,
and C(#11).
using ktsu.Semantics.Music;
Key cMajor = Key.Create(PitchClass.Create(0), Mode.Major);
string label = cMajor.RomanNumeralOf(Chord.Parse("Dm7")); // "ii7"
Chord five = cMajor.ChordFromRomanNumeral("V7"); // G7using ktsu.Semantics.Music;
// Chart style: a leading time signature, bars separated by "|", and a beat slash "/"
// extends the preceding chord by one beat (so "Cmaj7 / / /" is a whole bar of 4/4).
Progression prog = Progression.Parse("4/4 Dm7 / / / | G7 / / / | Cmaj7 / / / | Cmaj7 / / /");
Key key = prog.InferKey()!; // C major (quality- and chord-tone-weighted fit)
IReadOnlyList<string> roman = prog.RomanNumerals(key); // ii7, V7, Imaj7, Imaj7
IReadOnlyList<HarmonicFunction> fns = prog.Functions(key); // Predominant, Dominant, Tonic, Tonic
IReadOnlyList<CadenceInstance> cadences = prog.Cadences(key); // Authentic at the resolution
// Chromatic analysis: secondary dominants, borrowed chords, Neapolitan, augmented sixths
IReadOnlyList<ChromaticAnalysis> chromatic =
Progression.Parse("4/4 C / / / | A7 / / / | Dm / / / | G7 / / /").ChromaticChords(key); // A7 -> secondary dominant
// Structure: sections -> arrangement -> form
Progression verse = Progression.Parse("4/4 C / / / | G / / / | Am / / / | F / / / | C / / / | G / / / | F / / / | C / / /");
Progression bridge = Progression.Parse("4/4 F / / / | C / / / | G / / / | C / / / | F / / / | C / / / | D / / / | G / / /");
Arrangement song = Arrangement.Create(key,
[
Section.Create(SectionType.Verse, verse, "Verse 1"),
Section.Create(SectionType.Verse, verse, "Verse 2"),
Section.Create(SectionType.Bridge, bridge, "Bridge"),
Section.Create(SectionType.Verse, verse, "Verse 3"),
]);
Form form = song.Form; // Pattern "AABA", Name NamedForm.ThirtyTwoBarAABA (four eight-bar sections)using ktsu.Semantics.Music;
// Compiler-enforced construction — no strings to mistype
Pitch p = Pitch.Create(NoteLetter.C, Accidental.Sharp, octave: 4); // C#4
// Every in-scope type has a canonical ToString() and a Parse/TryParse that inverts it
Chord c = Chord.Parse("Cmaj7");
Chord same = Chord.Parse(c.ToString()); // c == same
// Progressions read like a lead sheet and round-trip through their chart form
Progression prog = Progression.Parse("4/4 Dm7 / G7 / | Cmaj7 / / /");
Progression reparsed = Progression.Parse(prog.ToString()); // prog == reparsed
// TryParse never throws; Parse throws FormatException on malformed text
bool ok = Interval.TryParse("-5", out Interval? descendingFourth);| Type | Description | Key factories / members |
|---|---|---|
PitchClass |
One of twelve pitch classes, folded to 0..11. | Create(int), Create(NoteLetter, Accidental), Parse/TryParse, Value, Name |
Pitch |
MIDI pitch 0..127 (60 = C4). | Create(int), Create(NoteLetter, Accidental, int), Parse/TryParse, FromFrequency(double), Transpose(int), FrequencyHz, Octave |
Interval |
Signed interval in semitones. | Create(int), Between(Pitch, Pitch), Parse/TryParse, Semitones, Cents, Folded |
Mode |
Scale shape by semitone offsets. | Parse/TryParse, presets (Major, Dorian, HarmonicMinor, WholeTone, MajorPentatonic, ...), Intervals |
Scale |
A mode rooted at a pitch class. | Create(PitchClass, Mode), Parse/TryParse, Contains, DegreeOf, Transpose |
Chord |
Chord parsed from a symbol. | Parse/TryParse, ChordTones(), Voice(octave), Voice(octave, inversion), Transpose |
Key |
Tonic in a mode, resolves function. | Create(PitchClass, Mode), Parse/TryParse, RomanNumeralOf(Chord), ChordFromRomanNumeral(string), FunctionOf, Scale |
Duration |
Exact rational fraction of a whole note. | Create(int, int), Parse/TryParse, presets (Whole..Sixteenth), Dotted(), Add, Multiply, AsWholeNotes |
TimeSignature |
Bar and beat lengths. | Create(int, int), Parse/TryParse, BarDuration, BeatDuration |
Note / Rest / ChordEvent |
Timed musical events (IMusicalEvent). |
Create(...), Parse/TryParse, Duration, Seconds(Tempo) |
Velocity |
MIDI velocity 0..127. | Create(int), Parse/TryParse, presets (Piano..Fortissimo) |
Tempo |
Beats per minute with a beat unit. | Create(double), Parse/TryParse, Seconds(Duration), SecondsPerBeat |
| Type | Description | Key members |
|---|---|---|
Progression |
Chord sequence with bar-based harmonic rhythm. | Parse(string), Create(...), RomanNumerals(Key), Functions(Key), Cadences(Key), InferKey(), InferKeys(), ChromaticChords(Key) |
Section |
Labeled structural unit. | Create(SectionType, Progression, label?, key?), Parse/TryParse, IsSameStructure(Section) |
Arrangement |
Sections in performance order. | Create(Key, IEnumerable<Section>), Parse/TryParse, Form, TotalBars |
Form |
Structural pattern and its name. | Of(Arrangement), Parse/TryParse, Pattern, Name |
HarmonicFunction |
enum: Tonic, Predominant, Dominant, Chromatic. | |
CadenceInstance |
A cadence at a resolution index. | Index, Type (Cadence enum: Authentic, Plagal, Half, Deceptive) |
ChromaticAnalysis |
A non-diatonic chord classification. | Index, Kind (ChromaticKind), Detail |
NamedForm |
enum: ThirtyTwoBarAABA, TwelveBarBlues, VerseChorus, Binary, Ternary, Rondo, Strophic, ... |
Roman numerals, functions and cadences in a mode with fewer than seven degrees (pentatonic, blues,
whole tone) are spelled against major or natural minor on the same tonic, chosen by the mode's third,
so G in C major pentatonic is V and G → C is authentic. A minor key's own dominant (E or E7 in
A minor) is diatonic to the chromatic analysis and to key inference. Augmented sixths sit on the
lowered sixth degree and are told apart by shape, since a chord holds pitch classes rather than
spellings: Ab7no5 is It+6 and Ab7b5 is Fr+6 in C, while Ab7 is Ger+6 only when it resolves
to the dominant or to a cadential six-four.
InferKey() returns Key? (null for all-chromatic input), while InferKeys() returns the full ranked IReadOnlyList<KeyMatch>. See the complete library guide for a wider tour.
Contributions are welcome! Feel free to open issues or submit pull requests.
This project is licensed under the MIT License. See the LICENSE.md file for details.