annotate constant-q-cpp/src/CQKernel.cpp @ 372:af71cbdab621 tip

Update bqvec code
author Chris Cannam
date Tue, 19 Nov 2019 10:13:32 +0000
parents 5d0a2ebb4d17
children
rev   line source
Chris@366 1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
Chris@366 2 /*
Chris@366 3 Constant-Q library
Chris@366 4 Copyright (c) 2013-2014 Queen Mary, University of London
Chris@366 5
Chris@366 6 Permission is hereby granted, free of charge, to any person
Chris@366 7 obtaining a copy of this software and associated documentation
Chris@366 8 files (the "Software"), to deal in the Software without
Chris@366 9 restriction, including without limitation the rights to use, copy,
Chris@366 10 modify, merge, publish, distribute, sublicense, and/or sell copies
Chris@366 11 of the Software, and to permit persons to whom the Software is
Chris@366 12 furnished to do so, subject to the following conditions:
Chris@366 13
Chris@366 14 The above copyright notice and this permission notice shall be
Chris@366 15 included in all copies or substantial portions of the Software.
Chris@366 16
Chris@366 17 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
Chris@366 18 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
Chris@366 19 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
Chris@366 20 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
Chris@366 21 CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
Chris@366 22 CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
Chris@366 23 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Chris@366 24
Chris@366 25 Except as contained in this notice, the names of the Centre for
Chris@366 26 Digital Music; Queen Mary, University of London; and Chris Cannam
Chris@366 27 shall not be used in advertising or otherwise to promote the sale,
Chris@366 28 use or other dealings in this Software without prior written
Chris@366 29 authorization.
Chris@366 30 */
Chris@366 31
Chris@366 32 #include "CQKernel.h"
Chris@366 33
Chris@366 34 #include "dsp/MathUtilities.h"
Chris@366 35 #include "dsp/FFT.h"
Chris@366 36 #include "dsp/Window.h"
Chris@366 37
Chris@366 38 #include <cmath>
Chris@366 39 #include <cassert>
Chris@366 40 #include <vector>
Chris@366 41 #include <iostream>
Chris@366 42 #include <algorithm>
Chris@366 43
Chris@366 44 #include <cmath>
Chris@366 45
Chris@366 46 using std::vector;
Chris@366 47 using std::complex;
Chris@366 48 using std::cerr;
Chris@366 49 using std::endl;
Chris@366 50
Chris@366 51 typedef std::complex<double> C;
Chris@366 52
Chris@366 53 //#define DEBUG_CQ_KERNEL 1
Chris@366 54
Chris@366 55 CQKernel::CQKernel(CQParameters params) :
Chris@366 56 m_inparams(params),
Chris@366 57 m_valid(false),
Chris@366 58 m_fft(0)
Chris@366 59 {
Chris@366 60 m_p.sampleRate = params.sampleRate;
Chris@366 61 m_p.maxFrequency = params.maxFrequency;
Chris@366 62 m_p.binsPerOctave = params.binsPerOctave;
Chris@366 63 m_valid = generateKernel();
Chris@366 64 }
Chris@366 65
Chris@366 66 CQKernel::~CQKernel()
Chris@366 67 {
Chris@366 68 delete m_fft;
Chris@366 69 }
Chris@366 70
Chris@366 71 vector<double>
Chris@366 72 CQKernel::makeWindow(int len) const
Chris@366 73 {
Chris@366 74 // The MATLAB version uses a symmetric window, but our windows
Chris@366 75 // are periodic. A symmetric window of size N is a periodic
Chris@366 76 // one of size N-1 with the first element stuck on the end.
Chris@366 77
Chris@366 78 WindowType wt(BlackmanHarrisWindow);
Chris@366 79
Chris@366 80 switch (m_inparams.window) {
Chris@366 81 case CQParameters::SqrtBlackmanHarris:
Chris@366 82 case CQParameters::BlackmanHarris:
Chris@366 83 wt = BlackmanHarrisWindow;
Chris@366 84 break;
Chris@366 85 case CQParameters::SqrtBlackman:
Chris@366 86 case CQParameters::Blackman:
Chris@366 87 wt = BlackmanWindow;
Chris@366 88 break;
Chris@366 89 case CQParameters::SqrtHann:
Chris@366 90 case CQParameters::Hann:
Chris@366 91 wt = HanningWindow;
Chris@366 92 break;
Chris@366 93 }
Chris@366 94
Chris@366 95 Window<double> w(wt, len-1);
Chris@366 96 vector<double> win = w.getWindowData();
Chris@366 97 win.push_back(win[0]);
Chris@366 98
Chris@366 99 switch (m_inparams.window) {
Chris@366 100 case CQParameters::SqrtBlackmanHarris:
Chris@366 101 case CQParameters::SqrtBlackman:
Chris@366 102 case CQParameters::SqrtHann:
Chris@366 103 for (int i = 0; i < (int)win.size(); ++i) {
Chris@366 104 win[i] = sqrt(win[i]) / len;
Chris@366 105 }
Chris@366 106 break;
Chris@366 107 case CQParameters::BlackmanHarris:
Chris@366 108 case CQParameters::Blackman:
Chris@366 109 case CQParameters::Hann:
Chris@366 110 for (int i = 0; i < (int)win.size(); ++i) {
Chris@366 111 win[i] = win[i] / len;
Chris@366 112 }
Chris@366 113 break;
Chris@366 114 }
Chris@366 115
Chris@366 116 return win;
Chris@366 117 }
Chris@366 118
Chris@366 119 bool
Chris@366 120 CQKernel::generateKernel()
Chris@366 121 {
Chris@366 122 double q = m_inparams.q;
Chris@366 123 double atomHopFactor = m_inparams.atomHopFactor;
Chris@366 124 double thresh = m_inparams.threshold;
Chris@366 125
Chris@366 126 double bpo = m_p.binsPerOctave;
Chris@366 127
Chris@366 128 m_p.minFrequency = (m_p.maxFrequency / 2) * pow(2, 1.0/bpo);
Chris@366 129 m_p.Q = q / (pow(2, 1.0/bpo) - 1.0);
Chris@366 130
Chris@366 131 double maxNK = int(m_p.Q * m_p.sampleRate / m_p.minFrequency + 0.5);
Chris@366 132 double minNK = int
Chris@366 133 (m_p.Q * m_p.sampleRate /
Chris@366 134 (m_p.minFrequency * pow(2, (bpo - 1.0) / bpo)) + 0.5);
Chris@366 135
Chris@366 136 if (minNK == 0 || maxNK == 0) {
Chris@366 137 // most likely pathological parameters of some sort
Chris@366 138 cerr << "WARNING: CQKernel::generateKernel: minNK or maxNK is zero (minNK == " << minNK << ", maxNK == " << maxNK << "), not generating a kernel" << endl;
Chris@366 139 m_p.atomSpacing = 0;
Chris@366 140 m_p.firstCentre = 0;
Chris@366 141 m_p.fftSize = 0;
Chris@366 142 m_p.atomsPerFrame = 0;
Chris@366 143 m_p.lastCentre = 0;
Chris@366 144 m_p.fftHop = 0;
Chris@366 145 return false;
Chris@366 146 }
Chris@366 147
Chris@366 148 m_p.atomSpacing = int(minNK * atomHopFactor + 0.5);
Chris@366 149 m_p.firstCentre = m_p.atomSpacing * ceil(ceil(maxNK / 2.0) / m_p.atomSpacing);
Chris@366 150 m_p.fftSize = MathUtilities::nextPowerOfTwo
Chris@366 151 (m_p.firstCentre + ceil(maxNK / 2.0));
Chris@366 152
Chris@366 153 m_p.atomsPerFrame = floor
Chris@366 154 (1.0 + (m_p.fftSize - ceil(maxNK / 2.0) - m_p.firstCentre) / m_p.atomSpacing);
Chris@366 155
Chris@366 156 #ifdef DEBUG_CQ_KERNEL
Chris@366 157 cerr << "atomsPerFrame = " << m_p.atomsPerFrame << " (q = " << q << ", Q = " << m_p.Q << ", atomHopFactor = " << atomHopFactor << ", atomSpacing = " << m_p.atomSpacing << ", fftSize = " << m_p.fftSize << ", maxNK = " << maxNK << ", firstCentre = " << m_p.firstCentre << ")" << endl;
Chris@366 158 #endif
Chris@366 159
Chris@366 160 m_p.lastCentre = m_p.firstCentre + (m_p.atomsPerFrame - 1) * m_p.atomSpacing;
Chris@366 161
Chris@366 162 m_p.fftHop = (m_p.lastCentre + m_p.atomSpacing) - m_p.firstCentre;
Chris@366 163
Chris@366 164 #ifdef DEBUG_CQ_KERNEL
Chris@366 165 cerr << "fftHop = " << m_p.fftHop << endl;
Chris@366 166 #endif
Chris@366 167
Chris@366 168 m_fft = new FFT(m_p.fftSize);
Chris@366 169
Chris@366 170 for (int k = 1; k <= m_p.binsPerOctave; ++k) {
Chris@366 171
Chris@366 172 int nk = int(m_p.Q * m_p.sampleRate /
Chris@366 173 (m_p.minFrequency * pow(2, ((k-1.0) / bpo))) + 0.5);
Chris@366 174
Chris@366 175 vector<double> win = makeWindow(nk);
Chris@366 176
Chris@366 177 double fk = m_p.minFrequency * pow(2, ((k-1.0) / bpo));
Chris@366 178
Chris@366 179 vector<double> reals, imags;
Chris@366 180
Chris@366 181 for (int i = 0; i < nk; ++i) {
Chris@366 182 double arg = (2.0 * M_PI * fk * i) / m_p.sampleRate;
Chris@366 183 reals.push_back(win[i] * cos(arg));
Chris@366 184 imags.push_back(win[i] * sin(arg));
Chris@366 185 }
Chris@366 186
Chris@366 187 int atomOffset = m_p.firstCentre - int(ceil(nk/2.0));
Chris@366 188
Chris@366 189 for (int i = 0; i < m_p.atomsPerFrame; ++i) {
Chris@366 190
Chris@366 191 int shift = atomOffset + (i * m_p.atomSpacing);
Chris@366 192
Chris@366 193 vector<double> rin(m_p.fftSize, 0.0);
Chris@366 194 vector<double> iin(m_p.fftSize, 0.0);
Chris@366 195
Chris@366 196 for (int j = 0; j < nk; ++j) {
Chris@366 197 rin[j + shift] = reals[j];
Chris@366 198 iin[j + shift] = imags[j];
Chris@366 199 }
Chris@366 200
Chris@366 201 vector<double> rout(m_p.fftSize, 0.0);
Chris@366 202 vector<double> iout(m_p.fftSize, 0.0);
Chris@366 203
Chris@366 204 m_fft->process(false,
Chris@366 205 rin.data(), iin.data(),
Chris@366 206 rout.data(), iout.data());
Chris@366 207
Chris@366 208 // Keep this dense for the moment (until after
Chris@366 209 // normalisation calculations)
Chris@366 210
Chris@366 211 vector<C> row;
Chris@366 212
Chris@366 213 for (int j = 0; j < m_p.fftSize; ++j) {
Chris@366 214 if (sqrt(rout[j] * rout[j] + iout[j] * iout[j]) < thresh) {
Chris@366 215 row.push_back(C(0, 0));
Chris@366 216 } else {
Chris@366 217 row.push_back(C(rout[j] / m_p.fftSize,
Chris@366 218 iout[j] / m_p.fftSize));
Chris@366 219 }
Chris@366 220 }
Chris@366 221
Chris@366 222 m_kernel.origin.push_back(0);
Chris@366 223 m_kernel.data.push_back(row);
Chris@366 224 }
Chris@366 225 }
Chris@366 226
Chris@366 227 assert((int)m_kernel.data.size() == m_p.binsPerOctave * m_p.atomsPerFrame);
Chris@366 228
Chris@366 229 // print density as diagnostic
Chris@366 230
Chris@366 231 int nnz = 0;
Chris@366 232 for (int i = 0; i < (int)m_kernel.data.size(); ++i) {
Chris@366 233 for (int j = 0; j < (int)m_kernel.data[i].size(); ++j) {
Chris@366 234 if (m_kernel.data[i][j] != C(0, 0)) {
Chris@366 235 ++nnz;
Chris@366 236 }
Chris@366 237 }
Chris@366 238 }
Chris@366 239
Chris@366 240 #ifdef DEBUG_CQ_KERNEL
Chris@366 241 cerr << "size = " << m_kernel.data.size() << "*" << m_kernel.data[0].size() << " (fft size = " << m_p.fftSize << ")" << endl;
Chris@366 242 #endif
Chris@366 243
Chris@366 244 assert((int)m_kernel.data.size() == m_p.binsPerOctave * m_p.atomsPerFrame);
Chris@366 245 assert((int)m_kernel.data[0].size() == m_p.fftSize);
Chris@366 246
Chris@366 247 #ifdef DEBUG_CQ_KERNEL
Chris@366 248 cerr << "density = " << double(nnz) / double(m_p.binsPerOctave * m_p.atomsPerFrame * m_p.fftSize) << " (" << nnz << " of " << m_p.binsPerOctave * m_p.atomsPerFrame * m_p.fftSize << ")" << endl;
Chris@366 249 #endif
Chris@366 250
Chris@366 251 finaliseKernel();
Chris@366 252 return true;
Chris@366 253 }
Chris@366 254
Chris@366 255 static bool ccomparator(C &c1, C &c2)
Chris@366 256 {
Chris@366 257 return abs(c1) < abs(c2);
Chris@366 258 }
Chris@366 259
Chris@366 260 static int maxidx(vector<C> &v)
Chris@366 261 {
Chris@366 262 return std::max_element(v.begin(), v.end(), ccomparator) - v.begin();
Chris@366 263 }
Chris@366 264
Chris@366 265 void
Chris@366 266 CQKernel::finaliseKernel()
Chris@366 267 {
Chris@366 268 // calculate weight for normalisation
Chris@366 269
Chris@366 270 int wx1 = maxidx(m_kernel.data[0]);
Chris@366 271 int wx2 = maxidx(m_kernel.data[m_kernel.data.size()-1]);
Chris@366 272
Chris@366 273 vector<vector<C> > subset(m_kernel.data.size());
Chris@366 274 for (int j = wx1; j <= wx2; ++j) {
Chris@366 275 for (int i = 0; i < (int)m_kernel.data.size(); ++i) {
Chris@366 276 subset[i].push_back(m_kernel.data[i][j]);
Chris@366 277 }
Chris@366 278 }
Chris@366 279
Chris@366 280 int nrows = subset.size();
Chris@366 281 int ncols = subset[0].size();
Chris@366 282 vector<vector<C> > square(ncols); // conjugate transpose of subset * subset
Chris@366 283
Chris@366 284 for (int i = 0; i < nrows; ++i) {
Chris@366 285 assert((int)subset[i].size() == ncols);
Chris@366 286 }
Chris@366 287
Chris@366 288 for (int j = 0; j < ncols; ++j) {
Chris@366 289 for (int i = 0; i < ncols; ++i) {
Chris@366 290 C v(0, 0);
Chris@366 291 for (int k = 0; k < nrows; ++k) {
Chris@366 292 v += subset[k][i] * conj(subset[k][j]);
Chris@366 293 }
Chris@366 294 square[i].push_back(v);
Chris@366 295 }
Chris@366 296 }
Chris@366 297
Chris@366 298 vector<double> wK;
Chris@366 299 double q = m_inparams.q;
Chris@366 300 for (int i = int(1.0/q + 0.5); i < ncols - int(1.0/q + 0.5) - 2; ++i) {
Chris@366 301 wK.push_back(abs(square[i][i]));
Chris@366 302 }
Chris@366 303
Chris@366 304 double weight = double(m_p.fftHop) / m_p.fftSize;
Chris@366 305 if (!wK.empty()) {
Chris@366 306 weight /= MathUtilities::mean(wK.data(), wK.size());
Chris@366 307 }
Chris@366 308 weight = sqrt(weight);
Chris@366 309
Chris@366 310 #ifdef DEBUG_CQ_KERNEL
Chris@366 311 cerr << "weight = " << weight << " (from " << wK.size() << " elements in wK, ncols = " << ncols << ", q = " << q << ")" << endl;
Chris@366 312 #endif
Chris@366 313
Chris@366 314 // apply normalisation weight, make sparse, and store conjugate
Chris@366 315 // (we use the adjoint or conjugate transpose of the kernel matrix
Chris@366 316 // for the forward transform, the plain kernel for the inverse
Chris@366 317 // which we expect to be less common)
Chris@366 318
Chris@366 319 KernelMatrix sk;
Chris@366 320
Chris@366 321 for (int i = 0; i < (int)m_kernel.data.size(); ++i) {
Chris@366 322
Chris@366 323 sk.origin.push_back(0);
Chris@366 324 sk.data.push_back(vector<C>());
Chris@366 325
Chris@366 326 int lastNZ = 0;
Chris@366 327 for (int j = (int)m_kernel.data[i].size()-1; j >= 0; --j) {
Chris@366 328 if (abs(m_kernel.data[i][j]) != 0.0) {
Chris@366 329 lastNZ = j;
Chris@366 330 break;
Chris@366 331 }
Chris@366 332 }
Chris@366 333
Chris@366 334 bool haveNZ = false;
Chris@366 335 for (int j = 0; j <= lastNZ; ++j) {
Chris@366 336 if (haveNZ || abs(m_kernel.data[i][j]) != 0.0) {
Chris@366 337 if (!haveNZ) sk.origin[i] = j;
Chris@366 338 haveNZ = true;
Chris@366 339 sk.data[i].push_back(conj(m_kernel.data[i][j]) * weight);
Chris@366 340 }
Chris@366 341 }
Chris@366 342 }
Chris@366 343
Chris@366 344 m_kernel = sk;
Chris@366 345 }
Chris@366 346
Chris@366 347 vector<C>
Chris@366 348 CQKernel::processForward(const vector<C> &cv)
Chris@366 349 {
Chris@366 350 // straightforward matrix multiply (taking into account m_kernel's
Chris@366 351 // slightly-sparse representation)
Chris@366 352
Chris@366 353 if (m_kernel.data.empty()) return vector<C>();
Chris@366 354
Chris@366 355 int nrows = m_p.binsPerOctave * m_p.atomsPerFrame;
Chris@366 356
Chris@366 357 vector<C> rv(nrows, C());
Chris@366 358
Chris@366 359 for (int i = 0; i < nrows; ++i) {
Chris@366 360 int len = m_kernel.data[i].size();
Chris@366 361 for (int j = 0; j < len; ++j) {
Chris@366 362 rv[i] += cv[j + m_kernel.origin[i]] * m_kernel.data[i][j];
Chris@366 363 }
Chris@366 364 }
Chris@366 365
Chris@366 366 return rv;
Chris@366 367 }
Chris@366 368
Chris@366 369 vector<C>
Chris@366 370 CQKernel::processInverse(const vector<C> &cv)
Chris@366 371 {
Chris@366 372 // matrix multiply by conjugate transpose of m_kernel. This is
Chris@366 373 // actually the original kernel as calculated, we just stored the
Chris@366 374 // conjugate-transpose of the kernel because we expect to be doing
Chris@366 375 // more forward transforms than inverse ones.
Chris@366 376
Chris@366 377 if (m_kernel.data.empty()) return vector<C>();
Chris@366 378
Chris@366 379 int ncols = m_p.binsPerOctave * m_p.atomsPerFrame;
Chris@366 380 int nrows = m_p.fftSize;
Chris@366 381
Chris@366 382 vector<C> rv(nrows, C());
Chris@366 383
Chris@366 384 for (int j = 0; j < ncols; ++j) {
Chris@366 385 int i0 = m_kernel.origin[j];
Chris@366 386 int i1 = i0 + m_kernel.data[j].size();
Chris@366 387 for (int i = i0; i < i1; ++i) {
Chris@366 388 rv[i] += cv[j] * conj(m_kernel.data[j][i - i0]);
Chris@366 389 }
Chris@366 390 }
Chris@366 391
Chris@366 392 return rv;
Chris@366 393 }
Chris@366 394
Chris@366 395