-
Notifications
You must be signed in to change notification settings - Fork 200
/
key.py
243 lines (199 loc) · 7.33 KB
/
key.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# encoding: utf-8
"""
This module contains key evaluation functionality.
"""
from collections import Counter
from . import EvaluationMixin, evaluation_io
from ..io import load_key
_KEY_TO_SEMITONE = {'c': 0, 'c#': 1, 'db': 1, 'd': 2, 'd#': 3, 'eb': 3, 'e': 4,
'f': 5, 'f#': 6, 'gb': 6, 'g': 7, 'g#': 8, 'ab': 8, 'a': 9,
'a#': 10, 'bb': 10, 'b': 11, 'cb': 11}
def key_label_to_class(key_label):
"""
Convert key label to key class number.
The key label must follow the MIREX syntax defined at
http://music-ir.org/mirex/wiki/2017:Audio_Key_Detection:
`tonic mode`, where tonic is in {C, C#, Db, ... Cb} and mode in {'major',
'maj', 'minor', 'min'}. The label will be converted into a class id based
on the root pitch id (c .. 0, c# .. 1, ..., cb ... 11) plus 12 if in minor
mode.
Parameters
----------
key_label : str
Key label.
Returns
-------
key_class : int
Key class.
Examples
--------
>>> from madmom.evaluation.key import key_label_to_class
>>> key_label_to_class('D major')
2
>>> key_label_to_class('D minor')
14
"""
tonic, mode = key_label.split()
if tonic.lower() not in _KEY_TO_SEMITONE.keys():
raise ValueError('Unknown tonic: {}'.format(tonic))
key_class = _KEY_TO_SEMITONE[tonic.lower()]
if mode in ['minor', 'min']:
key_class += 12
elif mode in ['major', 'maj']:
key_class += 0
else:
raise ValueError('Unknown mode: {}'.format(mode))
return key_class
def error_type(det_key, ann_key, strict_fifth=False):
"""
Compute the evaluation score and error category for a predicted key
compared to the annotated key.
Categories and evaluation scores follow the evaluation strategy used
for MIREX (see http://music-ir.org/mirex/wiki/2017:Audio_Key_Detection).
There are two evaluation modes for the 'fifth' category: by default,
a detection falls into the 'fifth' category if it is the fifth of the
annotation, or the annotation is the fifth of the detection.
If `strict_fifth` is `True`, only the former case is considered. This is
the mode used for MIREX.
Parameters
----------
det_key : int
Detected key class.
ann_key : int
Annotated key class.
strict_fifth: bool
Use strict interpretation of the 'fifth' category, as in MIREX.
Returns
-------
score, category : float, str
Evaluation score and error category.
"""
ann_root = ann_key % 12
ann_mode = ann_key // 12
det_root = det_key % 12
det_mode = det_key // 12
major, minor = 0, 1
if det_root == ann_root and det_mode == ann_mode:
return 1.0, 'correct'
if det_mode == ann_mode and ((det_root - ann_root) % 12 == 7):
return 0.5, 'fifth'
if not strict_fifth and (det_mode == ann_mode and
((det_root - ann_root) % 12 == 5)):
return 0.5, 'fifth'
if (ann_mode == major and det_mode != ann_mode and (
(det_root - ann_root) % 12 == 9)):
return 0.3, 'relative'
if (ann_mode == minor and det_mode != ann_mode and (
(det_root - ann_root) % 12 == 3)):
return 0.3, 'relative'
if det_mode != ann_mode and det_root == ann_root:
return 0.2, 'parallel'
else:
return 0.0, 'other'
class KeyEvaluation(EvaluationMixin):
"""
Provide the key evaluation score.
Parameters
----------
detection : str
File containing detected key
annotation : str
File containing annotated key
strict_fifth : bool, optional
Use strict interpretation of the 'fifth' category, as in MIREX.
name : str, optional
Name of the evaluation object (e.g., the name of the song).
"""
METRIC_NAMES = [
('score', 'Score'),
('error_category', 'Error Category')
]
def __init__(self, detection, annotation, strict_fifth=False, name=None,
**kwargs):
self.name = name or ''
self.detection = key_label_to_class(detection)
self.annotation = key_label_to_class(annotation)
self.score, self.error_category = error_type(
self.detection, self.annotation, strict_fifth
)
def tostring(self, **kwargs):
"""
Format the evaluation as a human readable string.
Returns
-------
str
Evaluation score and category as a human readable string.
"""
ret = '{}: '.format(self.name) if self.name else ''
ret += '{:3.1f}, {}'.format(self.score, self.error_category)
return ret
class KeyMeanEvaluation(EvaluationMixin):
"""
Class for averaging key evaluations.
Parameters
----------
eval_objects : list
Key evaluation objects.
name : str, optional
Name to be displayed.
"""
METRIC_NAMES = [
('correct', 'Correct'),
('fifth', 'Fifth'),
('relative', 'Relative'),
('parallel', 'Parallel'),
('other', 'Other'),
('weighted', 'Weighted'),
]
def __init__(self, eval_objects, name=None):
self.name = name or 'mean for {:d} files'.format(len(eval_objects))
n = len(eval_objects)
c = Counter(e.error_category for e in eval_objects)
self.correct = float(c['correct']) / n
self.fifth = float(c['fifth']) / n
self.relative = float(c['relative']) / n
self.parallel = float(c['parallel']) / n
self.other = float(c['other']) / n
self.weighted = sum(e.score for e in eval_objects) / n
def tostring(self, **kwargs):
return ('{}\n Weighted: {:.3f} Correct: {:.3f} Fifth: {:.3f} '
'Relative: {:.3f} Parallel: {:.3f} Other: {:.3f}'.format(
self.name, self.weighted, self.correct, self.fifth,
self.relative, self.parallel, self.other))
def add_parser(parser):
"""
Add a key evaluation sub-parser to an existing parser.
Parameters
----------
parser : argparse parser instance
Existing argparse parser object.
Returns
-------
sub_parser : argparse sub-parser instance
Key evaluation sub-parser.
"""
import argparse
# add key evaluation sub-parser to the existing parser
p = parser.add_parser(
'key', help='key evaluation',
formatter_class=argparse.RawDescriptionHelpFormatter,
description='''
This program evaluates pairs of files containing global key annotations
and predictions. Suffixes can be given to filter them from the list of
files.
Each file must contain only the global key and follow the syntax outlined
in http://music-ir.org/mirex/wiki/2017:Audio_Key_Detection:
`tonic mode`, where tonic is in {C, C#, Db, ... Cb} and mode in {'major',
'maj', 'minor', 'min'}.
To maintain compatibility with MIREX evaluation scores, use the
--strict_fifth flag.
''')
# set defaults
p.set_defaults(eval=KeyEvaluation, mean_eval=KeyMeanEvaluation,
sum_eval=None, load_fn=load_key)
# file I/O
evaluation_io(p, ann_suffix='.key', det_suffix='.key.txt')
p.add_argument('--strict_fifth', dest='strict_fifth', action='store_true',
help='Strict interpretation of the \"fifth\" category.')
# return the sub-parser and evaluation argument group
return p