Celeritas Cookbook

Common patterns and recipes for music analysis and composition tasks.

📖 Contents


Quick Start

Parse a melody and analyze its key

using Celeritas.Core;

var melody = MusicNotation.Parse("C4/4 E4/4 G4/4 B4/2 C5/2");
var key = KeyAnalyzer.DetectKey(melody);
Console.WriteLine($"Key: {key}");  // Output: C Major

Identify a chord progression

var chords = new[] {
    "C4 E4 G4",
    "D4 F4 A4",
    "G3 B3 D4 F4",
    "C4 E4 G4"
};

foreach (var chord in chords)
{
    var info = ChordAnalyzer.Identify(chord);
    Console.WriteLine(info);
}
// Output: C Major, D Minor, G Dominant7, C Major

Parsing & Notation

Note arithmetic (pitch classes and transposition)

using Celeritas.Core;

// Pitch-class arithmetic (mod 12)
var pc = PitchClass.B;
Console.WriteLine((pc + 1).ToName()); // C

// Intervals between pitch classes
var up = PitchClass.C - PitchClass.B;                    // +1 (ascending wrap)
var down = PitchClass.C.SignedIntervalTo(PitchClass.B);  // -1 (shortest signed)
Console.WriteLine(up.SimpleName);   // m2
Console.WriteLine(down.Semitones); // -1

// Notes (SPN) with octave + transposition
var note = SpnNote.Db(4);
var transposed = note + ChromaticInterval.PerfectFifth;
Console.WriteLine(transposed.ToNotation(preferSharps: false)); // Ab4

Circle of fifths / fourths (keys and chord roots)

using Celeritas.Core;

// Pitch-class circle (C → G → D → ...)
var circle = CircleOfFifths.PitchClasses(PitchClass.C, CircleDirection.Clockwise);
Console.WriteLine(string.Join(" ", circle.Select(pc => pc.ToName())));

// Major chords along the circle
var majorChords = CircleOfFifths.MajorChordSymbols(PitchClass.C);
Console.WriteLine(string.Join(" ", majorChords));

// Major + relative minor pairs
var pairs = CircleOfFifths.MajorWithRelativeMinors(PitchClass.C, preferSharps: false);
Console.WriteLine(string.Join(" | ", pairs.Select(p => $"{p.Major}/{p.RelativeMinor}")));

// You can also use KeySignature helpers:
var cMajor = new KeySignature(0, isMajor: true);
Console.WriteLine(cMajor.GetDominantKey());    // G Major
Console.WriteLine(cMajor.GetSubdominantKey()); // F Major
Console.WriteLine(cMajor.GetRelativeKey());    // A Minor

Functional progressions (ii–V–I, turnaround, full circle)

using Celeritas.Core;

var cMajor = new KeySignature(PitchClass.C.Value, isMajor: true);

var twoFiveOne = FunctionalProgressions.TwoFiveOne(cMajor, DiatonicChordType.Seventh);
Console.WriteLine(string.Join(" ", twoFiveOne.Select(c => c.Symbol())));
// Dm7 G7 Cmaj7

var turnaround = FunctionalProgressions.Turnaround(cMajor, DiatonicChordType.Seventh);
Console.WriteLine(string.Join(" ", turnaround.Select(c => c.Symbol())));
// Cmaj7 Am7 Dm7 G7 Cmaj7

var circle = FunctionalProgressions.Circle(cMajor, DiatonicChordType.Triad);
Console.WriteLine(string.Join(" ", circle.Select(c => c.Symbol(preferSharps: false))));
// C F Bdim Em Am Dm G C   (diatonic descending fifths: I IV vii° iii vi ii V I)

var aMinor = new KeySignature(PitchClass.A.Value, isMajor: false);
var minorCadence = FunctionalProgressions.TwoFiveOne(aMinor, DiatonicChordType.Seventh, minorDominant: MinorDominantStyle.Harmonic);
Console.WriteLine(string.Join(" ", minorCadence.Select(c => c.Symbol())));
// Bm7b5 E7 Am7

var chain = FunctionalProgressions.ThreeSixTwoFiveOne(cMajor, DiatonicChordType.Seventh);
Console.WriteLine(string.Join(" ", chain.Select(c => c.Symbol())));
// Em7 Am7 Dm7 G7 Cmaj7

var vOfIi = FunctionalProgressions.SecondaryDominantTo(cMajor, ScaleDegree.Ii, DiatonicChordType.Seventh);
Console.WriteLine($"{vOfIi.RomanNumeral} = {vOfIi.Symbol()}");
// V7/ii = A7

Parse complex notation with time signatures

