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.
| Dromos | Intervals (semitones) | Family | Parent / relatives | Characteristic chords | Notes |
|---|---|---|---|---|---|
| Minor | 0, 2, 3, 5, 7, 8, 10 | Minor | Same PC set as Major (relative), Ousak (mode) | Im, IVm, bVII | Natural minor / Aeolian. The default minor backdrop. |
| Harmonic Minor | 0, 2, 3, 5, 7, 8, 11 | Minor | Same PC set as Hidjaz at 5th, Nikriz at 4th | Im, IVm, V, V7 | Lifted leading tone gives the characteristic V → Im cadence common in laiko / rebetiko. |
| Niavent | 0, 2, 3, 6, 7, 8, 11 | Minor | Same PC set as Hidjazkar at 5th | Im, #IVdim, V, bVI | "Double-harmonic minor" with an augmented 2nd between b3 and #4. |
| Nikriz | 0, 2, 3, 6, 7, 9, 10 | Minor | Same PC set as Harmonic Minor at 4th | Im, II (major), bVII, #IVdim | Dorian #4 with a natural b7 — the major II is its tell. |
| Hidjaz | 0, 1, 4, 5, 7, 8, 10 | Hidjaz | Same PC set as Harmonic Minor at 5th, Nikriz at 4th | I, bII, IVm, bVIIm | Phrygian dominant. The defining Greek/Asia-Minor sound. |
| Hidjazkar | 0, 1, 4, 5, 7, 8, 11 | Hidjaz | Same PC set as Niavent at 5th | I, bII, IVm, V (major) | Double-harmonic major: two augmented seconds in the scale. |
| Peiraiotikos | 0, 1, 4, 6, 7, 8, 11 | Major | Unique PC set (no relatives among the 11) | I, bII, #IVdim, V | A Piraeus-flavoured Hidjazkar-with-#IV; rare but distinctive. |
| Ousak | 0, 1, 3, 5, 7, 8, 10 | Minor | Same PC set as Minor (mode), Major | Im, bII, IVm, bVII | Phrygian, with the bII signature. Ubiquitous in rebetiko vocals. |
| Sabah | 0, 2, 3, 4, 7, 8, 10 | Minor | Unique PC set | Im, bIII, III (major), bVII, bVII7 | Microtonal in practice — the b4 lives between F and F# and rounds to F# in 12-TET. |
| Major | 0, 2, 4, 5, 7, 9, 11 | Major | Same PC set as Minor (relative), Ousak (mode) | I, IV, V, V7 | Ionian / Western major. Doubles as Rast for detection — see below. |
| Huzam | 0, 3, 4, 5, 7, 9, 11 | Major | Unique PC set | I, Im, V, V7, bIII | Microtonal 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:
- Quality of the I chord at the detected tonic.
- Quality of the V chord.
- 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:46 — disambiguation_lookup:
| I quality | V quality | bII present? | Candidate scales |
|---|---|---|---|
| Maj | Maj | no | Major |
| Maj | Dom7 | no | Major |
| Maj | Maj | yes | Hidjazkar, Peiraiotikos |
| Maj | Dom7 | yes | Hidjazkar, Peiraiotikos |
| Maj | Dim | yes | Hidjaz |
| Maj | Dim | no | Hidjaz |
| Min | Min | no | Minor, Nikriz |
| Min | Maj | no | Harmonic Minor, Niavent |
| Min | Dom7 | no | Harmonic Minor, Niavent |
| Min | Dim | yes | Ousak |
| Min | Min | yes | Sabah |
| anything else | falls 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):
| Bucket | Suffixes |
|---|---|
| Maj | (bare root), maj7, 6, aug, sus2, sus4 |
| Min | m, m7, m6, mMaj7 |
| Dom7 | 7 |
| Dim | dim, 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_fitpractice_fit— fraction of chord time that matches the scale's practice-chord table (e.g.PC_MINORatrust/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 atrust/rast-analysis/src/key_detection.rs:970.chroma_fit— Krumhansl-style in-scale fraction with tonic + fifth emphasis (score_chroma_fit_v2atrust/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:
- Walk the scale's seven intervals starting from a candidate spelling of the root (sharp, then flat, when the root has both).
- At each step, advance by one alphabetical letter (
C → D → E → F → G → A → B). - 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.
- 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.
- 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 usesBb, notA#(pc=10→"Bb").get_scale_spelling(2, "Hidjaz")→ D Hidjaz uses bothEb(b2) andF#(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 fromBb, walks throughB 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.rsandrust/rast-theory/src/chords.rs. - The two-stage detector entry point:
rust/rast-analysis/src/key_detection.rs:1395(detect_key_refined).