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