var music = MusicNotation.Parse(@"
    4/4: C4/4 E4/4 G4/4 C5/4 |
    3/4: D4/2 F4/4 |
    6/8: E4/4. F4/4.",
    validateMeasures: true);

Round-trip: parse and format back

var original = "@bpm 120 @dynamics mf [C4 E4 G4]/4 E4/4 G4/2";
var parsed = MusicNotation.ParseFull(original);
var formatted = MusicNotation.FormatWithDirectives(
    parsed.Notes, parsed.Directives, groupChords: true);
// formatted == original

Work with polyphony (multiple voices)

// Piano: bass + melody
var piano = MusicNotation.Parse("<< C2/1 | C4/4 D4/4 E4/4 F4/4 >>");

// SATB choir - voices in score order: soprano, alto, tenor, bass
var satb = MusicNotation.Parse("<< C5/2 | G4/2 | E4/2 | C3/2 >>");

Chord Analysis

Analyze chord with inversions

ChordAnalyzer.Identify works on the pitch-class set, so every voicing of C-E-G identifies as the same chord and no slash notation is ever produced. Ask ProgressionAdvisor for the inversion instead.

using Celeritas.Core;
using Celeritas.Core.Analysis;

Console.WriteLine(ChordAnalyzer.Identify("C4 E4 G4"));  // Output: C Major
Console.WriteLine(ChordAnalyzer.Identify("E3 G3 C4"));  // Output: C Major
Console.WriteLine(ChordAnalyzer.Identify("G3 C4 E4"));  // Output: C Major

// GetInversion compares the bass note against the identified root:
foreach (var voicing in new[] { "C4 E4 G4", "E3 G3 C4", "G3 C4 E4" })
{
    int[] pitches = MusicNotation.Parse(voicing).Select(n => n.Pitch).ToArray();
    var inversion = ProgressionAdvisor.GetInversion(pitches);
    Console.WriteLine($"{ChordAnalyzer.Identify(pitches)} ({ProgressionAdvisor.GetInversionName(inversion)})");
}
// Output: C Major (root position)
//         C Major (1st inversion)
//         C Major (2nd inversion)

Analyze jazz chords

var dm7 = ChordAnalyzer.Identify("D4 F4 A4 C5");
Console.WriteLine(dm7);  // Output: D Minor7

var g7 = ChordAnalyzer.Identify("G3 B3 D4 F4");
Console.WriteLine(g7);  // Output: G Dominant7

var cmaj7 = ChordAnalyzer.Identify("C4 E4 G4 B4");
Console.WriteLine(cmaj7);  // Output: C Major7

var am7 = ChordAnalyzer.Identify("A3 C4 E4 G4");
Console.WriteLine(am7);  // Output: A Minor7

Get detailed chord information

var chord = ChordAnalyzer.Identify("C4 E4 G4 B4");
Console.WriteLine($"Root: {chord.Root}");                      // C
Console.WriteLine($"RootPitchClass: {chord.RootPitchClass}");  // 0
Console.WriteLine($"Quality: {chord.Quality}");                // Major7
Console.WriteLine(chord);                                      // C Major7

ChordLibrary registers templates of at most four notes, so a five-note stack such as "C4 E4 G4 B4 D5" comes back as C Unknown. Fold the extension into four notes instead - "C4 E4 G4 D5" identifies as C Add9.

When the quality is Unknown the root beside it is a placeholder, not a detected root: an unrecognized set has none, and the library pairs Unknown with pitch class zero. So "E4 G4 B4 D5 F#5" also reads C Unknown, with no C anywhere in it. Test chord.Quality != ChordQuality.Unknown before reading chord.Root.

Nashville Number System

Every chord in a progression report carries its Nashville number (scale degree + quality suffix), and any RomanNumeralChord can produce one with ToNashville().

using Celeritas.Core.Analysis;

var report = ProgressionAdvisor.Analyze(["C", "Am", "F", "G"]);
Console.WriteLine(string.Join(" ", report.Chords.Select(c => c.Nashville)));
// Output: 1 6m 4 5

// Or a single chord relative to a key:
var key = new KeySignature("C", isMajor: true);
var g7 = KeyAnalyzer.Analyze(MusicNotation.Parse("G4 B4 D5 F5").Select(n => n.Pitch).ToArray(), key);
Console.WriteLine(g7.ToNashville());   // 57

Pitch-class set analysis (Forte)

Get the normal order, prime form, and interval vector of any set of notes.

using Celeritas.Core.Analysis;

