annotate utils/OnsetDetectBase.py @ 19:890cfe424f4a tip

added annotations
author mitian
date Fri, 11 Dec 2015 09:47:40 +0000
parents 26838b1f560f
children
rev   line source
mi@0 1 from vampy import *
mi@0 2 from numpy import zeros,float64, float32, array
mi@0 3 import sys
mi@0 4 from scipy.signal import butter
mi@0 5 import numpy as np
mi@0 6
mi@0 7 '''
mi@0 8 Define a common base class where we define the methods common to plugins without
mi@0 9 a fusion process involved.
mi@0 10 The base class implements adaptive whitening and onset location backtracking.
mi@0 11 This also makes individual plugins easier to change / manage / overview.
mi@0 12 '''
mi@0 13
mi@0 14 class OnsetDetectBase(object):
mi@0 15 # WARNING: Apparently vampy doesn't handle errors in super classes (bases) as gracefully as they are handeled
mi@0 16 # in the single class scenario with no inheritnace. This is to be fixed later in vampy itself.
mi@0 17 # For now syntax errors, missing imports, etc... are likely to cause segfault, without printing a detailed python traceback.
mi@0 18 # However, the source of the error is still printed in debug mode, so at least we know which function to fix.
mi@0 19
mi@0 20 def __init__(self,inputSampleRate):
mi@0 21 self.vampy_flags = vf_DEBUG
mi@0 22
mi@0 23 # basic common parameters
mi@0 24 self.preferredStepSecs = 0.01161
mi@0 25 # self.preferredStepSecs = 0.02322
mi@0 26 self.inputSampleRate = inputSampleRate
mi@0 27 self.stepSize = 0
mi@0 28 self.blockSize = 0
mi@0 29 self.channels = 0
mi@0 30
mi@0 31 # user configurable parameters
mi@0 32 self.threshold = 50
mi@0 33 self.delta_threshold = 0.0
mi@0 34 self.backtracking_threshold = 1.9
mi@0 35 self.polyfitting_on = True
mi@0 36 self.medfilter_on = True
mi@0 37 self.LPfilter_on = True
mi@0 38 self.whitening_on = False
mi@0 39 self.simplePick = False
mi@0 40
mi@0 41 # whitening
mi@0 42 self.whitenRelaxCoeff = 0.9997
mi@0 43 self.whitenFloor = 0.01
mi@0 44 self.magPeaks = None
mi@0 45 self.medianWin = 7
mi@0 46 self.aCoeffs = [1.0000, -0.5949, 0.2348]
mi@0 47 self.bCoeffs = [0.1600, 0.3200, 0.1600]
mi@0 48 self.cutoff = 0.34
mi@0 49
mi@0 50
mi@0 51 def initialise(self,channels,stepSize,blockSize):
mi@0 52 self.channels = channels
mi@0 53 self.stepSize = stepSize
mi@0 54 self.blockSize = blockSize
mi@0 55 self.half_length = self.blockSize * 0.5 + 1.0
mi@0 56 self.magPeaks = zeros(self.half_length, dtype = float64)
mi@0 57 return True
mi@0 58
mi@0 59 def reset(self):
mi@0 60 self.magPeaks = None
mi@0 61 return None
mi@0 62
mi@0 63 def getMaker(self):
mi@0 64 return 'Mi Tian, Testing'
mi@0 65
mi@0 66 def getIdentifier(self):
mi@0 67 return 'vampy-base'
mi@0 68
mi@0 69 def getPreferredBlockSize(self):
mi@0 70 '''Preferred window size is twice the preferred step size'''
mi@0 71 # return 2048
mi@0 72 return int(self.getPreferredStepSize() * 2)
mi@0 73
mi@0 74 def getPreferredStepSize(self):
mi@0 75 '''Preferred block size is set to 256 in the QM Vamp plugin in case SR is 22.5kHz'''
mi@0 76 step = int(self.inputSampleRate * self.preferredStepSecs + 0.0001)
mi@0 77 if step < 1 : return 1
mi@0 78 return step
mi@0 79 # return 1024
mi@0 80
mi@0 81 def getMaxChannelCount(self):
mi@0 82 return 1
mi@0 83
mi@0 84 def getInputDomain(self):
mi@0 85 return FrequencyDomain
mi@0 86
mi@0 87 def getParameterDescriptors(self):
mi@0 88 '''Define all common parameters of the plugins.'''
mi@0 89
mi@0 90 threshold = ParameterDescriptor()
mi@0 91 threshold.identifier ='threshold'
mi@0 92 threshold.name ='Detection Sensitivity'
mi@0 93 threshold.description = 'Detection Sensitivity'
mi@0 94 threshold.unit = '%'
mi@0 95 threshold.minValue = 0
mi@0 96 threshold.maxValue = 100
mi@0 97 threshold.defaultValue = 50
mi@0 98 threshold.isQuantized = False
mi@0 99
mi@0 100 delta_thd = ParameterDescriptor()
mi@0 101 delta_thd.identifier ='dthreshold'
mi@0 102 delta_thd.name ='Delta Threshold'
mi@0 103 delta_thd.description = 'Delta threshold used for adaptive theresholding using the median of the detection function'
mi@0 104 delta_thd.unit = ''
mi@0 105 delta_thd.minValue = -1.0
mi@0 106 delta_thd.maxValue = 1.0
mi@0 107 delta_thd.defaultValue = 0.0
mi@0 108 delta_thd.isQuantized = False
mi@0 109
mi@0 110 # NOTE: GF: Not sure this should really be called a threshold. 'Tolerance' may be better.
mi@0 111 bt_thd = ParameterDescriptor()
mi@0 112 bt_thd.identifier ='bt-threshold'
mi@0 113 bt_thd.name ='Backtracking Threshold'
mi@0 114 bt_thd.description = 'Backtracking threshold used determine the stopping condition for backtracking the onset location'
mi@0 115 bt_thd.unit = ''
mi@0 116 bt_thd.minValue = -1.0
mi@0 117 bt_thd.maxValue = 3.0
mi@0 118 bt_thd.defaultValue = 1.9
mi@0 119 bt_thd.isQuantized = False
mi@0 120
mi@0 121 cutoff = ParameterDescriptor()
mi@0 122 cutoff.identifier ='cut-off'
mi@0 123 cutoff.name ='cut off value'
mi@0 124 cutoff.description = 'low pass filter cut off value'
mi@0 125 cutoff.unit = ''
mi@0 126 cutoff.minValue = 0.1
mi@0 127 cutoff.maxValue = 0.6
mi@0 128 cutoff.defaultValue = 0.34
mi@0 129 cutoff.isQuantized = False
mi@0 130
mi@0 131 med_thd = ParameterDescriptor()
mi@0 132 med_thd.identifier ='med-threshold'
mi@0 133 med_thd.name ='Median filter window'
mi@0 134 med_thd.description = 'Median filter window size'
mi@0 135 med_thd.unit = ''
mi@0 136 med_thd.minValue = 3.0
mi@0 137 med_thd.maxValue = 12.0
mi@0 138 med_thd.defaultValue = 7.0
mi@0 139 med_thd.isQuantized = True
mi@0 140 med_thd.quantizeStep = 1
mi@0 141
mi@0 142 # save some typing by defining a descriptor type
mi@0 143 boolDescriptor = ParameterDescriptor()
mi@0 144 boolDescriptor.isQuantized = True
mi@0 145 boolDescriptor.minValue= 0
mi@0 146 boolDescriptor.maxValue= 1
mi@0 147 boolDescriptor.quantizeStep = 1
mi@0 148
mi@0 149 polyfit = ParameterDescriptor(boolDescriptor)
mi@0 150 polyfit.identifier='polyfit'
mi@0 151 polyfit.name='polynomial fitting'
mi@0 152 polyfit.description='Use polynomial fitting to evaluate detection function peaks.'
mi@0 153
mi@0 154 medfilt = ParameterDescriptor(boolDescriptor)
mi@0 155 medfilt.identifier='medfilt'
mi@0 156 medfilt.name='median filtering'
mi@0 157 medfilt.description='Use median filtering'
mi@0 158
mi@0 159 filtfilt = ParameterDescriptor(boolDescriptor)
mi@0 160 filtfilt.identifier='filtfilt'
mi@0 161 filtfilt.name='low-pass filtering'
mi@0 162 filtfilt.description='Use zero-phase foward-backward low-pass filtering'
mi@0 163
mi@0 164 whitening = ParameterDescriptor(boolDescriptor)
mi@0 165 whitening.identifier='whitening'
mi@0 166 whitening.name='Adaptive whitening'
mi@0 167 whitening.description='Turn adaptive whitening on or off'
mi@0 168 whitening.defaultValue = False
mi@0 169
mi@0 170 return ParameterList(threshold, delta_thd, bt_thd, cutoff, med_thd, whitening, polyfit, medfilt, filtfilt)
mi@0 171
mi@0 172
mi@0 173 def setParameter(self,paramid,newval):
mi@0 174 if paramid == 'threshold' :
mi@0 175 self.threshold = newval
mi@0 176 print >> sys.stderr, "sensitivity threshold: ", newval
mi@0 177 if paramid == 'dthreshold' :
mi@0 178 self.delta_threshold = newval
mi@0 179 print >> sys.stderr, "delta threshold: ", newval
mi@0 180 if paramid == 'bt-threshold' :
mi@0 181 self.backtracking_threshold = newval
mi@0 182 print >> sys.stderr, "backtracking threshold: ", newval
mi@0 183 if paramid == 'cut-off' :
mi@0 184 self.cutoff = newval
mi@0 185 self.bCoeffs, self.aCoeffs = butter(2, self.cutoff)
mi@0 186 print >> sys.stderr, "low pass filter cut off value: ", newval
mi@0 187 if paramid == 'med-threshold' :
mi@0 188 self.medianWin = newval
mi@0 189 print >> sys.stderr, "meidan filter windown: ", newval
mi@0 190 if paramid == 'medfilt' :
mi@0 191 self.medfilter_on = newval == 1.0
mi@0 192 print >> sys.stderr, "median filering: ", self.medfilter_on, newval
mi@0 193 if paramid == 'filtfilt' :
mi@0 194 self.LPfilter_on = newval == 1.0
mi@0 195 print >> sys.stderr, "foward-backward filering: ", self.LPfilter_on, newval
mi@0 196 if paramid == 'polyfit' :
mi@0 197 self.polyfitting_on = newval == 1.0
mi@0 198 print >> sys.stderr, "polynomial fitting: ", self.polyfitting_on, newval
mi@0 199 if paramid == 'whitening' :
mi@0 200 self.whitening_on = newval == 1.0
mi@0 201 print >> sys.stderr, "whitening: ", self.whitening_on, newval
mi@0 202
mi@0 203 return None
mi@0 204
mi@0 205
mi@0 206 def getParameter(self,paramid):
mi@0 207 if paramid == 'whitening' :
mi@0 208 if self.whitening_on :
mi@0 209 return 1.0
mi@0 210 else :
mi@0 211 return 0.0
mi@0 212 if paramid == 'medfilt' :
mi@0 213 if self.medfilter_on :
mi@0 214 return 1.0
mi@0 215 else :
mi@0 216 return 0.0
mi@0 217 if paramid == 'filtfilt' :
mi@0 218 if self.LPfilter_on :
mi@0 219 return 1.0
mi@0 220 else :
mi@0 221 return 0.0
mi@0 222 if paramid == 'polyfit' :
mi@0 223 if self.polyfitting_on :
mi@0 224 return 1.0
mi@0 225 else :
mi@0 226 return 0.0
mi@0 227 if paramid == 'threshold' :
mi@0 228 return self.threshold
mi@0 229 if paramid == 'dthreshold' :
mi@0 230 return self.delta_threshold
mi@0 231 if paramid == 'bt-threshold' :
mi@0 232 return self.backtracking_threshold
mi@0 233 # if paramid == 'tol-threshold' :
mi@0 234 if paramid == 'med-threshold' :
mi@0 235 return self.medianWin
mi@0 236 if paramid == 'cut-off' :
mi@0 237 return self.cutoff
mi@0 238 return 0.0
mi@0 239
mi@0 240
mi@0 241
mi@0 242 def getGenericOutputDescriptors(self):
mi@0 243 '''Define 3 outputs ike in the QM plugin. First is the raw detecion function, second is the smoothed one,
mi@0 244 third is the actual note onset outputs. Note: in QM-Vamp the onsets are the first output.'''
mi@0 245 # We call this getGenericOutputDescriptors as we don't want the base to have real outputs.
mi@0 246 # Identifiers shoudl be defined in the sub-classes therefore they are ommitted here.
mi@0 247
mi@0 248 DF_Descriptor = OutputDescriptor()
mi@0 249 DF_Descriptor.hasFixedBinCount=True
mi@0 250 DF_Descriptor.binCount=1
mi@0 251 DF_Descriptor.hasKnownExtents=False
mi@0 252 DF_Descriptor.isQuantized=False
mi@0 253 DF_Descriptor.sampleType = OneSamplePerStep
mi@0 254 DF_Descriptor.unit = ''
mi@0 255 DF_Descriptor.name = 'Onset Detection Function'
mi@0 256 DF_Descriptor.description ='Onset Detection Function'
mi@0 257
mi@0 258 # NOTE: Just change what we need, all oter parameters are inherited from DF_Descriptor
mi@0 259 SDF_Descriptor = OutputDescriptor(DF_Descriptor)
mi@0 260 SDF_Descriptor.name = 'Smoothed Onset Detection Function'
mi@0 261 SDF_Descriptor.description ='Smoothed Onset Detection Function'
mi@0 262 SDF_Descriptor.sampleType = VariableSampleRate
mi@0 263 SDF_Descriptor.sampleRate = 1.0 / self.preferredStepSecs
mi@0 264
mi@0 265 Onset_Descriptor = OutputDescriptor()
mi@0 266 Onset_Descriptor.name = 'Onsets'
mi@0 267 Onset_Descriptor.description ='Onsets using spectral difference'
mi@0 268 Onset_Descriptor.hasFixedBinCount=True
mi@0 269 Onset_Descriptor.binCount=0
mi@0 270 Onset_Descriptor.hasKnownExtents=False
mi@0 271 Onset_Descriptor.isQuantized=False
mi@0 272 Onset_Descriptor.sampleType = VariableSampleRate
mi@0 273 Onset_Descriptor.unit = ''
mi@0 274
mi@0 275 return DF_Descriptor, SDF_Descriptor, Onset_Descriptor
mi@0 276
mi@0 277
mi@0 278 def backtrack(self, onset_index, smoothed_df):
mi@0 279 '''Backtrack the onsets to an earlier 'perceived' location from the actually detected peak...
mi@0 280 This is based on the rationale that the perceived onset tends to be a few frames before the detected peak.
mi@0 281 This tracks the position in the detection function back to where the peak is startng to build up.
mi@0 282 Notice the "out of the blue" parameter: 0.9. (Ideally, this should be tested, evaluated and reported...)'''
mi@0 283 prevDiff = 0.0
mi@0 284 while (onset_index > 1) :
mi@0 285 diff = smoothed_df[onset_index] - smoothed_df[onset_index-1]
mi@0 286 if diff < prevDiff * self.backtracking_threshold : break
mi@0 287 prevDiff = diff
mi@0 288 onset_index -= 1
mi@0 289 return onset_index
mi@0 290
mi@0 291 def trackDF(self, onset1_index, df2):
mi@0 292 '''In the second round of detection, remove the known onsets from the DF by tracking from the peak given by the first round
mi@0 293 to a valley to deminish the recognised peaks on top of which to start new detection.'''
mi@0 294 for idx in xrange(len(onset1_index)) :
mi@0 295 remove = True
mi@0 296 for i in xrange(onset1_index[idx], 1, -1) :
mi@0 297 if remove :
mi@0 298 if df2[i] >= df2[i-1] :
mi@0 299 df2[i] == 0.0
mi@0 300 else:
mi@0 301 remove = False
mi@0 302 return df2
mi@0 303
mi@0 304 def whiten(self,magnitudeSpectrum):
mi@0 305 '''This function reproduces adaptive whitening as described in Dan Stowell's paper.'''
mi@0 306 if self.magPeaks is None :
mi@0 307 self.magPeaks = zeros(self.half_length, dtype = float32)
mi@0 308 m = array(magnitudeSpectrum, dtype=float32)
mi@0 309 idx = m < self.magPeaks
mi@0 310 # print " m", m[idx]
mi@0 311
mi@0 312 m[idx] += (self.magPeaks[idx] - m[idx]) * self.whitenRelaxCoeff
mi@0 313 m[m < self.whitenFloor] = self.whitenFloor
mi@0 314 self.magPeaks = m
mi@0 315
mi@0 316 magnitudeSpectrum /= m
mi@0 317
mi@0 318 return magnitudeSpectrum
mi@0 319
mi@0 320
mi@0 321
mi@0 322