instr3 commited on
Commit
55bfe14
·
1 Parent(s): 488abe2

Fix chord spelling for non-C candidate roots

Browse files
README.md CHANGED
@@ -48,9 +48,11 @@ Built on [MERT-v2-FullSong](https://huggingface.co/m-a-p/MERT-v2-FullSong), with
48
 
49
  **Update log:**
50
 
 
 
51
  **2026-09-21:** Fixed missing enharmonic spelling correction for key and chord labels. Chord spelling now follows the local key. This updates key/chord labels and ABC notation without changing musical pitches or timing.
52
 
53
- If you downloaded the repository or model before this update (2026-09-21), please update to the latest repository code. **Model weights are unchanged.**
54
 
55
  <a id="quick-start"></a>
56
 
 
48
 
49
  **Update log:**
50
 
51
+ **2026-09-24:** Fixed a bug in key-aware chord spelling. Candidate chord tones were spelled incorrectly, which favored sharp-side names for chromatic chords (e.g., `B#` instead of `C` in E major, `A#` instead of `Bb` in G major). Only chord label names change; pitches, timing, key labels, and model weights are unchanged.
52
+
53
  **2026-09-21:** Fixed missing enharmonic spelling correction for key and chord labels. Chord spelling now follows the local key. This updates key/chord labels and ABC notation without changing musical pitches or timing.
54
 
55
+ If you downloaded the repository or model before this update (2026-09-24), please update to the latest repository code. **Model weights are unchanged.**
56
 
57
  <a id="quick-start"></a>
58
 
chord_spelling_sheetsage2.py CHANGED
@@ -102,6 +102,25 @@ def get_chord_spelling_table():
102
  CHORD_SPELLING_TABLE = chord_spelling_table
103
  return chord_spelling_table
104
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  def score_spelling_under_key(spelling, key):
106
  '''
107
  Score the chord spelling under a given key.
@@ -144,7 +163,7 @@ def correct_chord_spelling(label, key_name):
144
  possible_root_id,
145
  (root_semitone - mir_eval.chord.pitch_class_to_semitone(possible_root) + 6) % 12 - 6
146
  ])
147
- chord_spelling = get_chord_spelling_table()[quality] + root_spelling[None, :]
148
  scores.append(score_spelling_under_key(chord_spelling, key_spelling))
149
  best_root_id = np.argmin(scores)
150
  best_root = NOTE_NAMES[best_root_id]
 
102
  CHORD_SPELLING_TABLE = chord_spelling_table
103
  return chord_spelling_table
104
 
105
+ # Circle-of-fifths offsets of major-scale degrees 1-7 from the tonic.
106
+ MAJOR_SCALE_FIFTHS = np.array([0, 2, 4, -1, 1, 3, 5])
107
+
108
+ def spell_chord_tones(root_spelling, quality):
109
+ '''
110
+ Spell every chord tone from a root spelling.
111
+ Template intervals are relative to the root's own major scale, so adding
112
+ them on the circle of fifths keeps each tone's pitch class; e.g., E:maj
113
+ gives E, G#, B, and Db:hdim7 gives Db, Fb, Abb, Cb.
114
+ :param root_spelling: Numpy array of shape (2,) with note letter id and accidental offset.
115
+ :param quality: Chord quality in QUALITIES.
116
+ :return: Numpy array of shape (N, 2); the first row is the root.
117
+ '''
118
+ intervals = get_chord_spelling_table()[quality]
119
+ letters = (root_spelling[0] + intervals[:, 0]) % 7
120
+ root_pos = CIRCLE_OF_FIFTH_INV[root_spelling[0]] + root_spelling[1] * 7
121
+ tone_pos = root_pos + MAJOR_SCALE_FIFTHS[intervals[:, 0]] + intervals[:, 1] * 7
122
+ return np.stack([letters, (tone_pos - CIRCLE_OF_FIFTH_INV[letters]) // 7], axis=1)
123
+
124
  def score_spelling_under_key(spelling, key):
125
  '''
126
  Score the chord spelling under a given key.
 
163
  possible_root_id,
164
  (root_semitone - mir_eval.chord.pitch_class_to_semitone(possible_root) + 6) % 12 - 6
165
  ])