int[] cmaj7 = MusicNotation.Parse("C4 E4 G4 B4").Select(n => n.Pitch).ToArray();
var pcs = PitchClassSetAnalyzer.Analyze(cmaj7);

Console.WriteLine(pcs.PitchClassesText);    // {0,4,7,11}
Console.WriteLine(pcs.NormalOrderText);     // {11,0,4,7}
Console.WriteLine(pcs.PrimeFormText);       // {0,1,5,8}
Console.WriteLine(pcs.IntervalVectorText);  // <1,0,1,2,2,0>

Key & Scale Detection

Detect key from melody

var melody = MusicNotation.Parse("C4/4 D4/4 E4/4 F4/4 G4/4 A4/4 B4/4 C5/2");
var key = KeyAnalyzer.DetectKey(melody);
Console.WriteLine(key);  // C Major

Detect mode with hint

using Celeritas.Core.Analysis;

var dorianScale = MusicNotation.Parse("D4 E4 F4 G4 A4 B4 C5 D5");
var (mode, confidence) = ModeLibrary.DetectModeWithRoot(dorianScale, rootHint: 2); // D = 2
Console.WriteLine($"{mode} (confidence: {confidence:P0})");  // D Dorian (confidence: 19%)

Analyze scale degrees

var key = new KeySignature("C", isMajor: true);
var scale = key.GetScale();   // the key's diatonic pitch classes, in degree order
var notes = MusicNotation.Parse("C4 E4 G4");

foreach (var note in notes)
{
    var degree = Array.IndexOf(scale, note.Pitch % 12) + 1;   // 0 = not in the key
    Console.WriteLine($"{MusicNotation.ToNotation(note.Pitch)} = {degree}");
}
// C4 = 1, E4 = 3, G4 = 5

Harmonization

Auto-harmonize a melody

using Celeritas.Core.Harmonization;

var melody = MusicNotation.Parse("C4/4 D4/4 E4/4 F4/4 G4/2");
var key = new KeySignature("C", true);

var harmonizer = new MelodyHarmonizer();
var result = harmonizer.Harmonize(melody, key);

foreach (var chord in result.Chords)
{
    Console.WriteLine($"{chord.Start}: {chord.Chord}");
}
// 0: C Major
// 1/4: G Major
// 1/2: C Major
// 3/4: F Major
// 1: C Major

One chord per beat, or one per note when the note is longer than a beat: the half-note G4 gets a single chord, so five melody notes come back as five chords.

Generate figured bass realization

using Celeritas.Core.FiguredBass;

var realizer = new FiguredBassRealizer();
var symbol = new FiguredBassSymbol
{
    BassPitch = 60,  // C
    Figures = new[] { 6 },  // First inversion
    Time = Rational.Zero,
    Duration = new Rational(1, 1)
};

var voicing = realizer.RealizeSymbol(symbol);
// NoteEvent[] { C4, E4, A4 } - the bass plus the intervals the figure asks for.
// The voice count follows the figure, so it is not always four: [6] yields three
// notes, [7] yields four (C4 E4 G4 B4).

Voice leading for chord progression

using Celeritas.Core.VoiceLeading;

var solver = new VoiceLeadingSolver();
var chords = new[] { "C", "F", "G", "C" };
var solution = solver.SolveFromSymbols(chords);

foreach (var voicing in solution.Voicings)
{
    Console.WriteLine($"S: {voicing.Soprano}, A: {voicing.Alto}, " +
                     $"T: {voicing.Tenor}, B: {voicing.Bass}");
}

Melody, Rhythm & Ornaments

Analyze a melody's contour and intervals

using Celeritas.Core.Analysis;

int[] melody = MusicNotation.Parse("C4 E4 G4 E4 C4 G4 C5").Select(n => n.Pitch).ToArray();
var m = MelodyAnalyzer.Analyze(melody);

Console.WriteLine(m.Contour);                     // Wave
Console.WriteLine(m.AmbitusDescription);          // moderate range: C4 to C5 (P8)
Console.WriteLine($"avg interval: {m.Statistics.AverageInterval:F1} semitones");   // 4.3
Console.WriteLine($"largest leap: {m.Statistics.LargestLeap}");                    // 7

m.Motifs lists recurring interval patterns when the melody has any.

Predict the next rhythm in a style

using Celeritas.Core.Analysis;

var recent = new[] { Rational.Quarter, Rational.Eighth, Rational.Eighth, Rational.Quarter };
var predictor = RhythmModels.GetStyleModel("jazz");   // classical, jazz, rock, latin, waltz
var prediction = predictor.Predict(recent);

Console.WriteLine($"next: {prediction.MostLikely} ({prediction.Confidence:P0})");   // next: 1/8 (38%)

