2 OnsetsDS - real time musical onset detection library.
3 Copyright (c) 2007 Dan Stowell. All rights reserved.
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or
8 (at your option) any later version.
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
23 #define ODS_DEBUG_POST_CSV 0
26 // msvc doesn't support c99
27 #define hypotf _hypotf
28 #define inline /* inline */
31 static inline float onsetsds_phase_rewrap(float phase
){
32 return (phase
>MINUSPI
&& phase
<PI
) ? phase
: phase
+ TWOPI
* (1.f
+ floorf((MINUSPI
- phase
) * INV_TWOPI
));
36 size_t onsetsds_memneeded (int odftype
, size_t fftsize
, unsigned int medspan
){
40 - median calculation (2 * medspan floats)
41 - storing old values (whether as OdsPolarBuf or as weirder float lists)
42 - storing the OdsPolarBuf (size is NOT sizeof(OdsPolarBuf) but is fftsize)
43 - storing the PSP (numbins + 2 values)
47 int numbins
= (fftsize
>> 1) - 1; // No of bins, not counting DC/nyq
53 // No old FFT frames needed, easy:
54 return (medspan
+medspan
+ fftsize
+ numbins
+ 2) * sizeof(float);
57 case ODS_ODF_RCOMPLEX
:
59 return (medspan
+medspan
+ fftsize
+ numbins
+ 2
60 // For each bin (NOT dc/nyq) we store mag, phase and d_phase
61 + numbins
+ numbins
+ numbins
67 return (medspan
+medspan
+ fftsize
+ numbins
+ 2
68 // For each bin (NOT dc/nyq) we store phase and d_phase
74 return (medspan
+medspan
+ fftsize
+ numbins
+ 2
75 // For each bin (NOT dc/nyq) we store mag
87 void onsetsds_init(OnsetsDS
*ods
, float *odsdata
, int fftformat
,
88 int odftype
, size_t fftsize
, unsigned int medspan
, float srate
){
90 int numbins
, realnumbins
;
92 // The main pointer to the processing area - other pointers will indicate areas within this
94 // Set all vals in processing area to zero
95 memset(odsdata
, 0, onsetsds_memneeded(odftype
, fftsize
, medspan
));
99 numbins
= (fftsize
>> 1) - 1; // No of bins, not counting DC/nyq
100 realnumbins
= numbins
+ 2;
102 // Also point the other pointers to the right places
103 ods
->curr
= (OdsPolarBuf
*) odsdata
;
104 ods
->psp
= odsdata
+ fftsize
;
105 ods
->odfvals
= odsdata
+ fftsize
+ realnumbins
;
106 ods
->sortbuf
= odsdata
+ fftsize
+ realnumbins
+ medspan
;
107 ods
->other
= odsdata
+ fftsize
+ realnumbins
+ medspan
+ medspan
;
109 // Default settings for Adaptive Whitening, user can set own values after init
110 onsetsds_setrelax(ods
, 1.f
, fftsize
>>1);
115 ods
->odfparam
= 0.01; // "powthresh" in SC code
116 ods
->normfactor
= 2560.f
/ (realnumbins
* fftsize
);
119 ods
->odfparam
= 0.01; // "powthresh" in SC code
120 ods
->normfactor
= 113.137085f
/ (realnumbins
* sqrt(fftsize
));
122 case ODS_ODF_COMPLEX
:
123 ods
->odfparam
= 0.01; // "powthresh" in SC code
124 ods
->normfactor
= 231.70475f
/ pow(fftsize
, 1.5);// / fftsize;
126 case ODS_ODF_RCOMPLEX
:
127 ods
->odfparam
= 0.01; // "powthresh" in SC code
128 ods
->normfactor
= 231.70475f
/ pow(fftsize
, 1.5);// / fftsize;
131 ods
->odfparam
= 0.01; // "powthresh" in SC code
132 ods
->normfactor
= 5.12f
/ fftsize
;// / fftsize;
135 ods
->odfparam
= 0.0001; // "powthresh" in SC code. For WPHASE it's kind of superfluous.
136 ods
->normfactor
= 115.852375f
/ pow(fftsize
, 1.5);// / fftsize;
139 ods
->odfparam
= 0.01; // EPSILON parameter. Brossier recommends 1e-6 but I (ICMC 2007) found larger vals (e.g 0.01) to work better
140 ods
->normfactor
= 7.68f
* 0.25f
/ fftsize
;
143 printf("onsetsds_init ERROR: \"odftype\" is not a recognised value\n");
146 ods
->odfvalpost
= 0.f
;
147 ods
->odfvalpostprev
= 0.f
;
149 ods
->logmags
= false;
151 ods
->odftype
= odftype
;
152 ods
->whtype
= ODS_WH_ADAPT_MAX1
;
153 ods
->fftformat
= fftformat
;
155 ods
->whiten
= (odftype
!= ODS_ODF_MKL
); // Deactivate whitening for MKL by default
156 ods
->detected
= false;
157 ods
->med_odd
= (medspan
& 1) != 0;
159 ods
->medspan
= medspan
;
164 ods
->fftsize
= fftsize
;
165 ods
->numbins
= numbins
;
167 //printf("End of _init: normfactor is %g\n", ods->normfactor);
171 bool onsetsds_process(OnsetsDS
* ods
, float* fftbuf
){
172 onsetsds_loadframe(ods
, fftbuf
);
174 onsetsds_whiten(ods
);
176 onsetsds_detect(ods
);
178 return ods
->detected
;
182 void onsetsds_setrelax(OnsetsDS
* ods
, float time
, size_t hopsize
){
183 ods
->relaxtime
= time
;
184 ods
->relaxcoef
= (time
== 0.0f
) ? 0.0f
: exp((ods_log1
* hopsize
)/(time
* ods
->srate
));
189 void onsetsds_loadframe(OnsetsDS
* ods
, float* fftbuf
){
191 float *pos
, *pos2
, imag
, real
;
194 switch(ods
->fftformat
){
195 case ODS_FFT_SC3_POLAR
:
196 // The format is the same! dc, nyq, mag[1], phase[1], ...
197 memcpy(ods
->curr
, fftbuf
, ods
->fftsize
* sizeof(float));
200 case ODS_FFT_SC3_COMPLEX
:
202 ods
->curr
->dc
= fftbuf
[0];
203 ods
->curr
->nyq
= fftbuf
[1];
205 // Then convert cartesian to polar:
207 for(i
=0; i
< (ods
->numbins
<< 1); i
+= 2){
209 imag
= pos
[i
+1]; // Plus 1 rather than increment; seems to avoid LSU reject on my PPC
210 ods
->curr
->bin
[i
].mag
= hypotf(imag
, real
);
211 ods
->curr
->bin
[i
].phase
= atan2f(imag
, real
);
215 case ODS_FFT_FFTW3_HC
:
217 ods
->curr
->dc
= fftbuf
[0];
218 ods
->curr
->nyq
= fftbuf
[ods
->fftsize
>>1];
220 // Then convert cartesian to polar:
221 // (Starting positions: real and imag for bin 1)
223 pos2
= fftbuf
+ ods
->fftsize
- 1;
224 for(i
=0; i
<ods
->numbins
; i
++){
227 ods
->curr
->bin
[i
].mag
= hypotf(imag
, real
);
228 ods
->curr
->bin
[i
].phase
= atan2f(imag
, real
);
232 case ODS_FFT_FFTW3_R2C
:
234 ods
->curr
->dc
= fftbuf
[0];
235 ods
->curr
->nyq
= fftbuf
[ods
->fftsize
];
237 // Then convert cartesian to polar:
239 for(i
=0; i
<ods
->numbins
; i
++){
242 ods
->curr
->bin
[i
].mag
= hypotf(imag
, real
);
243 ods
->curr
->bin
[i
].phase
= atan2f(imag
, real
);
249 // Conversion to log-domain magnitudes, including re-scaling to aim back at the zero-to-one range.
250 // Not well tested yet.
252 for(i
=0; i
<ods
->numbins
; i
++){
253 ods
->curr
->bin
[i
].mag
=
254 (log(ods_max(ods
->curr
->bin
[i
].mag
, ODS_LOG_LOWER_LIMIT
)) - ODS_LOGOF_LOG_LOWER_LIMIT
) * ODS_ABSINVOF_LOGOF_LOG_LOWER_LIMIT
;
257 (log(ods_max(ods_abs(ods
->curr
->dc
), ODS_LOG_LOWER_LIMIT
)) - ODS_LOGOF_LOG_LOWER_LIMIT
) * ODS_ABSINVOF_LOGOF_LOG_LOWER_LIMIT
;
259 (log(ods_max(ods_abs(ods
->curr
->nyq
), ODS_LOG_LOWER_LIMIT
)) - ODS_LOGOF_LOG_LOWER_LIMIT
) * ODS_ABSINVOF_LOGOF_LOG_LOWER_LIMIT
;
264 void onsetsds_whiten(OnsetsDS
* ods
){
266 float val
,oldval
, relaxcoef
, floor
;
270 float *pspp1
; // Offset by 1, avoids quite a lot of "+1"s in the following code
272 if(ods
->whtype
== ODS_WH_NONE
){
273 //printf("onsetsds_whiten(): ODS_WH_NONE, skipping\n");
277 // NB: Apart from the above, ods->whtype is currently IGNORED and only one mode is used.
279 relaxcoef
= ods
->relaxcoef
;
280 numbins
= ods
->numbins
;
286 //printf("onsetsds_whiten: relaxcoef=%g, relaxtime=%g, floor=%g\n", relaxcoef, ods->relaxtime, floor);
288 ////////////////////// For each bin, update the record of the peak value /////////////////////
290 val
= fabs(curr
->dc
); // Grab current magnitude
292 // If it beats the amplitude stored then that's our new amplitude;
293 // otherwise our new amplitude is a decayed version of the old one
295 val
= val
+ (oldval
- val
) * relaxcoef
;
297 psp
[0] = val
; // Store the "amplitude trace" back
299 val
= fabs(curr
->nyq
);
300 oldval
= pspp1
[numbins
];
302 val
= val
+ (oldval
- val
) * relaxcoef
;
304 pspp1
[numbins
] = val
;
306 for(i
=0; i
<numbins
; ++i
){
307 val
= fabs(curr
->bin
[i
].mag
);
310 val
= val
+ (oldval
- val
) * relaxcoef
;
315 //////////////////////////// Now for each bin, rescale the current magnitude ////////////////////////////
316 curr
->dc
/= ods_max(floor
, psp
[0]);
317 curr
->nyq
/= ods_max(floor
, pspp1
[numbins
]);
318 for(i
=0; i
<numbins
; ++i
){
319 curr
->bin
[i
].mag
/= ods_max(floor
, pspp1
[i
]);
323 void onsetsds_odf(OnsetsDS
* ods
){
325 int numbins
= ods
->numbins
;
326 OdsPolarBuf
*curr
= ods
->curr
;
327 float* val
= ods
->odfvals
;
329 float deviation
, diff
, curmag
;
331 float predmag
, predphase
, yesterphase
, yesterphasediff
;
337 // Here we shunt the "old" ODF values down one place
338 memcpy(val
+ 1, val
, (ods
->medspan
- 1)*sizeof(float));
340 // Now calculate a new value and store in ods->odfvals[0]
341 switch(ods
->odftype
){
344 *val
= (curr
->nyq
* curr
->nyq
) + (curr
->dc
* curr
->dc
);
345 for(i
=0; i
<numbins
; i
++){
346 *val
+= curr
->bin
[i
].mag
* curr
->bin
[i
].mag
;
352 *val
= ods_abs(curr
->nyq
) + ods_abs(curr
->dc
);
354 for(i
=0; i
<numbins
; i
++){
355 *val
+= ods_abs(curr
->bin
[i
].mag
);
359 case ODS_ODF_COMPLEX
:
361 // ...and then drop through to:
362 case ODS_ODF_RCOMPLEX
:
364 // Note: "other" buf is stored in this format: mag[0],phase[0],d_phase[0],mag[1],phase[1],d_phase[1], ...
366 // Iterate through, calculating the deviation from expected value.
369 for (i
=0; i
<numbins
; ++i
) {
370 curmag
= ods_abs(curr
->bin
[i
].mag
);
372 // Predict mag as yestermag
373 predmag
= ods
->other
[tbpointer
++];
374 yesterphase
= ods
->other
[tbpointer
++];
375 yesterphasediff
= ods
->other
[tbpointer
++];
377 // Thresholding as Brossier did - discard (ignore) bin's deviation if bin's power is minimal
378 if(curmag
> ods
->odfparam
) {
379 // If rectifying, ignore decreasing bins
380 if((!rectify
) || !(curmag
< predmag
)){
382 // Predict phase as yesterval + yesterfirstdiff
383 predphase
= yesterphase
+ yesterphasediff
;
385 // Here temporarily using the "deviation" var to store the phase difference
386 // so that the rewrap macro can use it more efficiently
387 deviation
= predphase
- curr
->bin
[i
].phase
;
389 // Deviation is Euclidean distance between predicted and actual.
390 // In polar coords: sqrt(r1^2 + r2^2 - r1r2 cos (theta1 - theta2))
391 deviation
= sqrtf(predmag
* predmag
+ curmag
* curmag
392 - predmag
* curmag
* cosf(onsetsds_phase_rewrap(deviation
))
400 // totdev will be the output, but first we need to fill tempbuf with today's values, ready for tomorrow.
402 for (i
=0; i
<numbins
; ++i
) {
403 ods
->other
[tbpointer
++] = ods_abs(curr
->bin
[i
].mag
); // Storing mag
404 diff
= curr
->bin
[i
].phase
- ods
->other
[tbpointer
]; // Retrieving yesterphase from buf
405 ods
->other
[tbpointer
++] = curr
->bin
[i
].phase
; // Storing phase
406 // Wrap onto +-PI range
407 diff
= onsetsds_phase_rewrap(diff
);
409 ods
->other
[tbpointer
++] = diff
; // Storing first diff to buf
412 *val
= (float)totdev
;
418 rectify
= false; // So, actually, "rectify" means "useweighting" in this context
419 // ...and then drop through to:
422 // Note: "other" buf is stored in this format: phase[0],d_phase[0],phase[1],d_phase[1], ...
424 // Iterate through, calculating the deviation from expected value.
427 for (i
=0; i
<numbins
; ++i
) {
428 // Thresholding as Brossier did - discard (ignore) bin's phase deviation if bin's power is low
429 if(ods_abs(curr
->bin
[i
].mag
) > ods
->odfparam
) {
431 // Deviation is the *second difference* of the phase, which is calc'ed as curval - yesterval - yesterfirstdiff
432 deviation
= curr
->bin
[i
].phase
- ods
->other
[tbpointer
++] - ods
->other
[tbpointer
++];
433 // Wrap onto +-PI range
434 deviation
= onsetsds_phase_rewrap(deviation
);
436 if(rectify
){ // "rectify" meaning "useweighting"...
437 totdev
+= fabs(deviation
* ods_abs(curr
->bin
[i
].mag
));
439 totdev
+= fabs(deviation
);
444 // totdev will be the output, but first we need to fill tempbuf with today's values, ready for tomorrow.
446 for (i
=0; i
<numbins
; ++i
) {
447 diff
= curr
->bin
[i
].phase
- ods
->other
[tbpointer
]; // Retrieving yesterphase from buf
448 ods
->other
[tbpointer
++] = curr
->bin
[i
].phase
; // Storing phase
449 // Wrap onto +-PI range
450 diff
= onsetsds_phase_rewrap(diff
);
452 ods
->other
[tbpointer
++] = diff
; // Storing first diff to buf
455 *val
= (float)totdev
;
461 // Iterate through, calculating the Modified Kullback-Liebler distance
464 for (i
=0; i
<numbins
; ++i
) {
465 curmag
= ods_abs(curr
->bin
[i
].mag
);
466 yestermag
= ods
->other
[tbpointer
];
468 // Here's the main implementation of Brossier's MKL eq'n (eqn 2.9 from his thesis):
469 deviation
= ods_abs(curmag
) / (ods_abs(yestermag
) + ods
->odfparam
);
470 totdev
+= log(1.f
+ deviation
);
472 // Store the mag as yestermag
473 ods
->other
[tbpointer
++] = curmag
;
475 *val
= (float)totdev
;
480 #if ODS_DEBUG_POST_CSV
482 printf("%g,", ods
->odfvals
[0] * ods
->normfactor
);
485 ods
->odfvals
[0] *= ods
->normfactor
;
487 // End of ODF function
489 void SelectionSort(float *array
, int length
);
490 void SelectionSort(float *array
, int length
)
492 // Algo is simply based on http://en.wikibooks.org/wiki/Algorithm_implementation/Sorting/Selection_sort
498 for(i
= 1; i
< length
; i
++)
499 if(array
[i
] > array
[max
])
501 temp
= array
[length
-1];
502 array
[length
-1] = array
[max
];
509 void onsetsds_detect(OnsetsDS
* ods
){
511 float* sortbuf
= ods
->sortbuf
;
512 int medspan
= ods
->medspan
;
514 // Shift the yesterval to its rightful place
515 ods
->odfvalpostprev
= ods
->odfvalpost
;
517 ///////// MEDIAN REMOVAL ////////////
519 // Copy odfvals to sortbuf
520 memcpy(sortbuf
, ods
->odfvals
, medspan
* sizeof(float));
523 SelectionSort(sortbuf
, medspan
);
525 // Subtract the middlest value === the median
527 ods
->odfvalpost
= ods
->odfvals
[0]
528 - sortbuf
[(medspan
- 1) >> 1];
530 ods
->odfvalpost
= ods
->odfvals
[0]
531 - ((sortbuf
[medspan
>> 1]
532 + sortbuf
[(medspan
>> 1) - 1]) * 0.5f
);
536 // Detection not allowed if we're too close to a previous detection.
537 if(ods
->gapleft
!= 0) {
539 ods
->detected
= false;
541 // Now do the detection.
542 ods
->detected
= (ods
->odfvalpost
> ods
->thresh
) && (ods
->odfvalpostprev
<= ods
->thresh
);
544 ods
->gapleft
= ods
->mingap
;
547 #if ODS_DEBUG_POST_CSV
548 printf("%g\n", ods
->odfvalpost
);