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