Français
♩ Enter notes
⬆ Import
Dur:
Figures for selected note (Tab)
pitch  ⇧↑↓ octave 7 whole  6 half  5 quarter  4 8th  3 16th . dot  R rest  = tie - ♭  * ♮  + navigate   delete Tab figures
𝄞
Drop your MusicXML file here
or click to browse — .xml / .musicxml files, max 10 MB
📄
← or load sample file

Analyzing bass line and realizing harmony…

Realization Complete
Continuo realization generated successfully.

Output preview

How It Works

Algorithm Overview

The realizer implements the decision tree described by Wead & Knopke (ICMC 2007), drawing on two primary Baroque treatises:

  • Francesco GaspariniL'Armonico Pratico al Cimbalo (1708, Venice)
  • Denis DelairTraité d'Accompagnement pour le Théorbe et le Clavecin (1724, Paris)

Processing Pipeline

  • 1. Parse — MusicXML is read; the lowest-pitched part is selected as the bass line.
  • 2. Figured Bass Detection — If <figured-bass> elements are present, they are used directly. Otherwise, the unfigured bass decision tree runs.
  • 3. Unfigured Decision Tree — Computes scale degree (1–7) and melodic motion (step/leap, up/down), then selects figures per Gasparini/Delair:
    • Scale degree 7 (leading tone) → 6 (V⁶)
    • Scale degrees 2, 3 → 6 (first inversion)
    • Scale degree 5 approaching tonic by step → 7 (V⁷)
    • Ascending step on degree 4 → 6 (passing)
    • All others → 5 (root position)
  • 4. Interval Expansion — Abbreviated figures are expanded: 6→6, 7→7 5, 6 5→6 5, 4 3→4 3, 2→4 2.
  • 5. Voice Realization — Three upper voices (soprano, alto, tenor) are chosen to minimize total voice movement (law of the shortest way) while enforcing:
    • No parallel perfect fifths or octaves
    • No voice crossing or doubling of the leading tone
    • Voice ranges: soprano C4–G5, alto G3–C5, tenor C3–E4
  • 6. Export — Output is a two-part MusicXML: Part 1 = original bass, Part 2 = realized realization (voices 1–3 upper + voice 4 bass).

Figured Bass Notation Reference

Triads
(none) / 55root position triad
66first inversion triad
6 46 4second inversion triad — cadential or passing
Seventh Chords
77 5seventh chord, root position
6 56 5seventh chord, first inversion
4 34 3seventh chord, second inversion / 4–3 suspension
2 (= 4 2)4 2seventh chord, third inversion
Ninths
99 5ninth chord / 9–8 suspension
9 79 7 5ninth with seventh
Suspensions
7 67 67–6 suspension
5 45 45–4 (–3) suspension
Special
88explicit octave above bass
♯ / ♭ / ♮accidental prefix — ♯ raises, ♭ lowers, ♮ cancels the figured interval
Bibliography
Voice Leading Rules (24)
Voice Range Gasparini 1708, p. 12
P10 enabled

Il Soprano deve stare fra il Do e il Sol del quinto rigo; il Contralto fra il Sol del terzo e il Do del quinto; il Tenore fra il Do del terzo e il Mi del quarto.

The Soprano must stay between C4 and G5; the Alto between G3 and C5; the Tenor between C3 and E4.

Implementation
$cost = 0.0;
$ranges = $ctx['ranges'];
$voiceNames = ['tenor', 'alto', 'soprano'];
foreach ($ctx['curr'] as $i => $midi) {
    $vName = $voiceNames[$i] ?? 'soprano';
    [$lo, $hi] = $ranges[$vName];
    if ($midi < $lo) { $cost += ($lo - $midi) * 3; }
    if ($midi > $hi) { $cost += ($midi - $hi) * 3; }
}
return $cost;
Tenor Min G3 Gasparini 1708, p. 12; Delair 1724, règle 3
P11 enabled

Il Tenore non deve scendere sotto il Sol del terzo rigo (Sol3) per evitare confusione con il Basso.

The Tenor must never descend below G3 (MIDI 55) to avoid muddiness in the bass register.

Implementation
$tenor = $ctx['curr'][0] ?? 55;
if ($tenor < 55) { return (55 - $tenor) * 50.0; }
return 0.0;
Chord Third Present Gasparini 1708, p. 14; Delair 1724, règle 4
P12 enabled

Il terzo dell'accordo deve essere sempre presente nelle voci superiori per garantire la piena sonorità dell'accordo.

