1# -*- coding: utf-8 -*-
2# ------------------------------------------------------------------------------
3# Name:         midi/percussion.py
4# Purpose:      music21 classes for representing pitches
5#
6# Authors:      Michael Scott Cuthbert
7#               Ben Houge
8#
9# Copyright:    Copyright © 2012, 2017 Michael Scott Cuthbert and the music21 Project
10# License:      BSD, see license.txt
11# ------------------------------------------------------------------------------
12import unittest
13import copy
14
15from music21 import pitch
16from music21 import exceptions21
17from music21 import instrument
18
19
20class MIDIPercussionException(exceptions21.Music21Exception):
21    pass
22
23
24class PercussionMapper:
25    '''
26    PercussionMapper provides tools to convert between MIDI notes and music21 instruments,
27    based on the official General MIDI Level 1 Percussion Key Map.
28    This mapping is conventionally applied to MIDI channel 10;
29    see https://www.midi.org/specifications/item/gm-level-1-sound-set for more info.
30
31    Give me the instrument that corresponds to MIDI note 58!
32
33
34    >>> pm = midi.percussion.PercussionMapper()
35    >>> pm.reverseInstrumentMapping[58]
36    <class 'music21.instrument.Vibraslap'>
37
38    That's right, vibraslap.
39
40    But you're better off using the midiPitchToInstrument() method below!
41
42    .. warning::
43
44        Accepts 1-indexed MIDI programs, unlike music21's 0-indexed `.midiProgram`
45        and `.midiChannel` attributes on Instrument instances.
46    '''
47
48    i = instrument
49    reverseInstrumentMapping = {35: i.BassDrum,  # Acoustic Bass Drum
50                                36: i.BassDrum,  # Bass Drum 1
51                                37: i.SnareDrum,  # Side Stick
52                                38: i.SnareDrum,  # Acoustic Snare
53                                # 39: i.Hand Clap,
54                                40: i.SnareDrum,  # Electric Snare
55                                41: i.TomTom,  # Low Floor Tom
56                                42: i.HiHatCymbal,  # Closed Hi Hat
57                                43: i.TomTom,  # High Floor Tom
58                                44: i.HiHatCymbal,  # Pedal Hi-Hat
59                                45: i.TomTom,  # Low Tom
60                                46: i.HiHatCymbal,  # Open Hi-Hat
61                                47: i.TomTom,  # Low-Mid Tom
62                                48: i.TomTom,  # Hi-Mid Tom
63                                49: i.CrashCymbals,  # Crash Cymbal 1
64                                50: i.TomTom,  # High Tom
65                                # 51: i.Ride Cymbal 1,
66                                # 52: i.Chinese Cymbal,
67                                # 53: i.Ride Bell,
68                                54: i.Tambourine,
69                                # 55: i.Splash Cymbal,
70                                56: i.Cowbell,
71                                57: i.CrashCymbals,  # Crash Cymbal 2
72                                58: i.Vibraslap,
73                                # 59: i.Ride Cymbal 2,
74                                60: i.BongoDrums,  # Hi Bongo
75                                61: i.BongoDrums,  # Low Bongo
76                                62: i.CongaDrum,  # Mute Hi Conga
77                                63: i.CongaDrum,  # Open Hi Conga
78                                64: i.CongaDrum,  # Low Conga
79                                65: i.Timbales,  # High Timbale
80                                66: i.Timbales,  # Low Timbale
81                                67: i.Agogo,  # High Agogo
82                                68: i.Agogo,  # Low Agogo
83                                # 69: i.Cabasa,
84                                70: i.Maracas,
85                                71: i.Whistle,  # Short Whistle
86                                72: i.Whistle,  # Long Whistle
87                                # 73: i.Short Guiro,
88                                # 74: i.Long Guiro,
89                                # 75: i.Claves,
90                                76: i.Woodblock,  # Hi Wood Block
91                                77: i.Woodblock,  # Low Wood Block
92                                # 78: i.Mute Cuica,
93                                # 79: i.Open Cuica,
94                                80: i.Triangle,  # Mute Triangle
95                                81: i.Triangle,  # Open Triangle
96                                }
97
98    # MIDI percussion mappings from https://www.midi.org/specifications/item/gm-level-1-sound-set
99
100    def midiPitchToInstrument(self, midiPitch):
101        '''
102        Takes a pitch.Pitch object or int and returns the corresponding
103        instrument in the GM Percussion Map, using 1-indexed MIDI programs.
104
105
106        >>> pm = midi.percussion.PercussionMapper()
107        >>> cowPitch = pitch.Pitch(56)
108        >>> cowbell = pm.midiPitchToInstrument(cowPitch)
109        >>> cowbell
110        <music21.instrument.Cowbell 'Cowbell'>
111
112        Or it can just take an integer (representing MIDI note) for the pitch instead...
113
114        >>> moreCowbell = pm.midiPitchToInstrument(56)
115        >>> moreCowbell
116        <music21.instrument.Cowbell 'Cowbell'>
117
118        The standard GM Percussion list goes from 35 to 81;
119        pitches outside this range raise an exception.
120
121        >>> bassDrum1Pitch = pitch.Pitch('B-1')
122        >>> pm.midiPitchToInstrument(bassDrum1Pitch)
123        Traceback (most recent call last):
124        music21.midi.percussion.MIDIPercussionException: 34 does not map to a valid instrument!
125
126        Also, certain GM instruments do not have corresponding music21 instruments,
127        so at present they also raise an exception.
128
129        >>> cabasaPitch = 69
130        >>> pm.midiPitchToInstrument(cabasaPitch)
131        Traceback (most recent call last):
132        music21.midi.percussion.MIDIPercussionException: 69 does not map to a valid instrument!
133
134
135        Some music21 Instruments have more than one MidiPitch.  In this case you'll
136        get the same Instrument object but with a different modifier
137
138        >>> acousticBassDrumPitch = pitch.Pitch(35)
139        >>> acousticBDInstrument = pm.midiPitchToInstrument(acousticBassDrumPitch)
140        >>> acousticBDInstrument
141        <music21.instrument.BassDrum 'Bass Drum'>
142        >>> acousticBDInstrument.modifier
143        'acoustic'
144
145        >>> oneBassDrumPitch = pitch.Pitch(36)
146        >>> oneBDInstrument = pm.midiPitchToInstrument(oneBassDrumPitch)
147        >>> oneBDInstrument
148        <music21.instrument.BassDrum 'Bass Drum'>
149        >>> oneBDInstrument.modifier
150        '1'
151
152        '''
153
154        if isinstance(midiPitch, int):
155            midiNumber = midiPitch
156        else:
157            midiNumber = midiPitch.midi
158        if midiNumber not in self.reverseInstrumentMapping:
159            raise MIDIPercussionException(f'{midiNumber!r} does not map to a valid instrument!')
160        midiInstrument = self.reverseInstrumentMapping[midiNumber]
161
162        midiInstrumentObject = midiInstrument()
163        if (midiInstrumentObject.inGMPercMap is True
164                and hasattr(midiInstrumentObject, '_percMapPitchToModifier')):
165            if midiNumber in midiInstrumentObject._percMapPitchToModifier:
166                modifier = midiInstrumentObject._percMapPitchToModifier[midiNumber]
167                midiInstrumentObject.modifier = modifier
168
169        return midiInstrumentObject
170
171    def midiInstrumentToPitch(self, midiInstrument):
172        '''
173        Takes an instrument.Instrument object and returns a pitch object
174        with the corresponding 1-indexed MIDI note, according to the GM Percussion Map.
175
176
177        >>> pm = midi.percussion.PercussionMapper()
178        >>> myCow = instrument.Cowbell()
179        >>> cowPitch = pm.midiInstrumentToPitch(myCow)
180        >>> cowPitch.midi
181        56
182
183        Note that cowPitch is an actual pitch.Pitch object
184        even though it's meaningless!
185
186        >>> cowPitch
187        <music21.pitch.Pitch G#3>
188
189        If the instrument does not have an equivalent in the GM Percussion Map,
190        return an Exception:
191
192        >>> myBagpipes = instrument.Bagpipes()
193        >>> pipePitch = pm.midiInstrumentToPitch(myBagpipes)
194        Traceback (most recent call last):
195        music21.midi.percussion.MIDIPercussionException: <music21.instrument.Bagpipes 'Bagpipes'>
196            is not in the GM Percussion Map!
197        '''
198        if not hasattr(midiInstrument, 'inGMPercMap') or midiInstrument.inGMPercMap is False:
199            raise MIDIPercussionException(f'{midiInstrument!r} is not in the GM Percussion Map!')
200        midiPitch = midiInstrument.percMapPitch
201        pitchObject = pitch.Pitch()
202        pitchObject.midi = midiPitch
203        return pitchObject
204
205    _DOC_ORDER = [midiInstrumentToPitch, midiPitchToInstrument]
206
207
208class Test(unittest.TestCase):
209
210    def testCopyAndDeepcopy(self):
211        '''
212        Test copying all objects defined in this module
213        '''
214        import sys
215        import types
216        for part in sys.modules[self.__module__].__dict__.keys():
217            match = False
218            for skip in ['_', '__', 'Test', 'Exception']:
219                if part.startswith(skip) or part.endswith(skip):
220                    match = True
221            if match:
222                continue
223            name = getattr(sys.modules[self.__module__], part)
224            # noinspection PyTypeChecker
225            if callable(name) and not isinstance(name, types.FunctionType):
226                try:  # see if obj can be made w/o any args
227                    obj = name()
228                except TypeError:
229                    continue
230                junk = copy.copy(obj)
231                junk = copy.deepcopy(obj)
232
233
234# ------------------------------------------------------------------------------
235# define presented order in documentation
236_DOC_ORDER = [PercussionMapper]
237
238
239if __name__ == '__main__':
240    import music21
241    music21.mainTest(Test)
242
243