annotate dml-cla/python/chord_seq_key_relative.py @ 0:718306e29690 tip

commiting public release
author Daniel Wolff
date Tue, 09 Feb 2016 21:05:06 +0100
parents
children
rev   line source
Daniel@0 1 #!/usr/bin/python
Daniel@0 2 # Part of DML (Digital Music Laboratory)
Daniel@0 3 # Copyright 2014-2015 Daniel Wolff, City University
Daniel@0 4
Daniel@0 5 # This program is free software; you can redistribute it and/or
Daniel@0 6 # modify it under the terms of the GNU General Public License
Daniel@0 7 # as published by the Free Software Foundation; either version 2
Daniel@0 8 # of the License, or (at your option) any later version.
Daniel@0 9 #
Daniel@0 10 # This program is distributed in the hope that it will be useful,
Daniel@0 11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
Daniel@0 12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Daniel@0 13 # GNU General Public License for more details.
Daniel@0 14 #
Daniel@0 15 # You should have received a copy of the GNU General Public
Daniel@0 16 # License along with this library; if not, write to the Free Software
Daniel@0 17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Daniel@0 18
Daniel@0 19 # -*- coding: utf-8 -*-
Daniel@0 20 __author__="wolffd"
Daniel@0 21
Daniel@0 22 # json testfile
Daniel@0 23 #
Daniel@0 24 #{ "module":"chord_seq_key_relative",
Daniel@0 25 # "function":"aggregate",
Daniel@0 26 # "arguments": [[
Daniel@0 27 # {"keys": { "tag": "csv", "value":"D:\\mirg\\Chord_Analysis20141216\\Beethoven\\qm_vamp_key_standard.n3_50ac9\\1CD0000653_BD01_vamp_qm-vamp-plugins_qm-keydetector_key.csv"},
Daniel@0 28 # "chords": { "tag": "csv", "value":"D:\\mirg\\Chord_Analysis20141216\\Beethoven\\chordino_simple.n3_1a812\\1CD0000653_BD01_vamp_nnls-chroma_chordino_simplechord.csv"},
Daniel@0 29 # "trackuri": "Eins"},
Daniel@0 30 # {"keys": { "tag": "csv", "value":"D:\\mirg\\Chord_Analysis20141216\\Beethoven\\qm_vamp_key_standard.n3_50ac9\\1CD0000653_BD01_vamp_qm-vamp-plugins_qm-keydetector_key.csv"},
Daniel@0 31 # "chords": { "tag": "csv", "value":"D:\\mirg\\Chord_Analysis20141216\\Beethoven\\chordino_simple.n3_1a812\\1CD0000653_BD01_vamp_nnls-chroma_chordino_simplechord.csv"}}
Daniel@0 32 # ]]
Daniel@0 33 #}
Daniel@0 34
Daniel@0 35 # these for file reading etc
Daniel@0 36 import re
Daniel@0 37 import os
Daniel@0 38 import csv
Daniel@0 39 import numpy
Daniel@0 40
Daniel@0 41 # spmf functions
Daniel@0 42 import chord_seq_spmf_helper as spmf
Daniel@0 43
Daniel@0 44 from aggregate import *
Daniel@0 45 from csvutils import *
Daniel@0 46
Daniel@0 47 # ---
Daniel@0 48 # roots
Daniel@0 49 # ---
Daniel@0 50 chord_roots = ["C","D","E","F","G","A","B"]
Daniel@0 51
Daniel@0 52 # create a dictionary for efficiency
Daniel@0 53 roots_dic = dict(zip(chord_roots, [0,2,4,5,7,9,11]))
Daniel@0 54
Daniel@0 55 mode_lbls = ['major','minor']
Daniel@0 56 mode_dic = dict(zip(mode_lbls, range(0,2)))
Daniel@0 57 # ---
Daniel@0 58 # types
Daniel@0 59 # ---
Daniel@0 60 type_labels = ["", "6", "7", "m","m6", "m7", "maj7", "m7b5", "dim", "dim7", "aug"]
Daniel@0 61 type_dic = dict(zip(type_labels, range(0,len(type_labels))))
Daniel@0 62
Daniel@0 63 base_labels = ["1","","2","b3","3","4","","5","","6","b7","7"]
Daniel@0 64 #base_dic = dict(zip(base_labels, range(0,len(base_labels))))
Daniel@0 65
Daniel@0 66 # functions
Daniel@0 67 root_funs_maj = ['I','#I','II','#II','III','IV','#IV','V','#V','VI','#VI','VII']
Daniel@0 68 root_funs_min = ['I','#I','II','III','#III','IV','#IV','V','VI','#VI','VII','#VII']
Daniel@0 69 # dan's suggestion
Daniel@0 70 #root_funs_maj = ['I','#I','II','#II','(M)III','IV','#IV','V','#V','VI','#VI','(M)VII']
Daniel@0 71 #root_funs_min = ['I','#I','II','(m)III','#III','IV','#IV','V','VI','#VI','(m)VII','#VII']
Daniel@0 72
Daniel@0 73 fun_dic_maj = dict(zip(range(0,len(root_funs_maj)),root_funs_maj))
Daniel@0 74 fun_dic_min = dict(zip(range(0,len(root_funs_min)),root_funs_min))
Daniel@0 75 # regex that separates roots and types, and gets chord base
Daniel@0 76 # this only accepts chords with a sharp (#) and no flats
Daniel@0 77 p = re.compile(r'(?P<root>[A-G,N](#|b)*)(?P<type>[a-z,0-9]*)(/(?P<base>[A-G](#|b)*))*')
Daniel@0 78 p2 = re.compile(r'(?P<key>[A-G](#|b)*)(\s/\s[A-G](#|b)*)*\s(?P<mode>[major|minor]+)')
Daniel@0 79 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 80
Daniel@0 81
Daniel@0 82
Daniel@0 83 def chords_from_csv(filename):
Daniel@0 84 # we assume CSV: time, chord_string
Daniel@0 85 # return (time, chord_string)
Daniel@0 86 return csv_map_rows(filename,2, lambda row:(float(row[0]),row[1]))
Daniel@0 87
Daniel@0 88 def keys_from_csv(filename):
Daniel@0 89 # we assume CSV: time, key_code, key_string
Daniel@0 90 # return ( time, key_code, key_string)
Daniel@0 91 return csv_map_rows(filename,3, lambda row:(float(row[0]),row[1],row[2]))
Daniel@0 92
Daniel@0 93 # parsers for n3 / csv
Daniel@0 94 key_parser_table = { 'csv':keys_from_csv }
Daniel@0 95 chord_parser_table = { 'csv':chords_from_csv }
Daniel@0 96
Daniel@0 97 # extracts relative chord sequences from inputs of chord / key data
Daniel@0 98 # input list of pairs with instances of features:
Daniel@0 99 # (['chords'] chordino_simple.n3_1a812 , ['keys'] qm_vamp_key_standard.n3_50ac9,
Daniel@0 100 # optional: ['trackuri'] trackidentifier )
Daniel@0 101 # @note: in future we could add support for qm_key_tonic input
Daniel@0 102 #
Daniel@0 103 # opts : dictionary with opts["spm_algorithm"] = SPADE, TKS or ClaSP algorithm?
Daniel@0 104 # and opts["spm_options"] = "70%"
Daniel@0 105 # output:
Daniel@0 106 # 'sequences': seq, 'support': sup
Daniel@0 107
Daniel@0 108 trackctr = 0
Daniel@0 109
Daniel@0 110 def aggregate(inputs,opts={}):
Daniel@0 111 print_status('In chord_seq_key_relative')
Daniel@0 112
Daniel@0 113
Daniel@0 114 # SPADE, TKS or ClaSP algorithm?
Daniel@0 115 algo = opts.get("spm_algorithm","CM-SPADE")
Daniel@0 116
Daniel@0 117 # number of sequences
Daniel@0 118 maxseqs = int(opts.get("spm_maxseqs",500)/2)
Daniel@0 119
Daniel@0 120 # min. length of sequences
Daniel@0 121 minlen = int(opts.get("spm_minlen",2))
Daniel@0 122
Daniel@0 123 # min. length of sequences in seconds
Daniel@0 124 maxtime = int(opts.get("spm_maxtime",1*60)/2)
Daniel@0 125
Daniel@0 126 ignoreN = int(opts.get("spm_ignore_n",1))
Daniel@0 127
Daniel@0 128 # min. length of sequences
Daniel@0 129 minsup = int(opts.get("spm_minsupport",50))
Daniel@0 130
Daniel@0 131 # we now safe the mode of each piece
Daniel@0 132 # to treat them separately
Daniel@0 133 out_chords = [dict(), dict()];
Daniel@0 134 # generate dict[trackuri] = [ (time,key,mode,fun,typ,bfun) ]
Daniel@0 135 def accum(item):
Daniel@0 136 global trackctr
Daniel@0 137 # increase virtual identifier
Daniel@0 138 trackctr += 1
Daniel@0 139
Daniel@0 140 # get duration and normalised frequency for all tuning pitches (A3,A4,A5)
Daniel@0 141 keys = decode_tagged(key_parser_table,item['keys'])
Daniel@0 142
Daniel@0 143 # get most frequent key
Daniel@0 144 key,mode = most_frequent_key(keys)
Daniel@0 145
Daniel@0 146 relchords = []
Daniel@0 147 for (time,chord) in decode_tagged(chord_parser_table,item['chords']):
Daniel@0 148
Daniel@0 149 # ignore chords that are 'N':
Daniel@0 150 # a. the open pattern matching allows for arbitrary chords
Daniel@0 151 # to appear inbetween those in a sequence
Daniel@0 152 # b. the N chord potentially maps to any contents, so the
Daniel@0 153 # inclusion of N chord has limited (or no) use
Daniel@0 154
Daniel@0 155 # get chord function
Daniel@0 156 (root,fun,typ, bfun) = chord2function(chord, key,mode)
Daniel@0 157
Daniel@0 158 if not (ignoreN & (root == -1)):
Daniel@0 159 # translate into text
Daniel@0 160 txt = fun2txt(fun,typ, bfun, mode)
Daniel@0 161 # print 'Chord: ' + chord + ', function: ' + txt
Daniel@0 162
Daniel@0 163 # add to chords of this clip
Daniel@0 164 relchords.append((time,key,mode,fun,typ,bfun))
Daniel@0 165
Daniel@0 166 # save results into dict for this track
Daniel@0 167 trackuri = item.get('trackuri',trackctr)
Daniel@0 168 out_chords[mode][trackuri] = relchords
Daniel@0 169
Daniel@0 170 # collate relative chord information per file
Daniel@0 171 st=for_each(inputs,accum)
Daniel@0 172 # print_status('Finished accumulating')
Daniel@0 173
Daniel@0 174 if trackctr < 2:
Daniel@0 175 raise Exception("Need more than 1 track")
Daniel@0 176
Daniel@0 177 seq = [[],[]]
Daniel@0 178 sup = [[],[]]
Daniel@0 179
Daniel@0 180 for mode in [0,1]:
Daniel@0 181 # write to spmf file
Daniel@0 182 spmffile = spmf.relchords2spmf(out_chords[mode])
Daniel@0 183 #print_status('Wrote SPMF data ' + spmffile.name)
Daniel@0 184
Daniel@0 185
Daniel@0 186 # run sequential pattern matching
Daniel@0 187 if algo == "TKS":
Daniel@0 188 algoopts = opts.get("spm_options","")
Daniel@0 189 seqfile = spmf.spmf(spmffile.name,'TKS',[str(maxseqs), algoopts])
Daniel@0 190 elif algo == "ClaSP":
Daniel@0 191 algoopts = opts.get("spm_options",str(minsup) + "%")
Daniel@0 192 seqfile = spmf.spmf(spmffile.name,'ClaSP',[algoopts, str(minlen)], timeout = maxtime)
Daniel@0 193 elif algo == "SPADE":
Daniel@0 194 algoopts = opts.get("spm_options",str(minsup) + "%")
Daniel@0 195 seqfile = spmf.spmf(spmffile.name,'SPADE',[algoopts, str(minlen)], timeout = maxtime)
Daniel@0 196 else:
Daniel@0 197 print_status('Running CM-SPADE algo')
Daniel@0 198 algoopts = opts.get("spm_options",str(minsup) + "%")
Daniel@0 199 seqfile = spmf.spmf(spmffile.name,'CM-SPADE',[algoopts, str(minlen)], timeout = maxtime)
Daniel@0 200
Daniel@0 201 #seqfile = spmf.spmf(spmffile.name,'BIDE+',['70%'])
Daniel@0 202 #seqfile = "D:\mirg\Chord_Analysis20141216\Beethoven_60.txt"
Daniel@0 203
Daniel@0 204 #print_status('SPADE finished in ' + seqfile)
Daniel@0 205 # parse spmf output
Daniel@0 206 seq[mode],sup[mode] = spmf.spmf2table(seqfile)
Daniel@0 207
Daniel@0 208 #clean up
Daniel@0 209 os.remove(spmffile.name)
Daniel@0 210 os.remove(seqfile)
Daniel@0 211
Daniel@0 212 # fold back sequences and support
Daniel@0 213 # note that this results in the sequences being truncated together below
Daniel@0 214 seq = [item for sublist in seq for item in sublist]
Daniel@0 215 sup = [item for sublist in sup for item in sublist]
Daniel@0 216
Daniel@0 217 # filter according to min. sequencelength and number of sequences
Daniel@0 218 seq_out = []
Daniel@0 219 sup_out = []
Daniel@0 220 seq_count = 0
Daniel@0 221
Daniel@0 222 # sort in descending support and pick up sequences of sufficient length
Daniel@0 223 for i in numpy.argsort(sup)[::-1]:
Daniel@0 224 if len(seq[i]) >= minlen:
Daniel@0 225 seq_out.append(seq[i])
Daniel@0 226 sup_out.append(sup[i])
Daniel@0 227 seq_count += 1
Daniel@0 228
Daniel@0 229 if seq_count >= maxseqs:
Daniel@0 230 break
Daniel@0 231
Daniel@0 232 return { 'result': { 'sequences': seq_out, 'support': sup_out},
Daniel@0 233 'stats' : st }
Daniel@0 234
Daniel@0 235
Daniel@0 236 # most simple note2num
Daniel@0 237 def note2num(notein = 'Cb'):
Daniel@0 238 base = roots_dic[notein[0]]
Daniel@0 239 if len(notein) > 1:
Daniel@0 240 if notein[1] == 'b':
Daniel@0 241 return (base - 1) % 12
Daniel@0 242 elif notein[1] == '#':
Daniel@0 243 return (base + 1) % 12
Daniel@0 244 else:
Daniel@0 245 print "Error parsing chord " + notein
Daniel@0 246 raise
Daniel@0 247 else:
Daniel@0 248 return base % 12
Daniel@0 249
Daniel@0 250
Daniel@0 251 # convert key to number
Daniel@0 252 def key2num(keyin = 'C major'):
Daniel@0 253 # ---
Daniel@0 254 # parse key string: separate root from rest
Daniel@0 255 # ---
Daniel@0 256 sepstring = p2.match(keyin)
Daniel@0 257 if not sepstring:
Daniel@0 258 print "Error parsing key " + keyin
Daniel@0 259 raise
Daniel@0 260
Daniel@0 261 # get relative position of chord and adapt for flats
Daniel@0 262 key = sepstring.group('key')
Daniel@0 263 key = note2num(key)
Daniel@0 264
Daniel@0 265 # ---
Daniel@0 266 # parse mode. care for (unknown) string
Daniel@0 267 # ---
Daniel@0 268 mode = sepstring.group('mode')
Daniel@0 269
Daniel@0 270 if mode:
Daniel@0 271 mode = mode_dic[mode]
Daniel@0 272 else:
Daniel@0 273 mode = -1
Daniel@0 274
Daniel@0 275 return (key, mode)
Daniel@0 276
Daniel@0 277
Daniel@0 278
Daniel@0 279 # convert chord to relative function
Daniel@0 280 def chord2function(cin = 'B',key=3, mode=0):
Daniel@0 281 # ---
Daniel@0 282 # parse chord string: separate root from rest
Daniel@0 283 # ---
Daniel@0 284 sepstring = p.match(cin)
Daniel@0 285
Daniel@0 286 # test for N code -> no chord detected
Daniel@0 287 if sepstring.group('root') == 'N':
Daniel@0 288 return (-1,-1,-1,-1)
Daniel@0 289
Daniel@0 290 # get root and type otherwise
Daniel@0 291 root = note2num(sepstring.group('root'))
Daniel@0 292 type = sepstring.group('type')
Daniel@0 293
Daniel@0 294 typ = type_dic[type]
Daniel@0 295
Daniel@0 296 # get relative position
Daniel@0 297 fun = (root - key) % 12
Daniel@0 298
Daniel@0 299 #--- do we have a base key?
Daniel@0 300 # if yes return it relative to chord root
Daniel@0 301 # ---
Daniel@0 302 if sepstring.group('base'):
Daniel@0 303 broot = note2num(sepstring.group('base'))
Daniel@0 304 bfun = (broot - root) % 12
Daniel@0 305 else:
Daniel@0 306 # this standard gives 1 as a base key if not specified otherwise
Daniel@0 307 bfun = 0
Daniel@0 308
Daniel@0 309
Daniel@0 310 # ---
Daniel@0 311 # todo: integrate bfun in final type list
Daniel@0 312 # ---
Daniel@0 313
Daniel@0 314 return (root,fun,typ,bfun)
Daniel@0 315
Daniel@0 316 # reads in any csv and returns a list of structure
Daniel@0 317 # time(float), data1, data2 ....data2
Daniel@0 318 def read_vamp_csv(filein = ''):
Daniel@0 319 output = []
Daniel@0 320 with open(filein, 'rb') as csvfile:
Daniel@0 321 contents = csv.reader(csvfile, delimiter=',', quotechar='"')
Daniel@0 322 for row in contents:
Daniel@0 323 output.append([float(row[0])] + row[1:])
Daniel@0 324 return output
Daniel@0 325
Daniel@0 326
Daniel@0 327
Daniel@0 328 # histogram of the last entry in a list
Daniel@0 329 # returns the most frequently used key
Daniel@0 330 def histogram(keysin = []):
Daniel@0 331 # build histogram
Daniel@0 332 histo = dict()
Daniel@0 333 for row in keysin:
Daniel@0 334 histo[row[-1]] = histo.get(row[-1], 0) + 1
Daniel@0 335
Daniel@0 336 # return most frequent key
Daniel@0 337 return (histo, max(histo.iterkeys(), key=(lambda key: histo[key])))
Daniel@0 338
Daniel@0 339 def most_frequent_key(keys):
Daniel@0 340 # delete 'unknown' keys
Daniel@0 341 keys = [(time,knum,key) for (time,knum,key) in keys if not key == '(unknown)']
Daniel@0 342
Daniel@0 343 # aggregate to one key
Daniel@0 344 (histo, skey) = histogram(keys)
Daniel@0 345
Daniel@0 346 # bet key number
Daniel@0 347 (key,mode) = key2num(skey)
Daniel@0 348 return key,mode
Daniel@0 349
Daniel@0 350
Daniel@0 351
Daniel@0 352 def fun2txt(fun,typ, bfun,mode):
Daniel@0 353 # now we can interpret this function
Daniel@0 354 # when given the mode of major or minor.
Daniel@0 355 if (fun >= 0):
Daniel@0 356 if (mode == 1):
Daniel@0 357 pfun = fun_dic_min[fun]
Daniel@0 358 md = '(m)'
Daniel@0 359 elif (mode == 0):
Daniel@0 360 pfun = fun_dic_maj[fun]
Daniel@0 361 md = '(M)'
Daniel@0 362 else:
Daniel@0 363 return 'N'
Daniel@0 364
Daniel@0 365 #if typ == 'm':
Daniel@0 366 # print 'Key: ' + skey + ', chord: ' + chord + ' function ' + str(fun) + ' type ' + typ + ' bfun ' + str(bfun)
Daniel@0 367 type = type_labels[typ] if typ > 0 else ''
Daniel@0 368
Daniel@0 369 blb = '/' + base_labels[bfun] if (bfun >= 0 and base_labels[bfun]) else ''
Daniel@0 370 return md + pfun + type + blb
Daniel@0 371
Daniel@0 372 def fun2num(fun,typ, bfun,mode):
Daniel@0 373 # now we can interpret this function
Daniel@0 374 if not fun == -1:
Daniel@0 375 return (mode+1)* 1000000 + (fun+1) * 10000 + (typ+1) * 100 + (bfun+1)
Daniel@0 376 else:
Daniel@0 377 return 0
Daniel@0 378
Daniel@0 379
Daniel@0 380 if __name__ == "__main__":
Daniel@0 381 #chords2functions()
Daniel@0 382 print "Creates a key-independent chord histogram. Usage: chord2function path_vamp_chords path_vamp_keys"
Daniel@0 383 # sys.argv[1]
Daniel@0 384 result = folder2histogram()
Daniel@0 385 print "Please input a description for the chord function histogram"
Daniel@0 386 c2j.data2json(result)