Skip to content

Music theory

Technical specification of the music theory baked into Rast — the 11 dromoi, the chord-quality disambiguation rules used by the key detector, and the enharmonic-spelling algorithm. This page is the contract between rast-theory, rast-analysis::key_detection, and the rest of the codebase.

For a friendlier walk-through aimed at users, see How it works → Overview. The reference tables below are what the code actually does.

The 11 dromoi

Rast detects 11 modal scales (dromoi). They live in rust/rast-theory/src/scales.rs:16 as [u8; 7] semitone arrays.

DromosIntervals (semitones)FamilyParent / relativesCharacteristic chordsNotes
Minor0, 2, 3, 5, 7, 8, 10MinorSame PC set as Major (relative), Ousak (mode)Im, IVm, bVIINatural minor / Aeolian. The default minor backdrop.
Harmonic Minor0, 2, 3, 5, 7, 8, 11MinorSame PC set as Hidjaz at 5th, Nikriz at 4thIm, IVm, V, V7Lifted leading tone gives the characteristic V → Im cadence common in laiko / rebetiko.
Niavent0, 2, 3, 6, 7, 8, 11MinorSame PC set as Hidjazkar at 5thIm, #IVdim, V, bVI"Double-harmonic minor" with an augmented 2nd between b3 and #4.
Nikriz0, 2, 3, 6, 7, 9, 10MinorSame PC set as Harmonic Minor at 4thIm, II (major), bVII, #IVdimDorian #4 with a natural b7 — the major II is its tell.
Hidjaz0, 1, 4, 5, 7, 8, 10HidjazSame PC set as Harmonic Minor at 5th, Nikriz at 4thI, bII, IVm, bVIImPhrygian dominant. The defining Greek/Asia-Minor sound.
Hidjazkar0, 1, 4, 5, 7, 8, 11HidjazSame PC set as Niavent at 5thI, bII, IVm, V (major)Double-harmonic major: two augmented seconds in the scale.
Peiraiotikos0, 1, 4, 6, 7, 8, 11MajorUnique PC set (no relatives among the 11)I, bII, #IVdim, VA Piraeus-flavoured Hidjazkar-with-#IV; rare but distinctive.
Ousak0, 1, 3, 5, 7, 8, 10MinorSame PC set as Minor (mode), MajorIm, bII, IVm, bVIIPhrygian, with the bII signature. Ubiquitous in rebetiko vocals.
Sabah0, 2, 3, 4, 7, 8, 10MinorUnique PC setIm, bIII, III (major), bVII, bVII7Microtonal in practice — the b4 lives between F and F# and rounds to F# in 12-TET.
Major0, 2, 4, 5, 7, 9, 11MajorSame PC set as Minor (relative), Ousak (mode)I, IV, V, V7Ionian / Western major. Doubles as Rast for detection — see below.
Huzam0, 3, 4, 5, 7, 9, 11MajorUnique PC setI, Im, V, V7, bIIIMicrotonal in practice — the third sits between m3 and M3. The detector accepts both I and Im over a Huzam tonic; detect_mode has an explicit "balanced tonic" branch for this at rust/rast-analysis/src/key_detection.rs:888.

The full enumeration test that asserts these 11 scales (and only these) is at rust/rast-theory/src/scales.rs:198.

Dromos vs makam terminology

The project uses Greek dromos names throughout the codebase — Hidjaz, Sabah, Ousak, Niavent, Hidjazkar, Peiraiotikos, Huzam. Where Turkish/Arabic makam usage is relevant, the mapping is:

  • Rast (Turkish/Arabic makam) collapses into Major for detection because in 12-TET chroma analysis the seyir (ascending major 7th, descending minor 7th) is direction-blind. The codebase exposes only Major; users searching for "Rast" should match against Major-family results.
  • Kurdi (Dorian) is the same pitch-class set as Minor and is detected as Minor.
  • Huseyni (Mixolydian) is the same pitch-class set as Major and is detected as Major.
  • Hicaz (Turkish), Hijaz (Arabic), and Hidjaz (Greek transliteration) are the same scale.

