Mercurial > hg > constant-q-cpp
changeset 103:790832f18419
Merge
author | Chris Cannam <c.cannam@qmul.ac.uk> |
---|---|
date | Tue, 13 May 2014 13:31:24 +0100 |
parents | 96a4c8d32995 (current diff) b0c0051d647e (diff) |
children | b5ec88e05a65 |
files | cpp-qm-dsp/CQInterpolated.cpp cpp-qm-dsp/CQInterpolated.h cpp-qm-dsp/Makefile |
diffstat | 18 files changed, 1314 insertions(+), 454 deletions(-) [+] |
line wrap: on
line diff
--- a/Makefile.inc Thu May 08 09:35:10 2014 +0100 +++ b/Makefile.inc Tue May 13 13:31:24 2014 +0100 @@ -16,12 +16,14 @@ LDFLAGS := $(LDFLAGS) PLUGIN_LDFLAGS := $(LDFLAGS) $(PLUGIN_LDFLAGS) TEST_LDFLAGS := $(LDFLAGS) -lboost_unit_test_framework +PF_LDFLAGS := $(LDFLAGS) -lsndfile PLUGIN := cqvamp$(PLUGIN_EXT) TEST := $(LIB_DIR)/test +PF := $(LIB_DIR)/processfile -LIB_HEADERS := $(LIB_DIR)/CQKernel.h $(LIB_DIR)/ConstantQ.h $(LIB_DIR)/CQInterpolated.h -LIB_SOURCES := $(LIB_DIR)/CQKernel.cpp $(LIB_DIR)/ConstantQ.cpp $(LIB_DIR)/CQInterpolated.cpp +LIB_HEADERS := $(LIB_DIR)/CQBase.h $(LIB_DIR)/CQKernel.h $(LIB_DIR)/ConstantQ.h $(LIB_DIR)/CQSpectrogram.h $(LIB_DIR)/CQInverse.h +LIB_SOURCES := $(LIB_DIR)/CQKernel.cpp $(LIB_DIR)/ConstantQ.cpp $(LIB_DIR)/CQSpectrogram.cpp $(LIB_DIR)/CQInverse.cpp VAMP_HEADERS := $(VAMP_DIR)/CQVamp.h VAMP_SOURCES := $(VAMP_DIR)/CQVamp.cpp $(VAMP_DIR)/libmain.cpp @@ -30,12 +32,15 @@ SOURCES := $(LIB_SOURCES) $(VAMP_SOURCES) OBJECTS := $(SOURCES:.cpp=.o) -TEST_SOURCES := $(LIB_DIR)/test.cpp $(SOURCES) -TEST_OBJECTS := $(TEST_SOURCES:.cpp=.o) +TEST_SOURCES := $(LIB_DIR)/test.cpp +TEST_OBJECTS := $(TEST_SOURCES:.cpp=.o) $(OBJECTS) + +PF_SOURCES := $(LIB_DIR)/processfile.cpp +PF_OBJECTS := $(PF_SOURCES:.cpp=.o) $(OBJECTS) LIBS := $(QMDSP_DIR)/libqm-dsp.a $(VAMPSDK_DIR)/libvamp-sdk.a -lpthread -all: $(PLUGIN) $(TEST) +all: $(PLUGIN) $(TEST) $(PF) $(PLUGIN): $(OBJECTS) $(CXX) -o $@ $^ $(LIBS) $(PLUGIN_LDFLAGS) @@ -43,24 +48,40 @@ $(TEST): $(TEST_OBJECTS) $(CXX) -o $@ $^ $(LIBS) $(TEST_LDFLAGS) +$(PF): $(PF_OBJECTS) + $(CXX) -o $@ $^ $(LIBS) $(PF_LDFLAGS) + clean: - rm -f $(OBJECTS) + rm -f $(OBJECTS) $(TEST_OBJECTS) $(PF_OBJECTS) distclean: clean rm -f $(PLUGIN) depend: - makedepend -Y -fMakefile.inc $(SOURCES) $(HEADERS) + makedepend -Y -fMakefile.inc $(SOURCES) $(TEST_SOURCES) $(PF_SOURCES) $(HEADERS) # DO NOT DELETE cpp-qm-dsp/CQKernel.o: cpp-qm-dsp/CQKernel.h -cpp-qm-dsp/ConstantQ.o: cpp-qm-dsp/ConstantQ.h cpp-qm-dsp/CQKernel.h -vamp/CQVamp.o: vamp/CQVamp.h cpp-qm-dsp/CQInterpolated.h -vamp/CQVamp.o: cpp-qm-dsp/ConstantQ.h cpp-qm-dsp/CQKernel.h -vamp/libmain.o: vamp/CQVamp.h cpp-qm-dsp/CQInterpolated.h -vamp/libmain.o: cpp-qm-dsp/ConstantQ.h cpp-qm-dsp/CQKernel.h +cpp-qm-dsp/ConstantQ.o: cpp-qm-dsp/ConstantQ.h cpp-qm-dsp/CQBase.h cpp-qm-dsp/ConstantQ.o: cpp-qm-dsp/CQKernel.h -cpp-qm-dsp/CQInterpolated.o: cpp-qm-dsp/ConstantQ.h cpp-qm-dsp/CQKernel.h -vamp/CQVamp.o: cpp-qm-dsp/CQInterpolated.h cpp-qm-dsp/ConstantQ.h +cpp-qm-dsp/CQSpectrogram.o: cpp-qm-dsp/CQSpectrogram.h cpp-qm-dsp/ConstantQ.h +cpp-qm-dsp/CQSpectrogram.o: cpp-qm-dsp/CQBase.h cpp-qm-dsp/CQKernel.h +cpp-qm-dsp/CQInverse.o: cpp-qm-dsp/CQInverse.h cpp-qm-dsp/CQBase.h +cpp-qm-dsp/CQInverse.o: cpp-qm-dsp/CQKernel.h +vamp/CQVamp.o: vamp/CQVamp.h cpp-qm-dsp/CQSpectrogram.h +vamp/CQVamp.o: cpp-qm-dsp/ConstantQ.h cpp-qm-dsp/CQBase.h vamp/CQVamp.o: cpp-qm-dsp/CQKernel.h +vamp/libmain.o: vamp/CQVamp.h cpp-qm-dsp/CQSpectrogram.h +vamp/libmain.o: cpp-qm-dsp/ConstantQ.h cpp-qm-dsp/CQBase.h +vamp/libmain.o: cpp-qm-dsp/CQKernel.h +cpp-qm-dsp/test.o: cpp-qm-dsp/CQSpectrogram.h cpp-qm-dsp/ConstantQ.h +cpp-qm-dsp/test.o: cpp-qm-dsp/CQBase.h cpp-qm-dsp/CQKernel.h +cpp-qm-dsp/processfile.o: cpp-qm-dsp/ConstantQ.h cpp-qm-dsp/CQBase.h +cpp-qm-dsp/processfile.o: cpp-qm-dsp/CQKernel.h cpp-qm-dsp/CQInverse.h +cpp-qm-dsp/ConstantQ.o: cpp-qm-dsp/CQBase.h cpp-qm-dsp/CQKernel.h +cpp-qm-dsp/CQSpectrogram.o: cpp-qm-dsp/ConstantQ.h cpp-qm-dsp/CQBase.h +cpp-qm-dsp/CQSpectrogram.o: cpp-qm-dsp/CQKernel.h +cpp-qm-dsp/CQInverse.o: cpp-qm-dsp/CQBase.h cpp-qm-dsp/CQKernel.h +vamp/CQVamp.o: cpp-qm-dsp/CQSpectrogram.h cpp-qm-dsp/ConstantQ.h +vamp/CQVamp.o: cpp-qm-dsp/CQBase.h cpp-qm-dsp/CQKernel.h
--- a/Makefile.linux Thu May 08 09:35:10 2014 +0100 +++ b/Makefile.linux Tue May 13 13:31:24 2014 +0100 @@ -1,6 +1,6 @@ -#CFLAGS := -Wall -O3 -fPIC -I../vamp-plugin-sdk/ -CFLAGS := -g -fPIC -I../vamp-plugin-sdk +CFLAGS := -Wall -O3 -fPIC -I../vamp-plugin-sdk/ +#CFLAGS := -g -fPIC -I../vamp-plugin-sdk CXXFLAGS := $(CFLAGS)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cpp-qm-dsp/CQBase.h Tue May 13 13:31:24 2014 +0100 @@ -0,0 +1,60 @@ +/* -*- 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. +*/ + +#ifndef CQBASE_H +#define CQBASE_H + +#include <vector> +#include <complex> + +class CQBase // interface class +{ +public: + typedef std::complex<double> Complex; + typedef std::vector<double> RealSequence; + typedef std::vector<double> RealColumn; + typedef std::vector<Complex> ComplexSequence; + typedef std::vector<Complex> ComplexColumn; + typedef std::vector<RealColumn> RealBlock; + typedef std::vector<ComplexColumn> ComplexBlock; + + virtual double getSampleRate() const = 0; + virtual int getBinsPerOctave() const = 0; + virtual int getOctaves() const = 0; + virtual int getTotalBins() const = 0; + virtual int getColumnHop() const = 0; + virtual int getLatency() const = 0; + virtual double getMaxFrequency() const = 0; + virtual double getMinFrequency() const = 0; // actual min, not that passed to ctor + virtual double getBinFrequency(int bin) const = 0; +}; + +#endif
--- a/cpp-qm-dsp/CQInterpolated.cpp Thu May 08 09:35:10 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,247 +0,0 @@ -/* -*- 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 "CQInterpolated.h" - -#include <iostream> -#include <stdexcept> - -using std::vector; - -using std::cerr; -using std::endl; - -CQInterpolated::CQInterpolated(double sampleRate, - double minFreq, double maxFreq, - int binsPerOctave, - Interpolation interpolation) : - m_cq(sampleRate, minFreq, maxFreq, binsPerOctave), - m_interpolation(interpolation) -{ -} - -CQInterpolated::~CQInterpolated() -{ -} - -vector<vector<double> > -CQInterpolated::process(const vector<double> &td) -{ - return postProcess(m_cq.process(td), false); -} - -vector<vector<double> > -CQInterpolated::getRemainingBlocks() -{ - return postProcess(m_cq.getRemainingBlocks(), true); -} - -vector<vector<double> > -CQInterpolated::postProcess(const vector<vector<double> > &cq, bool insist) -{ - if (m_interpolation == None) { - return cq; - } - - int width = cq.size(); - - for (int i = 0; i < width; ++i) { - m_buffer.push_back(cq[i]); - } - - if (m_interpolation == Hold) { - return fetchHold(insist); - } else { - return fetchLinear(insist); - } -} - -vector<vector<double> > -CQInterpolated::fetchHold(bool) -{ - Grid out; - - int width = m_buffer.size(); - int height = getTotalBins(); - - for (int i = 0; i < width; ++i) { - - vector<double> col = m_buffer[i]; - - int thisHeight = col.size(); - int prevHeight = m_prevColumn.size(); - - for (int j = thisHeight; j < height; ++j) { - if (j < prevHeight) { - col.push_back(m_prevColumn[j]); - } else { - col.push_back(0.0); - } - } - - m_prevColumn = col; - out.push_back(col); - } - - m_buffer.clear(); - - return out; -} - -vector<vector<double> > -CQInterpolated::fetchLinear(bool insist) -{ - Grid out; - - //!!! This is surprisingly messy. I must be missing something. - - // We can only return any data when we have at least one column - // that has the full height in the buffer, that is not the first - // column. - // - // If the first col has full height, and there is another one - // later that also does, then we can interpolate between those, up - // to but not including the second full height column. Then we - // drop and return the columns we interpolated, leaving the second - // full-height col as the first col in the buffer. And repeat as - // long as enough columns are available. - // - // If the first col does not have full height, then (so long as - // we're following the logic above) we must simply have not yet - // reached the first full-height column in the CQ output, and we - // can interpolate nothing. - - int width = m_buffer.size(); - int height = getTotalBins(); - - if (width == 0) return out; - - int firstFullHeight = -1; - int secondFullHeight = -1; - - for (int i = 0; i < width; ++i) { - if ((int)m_buffer[i].size() == height) { - if (firstFullHeight == -1) { - firstFullHeight = i; - } else if (secondFullHeight == -1) { - secondFullHeight = i; - break; - } - } - } - - if (firstFullHeight < 0) { - if (insist) { - out = m_buffer; - m_buffer.clear(); - return out; - } else { - return out; - } - } else if (firstFullHeight > 0) { - // can interpolate nothing, stash up to first full height & recurse - out = Grid(m_buffer.begin(), m_buffer.begin() + firstFullHeight); - m_buffer = Grid(m_buffer.begin() + firstFullHeight, m_buffer.end()); - Grid more = fetchLinear(insist); - out.insert(out.end(), more.begin(), more.end()); - return out; - } else if (secondFullHeight < 0) { - // firstFullHeight == 0, but there is no second full height -- - // wait for it unless insist flag is set - if (insist) { - out = m_buffer; - m_buffer.clear(); - return out; - } else { - return out; - } - } else { - // firstFullHeight == 0 and secondFullHeight also valid. Can interpolate - out = linearInterpolated(m_buffer, 0, secondFullHeight); - m_buffer = Grid(m_buffer.begin() + secondFullHeight, m_buffer.end()); - Grid more = fetchLinear(insist); - out.insert(out.end(), more.begin(), more.end()); - return out; - } -} - -vector<vector<double> > -CQInterpolated::linearInterpolated(const Grid &g, int x0, int x1) -{ - // g must be a grid with full-height columns at x0 and x1 - - if (x0 >= x1) { - throw std::logic_error("x0 >= x1"); - } - if (x1 >= (int)g.size()) { - throw std::logic_error("x1 >= g.size()"); - } - if (g[x0].size() != g[x1].size()) { - throw std::logic_error("x0 and x1 are not the same height"); - } - - int height = g[x0].size(); - int width = x1 - x0; - - Grid out(g.begin() + x0, g.begin() + x1); - - for (int y = 0; y < height; ++y) { - - int spacing = width; - for (int i = 1; i < width; ++i) { - int thisHeight = g[x0 + i].size(); - if (thisHeight > height) { - throw std::logic_error("First column not full-height"); - } - if (thisHeight > y) { - spacing = i; - break; - } - } - - if (spacing < 2) continue; - - for (int i = 0; i + spacing <= width; i += spacing) { - for (int j = 1; j < spacing; ++j) { - double proportion = double(j)/double(spacing); - double interpolated = - g[x0 + i][y] * (1.0 - proportion) + - g[x0 + i + spacing][y] * proportion; - out[i + j].push_back(interpolated); - } - } - } - - return out; -} - - -
--- a/cpp-qm-dsp/CQInterpolated.h Thu May 08 09:35:10 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,79 +0,0 @@ -/* -*- 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. -*/ - -#ifndef CQ_INTERPOLATED_H -#define CQ_INTERPOLATED_H - -#include <vector> -#include "ConstantQ.h" - -class CQInterpolated -{ -public: - enum Interpolation { - None, // leave empty cells empty - Hold, // repeat prior cell - Linear, // linear interpolation between consecutive time cells - }; - - CQInterpolated(double sampleRate, - double minFreq, double maxFreq, - int binsPerOctave, - Interpolation interpolation); - ~CQInterpolated(); - - double getSampleRate() const { return m_cq.getSampleRate(); } - int getBinsPerOctave() const { return m_cq.getBinsPerOctave(); } - int getOctaves() const { return m_cq.getOctaves(); } - int getTotalBins() const { return m_cq.getTotalBins(); } - int getColumnHop() const { return m_cq.getColumnHop(); } - int getLatency() const { return m_cq.getLatency(); } - double getMaxFrequency() const { return m_cq.getMaxFrequency(); } - double getMinFrequency() const { return m_cq.getMinFrequency(); } - double getBinFrequency(int bin) const { return m_cq.getBinFrequency(bin); } - - std::vector<std::vector<double> > process(const std::vector<double> &); - std::vector<std::vector<double> > getRemainingBlocks(); - -private: - ConstantQ m_cq; - Interpolation m_interpolation; - - typedef std::vector<std::vector<double> > Grid; - Grid m_buffer; - Grid postProcess(const Grid &, bool insist); - Grid fetchHold(bool insist); - Grid fetchLinear(bool insist); - Grid linearInterpolated(const Grid &, int, int); - std::vector<double> m_prevColumn; -}; - -#endif
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cpp-qm-dsp/CQInverse.cpp Tue May 13 13:31:24 2014 +0100 @@ -0,0 +1,409 @@ +/* -*- 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 "CQInverse.h" + +#include "dsp/rateconversion/Resampler.h" +#include "maths/MathUtilities.h" +#include "dsp/transforms/FFT.h" + +#include <algorithm> +#include <iostream> +#include <stdexcept> + +using std::vector; +using std::cerr; +using std::endl; + +//#define DEBUG_CQ 1 + +CQInverse::CQInverse(double sampleRate, + double minFreq, + double maxFreq, + int binsPerOctave) : + m_sampleRate(sampleRate), + m_maxFrequency(maxFreq), + m_minFrequency(minFreq), + m_binsPerOctave(binsPerOctave), + m_fft(0) +{ + if (minFreq <= 0.0 || maxFreq <= 0.0) { + throw std::invalid_argument("Frequency extents must be positive"); + } + + initialise(); +} + +CQInverse::~CQInverse() +{ + delete m_fft; + for (int i = 0; i < (int)m_upsamplers.size(); ++i) { + delete m_upsamplers[i]; + } + delete m_kernel; +} + +double +CQInverse::getMinFrequency() const +{ + return m_p.minFrequency / pow(2.0, m_octaves - 1); +} + +double +CQInverse::getBinFrequency(int bin) const +{ + return getMinFrequency() * pow(2, (double(bin) / getBinsPerOctave())); +} + +void +CQInverse::initialise() +{ + m_octaves = int(ceil(log2(m_maxFrequency / m_minFrequency))); + m_kernel = new CQKernel(m_sampleRate, m_maxFrequency, m_binsPerOctave); + m_p = m_kernel->getProperties(); + + // Use exact powers of two for resampling rates. They don't have + // to be related to our actual samplerate: the resampler only + // cares about the ratio, but it only accepts integer source and + // target rates, and if we start from the actual samplerate we + // risk getting non-integer rates for lower octaves + + int sourceRate = pow(2, m_octaves); + vector<int> latencies; + + // top octave, no resampling + latencies.push_back(0); + m_upsamplers.push_back(0); + + for (int i = 1; i < m_octaves; ++i) { + + int factor = pow(2, i); + + Resampler *r = new Resampler + (sourceRate / factor, sourceRate, 60, 0.02); + +#ifdef DEBUG_CQ + cerr << "inverse: octave " << i << ": resample from " << sourceRate/factor << " to " << sourceRate << endl; +#endif + + // See ConstantQ.cpp for discussion on latency -- output + // latency here is at target rate which, this way around, is + // what we want + + latencies.push_back(r->getLatency()); + m_upsamplers.push_back(r); + } + + // additionally we will have fftHop latency at individual octave + // rate (before upsampling) for the overlap-add in each octave + for (int i = 0; i < m_octaves; ++i) { + latencies[i] += m_p.fftHop * pow(2, i); + } + + // Now reverse the drop adjustment made in ConstantQ to align the + // atom centres across different octaves (but this time at output + // sample rate) + + int emptyHops = m_p.firstCentre / m_p.atomSpacing; + + vector<int> pushes; + for (int i = 0; i < m_octaves; ++i) { + int factor = pow(2, i); + int pushHops = emptyHops * pow(2, m_octaves - i - 1) - emptyHops; + int push = ((pushHops * m_p.fftHop) * factor) / m_p.atomsPerFrame; + pushes.push_back(push); + } + + int maxLatLessPush = 0; + for (int i = 0; i < m_octaves; ++i) { + int latLessPush = latencies[i] - pushes[i]; + if (latLessPush > maxLatLessPush) maxLatLessPush = latLessPush; + } + + int totalLatency = maxLatLessPush + 10; + if (totalLatency < 0) totalLatency = 0; + + m_outputLatency = totalLatency + m_p.firstCentre * pow(2, m_octaves-1); + +#ifdef DEBUG_CQ + cerr << "totalLatency = " << totalLatency << ", m_outputLatency = " << m_outputLatency << endl; +#endif + + for (int i = 0; i < m_octaves; ++i) { + + // Calculate the difference between the total latency applied + // across all octaves, and the existing latency due to the + // upsampler for this octave. + + int latencyPadding = totalLatency - latencies[i] + pushes[i]; + +#ifdef DEBUG_CQ + cerr << "octave " << i << ": push " << pushes[i] << ", resampler latency inc overlap space " << latencies[i] << ", latencyPadding = " << latencyPadding << " (/factor = " << latencyPadding / pow(2, i) << ")" << endl; +#endif + + m_buffers.push_back(RealSequence(latencyPadding, 0.0)); + } + + for (int i = 0; i < m_octaves; ++i) { + // Fixed-size buffer for IFFT overlap-add + m_olaBufs.push_back(RealSequence(m_p.fftSize, 0.0)); + } + + m_fft = new FFTReal(m_p.fftSize); +} + +CQInverse::RealSequence +CQInverse::process(const ComplexBlock &block) +{ + // The input data is of the form produced by ConstantQ::process -- + // an unknown number N of columns of varying height. We assert + // that N is a multiple of atomsPerFrame * 2^(octaves-1), as must + // be the case for data that came directly from our ConstantQ + // implementation. + + int widthProvided = block.size(); + + if (widthProvided == 0) { + return drawFromBuffers(); + } + + int blockWidth = m_p.atomsPerFrame * int(pow(2, m_octaves - 1)); + + if (widthProvided % blockWidth != 0) { + cerr << "ERROR: CQInverse::process: Input block size (" + << widthProvided + << ") must be a multiple of processing block width " + << "(atoms-per-frame * 2^(octaves-1) = " + << m_p.atomsPerFrame << " * 2^(" << m_octaves << "-1) = " + << blockWidth << ")" << endl; + throw std::invalid_argument + ("Input block size must be a multiple of processing block width"); + } + + // Procedure: + // + // 1. Slice the list of columns into a set of lists of columns, + // one per octave, each of width N / (2^octave-1) and height + // binsPerOctave, containing the values present in that octave + // + // 2. Group each octave list by atomsPerFrame columns at a time, + // and stack these so as to achieve a list, for each octave, of + // taller columns of height binsPerOctave * atomsPerFrame + // + // 3. For each taller column, take the product with the inverse CQ + // kernel (which is the conjugate of the forward kernel) and + // perform an inverse FFT + // + // 4. Overlap-add each octave's resynthesised blocks (unwindowed) + // + // 5. Resample each octave's overlap-add stream to the original + // rate + // + // 6. Sum the resampled streams and return + + for (int i = 0; i < m_octaves; ++i) { + + // Step 1 + + ComplexBlock oct; + + for (int j = 0; j < widthProvided; ++j) { + int h = block[j].size(); + if (h < m_binsPerOctave * (i+1)) { + continue; + } + ComplexColumn col(block[j].begin() + m_binsPerOctave * i, + block[j].begin() + m_binsPerOctave * (i+1)); + oct.push_back(col); + } + + // Steps 2, 3, 4, 5 + processOctave(i, oct); + } + + // Step 6 + return drawFromBuffers(); +} + +CQInverse::RealSequence +CQInverse::drawFromBuffers() +{ + // 6. Sum the resampled streams and return + + int available = 0; + + for (int i = 0; i < m_octaves; ++i) { + if (i == 0 || int(m_buffers[i].size()) < available) { + available = m_buffers[i].size(); + } + } + + RealSequence result(available, 0); + + if (available == 0) { + return result; + } + + for (int i = 0; i < m_octaves; ++i) { + for (int j = 0; j < available; ++j) { + result[j] += m_buffers[i][j]; + } + m_buffers[i] = RealSequence(m_buffers[i].begin() + available, + m_buffers[i].end()); + } + + return result; +} + +CQInverse::RealSequence +CQInverse::getRemainingOutput() +{ + for (int i = 0; i < (m_p.fftSize - m_p.fftHop) / m_p.fftHop; ++i) { + for (int j = 0; j < m_octaves; ++j) { + overlapAddAndResample(j, RealSequence(m_olaBufs[j].size(), 0)); + } + } + + return drawFromBuffers(); +} + +void +CQInverse::processOctave(int octave, const ComplexBlock &columns) +{ + // 2. Group each octave list by atomsPerFrame columns at a time, + // and stack these so as to achieve a list, for each octave, of + // taller columns of height binsPerOctave * atomsPerFrame + + int ncols = columns.size(); + + if (ncols % m_p.atomsPerFrame != 0) { + cerr << "ERROR: CQInverse::process: Number of columns (" + << ncols + << ") in octave " << octave + << " must be a multiple of atoms-per-frame (" + << m_p.atomsPerFrame << ")" << endl; + throw std::invalid_argument + ("Columns in octave must be a multiple of atoms per frame"); + } + + for (int i = 0; i < ncols; i += m_p.atomsPerFrame) { + + ComplexColumn tallcol; + for (int b = 0; b < m_binsPerOctave; ++b) { + for (int a = 0; a < m_p.atomsPerFrame; ++a) { + tallcol.push_back(columns[i + a][m_binsPerOctave - b - 1]); + } + } + + processOctaveColumn(octave, tallcol); + } +} + +void +CQInverse::processOctaveColumn(int octave, const ComplexColumn &column) +{ + // 3. For each taller column, take the product with the inverse CQ + // kernel (which is the conjugate of the forward kernel) and + // perform an inverse FFT + + if ((int)column.size() != m_p.atomsPerFrame * m_binsPerOctave) { + cerr << "ERROR: CQInverse::processOctaveColumn: Height of column (" + << column.size() << ") in octave " << octave + << " must be atoms-per-frame * bins-per-octave (" + << m_p.atomsPerFrame << " * " << m_binsPerOctave << " = " + << m_p.atomsPerFrame * m_binsPerOctave << ")" << endl; + throw std::invalid_argument + ("Column height must match atoms-per-frame * bins-per-octave"); + } + + ComplexSequence transformed = m_kernel->processInverse(column); + + int halfLen = m_p.fftSize/2 + 1; + + RealSequence ri(halfLen, 0); + RealSequence ii(halfLen, 0); + + for (int i = 0; i < halfLen; ++i) { + ri[i] = transformed[i].real(); + ii[i] = transformed[i].imag(); + } + + RealSequence timeDomain(m_p.fftSize, 0); + + m_fft->inverse(ri.data(), ii.data(), timeDomain.data()); + + overlapAddAndResample(octave, timeDomain); +} + +void +CQInverse::overlapAddAndResample(int octave, const RealSequence &seq) +{ + // 4. Overlap-add each octave's resynthesised blocks (unwindowed) + // + // and + // + // 5. Resample each octave's overlap-add stream to the original + // rate + + if (seq.size() != m_olaBufs[octave].size()) { + cerr << "ERROR: CQInverse::overlapAdd: input sequence length (" + << seq.size() << ") is expected to match OLA buffer size (" + << m_olaBufs[octave].size() << ")" << endl; + throw std::invalid_argument + ("Input sequence length should match OLA buffer size"); + } + + RealSequence toResample(m_olaBufs[octave].begin(), + m_olaBufs[octave].begin() + m_p.fftHop); + + RealSequence resampled = + octave > 0 ? + m_upsamplers[octave]->process(toResample.data(), toResample.size()) : + toResample; + + m_buffers[octave].insert(m_buffers[octave].end(), + resampled.begin(), + resampled.end()); + + m_olaBufs[octave] = RealSequence(m_olaBufs[octave].begin() + m_p.fftHop, + m_olaBufs[octave].end()); + + RealSequence pad(m_p.fftHop, 0); + + m_olaBufs[octave].insert(m_olaBufs[octave].end(), + pad.begin(), + pad.end()); + + for (int i = 0; i < m_p.fftSize; ++i) { + m_olaBufs[octave][i] += seq[i]; + } +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cpp-qm-dsp/CQInverse.h Tue May 13 13:31:24 2014 +0100 @@ -0,0 +1,91 @@ +/* -*- 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. +*/ + +#ifndef CQINVERSE_H +#define CQINVERSE_H + +#include "CQBase.h" +#include "CQKernel.h" + +class Resampler; +class FFTReal; + +class CQInverse : public CQBase +{ +public: + CQInverse(double sampleRate, + double minFreq, double maxFreq, + int binsPerOctave); + virtual ~CQInverse(); + + virtual double getSampleRate() const { return m_sampleRate; } + virtual int getBinsPerOctave() const { return m_binsPerOctave; } + virtual int getOctaves() const { return m_octaves; } + virtual int getTotalBins() const { return m_octaves * m_binsPerOctave; } + virtual int getColumnHop() const { return m_p.fftHop / m_p.atomsPerFrame; } + virtual int getLatency() const { return m_outputLatency; } + virtual double getMaxFrequency() const { return m_p.maxFrequency; } + virtual double getMinFrequency() const; // actual min, not that passed to ctor + virtual double getBinFrequency(int bin) const; + + // Input is the format produced by ConstantQ class, + // i.e. uninterpolated complex, not the real-valued stuff produced + // by CQSpectrogram + + RealSequence process(const ComplexBlock &); + RealSequence getRemainingOutput(); + +private: + double m_sampleRate; + double m_maxFrequency; + double m_minFrequency; + int m_binsPerOctave; + int m_octaves; + + CQKernel *m_kernel; + CQKernel::Properties m_p; + + std::vector<Resampler *> m_upsamplers; + std::vector<RealSequence> m_buffers; + std::vector<RealSequence> m_olaBufs; // fixed-length, for overlap-add + + int m_outputLatency; + + FFTReal *m_fft; + + void initialise(); + void processOctave(int octave, const ComplexBlock &block); + void processOctaveColumn(int octave, const ComplexColumn &column); + void overlapAddAndResample(int octave, const RealSequence &); + RealSequence drawFromBuffers(); +}; + +#endif
--- a/cpp-qm-dsp/CQKernel.cpp Thu May 08 09:35:10 2014 +0100 +++ b/cpp-qm-dsp/CQKernel.cpp Tue May 13 13:31:24 2014 +0100 @@ -241,9 +241,10 @@ cerr << "weight = " << weight << endl; - // apply normalisation weight, make sparse, and store conjugates - // (our multiplication order means we will effectively be using - // the adjoint or conjugate transpose of the kernel matrix) + // 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; @@ -274,14 +275,14 @@ } vector<C> -CQKernel::process(const vector<C> &cv) +CQKernel::processForward(const vector<C> &cv) { - // matrix multiply m_kernel.data by in, converting in to complex - // as we go + // straightforward matrix multiply (taking into account m_kernel's + // slightly-sparse representation) int nrows = m_p.binsPerOctave * m_p.atomsPerFrame; - vector<C> rv(nrows, C(0, 0)); + vector<C> rv(nrows, C()); for (int i = 0; i < nrows; ++i) { for (int j = 0; j < (int)m_kernel.data[i].size(); ++j) { @@ -292,4 +293,28 @@ 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. + 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; +} + +
--- a/cpp-qm-dsp/CQKernel.h Thu May 08 09:35:10 2014 +0100 +++ b/cpp-qm-dsp/CQKernel.h Tue May 13 13:31:24 2014 +0100 @@ -59,7 +59,10 @@ Properties getProperties() const { return m_p; } - std::vector<std::complex<double> > process + std::vector<std::complex<double> > processForward + (const std::vector<std::complex<double> > &); + + std::vector<std::complex<double> > processInverse (const std::vector<std::complex<double> > &); private:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cpp-qm-dsp/CQSpectrogram.cpp Tue May 13 13:31:24 2014 +0100 @@ -0,0 +1,263 @@ +/* -*- 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 "CQSpectrogram.h" + +#include <iostream> +#include <stdexcept> + +using std::cerr; +using std::endl; + +CQSpectrogram::CQSpectrogram(double sampleRate, + double minFreq, double maxFreq, + int binsPerOctave, + Interpolation interpolation) : + m_cq(sampleRate, minFreq, maxFreq, binsPerOctave), + m_interpolation(interpolation) +{ +} + +CQSpectrogram::~CQSpectrogram() +{ +} + +CQSpectrogram::RealBlock +CQSpectrogram::process(const RealSequence &td) +{ + return postProcess(m_cq.process(td), false); +} + +CQSpectrogram::RealBlock +CQSpectrogram::getRemainingOutput() +{ + return postProcess(m_cq.getRemainingOutput(), true); +} + +CQSpectrogram::RealBlock +CQSpectrogram::postProcess(const ComplexBlock &cq, bool insist) +{ + int width = cq.size(); + + // convert to magnitudes + RealBlock spec; + for (int i = 0; i < width; ++i) { + int height = cq[i].size(); + RealColumn col(height, 0); + for (int j = 0; j < height; ++j) { + col[j] = abs(cq[i][j]); + } + spec.push_back(col); + } + + if (m_interpolation == InterpolateZeros) { + for (int i = 0; i < width; ++i) { + int sh = spec[i].size(); + int fh = getTotalBins(); + for (int j = sh; j < fh; ++j) { + spec[i].push_back(0); + } + } + return spec; + } + + for (int i = 0; i < width; ++i) { + m_buffer.push_back(spec[i]); + } + + if (m_interpolation == InterpolateHold) { + return fetchHold(insist); + } else { + return fetchLinear(insist); + } +} + +CQSpectrogram::RealBlock +CQSpectrogram::fetchHold(bool) +{ + RealBlock out; + + int width = m_buffer.size(); + int height = getTotalBins(); + + for (int i = 0; i < width; ++i) { + + RealColumn col = m_buffer[i]; + + int thisHeight = col.size(); + int prevHeight = m_prevColumn.size(); + + for (int j = thisHeight; j < height; ++j) { + if (j < prevHeight) { + col.push_back(m_prevColumn[j]); + } else { + col.push_back(0.0); + } + } + + m_prevColumn = col; + out.push_back(col); + } + + m_buffer.clear(); + + return out; +} + +CQSpectrogram::RealBlock +CQSpectrogram::fetchLinear(bool insist) +{ + RealBlock out; + + //!!! This is surprisingly messy. I must be missing something. + + // We can only return any data when we have at least one column + // that has the full height in the buffer, that is not the first + // column. + // + // If the first col has full height, and there is another one + // later that also does, then we can interpolate between those, up + // to but not including the second full height column. Then we + // drop and return the columns we interpolated, leaving the second + // full-height col as the first col in the buffer. And repeat as + // long as enough columns are available. + // + // If the first col does not have full height, then (so long as + // we're following the logic above) we must simply have not yet + // reached the first full-height column in the CQ output, and we + // can interpolate nothing. + + int width = m_buffer.size(); + int height = getTotalBins(); + + if (width == 0) return out; + + int firstFullHeight = -1; + int secondFullHeight = -1; + + for (int i = 0; i < width; ++i) { + if ((int)m_buffer[i].size() == height) { + if (firstFullHeight == -1) { + firstFullHeight = i; + } else if (secondFullHeight == -1) { + secondFullHeight = i; + break; + } + } + } + + if (firstFullHeight < 0) { + if (insist) { + out = m_buffer; + m_buffer.clear(); + return out; + } else { + return out; + } + } else if (firstFullHeight > 0) { + // can interpolate nothing, stash up to first full height & recurse + out = RealBlock(m_buffer.begin(), m_buffer.begin() + firstFullHeight); + m_buffer = RealBlock(m_buffer.begin() + firstFullHeight, m_buffer.end()); + RealBlock more = fetchLinear(insist); + out.insert(out.end(), more.begin(), more.end()); + return out; + } else if (secondFullHeight < 0) { + // firstFullHeight == 0, but there is no second full height -- + // wait for it unless insist flag is set + if (insist) { + out = m_buffer; + m_buffer.clear(); + return out; + } else { + return out; + } + } else { + // firstFullHeight == 0 and secondFullHeight also valid. Can interpolate + out = linearInterpolated(m_buffer, 0, secondFullHeight); + m_buffer = RealBlock(m_buffer.begin() + secondFullHeight, m_buffer.end()); + RealBlock more = fetchLinear(insist); + out.insert(out.end(), more.begin(), more.end()); + return out; + } +} + +CQSpectrogram::RealBlock +CQSpectrogram::linearInterpolated(const RealBlock &g, int x0, int x1) +{ + // g must be a grid with full-height columns at x0 and x1 + + if (x0 >= x1) { + throw std::logic_error("x0 >= x1"); + } + if (x1 >= (int)g.size()) { + throw std::logic_error("x1 >= g.size()"); + } + if (g[x0].size() != g[x1].size()) { + throw std::logic_error("x0 and x1 are not the same height"); + } + + int height = g[x0].size(); + int width = x1 - x0; + + RealBlock out(g.begin() + x0, g.begin() + x1); + + for (int y = 0; y < height; ++y) { + + int spacing = width; + for (int i = 1; i < width; ++i) { + int thisHeight = g[x0 + i].size(); + if (thisHeight > height) { + throw std::logic_error("First column not full-height"); + } + if (thisHeight > y) { + spacing = i; + break; + } + } + + if (spacing < 2) continue; + + for (int i = 0; i + spacing <= width; i += spacing) { + for (int j = 1; j < spacing; ++j) { + double proportion = double(j)/double(spacing); + double interpolated = + g[x0 + i][y] * (1.0 - proportion) + + g[x0 + i + spacing][y] * proportion; + out[i + j].push_back(interpolated); + } + } + } + + return out; +} + + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cpp-qm-dsp/CQSpectrogram.h Tue May 13 13:31:24 2014 +0100 @@ -0,0 +1,77 @@ +/* -*- 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. +*/ + +#ifndef CQSPECTROGRAM_H +#define CQSPECTROGRAM_H + +#include "ConstantQ.h" + +class CQSpectrogram : public CQBase +{ +public: + enum Interpolation { + InterpolateZeros, // leave empty cells as zero + InterpolateHold, // repeat prior cell + InterpolateLinear, // linear interpolation between consecutive time cells + }; + + CQSpectrogram(double sampleRate, + double minFreq, double maxFreq, + int binsPerOctave, + Interpolation interpolation); + virtual ~CQSpectrogram(); + + virtual double getSampleRate() const { return m_cq.getSampleRate(); } + virtual int getBinsPerOctave() const { return m_cq.getBinsPerOctave(); } + virtual int getOctaves() const { return m_cq.getOctaves(); } + virtual int getTotalBins() const { return m_cq.getTotalBins(); } + virtual int getColumnHop() const { return m_cq.getColumnHop(); } + virtual int getLatency() const { return m_cq.getLatency(); } + virtual double getMaxFrequency() const { return m_cq.getMaxFrequency(); } + virtual double getMinFrequency() const { return m_cq.getMinFrequency(); } + virtual double getBinFrequency(int bin) const { return m_cq.getBinFrequency(bin); } + + RealBlock process(const RealSequence &); + RealBlock getRemainingOutput(); + +private: + ConstantQ m_cq; + Interpolation m_interpolation; + + RealBlock m_buffer; + RealBlock postProcess(const ComplexBlock &, bool insist); + RealBlock fetchHold(bool insist); + RealBlock fetchLinear(bool insist); + RealBlock linearInterpolated(const RealBlock &, int, int); + RealColumn m_prevColumn; +}; + +#endif
--- a/cpp-qm-dsp/ConstantQ.cpp Thu May 08 09:35:10 2014 +0100 +++ b/cpp-qm-dsp/ConstantQ.cpp Tue May 13 13:31:24 2014 +0100 @@ -38,16 +38,14 @@ #include "dsp/transforms/FFT.h" #include <algorithm> -#include <complex> #include <iostream> #include <stdexcept> using std::vector; -using std::complex; using std::cerr; using std::endl; -typedef std::complex<double> C; +//#define DEBUG_CQ 1 ConstantQ::ConstantQ(double sampleRate, double minFreq, @@ -114,6 +112,10 @@ Resampler *r = new Resampler (sourceRate, sourceRate / factor, 60, 0.02); +#ifdef DEBUG_CQ + cerr << "forward: octave " << i << ": resample from " << sourceRate << " to " << sourceRate / factor << endl; +#endif + // We need to adapt the latencies so as to get the first input // sample to be aligned, in time, at the decimator output // across all octaves. @@ -172,26 +174,37 @@ if (latPlusDrop > maxLatPlusDrop) maxLatPlusDrop = latPlusDrop; } - // we want to design totalLatency such that totalLatency - - // latencies[0] - drops[0] is a multiple of m_p.fftHop, so that we - // can get identical results in octave 0 to our reference - // implementation, making for easier testing (though other octaves - // will differ because of different resampler implementations) + int totalLatency = maxLatPlusDrop; - int totalLatency = maxLatPlusDrop; int lat0 = totalLatency - latencies[0] - drops[0]; totalLatency = ceil(double(lat0 / m_p.fftHop) * m_p.fftHop) + latencies[0] + drops[0]; -// cerr << "total latency = " << totalLatency << endl; + // We want (totalLatency - latencies[i]) to be a multiple of 2^i + // for each octave i, so that we do not end up with fractional + // octave latencies below. In theory this is hard, in practice if + // we ensure it for the last octave we should be OK. + double finalOctLat = latencies[m_octaves-1]; + double finalOctFact = pow(2, m_octaves-1); + totalLatency = + int(round(finalOctLat + + finalOctFact * + ceil((totalLatency - finalOctLat) / finalOctFact))); + +#ifdef DEBUG_CQ + cerr << "total latency = " << totalLatency << endl; +#endif // Padding as in the reference (will be introduced with the // latency compensation in the loop below) m_outputLatency = totalLatency + m_bigBlockSize - m_p.firstCentre * pow(2, m_octaves-1); -// cerr << "m_bigBlockSize = " << m_bigBlockSize << ", firstCentre = " -// << m_p.firstCentre << ", m_octaves = " << m_octaves << ", so m_outputLatency = " << m_outputLatency << endl; +#ifdef DEBUG_CQ + cerr << "m_bigBlockSize = " << m_bigBlockSize << ", firstCentre = " + << m_p.firstCentre << ", m_octaves = " << m_octaves + << ", so m_outputLatency = " << m_outputLatency << endl; +#endif for (int i = 0; i < m_octaves; ++i) { @@ -208,24 +221,39 @@ double(totalLatency - latencies[i] - drops[i] + m_bigBlockSize) / factor; +#ifdef DEBUG_CQ + cerr << "octave " << i << ": resampler latency = " << latencies[i] + << ", drop " << drops[i] << " (/factor = " << drops[i]/factor + << "), octaveLatency = " << octaveLatency << " -> " + << int(round(octaveLatency)) << " (diff * factor = " + << (octaveLatency - round(octaveLatency)) << " * " + << factor << " = " + << (octaveLatency - round(octaveLatency)) * factor << ")" << endl; + + cerr << "double(" << totalLatency << " - " + << latencies[i] << " - " << drops[i] << " + " + << m_bigBlockSize << ") / " << factor << " = " + << octaveLatency << endl; +#endif + m_buffers.push_back - (vector<double>(int(round(octaveLatency)), 0.0)); + (RealSequence(int(round(octaveLatency)), 0.0)); } m_fft = new FFTReal(m_p.fftSize); } -vector<vector<double> > -ConstantQ::process(const vector<double> &td) +ConstantQ::ComplexBlock +ConstantQ::process(const RealSequence &td) { m_buffers[0].insert(m_buffers[0].end(), td.begin(), td.end()); for (int i = 1; i < m_octaves; ++i) { - vector<double> dec = m_decimators[i]->process(td.data(), td.size()); + RealSequence dec = m_decimators[i]->process(td.data(), td.size()); m_buffers[i].insert(m_buffers[i].end(), dec.begin(), dec.end()); } - vector<vector<double> > out; + ComplexBlock out; while (true) { @@ -236,8 +264,6 @@ bool enough = true; for (int i = 0; i < m_octaves; ++i) { int required = m_p.fftSize * pow(2, m_octaves - i - 1); -// cerr << "for octave " << i << ", buf len = "<< m_buffers[i].size() << " (need " << required << ")" << endl; - if ((int)m_buffers[i].size() < required) { enough = false; } @@ -247,7 +273,7 @@ int base = out.size(); int totalColumns = pow(2, m_octaves - 1) * m_p.atomsPerFrame; for (int i = 0; i < totalColumns; ++i) { - out.push_back(vector<double>()); + out.push_back(ComplexColumn()); } for (int octave = 0; octave < m_octaves; ++octave) { @@ -255,7 +281,7 @@ int blocksThisOctave = pow(2, (m_octaves - octave - 1)); for (int b = 0; b < blocksThisOctave; ++b) { - vector<vector<double> > block = processOctaveBlock(octave); + ComplexBlock block = processOctaveBlock(octave); for (int j = 0; j < m_p.atomsPerFrame; ++j) { @@ -266,7 +292,7 @@ while (int(out[target].size()) < m_p.binsPerOctave * (octave + 1)) { - out[target].push_back(0.0); + out[target].push_back(Complex()); } for (int i = 0; i < m_p.binsPerOctave; ++i) { @@ -281,42 +307,42 @@ return out; } -vector<vector<double> > -ConstantQ::getRemainingBlocks() +ConstantQ::ComplexBlock +ConstantQ::getRemainingOutput() { // Same as padding added at start, though rounded up int pad = ceil(double(m_outputLatency) / m_bigBlockSize) * m_bigBlockSize; - vector<double> zeros(pad, 0.0); + RealSequence zeros(pad, 0.0); return process(zeros); } -vector<vector<double> > +ConstantQ::ComplexBlock ConstantQ::processOctaveBlock(int octave) { - vector<double> ro(m_p.fftSize, 0.0); - vector<double> io(m_p.fftSize, 0.0); + RealSequence ro(m_p.fftSize, 0.0); + RealSequence io(m_p.fftSize, 0.0); m_fft->forward(m_buffers[octave].data(), ro.data(), io.data()); - vector<double> shifted; + RealSequence shifted; shifted.insert(shifted.end(), m_buffers[octave].begin() + m_p.fftHop, m_buffers[octave].end()); m_buffers[octave] = shifted; - vector<C> cv; + ComplexSequence cv; for (int i = 0; i < m_p.fftSize; ++i) { - cv.push_back(C(ro[i], io[i])); + cv.push_back(Complex(ro[i], io[i])); } - vector<C> cqrowvec = m_kernel->process(cv); + ComplexSequence cqrowvec = m_kernel->processForward(cv); - // Reform into a column matrix and use only the magnitude - vector<vector<double> > cqblock; + // Reform into a column matrix + ComplexBlock cqblock; for (int j = 0; j < m_p.atomsPerFrame; ++j) { - cqblock.push_back(vector<double>()); + cqblock.push_back(ComplexColumn()); for (int i = 0; i < m_p.binsPerOctave; ++i) { - cqblock[j].push_back(abs(cqrowvec[i * m_p.atomsPerFrame + j])); + cqblock[j].push_back(cqrowvec[i * m_p.atomsPerFrame + j]); } }
--- a/cpp-qm-dsp/ConstantQ.h Thu May 08 09:35:10 2014 +0100 +++ b/cpp-qm-dsp/ConstantQ.h Tue May 13 13:31:24 2014 +0100 @@ -32,33 +32,64 @@ #ifndef CONSTANTQ_H #define CONSTANTQ_H +#include "CQBase.h" #include "CQKernel.h" -#include <vector> - class Resampler; class FFTReal; -class ConstantQ +/** + * Calculate a complex sparse constant-Q representation from + * time-domain input. + * + * For a real (magnitude-only) or interpolated representation, see + * CQSpectrogram. + */ +class ConstantQ : public CQBase { public: ConstantQ(double sampleRate, double minFreq, double maxFreq, int binsPerOctave); - ~ConstantQ(); + virtual ~ConstantQ(); - double getSampleRate() const { return m_sampleRate; } - int getBinsPerOctave() const { return m_binsPerOctave; } - int getOctaves() const { return m_octaves; } - int getTotalBins() const { return m_octaves * m_binsPerOctave; } - int getColumnHop() const { return m_p.fftHop / m_p.atomsPerFrame; } - int getLatency() const { return m_outputLatency; } - double getMaxFrequency() const { return m_p.maxFrequency; } - double getMinFrequency() const; // actual min, not that passed to ctor - double getBinFrequency(int bin) const; + virtual double getSampleRate() const { return m_sampleRate; } + virtual int getBinsPerOctave() const { return m_binsPerOctave; } + virtual int getOctaves() const { return m_octaves; } + virtual int getTotalBins() const { return m_octaves * m_binsPerOctave; } + virtual int getColumnHop() const { return m_p.fftHop / m_p.atomsPerFrame; } + virtual int getLatency() const { return m_outputLatency; } + virtual double getMaxFrequency() const { return m_p.maxFrequency; } + virtual double getMinFrequency() const; + virtual double getBinFrequency(int bin) const; - std::vector<std::vector<double> > process(const std::vector<double> &); - std::vector<std::vector<double> > getRemainingBlocks(); + /** + * Given a series of time-domain samples, return a series of + * constant-Q columns. Any samples left over (that did not fit + * into a constant-Q processing block) are saved for the next call + * to process or getRemainingBlocks. + * + * Each column contains a series of constant-Q bin values ordered + * from highest to lowest frequency. + * + * Columns are of variable height: each will contain at least + * getBinsPerOctave() values, because the highest-frequency octave + * is always present, but a second octave (if requested) will + * appear only in alternate columns, a third octave only in every + * fourth column, and so on. + * + * If you need a format in which all columns are of equal height + * and every bin contains a value, use CQInterpolated instead of + * ConstantQ. + */ + ComplexBlock process(const RealSequence &); + + /** + * Return the remaining constant-Q columns following the end of + * processing. Any buffered input is padded so as to ensure that + * all input provided to process() will have been returned. + */ + ComplexBlock getRemainingOutput(); private: double m_sampleRate; @@ -72,14 +103,14 @@ int m_bigBlockSize; std::vector<Resampler *> m_decimators; - std::vector<std::vector<double> > m_buffers; + std::vector<RealSequence> m_buffers; int m_outputLatency; FFTReal *m_fft; void initialise(); - std::vector<std::vector<double> > processOctaveBlock(int octave); + ComplexBlock processOctaveBlock(int octave); }; #endif
--- a/cpp-qm-dsp/Makefile Thu May 08 09:35:10 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,38 +0,0 @@ - -DEFINES := -DUSE_PTHREADS - -CFLAGS := -I../.. $(CFLAGS) $(DEFINES) - -#CXXFLAGS := -I../.. -fPIC -Wall -g $(CXXFLAGS) $(DEFINES) -CXXFLAGS := -I../.. -fPIC -Wall -O3 -ffast-math -ftree-vectorize $(CXXFLAGS) $(DEFINES) - -LDFLAGS := $(LDFLAGS) - -#VG := valgrind - -LIBS := ../../qm-dsp/libqm-dsp.a -lpthread -PROGRAM_LIBS := -lsndfile - -SOURCES := CQKernel.cpp ConstantQ.cpp test.cpp - -OBJECTS := $(SOURCES:.cpp=.o) -OBJECTS := $(OBJECTS:.c=.o) - -PROGRAM := test - -all: $(PROGRAM) - -test: $(OBJECTS) - $(CXX) -o $@ $^ $(LDFLAGS) $(LIBS) $(PROGRAM_LIBS) - -clean: - rm -f *.o - -depend: - makedepend -Y $(SOURCES) - -# DO NOT DELETE - -CQKernel.o: CQKernel.h -ConstantQ.o: ConstantQ.h CQKernel.h -test.o: ConstantQ.h CQKernel.h
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cpp-qm-dsp/processfile.cpp Tue May 13 13:31:24 2014 +0100 @@ -0,0 +1,218 @@ + +#include "ConstantQ.h" +#include "CQInverse.h" + +#include <sndfile.h> + +#include <iostream> + +using std::vector; +using std::cerr; +using std::endl; + +#include <cstring> + +#include <unistd.h> +#include <sys/time.h> + +int main(int argc, char **argv) +{ + if (argc < 3 || argc > 4) { + cerr << "Usage: " << argv[0] << " infile.wav outfile.wav [differencefile.wav]" << endl; + return 2; + } + + char *fileName = strdup(argv[1]); + char *fileNameOut = strdup(argv[2]); + char *diffFileName = (argc == 4 ? strdup(argv[3]) : 0); + bool doDiff = (diffFileName != 0); + + SNDFILE *sndfile; + SNDFILE *sndfileOut; + SNDFILE *sndDiffFile = 0; + SF_INFO sfinfo; + SF_INFO sfinfoOut; + SF_INFO sfinfoDiff; + memset(&sfinfo, 0, sizeof(SF_INFO)); + + sndfile = sf_open(fileName, SFM_READ, &sfinfo); + if (!sndfile) { + cerr << "ERROR: Failed to open input file \"" << fileName << "\": " + << sf_strerror(sndfile) << endl; + return 1; + } + + sfinfoOut.channels = 1; + sfinfoOut.format = SF_FORMAT_WAV | SF_FORMAT_PCM_16; + sfinfoOut.frames = sfinfo.frames; + sfinfoOut.samplerate = sfinfo.samplerate; + sfinfoOut.sections = sfinfo.sections; + sfinfoOut.seekable = sfinfo.seekable; + + sndfileOut = sf_open(fileNameOut, SFM_WRITE, &sfinfoOut) ; + if (!sndfileOut) { + cerr << "ERROR: Failed to open output file \"" << fileNameOut << "\" for writing: " + << sf_strerror(sndfileOut) << endl; + return 1; + } + + if (doDiff) { + sfinfoDiff = sfinfoOut; + sndDiffFile = sf_open(diffFileName, SFM_WRITE, &sfinfoDiff); + if (!sndDiffFile) { + cerr << "ERROR: Failed to open diff output file \"" << diffFileName << "\" for writing: " + << sf_strerror(sndDiffFile) << endl; + return 1; + } + } + + int ibs = 1024; + int channels = sfinfo.channels; + float *fbuf = new float[channels * ibs]; + + ConstantQ cq(sfinfo.samplerate, + 100, sfinfo.samplerate / 3, + 60); + + CQInverse cqi(sfinfo.samplerate, + 100, sfinfo.samplerate / 3, + 60); + + cerr << "max freq = " << cq.getMaxFrequency() << ", min freq = " + << cq.getMinFrequency() << ", octaves = " << cq.getOctaves() << endl; + + cerr << "octave boundaries: "; + for (int i = 0; i < cq.getOctaves(); ++i) { + cerr << cq.getMaxFrequency() / pow(2, i) << " "; + } + cerr << endl; + + int inframe = 0; + int outframe = 0; + int latency = cq.getLatency() + cqi.getLatency(); + + vector<double> buffer; + + cerr << "forward latency = " << cq.getLatency() << ", inverse latency = " + << cqi.getLatency() << ", total = " << latency << endl; + + timeval tv; + (void)gettimeofday(&tv, 0); + + while (inframe < sfinfo.frames) { + + int count = -1; + + if ((count = sf_readf_float(sndfile, fbuf, ibs)) < 0) { + break; + } + + vector<double> cqin; + for (int i = 0; i < count; ++i) { + double v = fbuf[i * channels]; + if (channels > 1) { + for (int c = 1; c < channels; ++c) { + v += fbuf[i * channels + c]; + } + v /= channels; + } + cqin.push_back(v); + } + + if (doDiff) { + buffer.insert(buffer.end(), cqin.begin(), cqin.end()); + } + + vector<double> cqout = cqi.process(cq.process(cqin)); + + for (int i = 0; i < int(cqout.size()); ++i) { + if (cqout[i] > 1.0) cqout[i] = 1.0; + if (cqout[i] < -1.0) cqout[i] = -1.0; + } + + if (outframe >= latency) { + + sf_writef_double(sndfileOut, + cqout.data(), + cqout.size()); + + } else if (outframe + (int)cqout.size() >= latency) { + + int offset = latency - outframe; + sf_writef_double(sndfileOut, + cqout.data() + offset, + cqout.size() - offset); + } + + if (doDiff) { + for (int i = 0; i < (int)cqout.size(); ++i) { + if (outframe + i >= latency) { + cqout[i] -= buffer[outframe + i - latency]; + } + } + + if (outframe >= latency) { + + sf_writef_double(sndDiffFile, + cqout.data(), + cqout.size()); + + } else if (outframe + (int)cqout.size() >= latency) { + + int offset = latency - outframe; + sf_writef_double(sndDiffFile, + cqout.data() + offset, + cqout.size() - offset); + } + } + + inframe += count; + outframe += cqout.size(); + } + + vector<double> r = cqi.process(cq.getRemainingOutput()); + vector<double> r2 = cqi.getRemainingOutput(); + + r.insert(r.end(), r2.begin(), r2.end()); + + for (int i = 0; i < int(r.size()); ++i) { + if (r[i] > 1.0) r[i] = 1.0; + if (r[i] < -1.0) r[i] = -1.0; + } + + sf_writef_double(sndfileOut, r.data(), r.size()); + if (doDiff) { + for (int i = 0; i < (int)r.size(); ++i) { + if (outframe + i >= latency) { + r[i] -= buffer[outframe + i - latency]; + } + } + sf_writef_double(sndDiffFile, r.data(), r.size()); + } + outframe += r.size(); + + sf_close(sndfile); + sf_close(sndfileOut); + + if (doDiff) { + sf_close(sndDiffFile); + } + + cerr << "in: " << inframe << ", out: " << outframe - latency << endl; + + timeval etv; + (void)gettimeofday(&etv, 0); + + etv.tv_sec -= tv.tv_sec; + if (etv.tv_usec < tv.tv_usec) { + etv.tv_usec += 1000000; + etv.tv_sec -= 1; + } + etv.tv_usec -= tv.tv_usec; + + double sec = double(etv.tv_sec) + (double(etv.tv_usec) / 1000000.0); + cerr << "elapsed time (not counting init): " << sec << " sec, frames/sec at input: " << inframe/sec << endl; + + return 0; +} +
--- a/cpp-qm-dsp/test.cpp Thu May 08 09:35:10 2014 +0100 +++ b/cpp-qm-dsp/test.cpp Tue May 13 13:31:24 2014 +0100 @@ -28,7 +28,7 @@ authorization. */ -#include "ConstantQ.h" +#include "CQSpectrogram.h" #include <iostream> #include <vector> @@ -50,10 +50,10 @@ in.push_back(sin(i * M_PI / 2.0)); } - ConstantQ k(8, 1, 4, 4); + CQSpectrogram k(8, 1, 4, 4, CQSpectrogram::InterpolateZeros); vector<vector<double> > out = k.process(in); - vector<vector<double> > rest = k.getRemainingBlocks(); + vector<vector<double> > rest = k.getRemainingOutput(); out.insert(out.end(), rest.begin(), rest.end());
--- a/vamp/CQVamp.cpp Thu May 08 09:35:10 2014 +0100 +++ b/vamp/CQVamp.cpp Tue May 13 13:31:24 2014 +0100 @@ -47,7 +47,7 @@ m_maxMIDIPitch(84), m_tuningFrequency(440), m_bpo(24), - m_interpolation(CQInterpolated::Linear), + m_interpolation(CQSpectrogram::InterpolateLinear), m_cq(0), m_maxFrequency(inputSampleRate/2), m_minFrequency(46), @@ -155,7 +155,7 @@ desc.defaultValue = 2; desc.isQuantized = true; desc.quantizeStep = 1; - desc.valueNames.push_back("None, leave empty"); + desc.valueNames.push_back("None, leave as zero"); desc.valueNames.push_back("None, repeat prior value"); desc.valueNames.push_back("Linear interpolation"); list.push_back(desc); @@ -198,7 +198,7 @@ } else if (param == "bpo") { m_bpo = lrintf(value); } else if (param == "interpolation") { - m_interpolation = (CQInterpolated::Interpolation)lrintf(value); + m_interpolation = (CQSpectrogram::Interpolation)lrintf(value); } else { std::cerr << "WARNING: CQVamp::setParameter: unknown parameter \"" << param << "\"" << std::endl; @@ -224,7 +224,7 @@ m_maxFrequency = Pitch::getFrequencyForPitch (m_maxMIDIPitch, 0, m_tuningFrequency); - m_cq = new CQInterpolated + m_cq = new CQSpectrogram (m_inputSampleRate, m_minFrequency, m_maxFrequency, m_bpo, m_interpolation); @@ -236,7 +236,7 @@ { if (m_cq) { delete m_cq; - m_cq = new CQInterpolated + m_cq = new CQSpectrogram (m_inputSampleRate, m_minFrequency, m_maxFrequency, m_bpo, m_interpolation); } @@ -272,7 +272,7 @@ if (m_cq) { char name[20]; - for (int i = 0; i < d.binCount; ++i) { + for (int i = 0; i < (int)d.binCount; ++i) { float freq = m_cq->getBinFrequency(i); sprintf(name, "%.1f Hz", freq); d.binNames.push_back(name); @@ -314,7 +314,7 @@ CQVamp::FeatureSet CQVamp::getRemainingFeatures() { - vector<vector<double> > cqout = m_cq->getRemainingBlocks(); + vector<vector<double> > cqout = m_cq->getRemainingOutput(); return convertToFeatures(cqout); }
--- a/vamp/CQVamp.h Thu May 08 09:35:10 2014 +0100 +++ b/vamp/CQVamp.h Tue May 13 13:31:24 2014 +0100 @@ -34,7 +34,7 @@ #include <vamp-sdk/Plugin.h> -#include "cpp-qm-dsp/CQInterpolated.h" +#include "cpp-qm-dsp/CQSpectrogram.h" class ConstantQ; @@ -75,9 +75,9 @@ int m_maxMIDIPitch; float m_tuningFrequency; int m_bpo; - CQInterpolated::Interpolation m_interpolation; + CQSpectrogram::Interpolation m_interpolation; - CQInterpolated *m_cq; + CQSpectrogram *m_cq; float m_maxFrequency; float m_minFrequency; int m_stepSize;