The note a diatonic third above the bass must always be present in the upper voices. Omitting the third produces a hollow, incomplete sound.

Implementation
$tonicPc  = $ctx['keyMode'] === 'minor'
    ? ((($ctx['keyFifths'] * 7) - 3) % 12 + 12) % 12
    : (($ctx['keyFifths'] * 7) % 12 + 12) % 12;
$steps    = $ctx['keyMode'] === 'minor' ? [0,2,3,5,7,8,10] : [0,2,4,5,7,9,11];
$scalePcs = array_map(fn($i) => ($tonicPc + $i) % 12, $steps);
$bassPc   = $ctx['bassCurr'] % 12;
$deg      = array_search($bassPc, $scalePcs);
if ($deg === false) { return 0.0; } // chromatic bass — skip
$thirdPc  = $scalePcs[($deg + 2) % 7];
$upperPcs = array_map(fn($m) => $m % 12, $ctx['curr']);
if (!in_array($thirdPc, $upperPcs, true)) { return 25.0; }
return 0.0;
Soprano Upper Limit E5 St. Lambert 1707, p. 40; Christensen 1992, p. 40
P13 enabled

Le dessus ne doit jamais aller au-delà du mi ou du fa du cinquième rang.

The soprano must never go beyond E5 (or at most F5). Penalise any soprano pitch above MIDI 76 (E5).

Implementation
$soprano = $ctx['curr'][2] ?? 76;
if ($soprano > 76) { return ($soprano - 76) * 5.0; }
return 0.0;
Rh Span Limit Gasparini 1708, p. 12
P20 enabled

La distanza fra il Tenore e il Soprano nella mano destra non deve superare la nona.

The span between Tenor and Soprano in the right hand must not exceed a ninth (14 semitones).

Implementation
if (count($ctx['curr']) < 3) { return 0.0; }
$span = $ctx['curr'][2] - $ctx['curr'][0]; // soprano - tenor
if ($span > 14) { return ($span - 14) * 10.0; }
return 0.0;
No Parallel Fifths Delair 1724, règle 8
P30 enabled

Il faut éviter les quintes consécutives entre toutes les parties.

Consecutive (parallel) perfect fifths between any pair of voices must be avoided.

Implementation
if ($ctx['isStart'] || count($ctx['prev']) < 3) { return 0.0; }
$cost = 0.0;
$allCurr = array_merge($ctx['curr'], [$ctx['bassCurr']]);
$allPrev = array_merge($ctx['prev'], [$ctx['bassPrev']]);
$n = count($allCurr);
for ($a = 0; $a < $n; $a++) {
    for ($b = $a + 1; $b < $n; $b++) {
        if (!isset($allPrev[$a]) || !isset($allPrev[$b])) { continue; }
        $prevInt = abs($allPrev[$a] - $allPrev[$b]) % 12;
        $currInt = abs($allCurr[$a] - $allCurr[$b]) % 12;
        $moved = ($allPrev[$a] !== $allCurr[$a]) || ($allPrev[$b] !== $allCurr[$b]);
        if ($moved && $prevInt === 7 && $currInt === 7) { $cost += 40.0; }
    }
}
return $cost;
No Leading Tone Doubling Gasparini 1708, p. 16; Fux 1725, Gradus ad Parnassum §12
P32 enabled

La nota sensibile non si raddoppia mai, essendo nota a risoluzione obbligata verso la tonica.

The leading tone (semitone below the tonic) must never be doubled in any pair of voices, since it carries an obligatory upward resolution.

Implementation
$tonicPc = $ctx['keyMode'] === 'minor'
    ? ((($ctx['keyFifths'] * 7) - 3) % 12 + 12) % 12
    : (($ctx['keyFifths'] * 7) % 12 + 12) % 12;
$ltPc    = ($tonicPc + 11) % 12;
$allPcs  = array_map(fn($m) => $m % 12, array_merge($ctx['curr'], [$ctx['bassCurr']]));
$count   = count(array_filter($allPcs, fn($pc) => $pc === $ltPc));
if ($count > 1) { return 60.0 * ($count - 1); }
return 0.0;
No Chromatic Leading Tone Doubling Heinichen 1728, p. 65; Christensen 1992, p. 65
P33 enabled

Man verdoppele niemals eine chromatisch erhöhte Note, die als Leitton fungiert.

