-
Notifications
You must be signed in to change notification settings - Fork 0
/
modeclassifier.py
209 lines (156 loc) · 5.3 KB
/
modeclassifier.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
from music21 import *
import scipy
from collections import Counter
# enumeration of all the notes in a given mode, from 0 to 11 for 12 pitches in an octave
Chromatic = [0,1,2,3,4,5,6,7,8,9,10,11]
Ionian = [0,2,4,5,7,9,11]
Dorian = [0,2,3,5,7,9,10]
Phrygian = [0,1,3,5,7,8,10]
Lydian = [0,2,4,6,7,9,11]
Mixolydian = [0,2,4,5,7,9,10]
Aeolian = [0,2,3,5,7,8,10]
Locrian = [0,1,3,5,6,8,10]
#variations
Harmonic_minor = [0,2,3,5,7,8,11]
Melodic_minor = [0,2,3,5,7,9,11]
#Eastern classical modes
Bhairav = [0,1,4,5,7,8,11]
Poorvi = [0,1,4,6,7,8,11]
Marva = [0,1,4,6,7,9,11]
Todi = [0,2,3,6,7,8,11]
modeList = [Ionian, Dorian, Phrygian, Lydian, Mixolydian, Aeolian, Locrian, Harmonic_minor, Melodic_minor, Bhairav,Poorvi, Marva, Todi]
# modetuples has modenames and modes
# modename will be used later to name the given mode
# mode will be used later to compute hamming distance
modeTuples = [ ("Ionian" , Ionian),
("Dorian" , Dorian),
("Phrygian", Phrygian),
("Lydian" , Lydian),
("Mixolydian" , Mixolydian),
("Aeolian" , Aeolian),
("Locrian" , Locrian),
("Harmonic_minor", Harmonic_minor),
("Melodic_minor_ascend", Melodic_minor),
("Bhairav",Bhairav),
("Poorvi", Poorvi),
("Marva", Marva),
("Todi", Todi),
]
# notes referenced to C, initialized to zero
# notes will be used later to transpose other keys to the reference key
# the numbers will be used when rootnote is returned (at rootnote class later)
midinumbers = {
'c' : 0,
'C' : 0,
'c#' : 1,
'C#' : 1,
'd-' : 1,
'D-' : 1,
'd' : 2,
'D' :2,
'd#' : 3,
'D#' : 3,
'e-' : 3,
'E-' : 3,
'e' : 4,
'E' : 4,
'f' : 5,
'F' : 5,
'f#' : 6,
'F#' : 6,
'g-' :6,
'G-': 6,
'g' : 7,
'G' : 7,
'g#' : 8,
'G#' :8,
'a-' : 8,
'A-' :8,
'a' : 9,
'A' : 9,
'a#' : 10,
'A#' : 10,
'b-' : 10,
'B-' : 10,
'b' : 11,
'B' : 11,
}
# has arguments filepath and the condition for removing drums:true or false
def open_midi(midi_path, remove_drums):
mf = midi.MidiFile()
mf.open(midi_path)
mf.read()
mf.close()
# list tracks
print ((mf.tracks))
print(mf)
if (remove_drums):
for i in range(len(mf.tracks)):
#remove drum tracks
mf.tracks[i].events = [ev for ev in mf.tracks[i].events if ev.channel != 10]
# converted to stream
return midi.translate.midiFileToStream(mf)
# returns name of rootnote and enumerated value
# enum will be returned as midinumbers written in the above cell
class Rootnote:
def __init__(self, noteName):
self.noteName = noteName
def __str__(self):
return f"{self.noteName}"
def asnum(self):
return midinumbers[self.noteName]
# takes base_midi (which has midi filename) as argument
def extract_notes(midi_part):
parent_element = []
ret = []
for nt in midi_part.flat.notes:
if isinstance(nt, note.Note):
ret.append(max(0.0, nt.pitch.ps))
parent_element.append(nt)
elif isinstance(nt, chord.Chord):
for pitch in nt.pitches:
ret.append(max(0.0, pitch.ps))
parent_element.append(nt)
# returns appended pitch as ret
# returns pitches of chords as parent_element
return ret, parent_element
# takes filepath as an argument
def getMode(midi_filename):
base_midi = open_midi(midi_filename, True)
# gives key and scale as output. key is required, scale not required
music_analysis = base_midi.analyze('key')
# key stored as rootnote while scale is discarded
rootnote = Rootnote(format(music_analysis).split(' ')[0])
print("the root note is ", rootnote)
# calls extract_notes function has appended pitch returned as ret in a; b has pitches of chords but its not used
a, b = extract_notes(base_midi)
# each elements in 'a' (has pitches) is transposed/ treated as base note, starting from 0.
transposed = [x - rootnote.asnum() for x in a]
# each octave is transposed to -1 octave having midi numbers from 0 to 11.
refOctave = [x % 12 for x in transposed]
# frequency and their count is kept, organized/sorted as below:
freq = Counter(refOctave)
freq = dict (freq)
sorted_notes= sorted(freq.items(), key=lambda x:x[1], reverse=True)
print(sorted_notes)
# mostusednotes list has notes (counts in descending order), only notes with 7 highest counts taken as below:
mostusednotes = []
for i,j in sorted_notes:
mostusednotes.append(i)
mostusednotes = mostusednotes[:7]
# mostusednotes in ascending order (notes themselves in that order)
mostusednotes.sort()
print("most used notes : ", mostusednotes)
lowest = 999
# hamming distance calculation between mostusednotes and 'mode' from modetuple written above
# if distance is lowest, that mode is selected and returned
for modeName, mode in modeTuples:
distance = scipy.spatial.distance.hamming(mostusednotes, mode)
if distance < lowest:
lowest = distance
ourmode = modeName
return rootnote, ourmode
# filepath is passed to above getMode function.
ourmode = getMode("C:/Users/prabin/OneDrive/Desktop/Minor final/After-Mid-Term/BleedingMe.mid")
# the required mode obtained. The index [1] means only string (modename) is printed
print(ourmode[1])