Daniel@0
|
1 # Part of DML (Digital Music Laboratory)
|
Daniel@0
|
2 # Copyright 2014-2015 Daniel Wolff, City University
|
Daniel@0
|
3
|
Daniel@0
|
4 # This program is free software; you can redistribute it and/or
|
Daniel@0
|
5 # modify it under the terms of the GNU General Public License
|
Daniel@0
|
6 # as published by the Free Software Foundation; either version 2
|
Daniel@0
|
7 # of the License, or (at your option) any later version.
|
Daniel@0
|
8 #
|
Daniel@0
|
9 # This program is distributed in the hope that it will be useful,
|
Daniel@0
|
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
|
Daniel@0
|
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
Daniel@0
|
12 # GNU General Public License for more details.
|
Daniel@0
|
13 #
|
Daniel@0
|
14 # You should have received a copy of the GNU General Public
|
Daniel@0
|
15 # License along with this library; if not, write to the Free Software
|
Daniel@0
|
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
Daniel@0
|
17
|
Daniel@0
|
18 #!/usr/bin/python
|
Daniel@0
|
19 # -*- coding: utf-8 -*-
|
Daniel@0
|
20 __author__="Daniel Wolff"
|
Daniel@0
|
21
|
Daniel@0
|
22 import re
|
Daniel@0
|
23
|
Daniel@0
|
24 # these for file reading etc
|
Daniel@0
|
25 import fnmatch
|
Daniel@0
|
26 import os
|
Daniel@0
|
27 import csv
|
Daniel@0
|
28 import spmf
|
Daniel@0
|
29
|
Daniel@0
|
30 import sys
|
Daniel@0
|
31 sys.path.insert(0, '../tools/')
|
Daniel@0
|
32 import csv2json as c2j
|
Daniel@0
|
33
|
Daniel@0
|
34 # ---
|
Daniel@0
|
35 # roots
|
Daniel@0
|
36 # ---
|
Daniel@0
|
37 chord_roots = ["C","D","E","F","G","A","B"]
|
Daniel@0
|
38
|
Daniel@0
|
39 # create a dictionary for efficiency
|
Daniel@0
|
40 roots_dic = dict(zip(chord_roots, [0,2,4,5,7,9,11]))
|
Daniel@0
|
41
|
Daniel@0
|
42 mode_lbls = ['major','minor']
|
Daniel@0
|
43 mode_dic = dict(zip(mode_lbls, range(0,2)))
|
Daniel@0
|
44 # ---
|
Daniel@0
|
45 # types
|
Daniel@0
|
46 # ---
|
Daniel@0
|
47 type_labels = ["", "6", "7", "m","m6", "m7", "maj7", "m7b5", "dim", "dim7", "aug"]
|
Daniel@0
|
48 type_dic = dict(zip(type_labels, range(0,len(type_labels))))
|
Daniel@0
|
49
|
Daniel@0
|
50 base_labels = ["1","","2","b3","3","4","","5","","6","b7","7"]
|
Daniel@0
|
51 #base_dic = dict(zip(base_labels, range(0,len(base_labels))))
|
Daniel@0
|
52
|
Daniel@0
|
53 # functions
|
Daniel@0
|
54 root_funs_maj = ['I','#I','II','#II','III','IV','#IV','V','#V','VI','#VI','VII']
|
Daniel@0
|
55 root_funs_min = ['I','#I','II','III','#III','IV','#IV','V','VI','#VI','VII','#VII']
|
Daniel@0
|
56 # dan's suggestion
|
Daniel@0
|
57 #root_funs_maj = ['I','#I','II','#II','(M)III','IV','#IV','V','#V','VI','#VI','(M)VII']
|
Daniel@0
|
58 #root_funs_min = ['I','#I','II','(m)III','#III','IV','#IV','V','VI','#VI','(m)VII','#VII']
|
Daniel@0
|
59
|
Daniel@0
|
60 fun_dic_maj = dict(zip(range(0,len(root_funs_maj)),root_funs_maj))
|
Daniel@0
|
61 fun_dic_min = dict(zip(range(0,len(root_funs_min)),root_funs_min))
|
Daniel@0
|
62 # regex that separates roots and types, and gets chord base
|
Daniel@0
|
63 # this only accepts chords with a sharp (#) and no flats
|
Daniel@0
|
64 p = re.compile(r'(?P<root>[A-G,N](#|b)*)(?P<type>[a-z,0-9]*)(/(?P<base>[A-G](#|b)*))*')
|
Daniel@0
|
65 p2 = re.compile(r'(?P<key>[A-G](#|b)*)(\s/\s[A-G](#|b)*)*\s(?P<mode>[major|minor]+)')
|
Daniel@0
|
66 pclip = re.compile(r'(?P<clipid>[A-Z,0-9]+(\-|_)[A-Z,0-9]+((\-|_)[A-Z,0-9]+)*((\-|_)[A-Z,0-9]+)*)_(?P<type>vamp.*).(?P<ext>(csv|xml|txt|n3)+)')
|
Daniel@0
|
67
|
Daniel@0
|
68 ftype = {'key': 'vamp_qm-vamp-plugins_qm-keydetector_key',
|
Daniel@0
|
69 'chord': 'vamp_nnls-chroma_chordino_simplechord'}
|
Daniel@0
|
70
|
Daniel@0
|
71 # most simple note2num
|
Daniel@0
|
72 def note2num(notein = 'Cb'):
|
Daniel@0
|
73 base = roots_dic[notein[0]]
|
Daniel@0
|
74 if len(notein) > 1:
|
Daniel@0
|
75 if notein[1] == 'b':
|
Daniel@0
|
76 return (base - 1) % 12
|
Daniel@0
|
77 elif notein[1] == '#':
|
Daniel@0
|
78 return (base + 1) % 12
|
Daniel@0
|
79 else:
|
Daniel@0
|
80 print "Error parsing chord " + notein
|
Daniel@0
|
81 raise
|
Daniel@0
|
82 else:
|
Daniel@0
|
83 return base % 12
|
Daniel@0
|
84
|
Daniel@0
|
85
|
Daniel@0
|
86 # convert key to number
|
Daniel@0
|
87 def key2num(keyin = 'C major'):
|
Daniel@0
|
88 # ---
|
Daniel@0
|
89 # parse key string: separate root from rest
|
Daniel@0
|
90 # ---
|
Daniel@0
|
91 sepstring = p2.match(keyin)
|
Daniel@0
|
92 if not sepstring:
|
Daniel@0
|
93 print "Error parsing key " + keyin
|
Daniel@0
|
94 raise
|
Daniel@0
|
95
|
Daniel@0
|
96 # get relative position of chord and adapt for flats
|
Daniel@0
|
97 key = sepstring.group('key')
|
Daniel@0
|
98 key = note2num(key)
|
Daniel@0
|
99
|
Daniel@0
|
100 # ---
|
Daniel@0
|
101 # parse mode. care for (unknown) string
|
Daniel@0
|
102 # ---
|
Daniel@0
|
103 mode = sepstring.group('mode')
|
Daniel@0
|
104 if mode:
|
Daniel@0
|
105 mode = mode_dic[mode]
|
Daniel@0
|
106 else:
|
Daniel@0
|
107 mode = -1
|
Daniel@0
|
108
|
Daniel@0
|
109 return (key, mode)
|
Daniel@0
|
110
|
Daniel@0
|
111
|
Daniel@0
|
112
|
Daniel@0
|
113 # convert chord to relative function
|
Daniel@0
|
114 def chord2function(cin = 'B',key=3, mode=0):
|
Daniel@0
|
115 # ---
|
Daniel@0
|
116 # parse chord string: separate root from rest
|
Daniel@0
|
117 # ---
|
Daniel@0
|
118 sepstring = p.match(cin)
|
Daniel@0
|
119
|
Daniel@0
|
120 # test for N code -> no chord detected
|
Daniel@0
|
121 if sepstring.group('root') == 'N':
|
Daniel@0
|
122 return (-1,-1,-1,-1)
|
Daniel@0
|
123
|
Daniel@0
|
124 # get root and type otherwise
|
Daniel@0
|
125 root = note2num(sepstring.group('root'))
|
Daniel@0
|
126 type = sepstring.group('type')
|
Daniel@0
|
127
|
Daniel@0
|
128 typ = type_dic[type]
|
Daniel@0
|
129
|
Daniel@0
|
130 # get relative position
|
Daniel@0
|
131 fun = (root - key) % 12
|
Daniel@0
|
132
|
Daniel@0
|
133 #--- do we have a base key?
|
Daniel@0
|
134 # if yes return it relative to chord root
|
Daniel@0
|
135 # ---
|
Daniel@0
|
136 if sepstring.group('base'):
|
Daniel@0
|
137 broot = note2num(sepstring.group('base'))
|
Daniel@0
|
138 bfun = (broot - root) % 12
|
Daniel@0
|
139 else:
|
Daniel@0
|
140 # this standard gives 1 as a base key if not specified otherwise
|
Daniel@0
|
141 bfun = 0
|
Daniel@0
|
142
|
Daniel@0
|
143
|
Daniel@0
|
144 # ---
|
Daniel@0
|
145 # todo: integrate bfun in final type list
|
Daniel@0
|
146 # ---
|
Daniel@0
|
147
|
Daniel@0
|
148 return (root,fun,typ,bfun)
|
Daniel@0
|
149
|
Daniel@0
|
150 # reads in any csv and returns a list of structure
|
Daniel@0
|
151 # time(float), data1, data2 ....data2
|
Daniel@0
|
152 def read_vamp_csv(filein = ''):
|
Daniel@0
|
153 output = []
|
Daniel@0
|
154 with open(filein, 'rb') as csvfile:
|
Daniel@0
|
155 contents = csv.reader(csvfile, delimiter=',', quotechar='"')
|
Daniel@0
|
156 for row in contents:
|
Daniel@0
|
157 output.append([float(row[0])] + row[1:])
|
Daniel@0
|
158 return output
|
Daniel@0
|
159
|
Daniel@0
|
160 # legacy:: finds featurefile for given piece
|
Daniel@0
|
161 def find_features(clipin = '', type='key'):
|
Daniel@0
|
162 # ---
|
Daniel@0
|
163 # These Parametres are for the high-level parse functions
|
Daniel@0
|
164 # ---
|
Daniel@0
|
165 featuredirs = {'key':'.\qm_vamp_key_standard.n3_50ac9',
|
Daniel@0
|
166 'chord': '.\chordino_simple.n3_1a812'}
|
Daniel@0
|
167
|
Daniel@0
|
168 # search for featurefile
|
Daniel@0
|
169 featuredir = featuredirs[type].replace('\\', '/')
|
Daniel@0
|
170 for file in os.listdir(featuredir):
|
Daniel@0
|
171 if fnmatch.fnmatch(file, clipin+ '*' + ftype[type] + '*.csv'):
|
Daniel@0
|
172 return featuredirs[type] + '/' + file
|
Daniel@0
|
173
|
Daniel@0
|
174 # reads features for given clip and of specified type
|
Daniel@0
|
175 def get_features(clipin = '', type='key', featurefiles = 0):
|
Daniel@0
|
176 if not featurefiles:
|
Daniel@0
|
177 featurefiles = find_features(clipin, type)
|
Daniel@0
|
178 return read_vamp_csv(featurefiles[type])
|
Daniel@0
|
179
|
Daniel@0
|
180 # histogram of the last entry in a list
|
Daniel@0
|
181 # returns the most frequently used key
|
Daniel@0
|
182 def histogram(keysin = []):
|
Daniel@0
|
183 # build histogram
|
Daniel@0
|
184 histo = dict()
|
Daniel@0
|
185 for row in keysin:
|
Daniel@0
|
186 histo[row[-1]] = histo.get(row[-1], 0) + 1
|
Daniel@0
|
187
|
Daniel@0
|
188 # return most frequent key
|
Daniel@0
|
189 return (histo, max(histo.iterkeys(), key=(lambda key: histo[key])))
|
Daniel@0
|
190
|
Daniel@0
|
191
|
Daniel@0
|
192 # main function, processes all chords for one song
|
Daniel@0
|
193 def chords2functions(clipin = '1CD0006591_BD11-14',featurefiles = '', constkey = 1):
|
Daniel@0
|
194
|
Daniel@0
|
195 # get keys
|
Daniel@0
|
196 keys = get_features(clipin,'key',featurefiles)
|
Daniel@0
|
197
|
Daniel@0
|
198 relchords = []
|
Daniel@0
|
199 # chords
|
Daniel@0
|
200 chords = get_features(clipin,'chord',featurefiles)
|
Daniel@0
|
201 if constkey:
|
Daniel@0
|
202 # delete 'unknown' keys
|
Daniel@0
|
203 keys = [(time,knum,key) for (time,knum,key) in keys if not key == '(unknown)']
|
Daniel@0
|
204
|
Daniel@0
|
205 # aggregate to one key
|
Daniel@0
|
206 (histo, skey) = histogram(keys)
|
Daniel@0
|
207
|
Daniel@0
|
208 # bet key number
|
Daniel@0
|
209 (key,mode) = key2num(skey)
|
Daniel@0
|
210
|
Daniel@0
|
211 for (time,chord) in chords:
|
Daniel@0
|
212
|
Daniel@0
|
213 # get chord function
|
Daniel@0
|
214 (root,fun,typ, bfun) = chord2function(chord, key,mode)
|
Daniel@0
|
215
|
Daniel@0
|
216 # translate into text
|
Daniel@0
|
217 txt = fun2txt(fun,typ, bfun, mode)
|
Daniel@0
|
218 #print 'Key: ' + skey + ', chord: ' + chord + ', function: ' + txt
|
Daniel@0
|
219
|
Daniel@0
|
220 relchords.append((time,key,mode,fun,typ,bfun))
|
Daniel@0
|
221 return relchords
|
Daniel@0
|
222
|
Daniel@0
|
223 def tracks_in_dir(dirin = ''):
|
Daniel@0
|
224
|
Daniel@0
|
225 # ---
|
Daniel@0
|
226 # we now only search for tracks which have chord data
|
Daniel@0
|
227 # ---
|
Daniel@0
|
228
|
Daniel@0
|
229 # data is a dictionary that
|
Daniel@0
|
230 # for each filename contains the feature
|
Daniel@0
|
231 # files for chords and keys
|
Daniel@0
|
232
|
Daniel@0
|
233 data = dict();
|
Daniel@0
|
234 # traverse the file structure and get all track names
|
Daniel@0
|
235 count = 0
|
Daniel@0
|
236 errcount = 0
|
Daniel@0
|
237 for (dirpath, dirnames, filenames) in os.walk(dirin):
|
Daniel@0
|
238 for file in filenames:
|
Daniel@0
|
239 #print '\rChecked %d files' % (count),
|
Daniel@0
|
240 count = count + 1
|
Daniel@0
|
241 if file.endswith(".csv"):
|
Daniel@0
|
242 # parse filename to get clip_id
|
Daniel@0
|
243 parsed = pclip.match(file)
|
Daniel@0
|
244 if parsed:
|
Daniel@0
|
245 clipin = parsed.group('clipid')
|
Daniel@0
|
246
|
Daniel@0
|
247 # initialise dict if necessary
|
Daniel@0
|
248 if not data.has_key(clipin):
|
Daniel@0
|
249 data[clipin] = dict()
|
Daniel@0
|
250
|
Daniel@0
|
251 # add data to dictionary
|
Daniel@0
|
252 if parsed.group('type') == (ftype['chord']):
|
Daniel@0
|
253 data[clipin]['chord'] = os.path.join(dirpath, file).replace('\\', '/')
|
Daniel@0
|
254 elif parsed.group('type') == (ftype['key']):
|
Daniel@0
|
255 data[clipin]['key'] = os.path.join(dirpath, file).replace('\\', '/')
|
Daniel@0
|
256 else:
|
Daniel@0
|
257 errcount += 1
|
Daniel@0
|
258 print "Could not parse " + file
|
Daniel@0
|
259 raise
|
Daniel@0
|
260 return data
|
Daniel@0
|
261 # return list of tracknames
|
Daniel@0
|
262 # return list of feature dirs
|
Daniel@0
|
263
|
Daniel@0
|
264
|
Daniel@0
|
265 def fun2txt(fun,typ, bfun,mode):
|
Daniel@0
|
266 # now we can interpret this function
|
Daniel@0
|
267 # when given the mode of major or minor.
|
Daniel@0
|
268 if (fun >= 0):
|
Daniel@0
|
269 if (mode == 1):
|
Daniel@0
|
270 pfun = fun_dic_min[fun]
|
Daniel@0
|
271 md = '(m)'
|
Daniel@0
|
272 elif (mode == 0):
|
Daniel@0
|
273 pfun = fun_dic_maj[fun]
|
Daniel@0
|
274 md = '(M)'
|
Daniel@0
|
275 else:
|
Daniel@0
|
276 return 'N'
|
Daniel@0
|
277
|
Daniel@0
|
278 #if typ == 'm':
|
Daniel@0
|
279 # print 'Key: ' + skey + ', chord: ' + chord + ' function ' + str(fun) + ' type ' + typ + ' bfun ' + str(bfun)
|
Daniel@0
|
280 type = type_labels[typ] if typ > 0 else ''
|
Daniel@0
|
281
|
Daniel@0
|
282 blb = '/' + base_labels[bfun] if (bfun >= 0 and base_labels[bfun]) else ''
|
Daniel@0
|
283 return md + pfun + type + blb
|
Daniel@0
|
284
|
Daniel@0
|
285 def fun2num(fun,typ, bfun,mode):
|
Daniel@0
|
286 # now we can interpret this function
|
Daniel@0
|
287 if not fun == -1:
|
Daniel@0
|
288 return (mode+1)* 1000000 + (fun+1) * 10000 + (typ+1) * 100 + (bfun+1)
|
Daniel@0
|
289 else:
|
Daniel@0
|
290 return 0
|
Daniel@0
|
291
|
Daniel@0
|
292 def folder2functions(path):
|
Daniel@0
|
293 tracks = tracks_in_dir(path)
|
Daniel@0
|
294
|
Daniel@0
|
295 # get chords for all files
|
Daniel@0
|
296 #check for integrity: do we have keys and chords?
|
Daniel@0
|
297 output = dict()
|
Daniel@0
|
298 bfuns = []
|
Daniel@0
|
299
|
Daniel@0
|
300 for clip, featurefiles in tracks.iteritems():
|
Daniel@0
|
301 print clip
|
Daniel@0
|
302 if len(featurefiles) == 2:
|
Daniel@0
|
303 output[clip] = chords2functions(clip,featurefiles)
|
Daniel@0
|
304 return output
|
Daniel@0
|
305
|
Daniel@0
|
306 def folder2histogram(path= './'):
|
Daniel@0
|
307
|
Daniel@0
|
308 # get chord functions for the folder
|
Daniel@0
|
309 tracks = folder2functions(path)
|
Daniel@0
|
310
|
Daniel@0
|
311 # concatenate string form
|
Daniel@0
|
312 chords = []
|
Daniel@0
|
313 for track, contents in tracks.iteritems():
|
Daniel@0
|
314 for (time,key,mode,fun,typ,bfun) in contents:
|
Daniel@0
|
315 chords.append([fun2num(fun,typ,bfun,mode)])
|
Daniel@0
|
316
|
Daniel@0
|
317 # counts
|
Daniel@0
|
318 (v,w) = histogram(chords)
|
Daniel@0
|
319 print v
|
Daniel@0
|
320 return {"count":v.values(), "index":v.keys()}
|
Daniel@0
|
321
|
Daniel@0
|
322 if __name__ == "__main__":
|
Daniel@0
|
323 #chords2functions()
|
Daniel@0
|
324 print "Creates a key-independent chord histogram. Usage: chord2function path_vamp_chords path_vamp_keys"
|
Daniel@0
|
325 # sys.argv[1]
|
Daniel@0
|
326 result = folder2histogram()
|
Daniel@0
|
327 print "Please input a description for the chord function histogram"
|
Daniel@0
|
328 c2j.data2json(result) |