1"""Utilty functions for converting between MIDI data and human-readable/usable 2values 3 4""" 5 6import numpy as np 7import re 8 9from .constants import DRUM_MAP, INSTRUMENT_MAP, INSTRUMENT_CLASSES 10 11 12def key_number_to_key_name(key_number): 13 """Convert a key number to a key string. 14 15 Parameters 16 ---------- 17 key_number : int 18 Uses pitch classes to represent major and minor keys. 19 For minor keys, adds a 12 offset. 20 For example, C major is 0 and C minor is 12. 21 22 Returns 23 ------- 24 key_name : str 25 Key name in the format ``'(root) (mode)'``, e.g. ``'Gb minor'``. 26 Gives preference for keys with flats, with the exception of F#, G# and 27 C# minor. 28 """ 29 30 if not isinstance(key_number, int): 31 raise ValueError('`key_number` is not int!') 32 if not ((key_number >= 0) and (key_number < 24)): 33 raise ValueError('`key_number` is larger than 24') 34 35 # preference to keys with flats 36 keys = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 37 'G', 'Ab', 'A', 'Bb', 'B'] 38 39 # circle around 12 pitch classes 40 key_idx = key_number % 12 41 mode = key_number // 12 42 43 # check if mode is major or minor 44 if mode == 0: 45 return keys[key_idx] + ' Major' 46 elif mode == 1: 47 # preference to C#, F# and G# minor 48 if key_idx in [1, 6, 8]: 49 return keys[key_idx-1] + '# minor' 50 else: 51 return keys[key_idx] + ' minor' 52 53 54def key_name_to_key_number(key_string): 55 """Convert a key name string to key number. 56 57 Parameters 58 ---------- 59 key_string : str 60 Format is ``'(root) (mode)'``, where: 61 * ``(root)`` is one of ABCDEFG or abcdefg. A lowercase root 62 indicates a minor key when no mode string is specified. Optionally 63 a # for sharp or b for flat can be specified. 64 65 * ``(mode)`` is optionally specified either as one of 'M', 'Maj', 66 'Major', 'maj', or 'major' for major or 'm', 'Min', 'Minor', 'min', 67 'minor' for minor. If no mode is specified and the root is 68 uppercase, the mode is assumed to be major; if the root is 69 lowercase, the mode is assumed to be minor. 70 71 Returns 72 ------- 73 key_number : int 74 Integer representing the key and its mode. Integers from 0 to 11 75 represent major keys from C to B; 12 to 23 represent minor keys from C 76 to B. 77 """ 78 # Create lists of possible mode names (major or minor) 79 major_strs = ['M', 'Maj', 'Major', 'maj', 'major'] 80 minor_strs = ['m', 'Min', 'Minor', 'min', 'minor'] 81 # Construct regular expression for matching key 82 pattern = re.compile( 83 # Start with any of A-G, a-g 84 '^(?P<key>[ABCDEFGabcdefg])' 85 # Next, look for #, b, or nothing 86 '(?P<flatsharp>[#b]?)' 87 # Allow for a space between key and mode 88 ' ?' 89 # Next, look for any of the mode strings 90 '(?P<mode>(?:(?:' + 91 # Next, look for any of the major or minor mode strings 92 ')|(?:'.join(major_strs + minor_strs) + '))?)$') 93 # Match provided key string 94 result = re.match(pattern, key_string) 95 if result is None: 96 raise ValueError('Supplied key {} is not valid.'.format(key_string)) 97 # Convert result to dictionary 98 result = result.groupdict() 99 100 # Map from key string to pitch class number 101 key_number = {'c': 0, 'd': 2, 'e': 4, 'f': 5, 102 'g': 7, 'a': 9, 'b': 11}[result['key'].lower()] 103 # Increment or decrement pitch class if a flat or sharp was specified 104 if result['flatsharp']: 105 if result['flatsharp'] == '#': 106 key_number += 1 107 elif result['flatsharp'] == 'b': 108 key_number -= 1 109 # Circle around 12 pitch classes 110 key_number = key_number % 12 111 # Offset if mode is minor, or the key name is lowercase 112 if result['mode'] in minor_strs or (result['key'].islower() and 113 result['mode'] not in major_strs): 114 key_number += 12 115 116 return key_number 117 118 119def mode_accidentals_to_key_number(mode, num_accidentals): 120 """Convert a given number of accidentals and mode to a key number. 121 122 Parameters 123 ---------- 124 mode : int 125 0 is major, 1 is minor. 126 num_accidentals : int 127 Positive number is used for sharps, negative number is used for flats. 128 129 Returns 130 ------- 131 key_number : int 132 Integer representing the key and its mode. 133 """ 134 135 if not (isinstance(num_accidentals, int) and 136 num_accidentals > -8 and 137 num_accidentals < 8): 138 raise ValueError('Number of accidentals {} is not valid'.format( 139 num_accidentals)) 140 if mode not in (0, 1): 141 raise ValueError('Mode {} is not recognizable, must be 0 or 1'.format( 142 mode)) 143 144 sharp_keys = 'CGDAEBF' 145 flat_keys = 'FBEADGC' 146 147 # check if key signature has sharps or flats 148 if num_accidentals >= 0: 149 num_sharps = num_accidentals // 6 150 key = sharp_keys[num_accidentals % 7] + '#' * int(num_sharps) 151 else: 152 if num_accidentals == -1: 153 key = 'F' 154 else: 155 key = flat_keys[(-1 * num_accidentals - 1) % 7] + 'b' 156 157 # find major key number 158 key += ' Major' 159 160 # use routine to convert from string notation to number notation 161 key_number = key_name_to_key_number(key) 162 163 # if minor, offset 164 if mode == 1: 165 key_number = 12 + ((key_number - 3) % 12) 166 167 return key_number 168 169 170def key_number_to_mode_accidentals(key_number): 171 """Converts a key number to number of accidentals and mode. 172 173 Parameters 174 ---------- 175 key_number : int 176 Key number as used in ``pretty_midi``. 177 178 Returns 179 ------- 180 mode : int 181 0 for major, 1 for minor. 182 num_accidentals : int 183 Number of accidentals. 184 Positive is for sharps and negative is for flats. 185 """ 186 187 if not ((isinstance(key_number, int) and 188 key_number >= 0 and 189 key_number < 24)): 190 raise ValueError('Key number {} is not a must be an int between 0 and ' 191 '24'.format(key_number)) 192 193 pc_to_num_accidentals_major = {0: 0, 1: -5, 2: 2, 3: -3, 4: 4, 5: -1, 6: 6, 194 7: 1, 8: -4, 9: 3, 10: -2, 11: 5} 195 mode = key_number // 12 196 197 if mode == 0: 198 num_accidentals = pc_to_num_accidentals_major[key_number] 199 return mode, num_accidentals 200 elif mode == 1: 201 key_number = (key_number + 3) % 12 202 num_accidentals = pc_to_num_accidentals_major[key_number] 203 return mode, num_accidentals 204 else: 205 return None 206 207 208def qpm_to_bpm(quarter_note_tempo, numerator, denominator): 209 """Converts from quarter notes per minute to beats per minute. 210 211 Parameters 212 ---------- 213 quarter_note_tempo : float 214 Quarter note tempo. 215 numerator : int 216 Numerator of time signature. 217 denominator : int 218 Denominator of time signature. 219 220 Returns 221 ------- 222 bpm : float 223 Tempo in beats per minute. 224 """ 225 226 if not (isinstance(quarter_note_tempo, (int, float)) and 227 quarter_note_tempo > 0): 228 raise ValueError( 229 'Quarter notes per minute must be an int or float ' 230 'greater than 0, but {} was supplied'.format(quarter_note_tempo)) 231 if not (isinstance(numerator, int) and numerator > 0): 232 raise ValueError( 233 'Time signature numerator must be an int greater than 0, but {} ' 234 'was supplied.'.format(numerator)) 235 if not (isinstance(denominator, int) and denominator > 0): 236 raise ValueError( 237 'Time signature denominator must be an int greater than 0, but {} ' 238 'was supplied.'.format(denominator)) 239 240 # denominator is whole, half, quarter, eighth, sixteenth or 32nd note 241 if denominator in [1, 2, 4, 8, 16, 32]: 242 # simple triple 243 if numerator == 3: 244 return quarter_note_tempo * denominator / 4.0 245 # compound meter 6/8*n, 9/8*n, 12/8*n... 246 elif numerator % 3 == 0: 247 return quarter_note_tempo / 3.0 * denominator / 4.0 248 # strongly assume two eighths equal a beat 249 else: 250 return quarter_note_tempo * denominator / 4.0 251 else: 252 return quarter_note_tempo 253 254 255def note_number_to_hz(note_number): 256 """Convert a (fractional) MIDI note number to its frequency in Hz. 257 258 Parameters 259 ---------- 260 note_number : float 261 MIDI note number, can be fractional. 262 263 Returns 264 ------- 265 note_frequency : float 266 Frequency of the note in Hz. 267 268 """ 269 # MIDI note numbers are defined as the number of semitones relative to C0 270 # in a 440 Hz tuning 271 return 440.0*(2.0**((note_number - 69)/12.0)) 272 273 274def hz_to_note_number(frequency): 275 """Convert a frequency in Hz to a (fractional) note number. 276 277 Parameters 278 ---------- 279 frequency : float 280 Frequency of the note in Hz. 281 282 Returns 283 ------- 284 note_number : float 285 MIDI note number, can be fractional. 286 287 """ 288 # MIDI note numbers are defined as the number of semitones relative to C0 289 # in a 440 Hz tuning 290 return 12*(np.log2(frequency) - np.log2(440.0)) + 69 291 292 293def note_name_to_number(note_name): 294 """Converts a note name in the format 295 ``'(note)(accidental)(octave number)'`` (e.g. ``'C#4'``) to MIDI note 296 number. 297 298 ``'(note)'`` is required, and is case-insensitive. 299 300 ``'(accidental)'`` should be ``''`` for natural, ``'#'`` for sharp and 301 ``'!'`` or ``'b'`` for flat. 302 303 If ``'(octave)'`` is ``''``, octave 0 is assumed. 304 305 Parameters 306 ---------- 307 note_name : str 308 A note name, as described above. 309 310 Returns 311 ------- 312 note_number : int 313 MIDI note number corresponding to the provided note name. 314 315 Notes 316 ----- 317 Thanks to Brian McFee. 318 319 """ 320 321 # Map note name to the semitone 322 pitch_map = {'C': 0, 'D': 2, 'E': 4, 'F': 5, 'G': 7, 'A': 9, 'B': 11} 323 # Relative change in semitone denoted by each accidental 324 acc_map = {'#': 1, '': 0, 'b': -1, '!': -1} 325 326 # Reg exp will raise an error when the note name is not valid 327 try: 328 # Extract pitch, octave, and accidental from the supplied note name 329 match = re.match(r'^(?P<n>[A-Ga-g])(?P<off>[#b!]?)(?P<oct>[+-]?\d+)$', 330 note_name) 331 332 pitch = match.group('n').upper() 333 offset = acc_map[match.group('off')] 334 octave = int(match.group('oct')) 335 except: 336 raise ValueError('Improper note format: {}'.format(note_name)) 337 338 # Convert from the extrated ints to a full note number 339 return 12*(octave + 1) + pitch_map[pitch] + offset 340 341 342def note_number_to_name(note_number): 343 """Convert a MIDI note number to its name, in the format 344 ``'(note)(accidental)(octave number)'`` (e.g. ``'C#4'``). 345 346 Parameters 347 ---------- 348 note_number : int 349 MIDI note number. If not an int, it will be rounded. 350 351 Returns 352 ------- 353 note_name : str 354 Name of the supplied MIDI note number. 355 356 Notes 357 ----- 358 Thanks to Brian McFee. 359 360 """ 361 362 # Note names within one octave 363 semis = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] 364 365 # Ensure the note is an int 366 note_number = int(np.round(note_number)) 367 368 # Get the semitone and the octave, and concatenate to create the name 369 return semis[note_number % 12] + str(note_number//12 - 1) 370 371 372def note_number_to_drum_name(note_number): 373 """Converts a MIDI note number in a percussion instrument to the 374 corresponding drum name, according to the General MIDI standard. 375 376 Any MIDI note number outside of the valid range (note 35-81, zero-indexed) 377 will result in an empty string. 378 379 Parameters 380 ---------- 381 note_number : int 382 MIDI note number. If not an int, it will be rounded. 383 384 Returns 385 ------- 386 drum_name : str 387 Name of the drum for this note for a percussion instrument. 388 389 Notes 390 ----- 391 See http://www.midi.org/techspecs/gm1sound.php 392 393 """ 394 395 # Ensure note is an int 396 note_number = int(np.round(note_number)) 397 # General MIDI only defines drum names for notes 35-81 398 if note_number < 35 or note_number > 81: 399 return '' 400 else: 401 # Our DRUM_MAP starts from index 0; drum names start from 35 402 return DRUM_MAP[note_number - 35] 403 404 405def __normalize_str(name): 406 """Removes all non-alphanumeric characters from a string and converts 407 it to lowercase. 408 409 """ 410 return ''.join(ch for ch in name if ch.isalnum()).lower() 411 412 413def drum_name_to_note_number(drum_name): 414 """Converts a drum name to the corresponding MIDI note number for a 415 percussion instrument. Conversion is case, whitespace, and 416 non-alphanumeric character insensitive. 417 418 Parameters 419 ---------- 420 drum_name : str 421 Name of a drum which exists in the general MIDI standard. 422 If the drum is not found, a ValueError is raised. 423 424 Returns 425 ------- 426 note_number : int 427 The MIDI note number corresponding to this drum. 428 429 Notes 430 ----- 431 See http://www.midi.org/techspecs/gm1sound.php 432 433 """ 434 435 normalized_drum_name = __normalize_str(drum_name) 436 # Create a list of the entries DRUM_MAP, normalized, to search over 437 normalized_drum_names = [__normalize_str(name) for name in DRUM_MAP] 438 439 # If the normalized drum name is not found, complain 440 try: 441 note_index = normalized_drum_names.index(normalized_drum_name) 442 except: 443 raise ValueError('{} is not a valid General MIDI drum ' 444 'name.'.format(drum_name)) 445 446 # If an index was found, it will be 0-based; add 35 to get the note number 447 return note_index + 35 448 449 450def program_to_instrument_name(program_number): 451 """Converts a MIDI program number to the corresponding General MIDI 452 instrument name. 453 454 Parameters 455 ---------- 456 program_number : int 457 MIDI program number, between 0 and 127. 458 459 Returns 460 ------- 461 instrument_name : str 462 Name of the instrument corresponding to this program number. 463 464 Notes 465 ----- 466 See http://www.midi.org/techspecs/gm1sound.php 467 468 """ 469 470 # Check that the supplied program is in the valid range 471 if program_number < 0 or program_number > 127: 472 raise ValueError('Invalid program number {}, should be between 0 and' 473 ' 127'.format(program_number)) 474 # Just grab the name from the instrument mapping list 475 return INSTRUMENT_MAP[program_number] 476 477 478def instrument_name_to_program(instrument_name): 479 """Converts an instrument name to the corresponding General MIDI program 480 number. Conversion is case, whitespace, and non-alphanumeric character 481 insensitive. 482 483 Parameters 484 ---------- 485 instrument_name : str 486 Name of an instrument which exists in the general MIDI standard. 487 If the instrument is not found, a ValueError is raised. 488 489 Returns 490 ------- 491 program_number : int 492 The MIDI program number corresponding to this instrument. 493 494 Notes 495 ----- 496 See http://www.midi.org/techspecs/gm1sound.php 497 498 """ 499 500 normalized_inst_name = __normalize_str(instrument_name) 501 # Create a list of the entries INSTRUMENT_MAP, normalized, to search over 502 normalized_inst_names = [__normalize_str(name) for name in 503 INSTRUMENT_MAP] 504 505 # If the normalized drum name is not found, complain 506 try: 507 program_number = normalized_inst_names.index(normalized_inst_name) 508 except: 509 raise ValueError('{} is not a valid General MIDI instrument ' 510 'name.'.format(instrument_name)) 511 512 # Return the index (program number) if a match was found 513 return program_number 514 515 516def program_to_instrument_class(program_number): 517 """Converts a MIDI program number to the corresponding General MIDI 518 instrument class. 519 520 Parameters 521 ---------- 522 program_number : int 523 MIDI program number, between 0 and 127. 524 525 Returns 526 ------- 527 instrument_class : str 528 Name of the instrument class corresponding to this program number. 529 530 Notes 531 ----- 532 See http://www.midi.org/techspecs/gm1sound.php 533 534 """ 535 536 # Check that the supplied program is in the valid range 537 if program_number < 0 or program_number > 127: 538 raise ValueError('Invalid program number {}, should be between 0 and' 539 ' 127'.format(program_number)) 540 # Just grab the name from the instrument mapping list 541 return INSTRUMENT_CLASSES[int(program_number)//8] 542 543 544def pitch_bend_to_semitones(pitch_bend, semitone_range=2.): 545 """Convert a MIDI pitch bend value (in the range ``[-8192, 8191]``) to the 546 bend amount in semitones. 547 548 Parameters 549 ---------- 550 pitch_bend : int 551 MIDI pitch bend amount, in ``[-8192, 8191]``. 552 semitone_range : float 553 Convert to +/- this semitone range. Default is 2., which is the 554 General MIDI standard +/-2 semitone range. 555 556 Returns 557 ------- 558 semitones : float 559 Number of semitones corresponding to this pitch bend amount. 560 561 """ 562 563 return semitone_range*pitch_bend/8192.0 564 565 566def semitones_to_pitch_bend(semitones, semitone_range=2.): 567 """Convert a semitone value to the corresponding MIDI pitch bend integer. 568 569 Parameters 570 ---------- 571 semitones : float 572 Number of semitones for the pitch bend. 573 semitone_range : float 574 Convert to +/- this semitone range. Default is 2., which is the 575 General MIDI standard +/-2 semitone range. 576 577 Returns 578 ------- 579 pitch_bend : int 580 MIDI pitch bend amount, in ``[-8192, 8191]``. 581 582 """ 583 return int(8192*(semitones/semitone_range)) 584