This is purely a labelling choice — the 11 entries above cover the modal pitch-class universe Rast recognises.

Scale families

rast-theory groups scales into three families (rust/rast-theory/src/scales.rs:74):

  • Minor family: Minor, Harmonic Minor, Niavent, Nikriz, Ousak, Sabah.
  • Major family: Major, Huzam, Peiraiotikos.
  • Hidjaz family: Hidjaz, Hidjazkar.

The key detector picks one family from the chord stream first (Step 2 of detect_key_refined), then picks a dromos within that family (Step 3). See family_candidates at rust/rast-analysis/src/key_detection.rs:921.

Note: the family lookup table places Peiraiotikos under "Major" (it has a major I) even though its pitch-class set is closer to Hidjazkar — this is a detector convenience, not a theoretical claim.

Chord-quality disambiguation

Chroma alone cannot distinguish relative scales (D Harmonic Minor and A Hidjaz share all seven pitch classes). The chord stream is what reveals the tonic and the mode. The detector uses three signals:

  1. Quality of the I chord at the detected tonic.
  2. Quality of the V chord.
  3. Presence of a bII chord (a strong Phrygian-family signal).

These three buckets feed a lookup table at rust/rast-analysis/src/key_detection.rs:46disambiguation_lookup:

I qualityV qualitybII present?Candidate scales
MajMajnoMajor
MajDom7noMajor
MajMajyesHidjazkar, Peiraiotikos
MajDom7yesHidjazkar, Peiraiotikos
MajDimyesHidjaz
MajDimnoHidjaz
MinMinnoMinor, Nikriz
MinMajnoHarmonic Minor, Niavent
MinDom7noHarmonic Minor, Niavent
MinDimyesOusak
MinMinyesSabah
anything elsefalls through to a fallback list

When the lookup misses (e.g. only sus/aug chords on the tonic, or a quality combination not in the table), the detector falls back to a wider candidate list based on whether i_quality is Maj-bucketed (yields Major, Hidjaz, Hidjazkar, Peiraiotikos, Huzam) or not (yields Minor, Harmonic Minor, Niavent, Nikriz, Ousak, Sabah). That fallback is at rust/rast-analysis/src/key_detection.rs:327.

Chord quality buckets

rast-theory collapses every chord label into one of four practice-quality buckets (label_to_practice_quality at rust/rast-theory/src/chords.rs:74):

BucketSuffixes
Maj(bare root), maj7, 6, aug, sus2, sus4
Minm, m7, m6, mMaj7
Dom77
Dimdim, dim7, m7b5

Aug and sus* collapse into Maj for bucketing purposes, but the detector also tags them via is_ambiguous_third (rust/rast-analysis/src/key_detection.rs:162) so they don't vote in the I/V lookup — they have no real major/minor 3rd to contribute.

Dom7 as a positive cadence signal

The (Maj, Dom7, ...) and (Min, Dom7, no) rows reflect the fact that a V7 → I cadence is a strong cue for Major (when the tonic is major) and for Harmonic Minor / Niavent (when the tonic is minor). Treating Dom7 separately from plain Maj on the V degree lets the detector pick up the cadence without needing the leading tone in the chroma.

Fallback chroma scoring

When the chord-driven lookup yields a list of candidates (often 1–2), the second-stage scorer (detect_dromos at rust/rast-analysis/src/key_detection.rs:1006) ranks them by:

score = 0.50 * practice_fit + 0.30 * signature_bonus + 0.20 * chroma_fit
  • practice_fit — fraction of chord time that matches the scale's practice-chord table (e.g. PC_MINOR at rust/rast-theory/src/scales.rs:118).
  • signature_bonus — duration-weighted score for chords distinctive to a particular dromos (V quality for Minor vs Harmonic Minor; bII for Ousak / Peiraiotikos; bIII for Huzam; III major for Sabah; etc.). Full table at rust/rast-analysis/src/key_detection.rs:970.
  • chroma_fit — Krumhansl-style in-scale fraction with tonic + fifth emphasis (score_chroma_fit_v2 at rust/rast-analysis/src/key_detection.rs:622).