Never double a chromatically altered note that functions as a leading tone (i.e. any note outside the diatonic scale of the current key). One occurrence is permissible; two or more are forbidden.

Implementation
$tonicPc  = $ctx['keyMode'] === 'minor'
    ? ((($ctx['keyFifths'] * 7) - 3) % 12 + 12) % 12
    : (($ctx['keyFifths'] * 7) % 12 + 12) % 12;
$steps    = $ctx['keyMode'] === 'minor' ? [0,2,3,5,7,8,10] : [0,2,4,5,7,9,11];
$scalePcs = array_map(fn($i) => ($tonicPc + $i) % 12, $steps);
$allPcs   = array_map(fn($m) => $m % 12, array_merge($ctx['curr'], [$ctx['bassCurr']]));
$cost     = 0.0;
$chromCounts = [];
foreach ($allPcs as $pc) {
    if (!in_array($pc, $scalePcs, true)) {
        $chromCounts[$pc] = ($chromCounts[$pc] ?? 0) + 1;
    }
}
foreach ($chromCounts as $count) {
    if ($count > 1) { $cost += 50.0 * ($count - 1); }
}
return $cost;
No Seventh Doubling St. Lambert 1707, pp. 28–29; Heinichen 1728, pp. 76–77; Christensen 1992, pp. 28, 76
P34 enabled

La dissonance de septième ne se double jamais dans aucune des parties.

The dissonant seventh of a chord (the pitch a minor or major seventh above the bass) must never be doubled in any pair of voices.

Implementation
$bassPc = $ctx['bassCurr'] % 12;
$seventhCount = 0;
foreach ($ctx['curr'] as $m) {
    $interval = ($m % 12 - $bassPc + 12) % 12;
    if ($interval === 10 || $interval === 11) { $seventhCount++; }
}
if ($seventhCount > 1) { return 50.0 * ($seventhCount - 1); }
return 0.0;
No Ninth Doubling St. Lambert 1707, pp. 30–31; Heinichen 1728, pp. 81–82; Christensen 1992, pp. 30, 81
P36 enabled

La dissonance de neuvième ne se double jamais dans aucune des parties.

The dissonant ninth must never be doubled in any pair of voices. Only one voice may hold a ninth above the bass at a time.

Implementation
$bassPc = $ctx['bassCurr'];
$count = 0;
foreach ($ctx['curr'] as $m) {
    $sem = $m - $bassPc;
    // Ninth = 13 (minor) or 14 (major) semitones above bass; also allow compound ninths
    if ($sem === 13 || $sem === 14 || $sem === 25 || $sem === 26) { $count++; }
}
if ($count > 1) { return 50.0 * ($count - 1); }
return 0.0;
No Parallel Octaves Delair 1724, règle 7
P40 enabled

Il faut éviter les octaves consécutives entre toutes les parties.

Consecutive (parallel) octaves or unisons between any pair of voices must be avoided.

Implementation
if ($ctx['isStart'] || count($ctx['prev']) < 3) { return 0.0; }
$cost = 0.0;
$allCurr = array_merge($ctx['curr'], [$ctx['bassCurr']]);
$allPrev = array_merge($ctx['prev'], [$ctx['bassPrev']]);
$n = count($allCurr);
for ($a = 0; $a < $n; $a++) {
    for ($b = $a + 1; $b < $n; $b++) {
        if (!isset($allPrev[$a]) || !isset($allPrev[$b])) { continue; }
        $prevInt = abs($allPrev[$a] - $allPrev[$b]) % 12;
        $currInt = abs($allCurr[$a] - $allCurr[$b]) % 12;
        $moved = ($allPrev[$a] !== $allCurr[$a]) || ($allPrev[$b] !== $allCurr[$b]);
        if ($moved && $prevInt === 0 && $currInt === 0) { $cost += 60.0; }
    }
}
return $cost;
Leading Tone Resolves Up Gasparini 1708, p. 20; Delair 1724, règle 10
P45 enabled

La nota sensibile deve sempre salire verso la tonica; è vietato scendere o fermarsi su di essa.

The leading tone must always resolve upward; moving down from or remaining on the leading tone is forbidden.

Implementation
if ($ctx['isStart'] || count($ctx['prev']) < 3) { return 0.0; }
$tonicPc = $ctx['keyMode'] === 'minor'
    ? ((($ctx['keyFifths'] * 7) - 3) % 12 + 12) % 12
    : (($ctx['keyFifths'] * 7) % 12 + 12) % 12;
