annotate plugins/SimilarityPlugin.cpp @ 66:12516e68c81e

* Start work on fixes to similarity plugin -- avoid crash when running in chroma mode -- set up chroma to have same blocksize as mfcc -- unfortunately we now have meaningless beat spectra for chroma+rhythm mode -- probably something very trivial, but I don't see what right now
author Chris Cannam <c.cannam@qmul.ac.uk>
date Mon, 03 Mar 2008 18:07:27 +0000
parents 90fa946fda40
children e8e103090d97
rev   line source
c@41 1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
c@41 2
c@41 3 /*
c@41 4 * SegmenterPlugin.cpp
c@41 5 *
c@41 6 * Copyright 2008 Centre for Digital Music, Queen Mary, University of London.
c@41 7 * All rights reserved.
c@41 8 */
c@41 9
c@41 10 #include <iostream>
c@44 11 #include <cstdio>
c@41 12
c@41 13 #include "SimilarityPlugin.h"
c@42 14 #include "base/Pitch.h"
c@41 15 #include "dsp/mfcc/MFCC.h"
c@42 16 #include "dsp/chromagram/Chromagram.h"
c@41 17 #include "dsp/rateconversion/Decimator.h"
c@47 18 #include "dsp/rhythm/BeatSpectrum.h"
c@47 19 #include "maths/KLDivergence.h"
c@47 20 #include "maths/CosineDistance.h"
c@49 21 #include "maths/MathUtilities.h"
c@41 22
c@41 23 using std::string;
c@41 24 using std::vector;
c@41 25 using std::cerr;
c@41 26 using std::endl;
c@41 27 using std::ostringstream;
c@41 28
c@47 29 const float
c@47 30 SimilarityPlugin::m_noRhythm = 0.009;
c@47 31
c@47 32 const float
c@47 33 SimilarityPlugin::m_allRhythm = 0.991;
c@47 34
c@41 35 SimilarityPlugin::SimilarityPlugin(float inputSampleRate) :
c@41 36 Plugin(inputSampleRate),
c@42 37 m_type(TypeMFCC),
c@41 38 m_mfcc(0),
c@47 39 m_rhythmfcc(0),
c@42 40 m_chromagram(0),
c@41 41 m_decimator(0),
c@42 42 m_featureColumnSize(20),
c@48 43 m_rhythmWeighting(0.5f),
c@47 44 m_rhythmClipDuration(4.f), // seconds
c@47 45 m_rhythmClipOrigin(40.f), // seconds
c@47 46 m_rhythmClipFrameSize(0),
c@47 47 m_rhythmClipFrames(0),
c@47 48 m_rhythmColumnSize(20),
c@41 49 m_blockSize(0),
c@47 50 m_channels(0),
c@47 51 m_processRate(0),
c@47 52 m_frameNo(0),
c@47 53 m_done(false)
c@41 54 {
c@47 55 int rate = lrintf(m_inputSampleRate);
c@47 56 int internalRate = 22050;
c@47 57 int decimationFactor = rate / internalRate;
c@47 58 if (decimationFactor < 1) decimationFactor = 1;
c@47 59
c@47 60 // must be a power of two
c@47 61 while (decimationFactor & (decimationFactor - 1)) ++decimationFactor;
c@47 62
c@47 63 m_processRate = rate / decimationFactor; // may be 22050, 24000 etc
c@41 64 }
c@41 65
c@41 66 SimilarityPlugin::~SimilarityPlugin()
c@41 67 {
c@41 68 delete m_mfcc;
c@47 69 delete m_rhythmfcc;
c@42 70 delete m_chromagram;
c@41 71 delete m_decimator;
c@41 72 }
c@41 73
c@41 74 string
c@41 75 SimilarityPlugin::getIdentifier() const
c@41 76 {
c@41 77 return "qm-similarity";
c@41 78 }
c@41 79
c@41 80 string
c@41 81 SimilarityPlugin::getName() const
c@41 82 {
c@41 83 return "Similarity";
c@41 84 }
c@41 85
c@41 86 string
c@41 87 SimilarityPlugin::getDescription() const
c@41 88 {
c@42 89 return "Return a distance matrix for similarity between the input audio channels";
c@41 90 }
c@41 91
c@41 92 string
c@41 93 SimilarityPlugin::getMaker() const
c@41 94 {
c@50 95 return "Queen Mary, University of London";
c@41 96 }
c@41 97
c@41 98 int
c@41 99 SimilarityPlugin::getPluginVersion() const
c@41 100 {
c@41 101 return 1;
c@41 102 }
c@41 103
c@41 104 string
c@41 105 SimilarityPlugin::getCopyright() const
c@41 106 {
c@50 107 return "Plugin by Mark Levy, Kurt Jacobson and Chris Cannam. Copyright (c) 2008 QMUL - All Rights Reserved";
c@41 108 }
c@41 109
c@41 110 size_t
c@41 111 SimilarityPlugin::getMinChannelCount() const
c@41 112 {
c@43 113 return 1;
c@41 114 }
c@41 115
c@41 116 size_t
c@41 117 SimilarityPlugin::getMaxChannelCount() const
c@41 118 {
c@41 119 return 1024;
c@41 120 }
c@41 121
c@41 122 bool
c@41 123 SimilarityPlugin::initialise(size_t channels, size_t stepSize, size_t blockSize)
c@41 124 {
c@52 125 if (channels < getMinChannelCount()) return false;
c@52 126
c@52 127 // Using more than getMaxChannelCount is not actually a problem
c@52 128 // for us. Using "incorrect" step and block sizes would be fine
c@52 129 // for timbral or chroma similarity, but will break rhythmic
c@52 130 // similarity, so we'd better enforce these.
c@41 131
c@41 132 if (stepSize != getPreferredStepSize()) {
c@41 133 std::cerr << "SimilarityPlugin::initialise: supplied step size "
c@41 134 << stepSize << " differs from required step size "
c@41 135 << getPreferredStepSize() << std::endl;
c@41 136 return false;
c@41 137 }
c@41 138
c@41 139 if (blockSize != getPreferredBlockSize()) {
c@41 140 std::cerr << "SimilarityPlugin::initialise: supplied block size "
c@41 141 << blockSize << " differs from required block size "
c@41 142 << getPreferredBlockSize() << std::endl;
c@41 143 return false;
c@41 144 }
c@41 145
c@41 146 m_blockSize = blockSize;
c@41 147 m_channels = channels;
c@41 148
c@44 149 m_lastNonEmptyFrame = std::vector<int>(m_channels);
c@44 150 for (int i = 0; i < m_channels; ++i) m_lastNonEmptyFrame[i] = -1;
c@60 151
c@60 152 m_emptyFrameCount = std::vector<int>(m_channels);
c@60 153 for (int i = 0; i < m_channels; ++i) m_emptyFrameCount[i] = 0;
c@60 154
c@44 155 m_frameNo = 0;
c@44 156
c@41 157 int decimationFactor = getDecimationFactor();
c@41 158 if (decimationFactor > 1) {
c@42 159 m_decimator = new Decimator(m_blockSize, decimationFactor);
c@41 160 }
c@41 161
c@42 162 if (m_type == TypeMFCC) {
c@42 163
c@42 164 m_featureColumnSize = 20;
c@42 165
c@47 166 MFCCConfig config(m_processRate);
c@42 167 config.fftsize = 2048;
c@42 168 config.nceps = m_featureColumnSize - 1;
c@42 169 config.want_c0 = true;
c@45 170 config.logpower = 1;
c@42 171 m_mfcc = new MFCC(config);
c@42 172 m_fftSize = m_mfcc->getfftlength();
c@47 173 m_rhythmClipFrameSize = m_fftSize / 4;
c@42 174
c@53 175 // std::cerr << "MFCC FS = " << config.FS << ", FFT size = " << m_fftSize<< std::endl;
c@43 176
c@42 177 } else if (m_type == TypeChroma) {
c@42 178
c@42 179 m_featureColumnSize = 12;
c@42 180
c@66 181 // For simplicity, aim to have the chroma fft size equal to
c@66 182 // 2048, the same as the mfcc fft size (so the input block
c@66 183 // size does not depend on the feature type and we can use the
c@66 184 // same processing parameters for rhythm etc). This is also
c@66 185 // why getPreferredBlockSize can confidently return 2048 * the
c@66 186 // decimation factor.
c@66 187
c@66 188 // The fft size for a chromagram is the filterbank Q value
c@66 189 // times the sample rate, divided by the minimum frequency,
c@66 190 // rounded up to the nearest power of two.
c@66 191
c@66 192 double q = 1.0 / (pow(2.0, (1.0 / 12.0)) - 1.0);
c@66 193 double fmin = (q * m_processRate) / 2048.0;
c@66 194 // std::cerr << "chroma fmin = " << fmin;
c@66 195
c@66 196 // Round fmin up to the nearest MIDI pitch multiple of 12.
c@66 197 // So long as fmin is greater than 12 to start with, this
c@66 198 // should not change the resulting fft size.
c@66 199
c@66 200 int pmin = Pitch::getPitchForFrequency(float(fmin));
c@66 201 pmin = ((pmin / 12) + 1) * 12;
c@66 202 fmin = Pitch::getFrequencyForPitch(pmin);
c@66 203 // std::cerr << " -> " << fmin << " for pitch " << pmin << std::endl;
c@66 204
c@66 205 float fmax = Pitch::getFrequencyForPitch(pmin + 36);
c@66 206 // std::cerr << "fmax = " << fmax << " for pitch " << (pmin+36) << std::endl;
c@66 207
c@66 208
c@42 209 ChromaConfig config;
c@47 210 config.FS = m_processRate;
c@66 211 config.min = fmin;
c@66 212 config.max = fmax;
c@66 213 // config.min = Pitch::getFrequencyForPitch(24, 0, 440);
c@66 214 // config.max = Pitch::getFrequencyForPitch(96, 0, 440);
c@42 215 config.BPO = 12;
c@42 216 config.CQThresh = 0.0054;
c@49 217 // We don't normalise the chromagram's columns individually;
c@49 218 // we normalise the mean at the end instead
c@49 219 config.normalise = MathUtilities::NormaliseNone;
c@42 220 m_chromagram = new Chromagram(config);
c@42 221 m_fftSize = m_chromagram->getFrameSize();
c@66 222
c@66 223 if (m_fftSize != 2048) {
c@66 224 std::cerr << "WARNING: SimilarityPlugin::initialise: Internal processing FFT size " << m_fftSize << " != expected size 2048 in chroma mode" << std::endl;
c@66 225 }
c@42 226
c@53 227 // std::cerr << "fftsize = " << m_fftSize << std::endl;
c@47 228
c@66 229 m_rhythmClipFrameSize = m_fftSize / 4;
c@66 230
c@66 231 // m_rhythmClipFrameSize = m_fftSize / 16;
c@66 232 // while (m_rhythmClipFrameSize < 512) m_rhythmClipFrameSize *= 2;
c@66 233
c@53 234 // std::cerr << "m_rhythmClipFrameSize = " << m_rhythmClipFrameSize << std::endl;
c@47 235
c@53 236 // std::cerr << "min = "<< config.min << ", max = " << config.max << std::endl;
c@42 237
c@42 238 } else {
c@42 239
c@42 240 std::cerr << "SimilarityPlugin::initialise: internal error: unknown type " << m_type << std::endl;
c@42 241 return false;
c@42 242 }
c@41 243
c@47 244 if (needRhythm()) {
c@47 245 m_rhythmClipFrames =
c@47 246 int(ceil((m_rhythmClipDuration * m_processRate)
c@47 247 / m_rhythmClipFrameSize));
c@60 248 // std::cerr << "SimilarityPlugin::initialise: rhythm clip requires "
c@60 249 // << m_rhythmClipFrames << " frames of size "
c@60 250 // << m_rhythmClipFrameSize << " at process rate "
c@60 251 // << m_processRate << " ( = "
c@60 252 // << (float(m_rhythmClipFrames * m_rhythmClipFrameSize) / m_processRate) << " sec )"
c@60 253 // << std::endl;
c@47 254
c@47 255 MFCCConfig config(m_processRate);
c@47 256 config.fftsize = m_rhythmClipFrameSize;
c@47 257 config.nceps = m_featureColumnSize - 1;
c@47 258 config.want_c0 = true;
c@47 259 config.logpower = 1;
c@47 260 config.window = RectangularWindow; // because no overlap
c@47 261 m_rhythmfcc = new MFCC(config);
c@47 262 }
c@47 263
c@41 264 for (int i = 0; i < m_channels; ++i) {
c@47 265
c@42 266 m_values.push_back(FeatureMatrix());
c@47 267
c@47 268 if (needRhythm()) {
c@47 269 m_rhythmValues.push_back(FeatureColumnQueue());
c@47 270 }
c@41 271 }
c@41 272
c@47 273 m_done = false;
c@47 274
c@41 275 return true;
c@41 276 }
c@41 277
c@41 278 void
c@41 279 SimilarityPlugin::reset()
c@41 280 {
c@57 281 for (int i = 0; i < m_values.size(); ++i) {
c@57 282 m_values[i].clear();
c@57 283 }
c@57 284
c@57 285 for (int i = 0; i < m_rhythmValues.size(); ++i) {
c@57 286 m_rhythmValues[i].clear();
c@57 287 }
c@57 288
c@60 289 for (int i = 0; i < m_lastNonEmptyFrame.size(); ++i) {
c@60 290 m_lastNonEmptyFrame[i] = -1;
c@60 291 }
c@60 292
c@60 293 for (int i = 0; i < m_emptyFrameCount.size(); ++i) {
c@60 294 m_emptyFrameCount[i] = 0;
c@60 295 }
c@60 296
c@47 297 m_done = false;
c@41 298 }
c@41 299
c@41 300 int
c@41 301 SimilarityPlugin::getDecimationFactor() const
c@41 302 {
c@41 303 int rate = lrintf(m_inputSampleRate);
c@47 304 return rate / m_processRate;
c@41 305 }
c@41 306
c@41 307 size_t
c@41 308 SimilarityPlugin::getPreferredStepSize() const
c@41 309 {
c@42 310 if (m_blockSize == 0) calculateBlockSize();
c@47 311
c@47 312 // there is also an assumption to this effect in process()
c@47 313 // (referring to m_fftSize/2 instead of a literal post-decimation
c@47 314 // step size):
c@45 315 return m_blockSize/2;
c@41 316 }
c@41 317
c@41 318 size_t
c@41 319 SimilarityPlugin::getPreferredBlockSize() const
c@41 320 {
c@42 321 if (m_blockSize == 0) calculateBlockSize();
c@42 322 return m_blockSize;
c@42 323 }
c@42 324
c@42 325 void
c@42 326 SimilarityPlugin::calculateBlockSize() const
c@42 327 {
c@42 328 if (m_blockSize != 0) return;
c@42 329 int decimationFactor = getDecimationFactor();
c@66 330 m_blockSize = 2048 * decimationFactor;
c@41 331 }
c@41 332
c@41 333 SimilarityPlugin::ParameterList SimilarityPlugin::getParameterDescriptors() const
c@41 334 {
c@41 335 ParameterList list;
c@42 336
c@42 337 ParameterDescriptor desc;
c@42 338 desc.identifier = "featureType";
c@42 339 desc.name = "Feature Type";
c@48 340 desc.description = "Audio feature used for similarity measure. Timbral: use the first 20 MFCCs (19 plus C0). Chromatic: use 12 bin-per-octave chroma. Rhythmic: compare beat spectra of short regions.";
c@42 341 desc.unit = "";
c@42 342 desc.minValue = 0;
c@48 343 desc.maxValue = 4;
c@48 344 desc.defaultValue = 1;
c@42 345 desc.isQuantized = true;
c@42 346 desc.quantizeStep = 1;
c@48 347 desc.valueNames.push_back("Timbre");
c@48 348 desc.valueNames.push_back("Timbre and Rhythm");
c@48 349 desc.valueNames.push_back("Chroma");
c@48 350 desc.valueNames.push_back("Chroma and Rhythm");
c@48 351 desc.valueNames.push_back("Rhythm only");
c@42 352 list.push_back(desc);
c@48 353 /*
c@47 354 desc.identifier = "rhythmWeighting";
c@47 355 desc.name = "Influence of Rhythm";
c@47 356 desc.description = "Proportion of similarity measure made up from rhythmic similarity component, from 0 (entirely timbral or chromatic) to 100 (entirely rhythmic).";
c@47 357 desc.unit = "%";
c@47 358 desc.minValue = 0;
c@47 359 desc.maxValue = 100;
c@47 360 desc.defaultValue = 0;
c@48 361 desc.isQuantized = false;
c@47 362 desc.valueNames.clear();
c@47 363 list.push_back(desc);
c@48 364 */
c@41 365 return list;
c@41 366 }
c@41 367
c@41 368 float
c@41 369 SimilarityPlugin::getParameter(std::string param) const
c@41 370 {
c@42 371 if (param == "featureType") {
c@48 372
c@48 373 if (m_rhythmWeighting > m_allRhythm) {
c@48 374 return 4;
c@48 375 }
c@48 376
c@48 377 switch (m_type) {
c@48 378
c@48 379 case TypeMFCC:
c@48 380 if (m_rhythmWeighting < m_noRhythm) return 0;
c@48 381 else return 1;
c@48 382 break;
c@48 383
c@48 384 case TypeChroma:
c@48 385 if (m_rhythmWeighting < m_noRhythm) return 2;
c@48 386 else return 3;
c@48 387 break;
c@48 388 }
c@48 389
c@48 390 return 1;
c@48 391
c@48 392 // } else if (param == "rhythmWeighting") {
c@48 393 // return nearbyint(m_rhythmWeighting * 100.0);
c@42 394 }
c@42 395
c@41 396 std::cerr << "WARNING: SimilarityPlugin::getParameter: unknown parameter \""
c@41 397 << param << "\"" << std::endl;
c@41 398 return 0.0;
c@41 399 }
c@41 400
c@41 401 void
c@41 402 SimilarityPlugin::setParameter(std::string param, float value)
c@41 403 {
c@42 404 if (param == "featureType") {
c@48 405
c@42 406 int v = int(value + 0.1);
c@48 407
c@48 408 Type newType = m_type;
c@48 409
c@48 410 switch (v) {
c@48 411 case 0: newType = TypeMFCC; m_rhythmWeighting = 0.0f; break;
c@48 412 case 1: newType = TypeMFCC; m_rhythmWeighting = 0.5f; break;
c@48 413 case 2: newType = TypeChroma; m_rhythmWeighting = 0.0f; break;
c@48 414 case 3: newType = TypeChroma; m_rhythmWeighting = 0.5f; break;
c@48 415 case 4: newType = TypeMFCC; m_rhythmWeighting = 1.f; break;
c@48 416 }
c@48 417
c@48 418 if (newType != m_type) m_blockSize = 0;
c@48 419
c@48 420 m_type = newType;
c@42 421 return;
c@48 422
c@48 423 // } else if (param == "rhythmWeighting") {
c@48 424 // m_rhythmWeighting = value / 100;
c@48 425 // return;
c@42 426 }
c@42 427
c@41 428 std::cerr << "WARNING: SimilarityPlugin::setParameter: unknown parameter \""
c@41 429 << param << "\"" << std::endl;
c@41 430 }
c@41 431
c@41 432 SimilarityPlugin::OutputList
c@41 433 SimilarityPlugin::getOutputDescriptors() const
c@41 434 {
c@41 435 OutputList list;
c@41 436
c@41 437 OutputDescriptor similarity;
c@43 438 similarity.identifier = "distancematrix";
c@43 439 similarity.name = "Distance Matrix";
c@43 440 similarity.description = "Distance matrix for similarity metric. Smaller = more similar. Should be symmetrical.";
c@41 441 similarity.unit = "";
c@41 442 similarity.hasFixedBinCount = true;
c@41 443 similarity.binCount = m_channels;
c@41 444 similarity.hasKnownExtents = false;
c@41 445 similarity.isQuantized = false;
c@41 446 similarity.sampleType = OutputDescriptor::FixedSampleRate;
c@41 447 similarity.sampleRate = 1;
c@41 448
c@43 449 m_distanceMatrixOutput = list.size();
c@41 450 list.push_back(similarity);
c@41 451
c@43 452 OutputDescriptor simvec;
c@43 453 simvec.identifier = "distancevector";
c@43 454 simvec.name = "Distance from First Channel";
c@43 455 simvec.description = "Distance vector for similarity of each channel to the first channel. Smaller = more similar.";
c@43 456 simvec.unit = "";
c@43 457 simvec.hasFixedBinCount = true;
c@43 458 simvec.binCount = m_channels;
c@43 459 simvec.hasKnownExtents = false;
c@43 460 simvec.isQuantized = false;
c@43 461 simvec.sampleType = OutputDescriptor::FixedSampleRate;
c@43 462 simvec.sampleRate = 1;
c@43 463
c@43 464 m_distanceVectorOutput = list.size();
c@43 465 list.push_back(simvec);
c@43 466
c@44 467 OutputDescriptor sortvec;
c@44 468 sortvec.identifier = "sorteddistancevector";
c@44 469 sortvec.name = "Ordered Distances from First Channel";
c@44 470 sortvec.description = "Vector of the order of other channels in similarity to the first, followed by distance vector for similarity of each to the first. Smaller = more similar.";
c@44 471 sortvec.unit = "";
c@44 472 sortvec.hasFixedBinCount = true;
c@44 473 sortvec.binCount = m_channels;
c@44 474 sortvec.hasKnownExtents = false;
c@44 475 sortvec.isQuantized = false;
c@44 476 sortvec.sampleType = OutputDescriptor::FixedSampleRate;
c@44 477 sortvec.sampleRate = 1;
c@44 478
c@44 479 m_sortedVectorOutput = list.size();
c@44 480 list.push_back(sortvec);
c@44 481
c@41 482 OutputDescriptor means;
c@41 483 means.identifier = "means";
c@42 484 means.name = "Feature Means";
c@43 485 means.description = "Means of the feature bins. Feature time (sec) corresponds to input channel. Number of bins depends on selected feature type.";
c@41 486 means.unit = "";
c@41 487 means.hasFixedBinCount = true;
c@43 488 means.binCount = m_featureColumnSize;
c@41 489 means.hasKnownExtents = false;
c@41 490 means.isQuantized = false;
c@43 491 means.sampleType = OutputDescriptor::FixedSampleRate;
c@43 492 means.sampleRate = 1;
c@41 493
c@43 494 m_meansOutput = list.size();
c@41 495 list.push_back(means);
c@41 496
c@41 497 OutputDescriptor variances;
c@41 498 variances.identifier = "variances";
c@42 499 variances.name = "Feature Variances";
c@43 500 variances.description = "Variances of the feature bins. Feature time (sec) corresponds to input channel. Number of bins depends on selected feature type.";
c@41 501 variances.unit = "";
c@41 502 variances.hasFixedBinCount = true;
c@43 503 variances.binCount = m_featureColumnSize;
c@41 504 variances.hasKnownExtents = false;
c@41 505 variances.isQuantized = false;
c@43 506 variances.sampleType = OutputDescriptor::FixedSampleRate;
c@43 507 variances.sampleRate = 1;
c@41 508
c@43 509 m_variancesOutput = list.size();
c@41 510 list.push_back(variances);
c@41 511
c@47 512 OutputDescriptor beatspectrum;
c@47 513 beatspectrum.identifier = "beatspectrum";
c@47 514 beatspectrum.name = "Beat Spectra";
c@47 515 beatspectrum.description = "Rhythmic self-similarity vectors (beat spectra) for the input channels. Feature time (sec) corresponds to input channel. Not returned if rhythm weighting is zero.";
c@47 516 beatspectrum.unit = "";
c@47 517 if (m_rhythmClipFrames > 0) {
c@47 518 beatspectrum.hasFixedBinCount = true;
c@47 519 beatspectrum.binCount = m_rhythmClipFrames / 2;
c@47 520 } else {
c@47 521 beatspectrum.hasFixedBinCount = false;
c@47 522 }
c@47 523 beatspectrum.hasKnownExtents = false;
c@47 524 beatspectrum.isQuantized = false;
c@47 525 beatspectrum.sampleType = OutputDescriptor::FixedSampleRate;
c@47 526 beatspectrum.sampleRate = 1;
c@47 527
c@47 528 m_beatSpectraOutput = list.size();
c@47 529 list.push_back(beatspectrum);
c@47 530
c@41 531 return list;
c@41 532 }
c@41 533
c@41 534 SimilarityPlugin::FeatureSet
c@41 535 SimilarityPlugin::process(const float *const *inputBuffers, Vamp::RealTime /* timestamp */)
c@41 536 {
c@47 537 if (m_done) {
c@47 538 return FeatureSet();
c@47 539 }
c@47 540
c@41 541 double *dblbuf = new double[m_blockSize];
c@41 542 double *decbuf = dblbuf;
c@42 543 if (m_decimator) decbuf = new double[m_fftSize];
c@42 544
c@47 545 double *raw = new double[std::max(m_featureColumnSize,
c@47 546 m_rhythmColumnSize)];
c@41 547
c@43 548 float threshold = 1e-10;
c@43 549
c@47 550 bool someRhythmFrameNeeded = false;
c@47 551
c@41 552 for (size_t c = 0; c < m_channels; ++c) {
c@41 553
c@43 554 bool empty = true;
c@43 555
c@41 556 for (int i = 0; i < m_blockSize; ++i) {
c@43 557 float val = inputBuffers[c][i];
c@43 558 if (fabs(val) > threshold) empty = false;
c@43 559 dblbuf[i] = val;
c@41 560 }
c@41 561
c@47 562 if (empty) {
c@47 563 if (needRhythm() && ((m_frameNo % 2) == 0)) {
c@47 564 for (int i = 0; i < m_fftSize / m_rhythmClipFrameSize; ++i) {
c@47 565 if (m_rhythmValues[c].size() < m_rhythmClipFrames) {
c@47 566 FeatureColumn mf(m_rhythmColumnSize);
c@47 567 for (int i = 0; i < m_rhythmColumnSize; ++i) {
c@47 568 mf[i] = 0.0;
c@47 569 }
c@47 570 m_rhythmValues[c].push_back(mf);
c@47 571 }
c@47 572 }
c@47 573 }
c@60 574 m_emptyFrameCount[c]++;
c@47 575 continue;
c@47 576 }
c@47 577
c@44 578 m_lastNonEmptyFrame[c] = m_frameNo;
c@43 579
c@41 580 if (m_decimator) {
c@41 581 m_decimator->process(dblbuf, decbuf);
c@41 582 }
c@42 583
c@47 584 if (needTimbre()) {
c@47 585
c@66 586 FeatureColumn mf(m_featureColumnSize);
c@66 587
c@47 588 if (m_type == TypeMFCC) {
c@47 589 m_mfcc->process(decbuf, raw);
c@66 590 for (int i = 0; i < m_featureColumnSize; ++i) {
c@66 591 mf[i] = raw[i];
c@66 592 }
c@47 593 } else if (m_type == TypeChroma) {
c@66 594 double *chroma = m_chromagram->process(decbuf);
c@66 595 for (int i = 0; i < m_featureColumnSize; ++i) {
c@66 596 mf[i] = chroma[i];
c@66 597 }
c@47 598 }
c@41 599
c@47 600 m_values[c].push_back(mf);
c@44 601 }
c@41 602
c@47 603 // std::cerr << "needRhythm = " << needRhythm() << ", frame = " << m_frameNo << std::endl;
c@47 604
c@47 605 if (needRhythm() && ((m_frameNo % 2) == 0)) {
c@47 606
c@47 607 // The incoming frames are overlapping; we only use every
c@47 608 // other one, because we don't want the overlap (it would
c@47 609 // screw up the rhythm)
c@47 610
c@47 611 int frameOffset = 0;
c@47 612
c@47 613 while (frameOffset + m_rhythmClipFrameSize <= m_fftSize) {
c@47 614
c@47 615 bool needRhythmFrame = true;
c@47 616
c@47 617 if (m_rhythmValues[c].size() >= m_rhythmClipFrames) {
c@47 618
c@47 619 needRhythmFrame = false;
c@47 620
c@47 621 // assumes hopsize = framesize/2
c@47 622 float current = m_frameNo * (m_fftSize/2) + frameOffset;
c@47 623 current = current / m_processRate;
c@47 624 if (current - m_rhythmClipDuration < m_rhythmClipOrigin) {
c@47 625 needRhythmFrame = true;
c@47 626 m_rhythmValues[c].pop_front();
c@47 627 }
c@47 628
c@53 629 // if (needRhythmFrame) {
c@53 630 // std::cerr << "at current = " <<current << " (frame = " << m_frameNo << "), have " << m_rhythmValues[c].size() << ", need rhythm = " << needRhythmFrame << std::endl;
c@53 631 // }
c@47 632
c@47 633 }
c@47 634
c@47 635 if (needRhythmFrame) {
c@47 636
c@47 637 someRhythmFrameNeeded = true;
c@47 638
c@47 639 m_rhythmfcc->process(decbuf + frameOffset, raw);
c@47 640
c@47 641 FeatureColumn mf(m_rhythmColumnSize);
c@47 642 for (int i = 0; i < m_rhythmColumnSize; ++i) {
c@47 643 mf[i] = raw[i];
c@47 644 }
c@47 645
c@47 646 m_rhythmValues[c].push_back(mf);
c@47 647 }
c@47 648
c@47 649 frameOffset += m_rhythmClipFrameSize;
c@47 650 }
c@47 651 }
c@47 652 }
c@47 653
c@47 654 if (!needTimbre() && !someRhythmFrameNeeded && ((m_frameNo % 2) == 0)) {
c@53 655 // std::cerr << "done!" << std::endl;
c@47 656 m_done = true;
c@41 657 }
c@41 658
c@41 659 if (m_decimator) delete[] decbuf;
c@41 660 delete[] dblbuf;
c@47 661 delete[] raw;
c@41 662
c@44 663 ++m_frameNo;
c@44 664
c@41 665 return FeatureSet();
c@41 666 }
c@41 667
c@47 668 SimilarityPlugin::FeatureMatrix
c@47 669 SimilarityPlugin::calculateTimbral(FeatureSet &returnFeatures)
c@41 670 {
c@47 671 FeatureMatrix m(m_channels); // means
c@47 672 FeatureMatrix v(m_channels); // variances
c@41 673
c@41 674 for (int i = 0; i < m_channels; ++i) {
c@41 675
c@42 676 FeatureColumn mean(m_featureColumnSize), variance(m_featureColumnSize);
c@41 677
c@42 678 for (int j = 0; j < m_featureColumnSize; ++j) {
c@41 679
c@43 680 mean[j] = 0.0;
c@43 681 variance[j] = 0.0;
c@41 682 int count;
c@41 683
c@44 684 // We want to take values up to, but not including, the
c@44 685 // last non-empty frame (which may be partial)
c@43 686
c@60 687 int sz = m_lastNonEmptyFrame[i] - m_emptyFrameCount[i];
c@44 688 if (sz < 0) sz = 0;
c@60 689 if (sz >= m_values[i].size()) sz = m_values[i].size()-1;
c@43 690
c@41 691 count = 0;
c@43 692 for (int k = 0; k < sz; ++k) {
c@42 693 double val = m_values[i][k][j];
c@41 694 if (isnan(val) || isinf(val)) continue;
c@41 695 mean[j] += val;
c@41 696 ++count;
c@41 697 }
c@41 698 if (count > 0) mean[j] /= count;
c@41 699
c@41 700 count = 0;
c@43 701 for (int k = 0; k < sz; ++k) {
c@42 702 double val = ((m_values[i][k][j] - mean[j]) *
c@42 703 (m_values[i][k][j] - mean[j]));
c@41 704 if (isnan(val) || isinf(val)) continue;
c@41 705 variance[j] += val;
c@41 706 ++count;
c@41 707 }
c@41 708 if (count > 0) variance[j] /= count;
c@41 709 }
c@41 710
c@41 711 m[i] = mean;
c@41 712 v[i] = variance;
c@41 713 }
c@41 714
c@47 715 FeatureMatrix distances(m_channels);
c@42 716
c@48 717 if (m_type == TypeMFCC) {
c@48 718
c@48 719 // "Despite the fact that MFCCs extracted from music are
c@48 720 // clearly not Gaussian, [14] showed, somewhat surprisingly,
c@48 721 // that a similarity function comparing single Gaussians
c@48 722 // modelling MFCCs for each track can perform as well as
c@48 723 // mixture models. A great advantage of using single
c@48 724 // Gaussians is that a simple closed form exists for the KL
c@48 725 // divergence." -- Mark Levy, "Lightweight measures for
c@48 726 // timbral similarity of musical audio"
c@48 727 // (http://www.elec.qmul.ac.uk/easaier/papers/mlevytimbralsimilarity.pdf)
c@48 728
c@48 729 KLDivergence kld;
c@48 730
c@48 731 for (int i = 0; i < m_channels; ++i) {
c@48 732 for (int j = 0; j < m_channels; ++j) {
c@48 733 double d = kld.distanceGaussian(m[i], v[i], m[j], v[j]);
c@48 734 distances[i].push_back(d);
c@48 735 }
c@48 736 }
c@48 737
c@48 738 } else {
c@48 739
c@49 740 // We use the KL divergence for distributions of discrete
c@49 741 // variables, as chroma are histograms already. Or at least,
c@49 742 // they will be when we've normalised them like this:
c@49 743 for (int i = 0; i < m_channels; ++i) {
c@49 744 MathUtilities::normalise(m[i], MathUtilities::NormaliseUnitSum);
c@49 745 }
c@48 746
c@48 747 KLDivergence kld;
c@48 748
c@48 749 for (int i = 0; i < m_channels; ++i) {
c@48 750 for (int j = 0; j < m_channels; ++j) {
c@48 751 double d = kld.distanceDistribution(m[i], m[j], true);
c@48 752 distances[i].push_back(d);
c@48 753 }
c@41 754 }
c@41 755 }
c@47 756
c@44 757 Feature feature;
c@44 758 feature.hasTimestamp = true;
c@44 759
c@44 760 char labelBuffer[100];
c@43 761
c@41 762 for (int i = 0; i < m_channels; ++i) {
c@41 763
c@41 764 feature.timestamp = Vamp::RealTime(i, 0);
c@41 765
c@44 766 sprintf(labelBuffer, "Means for channel %d", i+1);
c@44 767 feature.label = labelBuffer;
c@44 768
c@41 769 feature.values.clear();
c@42 770 for (int k = 0; k < m_featureColumnSize; ++k) {
c@41 771 feature.values.push_back(m[i][k]);
c@41 772 }
c@41 773
c@43 774 returnFeatures[m_meansOutput].push_back(feature);
c@41 775
c@44 776 sprintf(labelBuffer, "Variances for channel %d", i+1);
c@44 777 feature.label = labelBuffer;
c@44 778
c@41 779 feature.values.clear();
c@42 780 for (int k = 0; k < m_featureColumnSize; ++k) {
c@41 781 feature.values.push_back(v[i][k]);
c@41 782 }
c@41 783
c@43 784 returnFeatures[m_variancesOutput].push_back(feature);
c@47 785 }
c@47 786
c@47 787 return distances;
c@47 788 }
c@47 789
c@47 790 SimilarityPlugin::FeatureMatrix
c@47 791 SimilarityPlugin::calculateRhythmic(FeatureSet &returnFeatures)
c@47 792 {
c@47 793 if (!needRhythm()) return FeatureMatrix();
c@47 794
c@60 795 // std::cerr << "SimilarityPlugin::initialise: rhythm clip for channel 0 contains "
c@60 796 // << m_rhythmValues[0].size() << " frames of size "
c@60 797 // << m_rhythmClipFrameSize << " at process rate "
c@60 798 // << m_processRate << " ( = "
c@60 799 // << (float(m_rhythmValues[0].size() * m_rhythmClipFrameSize) / m_processRate) << " sec )"
c@60 800 // << std::endl;
c@60 801
c@47 802 BeatSpectrum bscalc;
c@47 803 CosineDistance cd;
c@47 804
c@47 805 // Our rhythm feature matrix is a deque of vectors for practical
c@47 806 // reasons, but BeatSpectrum::process wants a vector of vectors
c@47 807 // (which is what FeatureMatrix happens to be).
c@47 808
c@47 809 FeatureMatrixSet bsinput(m_channels);
c@47 810 for (int i = 0; i < m_channels; ++i) {
c@47 811 for (int j = 0; j < m_rhythmValues[i].size(); ++j) {
c@47 812 bsinput[i].push_back(m_rhythmValues[i][j]);
c@47 813 }
c@47 814 }
c@47 815
c@47 816 FeatureMatrix bs(m_channels);
c@47 817 for (int i = 0; i < m_channels; ++i) {
c@47 818 bs[i] = bscalc.process(bsinput[i]);
c@47 819 }
c@47 820
c@47 821 FeatureMatrix distances(m_channels);
c@47 822 for (int i = 0; i < m_channels; ++i) {
c@47 823 for (int j = 0; j < m_channels; ++j) {
c@47 824 double d = cd.distance(bs[i], bs[j]);
c@47 825 distances[i].push_back(d);
c@47 826 }
c@47 827 }
c@47 828
c@47 829 Feature feature;
c@47 830 feature.hasTimestamp = true;
c@47 831
c@47 832 char labelBuffer[100];
c@47 833
c@47 834 for (int i = 0; i < m_channels; ++i) {
c@47 835
c@47 836 feature.timestamp = Vamp::RealTime(i, 0);
c@47 837
c@47 838 sprintf(labelBuffer, "Beat spectrum for channel %d", i+1);
c@47 839 feature.label = labelBuffer;
c@47 840
c@47 841 feature.values.clear();
c@47 842 for (int j = 0; j < bs[i].size(); ++j) {
c@47 843 feature.values.push_back(bs[i][j]);
c@47 844 }
c@47 845
c@47 846 returnFeatures[m_beatSpectraOutput].push_back(feature);
c@47 847 }
c@47 848
c@47 849 return distances;
c@47 850 }
c@47 851
c@47 852 double
c@47 853 SimilarityPlugin::getDistance(const FeatureMatrix &timbral,
c@47 854 const FeatureMatrix &rhythmic,
c@47 855 int i, int j)
c@47 856 {
c@47 857 double distance = 1.0;
c@47 858 if (needTimbre()) distance *= timbral[i][j];
c@47 859 if (needRhythm()) distance *= rhythmic[i][j];
c@47 860 return distance;
c@47 861 }
c@47 862
c@47 863 SimilarityPlugin::FeatureSet
c@47 864 SimilarityPlugin::getRemainingFeatures()
c@47 865 {
c@47 866 FeatureSet returnFeatures;
c@47 867
c@47 868 // We want to return a matrix of the distances between channels,
c@47 869 // but Vamp doesn't have a matrix return type so we will actually
c@47 870 // return a series of vectors
c@47 871
c@47 872 FeatureMatrix timbralDistances, rhythmicDistances;
c@47 873
c@47 874 if (needTimbre()) {
c@47 875 timbralDistances = calculateTimbral(returnFeatures);
c@47 876 }
c@47 877
c@47 878 if (needRhythm()) {
c@47 879 rhythmicDistances = calculateRhythmic(returnFeatures);
c@47 880 }
c@47 881
c@47 882 // We give all features a timestamp, otherwise hosts will tend to
c@47 883 // stamp them at the end of the file, which is annoying
c@47 884
c@47 885 Feature feature;
c@47 886 feature.hasTimestamp = true;
c@47 887
c@47 888 Feature distanceVectorFeature;
c@47 889 distanceVectorFeature.label = "Distance from first channel";
c@47 890 distanceVectorFeature.hasTimestamp = true;
c@47 891 distanceVectorFeature.timestamp = Vamp::RealTime::zeroTime;
c@47 892
c@47 893 std::map<double, int> sorted;
c@47 894
c@47 895 char labelBuffer[100];
c@47 896
c@47 897 for (int i = 0; i < m_channels; ++i) {
c@47 898
c@47 899 feature.timestamp = Vamp::RealTime(i, 0);
c@41 900
c@41 901 feature.values.clear();
c@41 902 for (int j = 0; j < m_channels; ++j) {
c@47 903 double dist = getDistance(timbralDistances, rhythmicDistances, i, j);
c@47 904 feature.values.push_back(dist);
c@41 905 }
c@43 906
c@44 907 sprintf(labelBuffer, "Distances from channel %d", i+1);
c@44 908 feature.label = labelBuffer;
c@41 909
c@43 910 returnFeatures[m_distanceMatrixOutput].push_back(feature);
c@43 911
c@47 912 double fromFirst =
c@47 913 getDistance(timbralDistances, rhythmicDistances, 0, i);
c@44 914
c@47 915 distanceVectorFeature.values.push_back(fromFirst);
c@47 916 sorted[fromFirst] = i;
c@41 917 }
c@41 918
c@43 919 returnFeatures[m_distanceVectorOutput].push_back(distanceVectorFeature);
c@43 920
c@44 921 feature.label = "Order of channels by similarity to first channel";
c@44 922 feature.values.clear();
c@44 923 feature.timestamp = Vamp::RealTime(0, 0);
c@44 924
c@44 925 for (std::map<double, int>::iterator i = sorted.begin();
c@44 926 i != sorted.end(); ++i) {
c@45 927 feature.values.push_back(i->second + 1);
c@44 928 }
c@44 929
c@44 930 returnFeatures[m_sortedVectorOutput].push_back(feature);
c@44 931
c@44 932 feature.label = "Ordered distances of channels from first channel";
c@44 933 feature.values.clear();
c@44 934 feature.timestamp = Vamp::RealTime(1, 0);
c@44 935
c@44 936 for (std::map<double, int>::iterator i = sorted.begin();
c@44 937 i != sorted.end(); ++i) {
c@44 938 feature.values.push_back(i->first);
c@44 939 }
c@44 940
c@44 941 returnFeatures[m_sortedVectorOutput].push_back(feature);
c@44 942
c@41 943 return returnFeatures;
c@41 944 }