When a basic_pitch note transcription is available, the chroma-fit term reads from the note-PC histogram instead of the chord-derived chroma — transcribed notes aren't fooled by the harmonic overtones that pollute audio chroma (e.g. the 5th harmonic of D inflating F# and faking a major third).

Last-chord rule for confusable pairs

The chord-root + bass-PC tiebreakers cannot reliably resolve Hidjaz ↔ Harmonic Minor and Hidjazkar ↔ Niavent (relative-mode pairs whose V chords accumulate similar mass). For these, apply_hidjaz_hm_last_chord_rule (rust/rast-analysis/src/key_detection.rs:1557) inspects the song's final chord — the strongest tonic signal we have — and walks the candidate list back accordingly. To avoid being fooled by stinger-style outro hits, only chord events of duration ≥ 1.0 s are considered "decisive" (constant MIN_DECISIVE_CHORD_DURATION_SEC at rust/rast-analysis/src/key_detection.rs:13).

Hidjaz override from a Phrygian-dominant footprint

Some Hidjaz songs never spell out the bII as a chord but use IVm + bVIIm extensively — the Phrygian-dominant footprint. detect_mode (rust/rast-analysis/src/key_detection.rs:823) overrides a Major-mode call to Hidjaz when either:

  • a major / dom7 / dim bII chord covers ≥ 5 % of total chord time, or
  • IVm + bVIIm together cover ≥ 10 % of total chord time.

See rust/rast-analysis/src/key_detection.rs:911 for the threshold check.

Enharmonic spelling

Once the detector picks a (root_pc, scale_name), the UI needs proper note names — G Minor must read G A Bb C D Eb F, never G A A# C D D# F. The algorithm in rust/rast-theory/src/enharmonic.rs:65 is:

  1. Walk the scale's seven intervals starting from a candidate spelling of the root (sharp, then flat, when the root has both).
  2. At each step, advance by one alphabetical letter (C → D → E → F → G → A → B).
  3. For each (letter, target pitch class), pick the simplest accidental that makes them agree (natural, single sharp, or single flat). If a double-accidental would be required, fall through to a sharp-spelling default.
  4. Score each walk by failure count, then by total accidental count. The winner is the spelling whose seven scale tones have the fewest forced double accidentals.
  5. Fill in the chromatic gaps (the five non-scale pitch classes) using either sharps or flats depending on which direction dominates the scale spelling.

Examples (from the test suite at rust/rast-theory/src/enharmonic.rs:182):

  • get_scale_spelling(7, "Minor") → G Minor uses Bb, not A# (pc=10"Bb").
  • get_scale_spelling(2, "Hidjaz") → D Hidjaz uses both Eb (b2) and F# (3). The walk picks the natural-letter choice for each scale degree — D, Eb, F#, G, A, Bb, C — even though the spelling mixes sharps and flats.
  • get_scale_spelling(0, "Major") → C Major has no accidentals on its scale tones; chromatic fillers default to sharps because the scale itself has no flats.
  • get_scale_spelling(10, "Hidjazkar") → Bb Hidjazkar starts from Bb, walks through B D# E F# G A#, and respells the chord stream accordingly.

The companion function respell_chord (rust/rast-theory/src/enharmonic.rs:152) takes a normalised chord label and reroots it to the chosen spelling — respell_chord("A#m7", &spelling_for_g_minor) returns "Bbm7".

See also

  • The user-friendly walk-through of how key detection feels in practice: How it works → Overview.
  • Crate boundaries and where each module lives: Architecture.
  • The pure-data scale and chord tables: rust/rast-theory/src/scales.rs and rust/rast-theory/src/chords.rs.
  • The two-stage detector entry point: rust/rast-analysis/src/key_detection.rs:1395 (detect_key_refined).