$ltPc = ($tonicPc + 11) % 12;
$cost = 0.0;
foreach ($ctx['curr'] as $i => $curr) {
    $prev = $ctx['prev'][$i] ?? null;
    if ($prev === null || ($prev % 12) !== $ltPc) { continue; }
    if ($curr <= $prev) { $cost += 40.0; } // stayed or went down — must resolve up
}
return $cost;
Seventh Resolves Down St. Lambert 1707, §10 p. 28; Heinichen 1728, pp. 76–78; Christensen 1992, pp. 28, 78
P46 enabled

La septième dissonante doit toujours se résoudre en descendant d'un degré.

Whatever the case, the dissonant seventh invariably resolves one step downward. A voice that held a seventh above the previous bass must move down in the current chord.

Implementation
if ($ctx['isStart'] || count($ctx['prev']) < 3) { return 0.0; }
$prevBassPc = $ctx['bassPrev'] % 12;
$cost = 0.0;
foreach ($ctx['curr'] as $i => $curr) {
    $prev = $ctx['prev'][$i] ?? null;
    if ($prev === null) { continue; }
    $interval = ($prev % 12 - $prevBassPc + 12) % 12;
    if ($interval === 10 || $interval === 11) {
        if ($curr >= $prev) { $cost += 50.0; } // must resolve downward
    }
}
return $cost;
Fourth Resolves Down St. Lambert 1707, p. 22; Heinichen 1728, p. 71; Christensen 1992, pp. 22, 71
P47 enabled

La quarte suspendue se résout toujours en descendant d'un degré vers la tierce (4–3).

A suspended fourth occurring in any upper voice is always sustained and resolved downward by step to the third. A voice that held a fourth (5 semitones) above the previous bass must move down.

Implementation
if ($ctx['isStart'] || count($ctx['prev']) < 3) { return 0.0; }
$prevBassPc = $ctx['bassPrev'] % 12;
$cost = 0.0;
foreach ($ctx['curr'] as $i => $curr) {
    $prev = $ctx['prev'][$i] ?? null;
    if ($prev === null) { continue; }
    $interval = ($prev % 12 - $prevBassPc + 12) % 12;
    if ($interval === 5) {
        if ($curr >= $prev) { $cost += 45.0; } // must resolve downward
    }
}
return $cost;
Ninth Resolves Down St. Lambert 1707, pp. 30–31; Heinichen 1728, pp. 81–82; Christensen 1992, pp. 30, 81
P48 enabled

La neuvième doit toujours se résoudre en descendant vers l'octave.

The suspended ninth invariably resolves downward by step to the octave. Any upper voice holding a ninth (13 or 14 semitones) above the preceding bass must move down in the next chord.

Implementation
if ($ctx['isStart'] || count($ctx['prev']) < 3) { return 0.0; }
$cost = 0.0;
foreach ($ctx['curr'] as $i => $curr) {
    $prev = $ctx['prev'][$i] ?? null;
    if ($prev === null) { continue; }
    $sem = $prev - $ctx['bassPrev'];
    if ($sem === 13 || $sem === 14 || $sem === 25 || $sem === 26) {
        if ($curr >= $prev) { $cost += 45.0; }
    }
}
return $cost;
Augmented Fifth Resolves Up Gasparini 1708, p. 18; Christensen 1992, p. 36
P49 enabled

