Skip to content

Commit

Permalink
Ejh 20170704 extended chord mapping (#252)
Browse files Browse the repository at this point in the history
* Updated test_chord to check for extended scale degrees.

* Isolated extended chord reduction bug in scale_degree_to_bitmap, tests failing.

* Fixes issue 251

* Removed crufty print statement
  • Loading branch information
ejhumphrey authored and craffel committed Jun 22, 2018
1 parent 09938c7 commit a6fc786
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 29 deletions.
27 changes: 18 additions & 9 deletions mir_eval/chord.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@ def scale_degree_to_semitone(scale_degree):
semitone : int
Relative semitone of the scale degree, wrapped to a single octave
Raises
------
InvalidChordException if `scale_degree` is invalid.
"""
semitone = 0
offset = 0
Expand All @@ -190,11 +193,12 @@ def scale_degree_to_semitone(scale_degree):
semitone = SCALE_DEGREES.get(scale_degree, None)
if semitone is None:
raise InvalidChordException(
"Scale degree improperly formed: %s" % scale_degree)
"Scale degree improperly formed: {}, expected one of {}."
.format(scale_degree, list(SCALE_DEGREES.keys())))
return semitone + offset


def scale_degree_to_bitmap(scale_degree):
def scale_degree_to_bitmap(scale_degree, modulo=False, length=BITMAP_LENGTH):
"""Create a bitmap representation of a scale degree.
Note that values in the bitmap may be negative, indicating that the
Expand All @@ -204,21 +208,25 @@ def scale_degree_to_bitmap(scale_degree):
----------
scale_degree : str
Spelling of a relative scale degree, e.g. 'b3', '7', '#5'
modulo : bool, default=True
If a scale degree exceeds the length of the bit-vector, modulo the
scale degree back into the bit-vector; otherwise it is discarded.
length : int, default=12
Length of the bit-vector to produce
Returns
-------
bitmap : np.ndarray, in [-1, 0, 1]
Bitmap representation of this scale degree (12-dim).
bitmap : np.ndarray, in [-1, 0, 1], len=`length`
Bitmap representation of this scale degree.
"""
sign = 1
if scale_degree.startswith("*"):
sign = -1
scale_degree = scale_degree.strip("*")
edit_map = [0] * BITMAP_LENGTH
edit_map = [0] * length
sd_idx = scale_degree_to_semitone(scale_degree)
if sd_idx < BITMAP_LENGTH:
edit_map[sd_idx % BITMAP_LENGTH] = sign
if sd_idx < length or modulo:
edit_map[sd_idx % length] = sign
return np.array(edit_map)


Expand Down Expand Up @@ -491,7 +499,8 @@ def encode(chord_label, reduce_extended_chords=False,
semitone_bitmap[0] = 1

for scale_degree in scale_degrees:
semitone_bitmap += scale_degree_to_bitmap(scale_degree)
semitone_bitmap += scale_degree_to_bitmap(scale_degree,
reduce_extended_chords)

semitone_bitmap = (semitone_bitmap > 0).astype(np.int)
if not semitone_bitmap[bass_number] and strict_bass_intervals:
Expand Down
76 changes: 56 additions & 20 deletions tests/test_chord.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,22 +44,47 @@ def test_pitch_class_to_semitone():


def test_scale_degree_to_semitone():
valid_degrees = ['b7', '#3', '1', 'b1', '#7', 'bb5']
valid_semitones = [10, 5, 0, -1, 12, 5]
valid_degrees = ['b7', '#3', '1', 'b1', '#7', 'bb5', '11', '#13']
valid_semitones = [10, 5, 0, -1, 12, 5, 17, 22]

for scale_degree, semitone in zip(valid_degrees, valid_semitones):
yield (__check_valid, mir_eval.chord.scale_degree_to_semitone,
(scale_degree,), semitone)

invalid_degrees = ['7b', '4#', '77']
invalid_degrees = ['7b', '4#', '77', '15']

for scale_degree in invalid_degrees:
yield (__check_exception, mir_eval.chord.scale_degree_to_semitone,
(scale_degree,), mir_eval.chord.InvalidChordException)


def test_scale_degree_to_bitmap():

def __check_bitmaps(function, parameters, result):
actual = function(*parameters)
assert np.all(actual == result), (actual, result)

valid_degrees = ['3', '*3', 'b1', '9']
valid_bitmaps = [[0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]]

for scale_degree, bitmap in zip(valid_degrees, valid_bitmaps):
yield (__check_bitmaps, mir_eval.chord.scale_degree_to_bitmap,
(scale_degree, True, 12), np.array(bitmap))

yield (__check_bitmaps, mir_eval.chord.scale_degree_to_bitmap,
('9', False, 12), np.array([0] * 12))

yield (__check_bitmaps, mir_eval.chord.scale_degree_to_bitmap,
('9', False, 15),
np.array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]))


def test_validate_chord_label():
valid_labels = ['C', 'Eb:min/5', 'A#:dim7', 'B:maj(*1,*5)/3', 'A#:sus4']
valid_labels = ['C', 'Eb:min/5', 'A#:dim7', 'B:maj(*1,*5)/3',
'A#:sus4', 'A:(9,11)']
# For valid labels, calling the function without an error = pass
for chord_label in valid_labels:
yield (mir_eval.chord.validate_chord_label, chord_label)
Expand Down Expand Up @@ -139,33 +164,44 @@ def __check_bitmaps(bitmaps, roots, expected_bitmaps):

def test_encode():
def __check_encode(label, expected_root, expected_intervals,
expected_bass):
expected_bass, reduce_extended_chords,
strict_bass_intervals):
''' Helper function for checking encode '''
root, intervals, bass = mir_eval.chord.encode(label)
assert root == expected_root
assert np.all(intervals == expected_intervals)
assert bass == expected_bass

labels = ['B:maj(*1,*3)/5', 'G:dim', 'C:(3)/3']
expected_roots = [11, 7, 0]
root, intervals, bass = mir_eval.chord.encode(
label, reduce_extended_chords=reduce_extended_chords,
strict_bass_intervals=strict_bass_intervals)
assert root == expected_root, (root, expected_root)
assert np.all(intervals == expected_intervals), (intervals,
expected_intervals)
assert bass == expected_bass, (bass, expected_bass)

labels = ['B:maj(*1,*3)/5', 'G:dim', 'C:(3)/3', 'A:9/b3']
expected_roots = [11, 7, 0, 9]
expected_intervals = [[0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
[1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]]
expected_bass = [7, 0, 4]
[1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
# Note that extended scale degrees are dropped.
[1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0]]
expected_bass = [7, 0, 4, 3]

for label, e_root, e_interval, e_bass in zip(labels,
expected_roots,
expected_intervals,
expected_bass):
yield (__check_encode, label, e_root, e_interval, e_bass)
args = zip(labels, expected_roots, expected_intervals, expected_bass)
for label, e_root, e_interval, e_bass in args:
yield (__check_encode, label, e_root, e_interval, e_bass, False, False)

# Non-chord bass notes *must* be explicitly named as extensions when
# strict_bass_intervals == True
yield (__check_exception, mir_eval.chord.encode,
('G:dim(4)/6', False, True), mir_eval.chord.InvalidChordException)

# Otherwise, we can cut a little slack.
yield (__check_encode, 'G:dim(4)/6', 7,
[1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0], 9)
[1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0], 9,
False, False)

# Check that extended scale degrees are mapped back into pitch classes.
yield (__check_encode, 'A:9', 9,
[1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0], 0,
True, False)


def test_encode_many():
Expand Down

0 comments on commit a6fc786

Please sign in to comment.