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)
|