La quinta eccedente (#5), quando si trova nella voce superiore, deve sempre salire alla sesta.

The augmented fifth (#5 = 8 semitones above the bass), when it appears in the soprano (top voice), must resolve upward to the sixth. It acts as a secondary leading tone.

Implementation
if ($ctx['isStart'] || count($ctx['prev']) < 3) { return 0.0; }
$soprano     = $ctx['curr'][2];
$sopranoPrev = $ctx['prev'][2];
$interval    = ($sopranoPrev - $ctx['bassPrev'] + 1200) % 12;
if ($interval === 8) { // augmented fifth mod 12
    if ($soprano <= $sopranoPrev) { return 30.0; } // must resolve upward
}
return 0.0;
No Hidden Fifths Delair 1724, règle 9
P50 enabled

Les quintes et octaves cachées entre la basse et le soprano sont défendues quand le soprano monte par saut.

Hidden (direct) fifths and octaves between outer voices are forbidden when the soprano moves by leap.

Implementation
if ($ctx['isStart'] || count($ctx['prev']) < 3) { return 0.0; }
$soprano     = $ctx['curr'][2];
$sopranoPrev = $ctx['prev'][2];
$sopranoLeap = abs($soprano - $sopranoPrev) > 2;
if (!$sopranoLeap) { return 0.0; }
$currOuterInt = abs($soprano - $ctx['bassCurr']) % 12;
$bothSameDir  = (($soprano - $sopranoPrev) > 0) === (($ctx['bassCurr'] - $ctx['bassPrev']) > 0);
if ($bothSameDir && ($currOuterInt === 7 || $currOuterInt === 0)) { return 30.0; }
return 0.0;
No Voice Crossing Gasparini 1708, p. 14
P60 enabled

Le parti non si devono incrociare l'una con l'altra.

The voices must not cross one another.

Implementation
$cost = 0.0;
for ($i = 0; $i < count($ctx['curr']) - 1; $i++) {
    if ($ctx['curr'][$i] > $ctx['curr'][$i + 1]) { $cost += 100.0; }
}
return $cost;
Common Tone Retention Delair 1724, règle 5; Gasparini 1708, p. 16
P65 enabled

Si la même note se trouve dans l'accord précédent, on la retient dans la même partie. Exception : si c'est le même accord répété, on change de position pour éviter de monter trop haut ou de descendre trop bas.

If a pitch class is common to both chords, keep it in the same voice at the same octave. Exception: if the harmony is literally repeated, shift voices toward their range centres to avoid drifting to extremes.

Implementation
if ($ctx['isStart'] || count($ctx['prev']) < 3) { return 0.0; }
$cost = 0.0;
$prevPcs = array_values(array_unique(array_map(fn($m) => $m % 12, array_merge($ctx['prev'], [$ctx['bassPrev']]))));
$currPcs = array_values(array_unique(array_map(fn($m) => $m % 12, array_merge($ctx['curr'], [$ctx['bassCurr']]))));
$aPrev = $prevPcs; $aCurr = $currPcs; sort($aPrev); sort($aCurr);
$sameChord = ($aPrev === $aCurr);
if ($sameChord) {
    $names = ['tenor', 'alto', 'soprano'];
    foreach ($ctx['curr'] as $i => $midi) {
        $prev = $ctx['prev'][$i] ?? null;
        if ($prev === null) { continue; }
        [$lo, $hi] = $ctx['ranges'][$names[$i]];
        if ($midi === $prev && abs($midi - ($lo + $hi) / 2) > ($hi - $lo) * 0.35) {
            $cost += 3.0;
        }
    }
    return $cost;
}
foreach ($ctx['curr'] as $i => $midi) {
    $prev = $ctx['prev'][$i] ?? null;
    if ($prev === null) { continue; }
    if (in_array($prev % 12, $currPcs, true) && ($midi % 12) !== ($prev % 12)) {
        $cost += 8.0;
    }
}
return $cost;
No Fourth Doubling Heinichen 1728, p. 71; Christensen 1992, p. 71
P66 enabled

La quarte suspendue ne se double jamais: une seule voix peut tenir la quarte au-dessus de la basse.

The suspended fourth must never be doubled. Only one upper voice may hold a fourth (5 semitones) above the bass at a time, since it is a dissonance requiring a single prepared resolution.

Implementation
$bassPc = $ctx['bassCurr'];
$count = 0;
foreach ($ctx['curr'] as $m) {
    $interval = ($m - $bassPc + 1200) % 12;
    if ($interval === 5) { $count++; } // perfect fourth = 5 semitones
}
if ($count > 1) { return 50.0 * ($count - 1); }
return 0.0;
Prefer Stepwise Motion Wead & Knopke ICMC 2007, §3.2
P70 enabled

Prefer common tones, then stepwise motion; penalize leaps according to size.

Common tones cost 0; steps (≤2) cost 1; small leaps (3–4) cost 4; leaps of a 5th/6th (5–7) cost 9; large leaps cost 3× the interval size.

Implementation
if ($ctx['isStart'] || empty($ctx['prev'])) { return 0.0; }
$cost = 0.0;
foreach ($ctx['curr'] as $i => $midi) {
    $prev = $ctx['prev'][$i] ?? null;
    if ($prev === null) { continue; }
    $motion = abs($midi - $prev);
    if ($motion === 0)      { /* common tone */ }
    elseif ($motion <= 2)   { $cost += 1.0; }
    elseif ($motion <= 4)   { $cost += 4.0; }
    elseif ($motion <= 7)   { $cost += 9.0; }
    else                    { $cost += $motion * 3.0; }
}
return $cost;
Seventh Prefer Fifth Over Octave St. Lambert 1707, p. 28; Heinichen 1728, p. 76; Christensen 1992, pp. 28, 76
P72 enabled

Avec la septième, il vaut mieux jouer la tierce et la quinte que la tierce et l'octave.

When realizing a seventh chord, it is better to include the fifth rather than the octave of the bass. If the chord contains a seventh but has an octave doubling of the bass instead of a fifth, apply a soft penalty.

Implementation
$bassPc = $ctx['bassCurr'] % 12;
$allPcs = array_map(fn($m) => $m % 12, $ctx['curr']);
$hasSeventh = false;
foreach ($allPcs as $pc) {
    $iv = ($pc - $bassPc + 12) % 12;
    if ($iv === 10 || $iv === 11) { $hasSeventh = true; break; }
}
if (!$hasSeventh) { return 0.0; }
$hasFifth  = false;
$hasOctave = false;
foreach ($allPcs as $pc) {
    $iv = ($pc - $bassPc + 12) % 12;
    if ($iv === 7) { $hasFifth  = true; }
    if ($iv === 0) { $hasOctave = true; }
}
if ($hasOctave && !$hasFifth) { return 15.0; }
return 0.0;
Contrary Motion Soprano Bass Delair 1724, règle 6; Gasparini 1708, p. 18
P75 enabled

Le Soprano et la Basse doivent, autant que possible, se mouvoir en sens contraire.

The Soprano and Bass should move in contrary motion whenever possible. Similar motion between outer voices is penalised.

Implementation
if ($ctx['isStart'] || count($ctx['prev']) < 3) { return 0.0; }
$bassDir = $ctx['bassCurr'] - $ctx['bassPrev'];
$sopDir  = $ctx['curr'][2] - $ctx['prev'][2];
if ($bassDir === 0 || $sopDir === 0) { return 0.0; }
if (($bassDir > 0) === ($sopDir > 0)) { return 6.0; }
return 0.0;
Seventh Sequence Alternate Fifth Octave Heinichen 1728, pp. 77–78; Telemann, quoted in Christensen 1992, p. 78
P78 enabled

Dans une suite de septièmes consécutives avec la basse montant par quarte ou descendant par quinte, on alterne la quinte et l'octave: quinte au premier accord, octave au second, et ainsi de suite.

In a sequence of seventh chords where the bass moves up a fourth or down a fifth, alternate between playing the fifth (and omitting the octave) in one chord and the octave (omitting the fifth) in the next. Two consecutive seventh chords that both include the fifth cause parallel fifths; two that both omit it sound thin.

Implementation
if ($ctx['isStart'] || count($ctx['prev']) < 3) { return 0.0; }
// Only applies when a 7th was present in the previous chord
$prevBassPc = $ctx['bassPrev'] % 12;
$hasPrevSeventh = false;
foreach ($ctx['prev'] as $m) {
    $iv = ($m % 12 - $prevBassPc + 12) % 12;
    if ($iv === 10 || $iv === 11) { $hasPrevSeventh = true; break; }
}
if (!$hasPrevSeventh) { return 0.0; }
// Check if current chord also has a seventh
$currBassPc = $ctx['bassCurr'] % 12;
$hasCurrSeventh = false;
foreach ($ctx['curr'] as $m) {
    $iv = ($m % 12 - $currBassPc + 12) % 12;
    if ($iv === 10 || $iv === 11) { $hasCurrSeventh = true; break; }
}
if (!$hasCurrSeventh) { return 0.0; }
// Bass motion: up a 4th (5 semitones) or down a 5th (7 semitones)
$bassMotion = ($ctx['bassCurr'] - $ctx['bassPrev'] + 12) % 12;
if ($bassMotion !== 5 && $bassMotion !== 7) { return 0.0; }
// Check if both chords have a fifth — that causes parallel fifths → penalise
$prevHasFifth = false; $currHasFifth = false;
foreach ($ctx['prev'] as $m) { if (($m % 12 - $prevBassPc + 12) % 12 === 7) { $prevHasFifth = true; break; } }
foreach ($ctx['curr'] as $m) { if (($m % 12 - $currBassPc + 12) % 12 === 7) { $currHasFifth = true; break; } }
if ($prevHasFifth && $currHasFifth) { return 20.0; } // both have fifth → likely parallel fifths
return 0.0;