166
+ chord_spelling = spell_chord_tones(root_spelling, quality)
167
  scores.append(score_spelling_under_key(chord_spelling, key_spelling))
168
  best_root_id = np.argmin(scores)
169
  best_root = NOTE_NAMES[best_root_id]
tests/test_spelling.py CHANGED
@@ -7,7 +7,8 @@ import unittest
7
  import mir_eval.chord
8
  import numpy as np
9
 
10
- from ..chord_spelling_sheetsage2 import correct_chord_rows, correct_chord_spelling, normalize_key_name
 
11
  from ..tokenization_sheetsage2 import SheetSage2Tokenizer as Tokenizer, FULL_CHORD_VOCABULARY
12
  from ..exports_sheetsage2 import export_result
13
  from ..notation_sheetsage2 import get_key_accidentals, note_to_abc
@@ -36,6 +37,24 @@ class SpellingTests(unittest.TestCase):
36
  ('C:min', 'C:min'), ('N', 'N'), ('X', 'X')]:
37
  self.assertEqual(correct_chord_spelling(raw, 'C:minor'), expected)
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  def test_local_midpoint_including_boundary_and_missing_keys(self):
40
  rows = [[0, 2, 'G#:maj'], [2, 6, 'G#:maj'], [6, 8, 'G#:maj']]
41
  keys = [[0, 4, 'C:minor'], [4, 8, 'E:major']]
 
7
  import mir_eval.chord
8
  import numpy as np
9
 
10
+ from ..chord_spelling_sheetsage2 import (QUALITIES, correct_chord_rows, correct_chord_spelling,
11
+ normalize_key_name, spell_chord_tones)
12
  from ..tokenization_sheetsage2 import SheetSage2Tokenizer as Tokenizer, FULL_CHORD_VOCABULARY
13
  from ..exports_sheetsage2 import export_result
14
  from ..notation_sheetsage2 import get_key_accidentals, note_to_abc
 
37
  ('C:min', 'C:min'), ('N', 'N'), ('X', 'X')]:
38
  self.assertEqual(correct_chord_spelling(raw, 'C:minor'), expected)
39
 
40
+ def test_chord_tones_keep_pitch_classes(self):
41
+ naturals = [0, 2, 4, 5, 7, 9, 11]
42
+ for quality, chroma in QUALITIES.items():
43
+ offsets = [i for i, value in enumerate(chroma) if value > 0]
44
+ for root_pc in range(12):
45
+ for letter in range(7):
46
+ acc = (root_pc - naturals[letter] + 6) % 12 - 6
47
+ tones = spell_chord_tones(np.array([letter, acc]), quality)
48
+ self.assertEqual([(naturals[l] + a) % 12 for l, a in tones],
49
+ [(root_pc + o) % 12 for o in offsets])
50
+
51
+ def test_chromatic_chords_use_key_side_spelling(self):
52
+ for raw, key, expected in [('C:maj', 'E:major', 'C:maj'), ('A#:maj', 'G:major', 'Bb:maj'),
53
+ ('D#:maj', 'G:major', 'Eb:maj'), ('G#:7', 'C:major', 'Ab:7'),
54
+ ('A#:min', 'C:major', 'Bb:min'), ('C#:hdim7', 'G:major', 'C#:hdim7'),
55
+ ('F:hdim7', 'G#:minor', 'E#:hdim7')]:
56
+ self.assertEqual(correct_chord_spelling(raw, key), expected)
57
+
58
  def test_local_midpoint_including_boundary_and_missing_keys(self):
59
  rows = [[0, 2, 'G#:maj'], [2, 6, 'G#:maj'], [6, 8, 'G#:maj']]
60
  keys = [[0, 4, 'C:minor'], [4, 8, 'E:major']]