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
|