annotate scripts/smoothiecore.py @ 105:edd82eb89b4b branch-tests tip

Merge
author Maria Panteli
date Sun, 15 Oct 2017 13:36:59 +0100
parents e50c63cf96be
children
rev   line source
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)