Maria@4
|
1 """Smoothie
|
Maria@4
|
2
|
Maria@4
|
3 A module aimed at melody salience and melody features. It contains
|
Maria@4
|
4
|
Maria@4
|
5 * a frequency transform inspired by constant-Q transforms
|
Maria@4
|
6 * an NMF-based melodic salience function
|
Maria@4
|
7 * methods that transform this salience into melody features (not implemented)
|
Maria@4
|
8 """
|
Maria@4
|
9
|
Maria@4
|
10 import sys
|
Maria@4
|
11 import numpy as np
|
Maria@4
|
12 from scipy.signal import hann
|
Maria@4
|
13 from scipy.sparse import csr_matrix
|
Maria@4
|
14 import librosa
|
Maria@4
|
15
|
Maria@4
|
16 p = {# smoothie q mapping parameters
|
Maria@4
|
17 'bpo' : 36,
|
Maria@4
|
18 'break1' : 500,
|
Maria@4
|
19 'break2' : 4000,
|
Maria@4
|
20 'f_max' : 8000,
|
Maria@4
|
21 'fs' : 16000,
|
Maria@4
|
22 # spectrogram parameters
|
Maria@4
|
23 'step_size' : 160,
|
Maria@4
|
24 'use_A_wtg' : False,
|
Maria@4
|
25 'harm_remove' : False,
|
Maria@4
|
26 # NMF and NMF dictionary parameters
|
Maria@4
|
27 's' : 0.85, # decay parameter
|
Maria@4
|
28 'n_harm' : 50, # I'm gonna do you no harm
|
Maria@4
|
29 'min_midi' : 25,
|
Maria@4
|
30 'max_midi' : 90.8,
|
Maria@4
|
31 'bps' : 5,
|
Maria@4
|
32 'n_iter' : 30
|
Maria@4
|
33 }
|
Maria@4
|
34
|
Maria@4
|
35 def update_nmf_simple(H, W, V):
|
Maria@4
|
36 """Update the gain matrix H using the multiplicative NMF update equation.
|
Maria@4
|
37
|
Maria@4
|
38 Keyword arguments:
|
Maria@4
|
39 H -- gain matrix
|
Maria@4
|
40 W -- template matrix
|
Maria@4
|
41 V -- target data matrix
|
Maria@4
|
42 """
|
Maria@4
|
43 WHV = np.dot(W, H) ** (-1) * V
|
Maria@4
|
44 H = H * np.dot(W.transpose(), WHV)
|
Maria@4
|
45 return H
|
Maria@4
|
46
|
Maria@4
|
47 def make_a_weight(f):
|
Maria@4
|
48 """Return the values of A-weighting at the input frequencies f."""
|
Maria@4
|
49 f = np.array(f)
|
Maria@4
|
50 zaehler = 12200 * 12200* (f**4)
|
Maria@4
|
51 nenner = (f*f + 20.6*20.6) * np.sqrt((f*f + 107.7*107.7) * (f*f + 737.9*737.9)) * (f*f + 12200*12200)
|
Maria@4
|
52 return zaehler / nenner
|
Maria@4
|
53
|
Maria@4
|
54 def nextpow2(x):
|
Maria@4
|
55 """Return the smallest integer n such that 2 ** n > x."""
|
Maria@4
|
56 return np.ceil(np.log2(x))
|
Maria@4
|
57
|
Maria@4
|
58 def get_smoothie_frequencies(p):
|
Maria@4
|
59 """Calculate filter centres and widths for the smooth Q transform."""
|
Maria@4
|
60 # I think this calculation should be reconsidered in order to have
|
Maria@4
|
61 # a more principled transition between linear and exponential
|
Maria@4
|
62 n = 1.0 / (2.0**(1.0/p['bpo']) - 1)
|
Maria@4
|
63 x = p['break1'] * 1.0 / n
|
Maria@4
|
64
|
Maria@4
|
65 # first linear stretch
|
Maria@4
|
66 # f = (np.arange(1, int(np.round(n)) + 1) * x).round().tolist()
|
Maria@4
|
67 f = (np.arange(1, int(np.round(n)) + 1) * x).tolist()
|
Maria@4
|
68
|
Maria@4
|
69 # exponential stretch
|
Maria@4
|
70 while max(f) < p['break2']:
|
Maria@4
|
71 f.append(max(f) * 2**(1.0/p['bpo']))
|
Maria@4
|
72
|
Maria@4
|
73 # final linear stretch
|
Maria@4
|
74 lastdiff = f[-1] - f[-2]
|
Maria@4
|
75 while max(f) < p['f_max']:
|
Maria@4
|
76 f.append(max(f) + lastdiff)
|
Maria@4
|
77
|
Maria@4
|
78 deltaf = np.diff(np.array(f))
|
Maria@4
|
79 f = f[:-1]
|
Maria@4
|
80
|
Maria@4
|
81 return f, deltaf
|
Maria@4
|
82
|
Maria@4
|
83 def create_smoothie_kernel(f, deltaf, fs):
|
Maria@4
|
84 """Create a sparse matrix that maps the complex DFT to the complex
|
Maria@4
|
85 smoothie representation.
|
Maria@4
|
86 """
|
Maria@4
|
87 print >>sys.stdout, "[ SMOOTHIE Q kernel calculation ... ]"
|
Maria@4
|
88 n_filter = len(f)
|
Maria@4
|
89 n_fft = 2**nextpow2(np.ceil(fs/min(deltaf)))
|
Maria@4
|
90
|
Maria@4
|
91 thresh = 0.0054
|
Maria@4
|
92 smoothie_kernel = np.zeros([n_fft, n_filter], np.complex64)
|
Maria@4
|
93
|
Maria@4
|
94 for i_filter in range(n_filter-1, -1, -1): # descending
|
Maria@4
|
95 # print i_filter
|
Maria@4
|
96 Q = f[i_filter] * 1.0 / deltaf[i_filter] # local Q for this filter
|
Maria@4
|
97 lgth = int(np.ceil(fs * 1.0 / deltaf[i_filter]))
|
Maria@4
|
98 lgthinv = 1.0 / lgth
|
Maria@4
|
99 offset = int(n_fft/2 - np.ceil(lgth * 0.5))
|
Maria@4
|
100 temp = hann(lgth) * lgthinv * \
|
Maria@4
|
101 np.exp(2j * np.pi * Q * (np.arange(lgth) - offset) * lgthinv)
|
Maria@4
|
102 # print(sum(hann(lgth)), Q, lgth, offset)
|
Maria@4
|
103 temp_kernel = np.zeros(n_fft, dtype = np.complex64)
|
Maria@4
|
104 temp_kernel[np.arange(lgth) + offset] = temp
|
Maria@4
|
105 spec_kernel = np.fft.fft(temp_kernel)
|
Maria@4
|
106 spec_kernel[abs(spec_kernel) <= thresh] = 0
|
Maria@4
|
107 smoothie_kernel[:,i_filter] = spec_kernel
|
Maria@4
|
108 return csr_matrix(smoothie_kernel.conj()/n_fft)
|
Maria@4
|
109
|
Maria@4
|
110 def smoothie_q_spectrogram(x, p):
|
Maria@4
|
111 """Calculate the actual spectrogram with smooth Q frequencies"""
|
Maria@4
|
112 print >>sys.stdout, "[ SMOOTHIE Q spectrogram ... ]"
|
Maria@4
|
113
|
Maria@4
|
114 # precalculate smoothie kernel
|
Maria@4
|
115 f, deltaf = get_smoothie_frequencies(p)
|
Maria@4
|
116 smoothie_kernel = create_smoothie_kernel(f, deltaf, p['fs'])
|
Maria@4
|
117 n_fft, n_filter = smoothie_kernel.shape
|
Maria@4
|
118
|
Maria@4
|
119 # some preparations
|
Maria@4
|
120 n_sample = len(x)
|
Maria@4
|
121 # n_frame = int(np.floor(n_sample / p['step_size']))
|
Maria@4
|
122 n_frame = int(np.ceil(n_sample / float(p['step_size']))) # added mp
|
Maria@4
|
123 t = (np.arange(n_frame) * p['step_size']) * 1.0 / p['fs']
|
Maria@4
|
124 smoothie_kernelT = smoothie_kernel.T
|
Maria@4
|
125
|
Maria@4
|
126 # allocate
|
Maria@4
|
127 s = np.zeros((n_filter, n_frame), dtype = np.complex64)
|
Maria@4
|
128
|
Maria@4
|
129 # pad (if wanted)
|
Maria@4
|
130 x = np.concatenate((np.zeros(n_fft/2), x, np.zeros(n_fft/2)))
|
Maria@4
|
131
|
Maria@4
|
132 for i_frame in range(n_frame):
|
Maria@4
|
133 smpl = p['step_size'] * i_frame
|
Maria@4
|
134 block = x[smpl + np.arange(n_fft)]
|
Maria@4
|
135 s[:, i_frame] = smoothie_kernelT.dot(np.fft.fft(block))
|
Maria@4
|
136
|
Maria@4
|
137 if p['use_A_wtg']:
|
Maria@4
|
138 a_wtg = make_a_weight(f)
|
Maria@4
|
139 s = s * a_wtg[:, np.newaxis]
|
Maria@4
|
140
|
Maria@4
|
141 return s, t
|
Maria@4
|
142
|
Maria@4
|
143 def mel_triangles(input_f):
|
Maria@4
|
144 """Make matrix with mel filters at the given frequencies.
|
Maria@4
|
145 Warning: this is a very coarse mel filterbank.
|
Maria@4
|
146 """
|
Maria@4
|
147 n_linearfilters = 3
|
Maria@4
|
148 n_logfilters0 = 30 # just something high, will be pruned later
|
Maria@4
|
149 linear_spacing = 100
|
Maria@4
|
150 log_spacing = 6.0/4
|
Maria@4
|
151 n_filter0 = n_linearfilters + n_logfilters0
|
Maria@4
|
152
|
Maria@4
|
153 freqs = np.zeros(n_filter0+2) # includes one more on either side, hence +2
|
Maria@4
|
154 freqs[range(n_linearfilters+1)] = \
|
Maria@4
|
155 np.arange(-1,n_linearfilters) * linear_spacing
|
Maria@4
|
156 freqs[range(n_linearfilters+1, n_filter0+2)] = \
|
Maria@4
|
157 freqs[n_linearfilters] * log_spacing ** np.arange(1, n_logfilters0+2)
|
Maria@4
|
158
|
Maria@4
|
159 centre_freqs = freqs[1:-1]
|
Maria@4
|
160 lower_freqs = freqs[0:-2]
|
Maria@4
|
161 upper_freqs = freqs[2:]
|
Maria@4
|
162
|
Maria@4
|
163 n_filter = list(np.nonzero(lower_freqs < max(input_f)))[0][-1] + 1
|
Maria@4
|
164
|
Maria@4
|
165 n_input_f = len(input_f)
|
Maria@4
|
166 mtr = np.zeros((n_input_f, n_filter))
|
Maria@4
|
167
|
Maria@4
|
168 for i_filter in range(n_filter):
|
Maria@4
|
169 for i_freq in range(n_input_f):
|
Maria@4
|
170 if input_f[i_freq] > lower_freqs[i_filter] \
|
Maria@4
|
171 and input_f[i_freq] <= upper_freqs[i_filter]:
|
Maria@4
|
172
|
Maria@4
|
173 if input_f[i_freq] <= centre_freqs[i_filter]:
|
Maria@4
|
174 mtr[i_freq, i_filter] = \
|
Maria@4
|
175 (input_f[i_freq] - lower_freqs[i_filter]) * 1.0 / \
|
Maria@4
|
176 (centre_freqs[i_filter] - lower_freqs[i_filter])
|
Maria@4
|
177 else:
|
Maria@4
|
178 mtr[i_freq, i_filter] = \
|
Maria@4
|
179 1 - (input_f[i_freq] - centre_freqs[i_filter]) / \
|
Maria@4
|
180 (upper_freqs[i_filter] - centre_freqs[i_filter])
|
Maria@4
|
181 return mtr
|
Maria@4
|
182
|
Maria@4
|
183 def create_smoothie_nfm_dict(smoothie_kernel, filterf, p):
|
Maria@4
|
184 """Create dictionary matrix with note templates."""
|
Maria@4
|
185 n_note = int((p['max_midi'] - p['min_midi']) * p['bps'] + 1)
|
Maria@4
|
186 n_fft, n_filter = smoothie_kernel.shape
|
Maria@4
|
187 t = ((np.arange(n_fft) + 1) - ((n_fft + 1)*0.5))/p['fs']
|
Maria@4
|
188
|
Maria@4
|
189 mtr = mel_triangles(filterf)
|
Maria@4
|
190 n_template = n_note + mtr.shape[1]
|
Maria@4
|
191
|
Maria@4
|
192 w = np.zeros((n_filter, n_template), dtype = np.complex64)
|
Maria@4
|
193 w[:,n_note:] = mtr
|
Maria@4
|
194 f0s = []
|
Maria@4
|
195
|
Maria@4
|
196 smoothie_kernelT = smoothie_kernel.T
|
Maria@4
|
197
|
Maria@4
|
198 for i_note in range(n_note):
|
Maria@4
|
199 midi = p['min_midi'] + i_note * 1.0 / p['bps']
|
Maria@4
|
200 f0 = 440 * 2 ** ((midi-69)*1.0/12)
|
Maria@4
|
201 f0s.append(f0)
|
Maria@4
|
202 sig = np.zeros(len(t))
|
Maria@4
|
203 for i_harm in range(p['n_harm']):
|
Maria@4
|
204 f = f0 * (i_harm + 1)
|
Maria@4
|
205 if f > p['fs'] * 0.5:
|
Maria@4
|
206 continue
|
Maria@4
|
207 x = np.sin(2*np.pi*f*t) * p['s']**(i_harm)
|
Maria@4
|
208 sig += x
|
Maria@4
|
209 w[:, i_note] = smoothie_kernelT.dot(np.fft.fft(sig))
|
Maria@4
|
210
|
Maria@4
|
211 for i in range(mtr.shape[1]):
|
Maria@4
|
212 f0s.append(np.nan)
|
Maria@4
|
213
|
Maria@4
|
214 w = abs(w)
|
Maria@4
|
215 col_sums = w.sum(axis = 0)
|
Maria@4
|
216 w = w / col_sums[np.newaxis, :] # normalisation
|
Maria@4
|
217 return w, np.array(f0s)
|
Maria@4
|
218
|
Maria@4
|
219 def smoothie_salience(x, p, do_tune = False):
|
Maria@4
|
220 """Calculate melodic salience."""
|
Maria@4
|
221 print >>sys.stdout, "[ SMOOTHIE Q salience ... ]"
|
Maria@4
|
222
|
Maria@4
|
223 # precalculate nmf kernel
|
Maria@4
|
224 f, deltaf = get_smoothie_frequencies(p)
|
Maria@4
|
225 smoothie_kernel = create_smoothie_kernel(f, deltaf, p['fs'])
|
Maria@4
|
226 w, f0s = create_smoothie_nfm_dict(smoothie_kernel, f, p)
|
Maria@4
|
227
|
Maria@4
|
228 # calculate smoothiegram
|
Maria@4
|
229 s, t = smoothie_q_spectrogram(x, p)
|
Maria@4
|
230 s[s==0] = np.spacing(1) # very small number
|
Maria@4
|
231 s = abs(s)
|
Maria@4
|
232
|
Maria@4
|
233 # run NMF
|
Maria@4
|
234 n_frame = len(t)
|
Maria@4
|
235 print >>sys.stdout, "[ SMOOTHIE Q : NMF updates ... ]"
|
Maria@4
|
236 sal = np.ones((w.shape[1], n_frame))
|
Maria@4
|
237 for i_iter in range(p['n_iter']):
|
Maria@4
|
238 sal = update_nmf_simple(sal, w, s)
|
Maria@4
|
239
|
Maria@4
|
240 if do_tune:
|
Maria@4
|
241 error("Tuning isn't yet implemented in the Python version")
|
Maria@4
|
242
|
Maria@4
|
243 return sal, t, f0s
|
Maria@4
|
244
|
Maria@4
|
245 def qnormalise(x, q, dim):
|
Maria@4
|
246 nrm = np.sum(x**q, axis=dim, keepdims=True)**(1./float(q))
|
Maria@4
|
247 nrmmatrix = np.repeat(nrm, x.shape[0], axis=0)
|
Maria@4
|
248 x = x / nrmmatrix
|
Maria@4
|
249 return x
|
Maria@4
|
250
|
Maria@4
|
251 def wrap_to_octave(sal, param):
|
Maria@4
|
252 epsilon = 0.00001
|
Maria@4
|
253 nsal = qnormalise(sal, 2, 0)
|
Maria@4
|
254
|
Maria@4
|
255 step = 1./float(param['bps'])
|
Maria@4
|
256 NMFpitch = np.arange(param['min_midi'],param['max_midi']+step,step)
|
Maria@4
|
257 nNote = len(NMFpitch)
|
Maria@4
|
258 nsal = nsal[0:nNote, :]
|
Maria@4
|
259
|
Maria@4
|
260 allsal = nsal
|
Maria@4
|
261 allsal[nsal<epsilon] = epsilon
|
Maria@4
|
262
|
Maria@4
|
263 # chroma mapping
|
Maria@4
|
264 n = param['bps']*12
|
Maria@4
|
265 mmap = np.tile(np.eye(n),(1,5))
|
Maria@4
|
266 allchroma = mmap.dot(allsal[0:(n*5),:])
|
Maria@4
|
267 return allchroma
|
Maria@4
|
268
|
Maria@4
|
269 def segment_audio(y, sr):
|
Maria@4
|
270 tracklength = y.shape[0]/float(sr)
|
Maria@4
|
271 startSample = 0
|
Maria@4
|
272 endSample = None
|
Maria@4
|
273 if tracklength > 90:
|
Maria@4
|
274 startPointSec = (tracklength/2.)-20
|
Maria@4
|
275 startSample = round(startPointSec*sr)
|
Maria@4
|
276 endSample = startSample+45*sr
|
Maria@4
|
277 y = y[startSample:endSample]
|
Maria@4
|
278 return y
|
Maria@4
|
279
|
Maria@4
|
280 def get_smoothie(filename = 'test.wav', param = None, segment=True, secondframedecomp=False, hopinsec=None):
|
Maria@4
|
281 if param is None:
|
Maria@4
|
282 param = p
|
Maria@4
|
283 param['fs'] = None
|
Maria@4
|
284 param['step_size'] = 128
|
Maria@4
|
285
|
Maria@4
|
286 y, sr = librosa.load(filename, sr = param['fs'])
|
Maria@4
|
287 param['fs'] = sr
|
Maria@4
|
288 if hopinsec is not None:
|
Maria@4
|
289 param['step_size'] = int(round(hopinsec*param['fs']))
|
Maria@4
|
290 if segment:
|
Maria@4
|
291 y = segment_audio(y,sr)
|
Maria@4
|
292 # sg, t = smoothie_q_spectrogram(y, param)
|
Maria@4
|
293 # nmf_dict = create_smoothie_nfm_dict(smoothie_kernel, f, param)
|
Maria@4
|
294 sal, t, f0s = smoothie_salience(y, param)
|
Maria@4
|
295 sal = sal[-np.isnan(f0s),:]
|
Maria@4
|
296 chroma = wrap_to_octave(sal, param)
|
Maria@4
|
297
|
Maria@4
|
298 if secondframedecomp:
|
Maria@4
|
299 chromasr = param['fs']/float(param['step_size'])
|
Maria@4
|
300 win2 = int(round(8*chromasr))
|
Maria@4
|
301 hop2 = int(round(0.5*chromasr))
|
Maria@4
|
302 nbins, norigframes = chroma.shape
|
Maria@4
|
303 nframes = int(1+np.floor((norigframes-win2)/float(hop2)))
|
Maria@4
|
304 avechroma = np.empty((nbins, nframes))
|
Maria@4
|
305 for i in range(nframes): # loop over all 8-sec frames
|
Maria@4
|
306 avechroma[:,i] = np.mean(chroma[:, (i*hop2):(i*hop2+win2)], axis=1, keepdims=True)
|
Maria@4
|
307 chroma = avechroma
|
Maria@4
|
308 return chroma
|
Maria@4
|
309
|
Maria@4
|
310 def get_smoothie_for_bihist(filename = 'test.wav', param = None, segment=True, hopinsec=None):
|
Maria@4
|
311 if param is None:
|
Maria@4
|
312 param = p
|
Maria@4
|
313 param['fs'] = None # no downsample
|
Maria@4
|
314 param['step_size'] = 128
|
Maria@4
|
315
|
Maria@4
|
316 y, sr = librosa.load(filename, sr = param['fs'])
|
Maria@4
|
317 param['fs'] = sr
|
Maria@4
|
318 if hopinsec is not None:
|
Maria@4
|
319 param['step_size'] = int(round(hopinsec*param['fs']))
|
Maria@4
|
320 if segment:
|
Maria@4
|
321 y = segment_audio(y,sr)
|
Maria@4
|
322 # sg, t = smoothie_q_spectrogram(y, param)
|
Maria@4
|
323 # nmf_dict = create_smoothie_nfm_dict(smoothie_kernel, f, param)
|
Maria@4
|
324 sal, t, f0s = smoothie_salience(y, param)
|
Maria@4
|
325 sal = sal[-np.isnan(f0s),:]
|
Maria@4
|
326 chroma = wrap_to_octave(sal, param)
|
Maria@4
|
327 chromasr = param['fs']/float(param['step_size'])
|
Maria@4
|
328 return (chroma, chromasr) |