Mercurial > hg > silvet
diff constant-q-cpp/src/CQKernel.cpp @ 366:5d0a2ebb4d17
Bring dependent libraries in to repo
author | Chris Cannam |
---|---|
date | Fri, 24 Jun 2016 14:47:45 +0100 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/constant-q-cpp/src/CQKernel.cpp Fri Jun 24 14:47:45 2016 +0100 @@ -0,0 +1,395 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ +/* + Constant-Q library + Copyright (c) 2013-2014 Queen Mary, University of London + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, copy, + modify, merge, publish, distribute, sublicense, and/or sell copies + of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + Except as contained in this notice, the names of the Centre for + Digital Music; Queen Mary, University of London; and Chris Cannam + shall not be used in advertising or otherwise to promote the sale, + use or other dealings in this Software without prior written + authorization. +*/ + +#include "CQKernel.h" + +#include "dsp/MathUtilities.h" +#include "dsp/FFT.h" +#include "dsp/Window.h" + +#include <cmath> +#include <cassert> +#include <vector> +#include <iostream> +#include <algorithm> + +#include <cmath> + +using std::vector; +using std::complex; +using std::cerr; +using std::endl; + +typedef std::complex<double> C; + +//#define DEBUG_CQ_KERNEL 1 + +CQKernel::CQKernel(CQParameters params) : + m_inparams(params), + m_valid(false), + m_fft(0) +{ + m_p.sampleRate = params.sampleRate; + m_p.maxFrequency = params.maxFrequency; + m_p.binsPerOctave = params.binsPerOctave; + m_valid = generateKernel(); +} + +CQKernel::~CQKernel() +{ + delete m_fft; +} + +vector<double> +CQKernel::makeWindow(int len) const +{ + // The MATLAB version uses a symmetric window, but our windows + // are periodic. A symmetric window of size N is a periodic + // one of size N-1 with the first element stuck on the end. + + WindowType wt(BlackmanHarrisWindow); + + switch (m_inparams.window) { + case CQParameters::SqrtBlackmanHarris: + case CQParameters::BlackmanHarris: + wt = BlackmanHarrisWindow; + break; + case CQParameters::SqrtBlackman: + case CQParameters::Blackman: + wt = BlackmanWindow; + break; + case CQParameters::SqrtHann: + case CQParameters::Hann: + wt = HanningWindow; + break; + } + + Window<double> w(wt, len-1); + vector<double> win = w.getWindowData(); + win.push_back(win[0]); + + switch (m_inparams.window) { + case CQParameters::SqrtBlackmanHarris: + case CQParameters::SqrtBlackman: + case CQParameters::SqrtHann: + for (int i = 0; i < (int)win.size(); ++i) { + win[i] = sqrt(win[i]) / len; + } + break; + case CQParameters::BlackmanHarris: + case CQParameters::Blackman: + case CQParameters::Hann: + for (int i = 0; i < (int)win.size(); ++i) { + win[i] = win[i] / len; + } + break; + } + + return win; +} + +bool +CQKernel::generateKernel() +{ + double q = m_inparams.q; + double atomHopFactor = m_inparams.atomHopFactor; + double thresh = m_inparams.threshold; + + double bpo = m_p.binsPerOctave; + + m_p.minFrequency = (m_p.maxFrequency / 2) * pow(2, 1.0/bpo); + m_p.Q = q / (pow(2, 1.0/bpo) - 1.0); + + double maxNK = int(m_p.Q * m_p.sampleRate / m_p.minFrequency + 0.5); + double minNK = int + (m_p.Q * m_p.sampleRate / + (m_p.minFrequency * pow(2, (bpo - 1.0) / bpo)) + 0.5); + + if (minNK == 0 || maxNK == 0) { + // most likely pathological parameters of some sort + cerr << "WARNING: CQKernel::generateKernel: minNK or maxNK is zero (minNK == " << minNK << ", maxNK == " << maxNK << "), not generating a kernel" << endl; + m_p.atomSpacing = 0; + m_p.firstCentre = 0; + m_p.fftSize = 0; + m_p.atomsPerFrame = 0; + m_p.lastCentre = 0; + m_p.fftHop = 0; + return false; + } + + m_p.atomSpacing = int(minNK * atomHopFactor + 0.5); + m_p.firstCentre = m_p.atomSpacing * ceil(ceil(maxNK / 2.0) / m_p.atomSpacing); + m_p.fftSize = MathUtilities::nextPowerOfTwo + (m_p.firstCentre + ceil(maxNK / 2.0)); + + m_p.atomsPerFrame = floor + (1.0 + (m_p.fftSize - ceil(maxNK / 2.0) - m_p.firstCentre) / m_p.atomSpacing); + +#ifdef DEBUG_CQ_KERNEL + 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; +#endif + + m_p.lastCentre = m_p.firstCentre + (m_p.atomsPerFrame - 1) * m_p.atomSpacing; + + m_p.fftHop = (m_p.lastCentre + m_p.atomSpacing) - m_p.firstCentre; + +#ifdef DEBUG_CQ_KERNEL + cerr << "fftHop = " << m_p.fftHop << endl; +#endif + + m_fft = new FFT(m_p.fftSize); + + for (int k = 1; k <= m_p.binsPerOctave; ++k) { + + int nk = int(m_p.Q * m_p.sampleRate / + (m_p.minFrequency * pow(2, ((k-1.0) / bpo))) + 0.5); + + vector<double> win = makeWindow(nk); + + double fk = m_p.minFrequency * pow(2, ((k-1.0) / bpo)); + + vector<double> reals, imags; + + for (int i = 0; i < nk; ++i) { + double arg = (2.0 * M_PI * fk * i) / m_p.sampleRate; + reals.push_back(win[i] * cos(arg)); + imags.push_back(win[i] * sin(arg)); + } + + int atomOffset = m_p.firstCentre - int(ceil(nk/2.0)); + + for (int i = 0; i < m_p.atomsPerFrame; ++i) { + + int shift = atomOffset + (i * m_p.atomSpacing); + + vector<double> rin(m_p.fftSize, 0.0); + vector<double> iin(m_p.fftSize, 0.0); + + for (int j = 0; j < nk; ++j) { + rin[j + shift] = reals[j]; + iin[j + shift] = imags[j]; + } + + vector<double> rout(m_p.fftSize, 0.0); + vector<double> iout(m_p.fftSize, 0.0); + + m_fft->process(false, + rin.data(), iin.data(), + rout.data(), iout.data()); + + // Keep this dense for the moment (until after + // normalisation calculations) + + vector<C> row; + + for (int j = 0; j < m_p.fftSize; ++j) { + if (sqrt(rout[j] * rout[j] + iout[j] * iout[j]) < thresh) { + row.push_back(C(0, 0)); + } else { + row.push_back(C(rout[j] / m_p.fftSize, + iout[j] / m_p.fftSize)); + } + } + + m_kernel.origin.push_back(0); + m_kernel.data.push_back(row); + } + } + + assert((int)m_kernel.data.size() == m_p.binsPerOctave * m_p.atomsPerFrame); + + // print density as diagnostic + + int nnz = 0; + for (int i = 0; i < (int)m_kernel.data.size(); ++i) { + for (int j = 0; j < (int)m_kernel.data[i].size(); ++j) { + if (m_kernel.data[i][j] != C(0, 0)) { + ++nnz; + } + } + } + +#ifdef DEBUG_CQ_KERNEL + cerr << "size = " << m_kernel.data.size() << "*" << m_kernel.data[0].size() << " (fft size = " << m_p.fftSize << ")" << endl; +#endif + + assert((int)m_kernel.data.size() == m_p.binsPerOctave * m_p.atomsPerFrame); + assert((int)m_kernel.data[0].size() == m_p.fftSize); + +#ifdef DEBUG_CQ_KERNEL + 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; +#endif + + finaliseKernel(); + return true; +} + +static bool ccomparator(C &c1, C &c2) +{ + return abs(c1) < abs(c2); +} + +static int maxidx(vector<C> &v) +{ + return std::max_element(v.begin(), v.end(), ccomparator) - v.begin(); +} + +void +CQKernel::finaliseKernel() +{ + // calculate weight for normalisation + + int wx1 = maxidx(m_kernel.data[0]); + int wx2 = maxidx(m_kernel.data[m_kernel.data.size()-1]); + + vector<vector<C> > subset(m_kernel.data.size()); + for (int j = wx1; j <= wx2; ++j) { + for (int i = 0; i < (int)m_kernel.data.size(); ++i) { + subset[i].push_back(m_kernel.data[i][j]); + } + } + + int nrows = subset.size(); + int ncols = subset[0].size(); + vector<vector<C> > square(ncols); // conjugate transpose of subset * subset + + for (int i = 0; i < nrows; ++i) { + assert((int)subset[i].size() == ncols); + } + + for (int j = 0; j < ncols; ++j) { + for (int i = 0; i < ncols; ++i) { + C v(0, 0); + for (int k = 0; k < nrows; ++k) { + v += subset[k][i] * conj(subset[k][j]); + } + square[i].push_back(v); + } + } + + vector<double> wK; + double q = m_inparams.q; + for (int i = int(1.0/q + 0.5); i < ncols - int(1.0/q + 0.5) - 2; ++i) { + wK.push_back(abs(square[i][i])); + } + + double weight = double(m_p.fftHop) / m_p.fftSize; + if (!wK.empty()) { + weight /= MathUtilities::mean(wK.data(), wK.size()); + } + weight = sqrt(weight); + +#ifdef DEBUG_CQ_KERNEL + cerr << "weight = " << weight << " (from " << wK.size() << " elements in wK, ncols = " << ncols << ", q = " << q << ")" << endl; +#endif + + // apply normalisation weight, make sparse, and store conjugate + // (we use the adjoint or conjugate transpose of the kernel matrix + // for the forward transform, the plain kernel for the inverse + // which we expect to be less common) + + KernelMatrix sk; + + for (int i = 0; i < (int)m_kernel.data.size(); ++i) { + + sk.origin.push_back(0); + sk.data.push_back(vector<C>()); + + int lastNZ = 0; + for (int j = (int)m_kernel.data[i].size()-1; j >= 0; --j) { + if (abs(m_kernel.data[i][j]) != 0.0) { + lastNZ = j; + break; + } + } + + bool haveNZ = false; + for (int j = 0; j <= lastNZ; ++j) { + if (haveNZ || abs(m_kernel.data[i][j]) != 0.0) { + if (!haveNZ) sk.origin[i] = j; + haveNZ = true; + sk.data[i].push_back(conj(m_kernel.data[i][j]) * weight); + } + } + } + + m_kernel = sk; +} + +vector<C> +CQKernel::processForward(const vector<C> &cv) +{ + // straightforward matrix multiply (taking into account m_kernel's + // slightly-sparse representation) + + if (m_kernel.data.empty()) return vector<C>(); + + int nrows = m_p.binsPerOctave * m_p.atomsPerFrame; + + vector<C> rv(nrows, C()); + + for (int i = 0; i < nrows; ++i) { + int len = m_kernel.data[i].size(); + for (int j = 0; j < len; ++j) { + rv[i] += cv[j + m_kernel.origin[i]] * m_kernel.data[i][j]; + } + } + + return rv; +} + +vector<C> +CQKernel::processInverse(const vector<C> &cv) +{ + // matrix multiply by conjugate transpose of m_kernel. This is + // actually the original kernel as calculated, we just stored the + // conjugate-transpose of the kernel because we expect to be doing + // more forward transforms than inverse ones. + + if (m_kernel.data.empty()) return vector<C>(); + + int ncols = m_p.binsPerOctave * m_p.atomsPerFrame; + int nrows = m_p.fftSize; + + vector<C> rv(nrows, C()); + + for (int j = 0; j < ncols; ++j) { + int i0 = m_kernel.origin[j]; + int i1 = i0 + m_kernel.data[j].size(); + for (int i = i0; i < i1; ++i) { + rv[i] += cv[j] * conj(m_kernel.data[j][i - i0]); + } + } + + return rv; +} + +