Expand an ornament to notes

using Celeritas.Core.Ornamentation;

var baseNote = new NoteEvent(MusicNotation.ParseNote("E4"), Rational.Zero, Rational.Half);
var trill = new Trill { BaseNote = baseNote, Interval = 2, Speed = 8 };

NoteEvent[] notes = trill.Expand();
Console.WriteLine(notes.Length);                                  // 16
Console.WriteLine(string.Join(", ", notes.Take(4).Select(n => n.Pitch)));   // 64, 66, 64, 66

MIDI Processing

Load MIDI file and analyze

using Celeritas.Core.Midi;

using var buffer = MidiIo.Import("song.mid");
var key = KeyAnalyzer.DetectKey(buffer);
Console.WriteLine($"Key: {key}");
Console.WriteLine($"Total notes: {buffer.Count}");

Export notes to MIDI

using Celeritas.Core.Midi;

var notes = MusicNotation.Parse("4/4: C4/4 E4/4 G4/4 C5/4");
using var buffer = new NoteBuffer(notes.Length);
buffer.AddRange(notes);
MidiIo.Export(buffer, "output.mid", new MidiExportOptions(Bpm: 120));

Transpose MIDI file

using Celeritas.Core.Midi;

using var buffer = MidiIo.Import("song.mid");

// Transpose up 2 semitones
MusicMath.Transpose(buffer, 2);

MidiIo.Export(buffer, "transposed.mid");

Notation (MusicXML)

Import a MusicXML score

using Celeritas.Core.Notation;

using var buffer = MusicXmlIo.Import("score.musicxml");
Console.WriteLine($"{buffer.Count} notes");

// ...or parse a string you already have in memory:
using var fromString = MusicXmlIo.Parse(xmlText);

Handles score-partwise: pitches, durations (via <divisions>), rests, chords, multiple parts, and tie merging. Reading is DTD-safe (no external fetch).

Export notes to MusicXML

using Celeritas.Core.Notation;

string xml = MusicXmlIo.ToXml(buffer);        // as a string
MusicXmlIo.Export(buffer, "out.musicxml");    // ...or straight to a file

Monophonic and block-chordal material round-trips exactly (Import -> ToXml -> Parse yields the same notes).

Convert and analyze from the command line

# MusicXML <-> MIDI (direction inferred from the extensions)
celeritas musicxml convert --in score.musicxml --out score.mid
celeritas musicxml convert --in score.mid --out score.musicxml

# Quick summary: notes, range, detected key, chord timeline
celeritas musicxml analyze --in score.musicxml

Performance Optimization

Use NoteBuffer for large operations

using Celeritas.Core;

using var buffer = new NoteBuffer(capacity: 10000);

// Add notes efficiently
foreach (var note in largeSequence)
    buffer.Add(note);

// SIMD-accelerated transpose (processes 16 notes at once)
MusicMath.Transpose(buffer, semitones: 5);

A NoteBuffer tracks whether it is still sorted by offset. If you fill it in arbitrary order - as the batch recipe below does with Clear + AddRange - call Sort() before GetChords(), which otherwise throws InvalidOperationException ("the buffer is not sorted by offset").

Batch chord analysis

using var buffer = new NoteBuffer(capacity: 16);
var chords = new List<string>();

foreach (var measureNotes in measures)
{
    buffer.Clear();
    buffer.AddRange(measureNotes);
    if (buffer.Count >= 3)
        chords.Add(ChordAnalyzer.Identify(buffer).ToString());
}

Parallel processing with PLINQ

using System.Linq;

var results = chordSequences
    .AsParallel()
    .Select(pitches => ChordAnalyzer.Identify(pitches).ToString())
    .ToList();

Tips & Best Practices

1. Always validate time signatures

var music = MusicNotation.Parse("4/4: C4/4 E4/4 G4/4 C5/4", 
    validateMeasures: true);

2. Use round-trip formatting for debugging

var formatted = MusicNotation.FormatNoteSequence(notes);
Console.WriteLine(formatted);  // Human-readable output

3. Leverage SIMD for large arrays

// Automatically uses AVX-512 / AVX2 / SSE2 / NEON
if (buffer.Count > 1000)
    MusicMath.Transpose(buffer, 3);

4. Cache key signatures

private static readonly KeySignature CMajor = new("C", true);
private static readonly KeySignature AMinor = new("A", false);

5. Use Rational for precise timing

var dotted = new Rational(3, 8);  // Dotted quarter
var triplet = new Rational(1, 6);  // Eighth note triplet

See Also


Need help? Open an issue on GitHub