Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ejh 20170704 extended chord mapping #252

Merged
merged 4 commits into from
Jul 24, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be positive int

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, does it make sense to support anything other than multiples of 12?


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