diff --git a/src/algorithms/standard/framebuffer.cpp b/src/algorithms/standard/framebuffer.cpp new file mode 100644 index 000000000..ceb1df760 --- /dev/null +++ b/src/algorithms/standard/framebuffer.cpp @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2006-2021 Music Technology Group - Universitat Pompeu Fabra + * + * This file is part of Essentia + * + * Essentia is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the Free + * Software Foundation (FSF), either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the Affero GNU General Public License + * version 3 along with this program. If not, see http://www.gnu.org/licenses/ + */ + +#include "framebuffer.h" +//#include +#include + +using namespace std; +using namespace essentia; +using namespace standard; + + +const char* FrameBuffer::name = "FrameBuffer"; +const char* FrameBuffer::category = "Standard"; +const char* FrameBuffer::description = DOC( +"This algorithm buffers input non-overlapping audio frames into longer overlapping frames with a hop sizes equal to input frame size.\n\n" +"In standard mode, each compute() call updates and outputs the gathered buffer.\n\n" +"Input frames can be of variate length. Input frames longer than the buffer size will be cropped. Empty input frames will raise an exception." +); + + +void FrameBuffer::configure() { + _bufferSize = parameter("bufferSize").toInt(); + _zeroPadding = parameter("zeroPadding").toBool(); + _buffer.resize(_bufferSize); + reset(); +} + +void FrameBuffer::reset() { + if (_zeroPadding) { + std::fill(_buffer.begin(), _buffer.end(), (Real) 0.); + _bufferUndefined = 0; + } + else { + _bufferUndefined = _bufferSize; + } +} + +void FrameBuffer::compute() { + const vector& frame = _frame.get(); + vector& bufferedFrame = _bufferedFrame.get(); + + if (frame.empty()) throw EssentiaException("FrameBuffer: the input frame is empty"); + + int shift = (int) frame.size(); + + if (shift >= _bufferSize) { + // Overwrite the entire buffer. + std::copy(frame.end() - _bufferSize, frame.end(), _buffer.begin()); + _bufferUndefined = 0; + // TODO E_WARNING for the case of shift > _bufferSize (not all input values fit the buffer) + } + else { + std::copy(_buffer.begin() + shift, _buffer.end(), _buffer.begin()); + std::copy(frame.begin(), frame.end(), _buffer.begin() + _bufferSize - shift); + if (_bufferUndefined) { + _bufferUndefined -= shift; + if (_bufferUndefined < 0) { + _bufferUndefined = 0; + } + } + } + + // output + if (!_bufferUndefined) { + bufferedFrame.resize(_bufferSize); + std::copy(_buffer.begin(), _buffer.end(), bufferedFrame.begin()); + } + else { + // Return emtpy frames until a full buffer is available. + bufferedFrame.clear(); + } +} diff --git a/src/algorithms/standard/framebuffer.h b/src/algorithms/standard/framebuffer.h new file mode 100644 index 000000000..f8f93efa8 --- /dev/null +++ b/src/algorithms/standard/framebuffer.h @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2006-2021 Music Technology Group - Universitat Pompeu Fabra + * + * This file is part of Essentia + * + * Essentia is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the Free + * Software Foundation (FSF), either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the Affero GNU General Public License + * version 3 along with this program. If not, see http://www.gnu.org/licenses/ + */ + +#ifndef ESSENTIA_FRAMEBUFFER_H +#define ESSENTIA_FRAMEBUFFER_H + +#include "algorithm.h" + +namespace essentia { +namespace standard { + +class FrameBuffer : public Algorithm { + + private: + Input > _frame; + Output > _bufferedFrame; + + std::vector _buffer; + int _bufferSize; + bool _zeroPadding; + int _bufferUndefined; // Number of undefined values in the buffer (= buffer size for the empty buffer on reset). + + public: + FrameBuffer() { + declareInput(_frame, "frame", "the input audio frame"); + declareOutput(_bufferedFrame, "frame", "the buffered audio frame"); + } + + void declareParameters() { + declareParameter("bufferSize", "the buffer size", "(0,inf)", 2048); + declareParameter("zeroPadding", "initialize the buffer with zeros (output zero-padded buffer frames if `true`, otherwise output empty frames until a full buffer is accumulated)", "{true,false}", true); + } + void compute(); + void configure(); + void reset(); + + static const char* name; + static const char* category; + static const char* description; + +}; + +} // namespace standard +} // namespace essentia + + +#include "streamingalgorithmwrapper.h" + +namespace essentia { +namespace streaming { + +class FrameBuffer : public StreamingAlgorithmWrapper { + + protected: + + Sink > _frame; + Source > _bufferedFrame; + + public: + FrameBuffer() { + declareAlgorithm("FrameBuffer"); + declareInput(_frame, TOKEN,"frame"); + declareOutput(_bufferedFrame, TOKEN, "frame"); + } +}; + +} // namespace streaming +} // namespace essentia + +#endif // ESSENTIA_FRAMEBUFFER_H diff --git a/src/algorithms/tonal/pitchyinfft.cpp b/src/algorithms/tonal/pitchyinfft.cpp index 5defae5f8..9be05b8ed 100644 --- a/src/algorithms/tonal/pitchyinfft.cpp +++ b/src/algorithms/tonal/pitchyinfft.cpp @@ -30,10 +30,38 @@ static const Real _freqsMask[] = {0., 20., 25., 31.5, 40., 50., 63., 80., 1600., 2000., 2500., 3150., 4000., 5000., 6300., 8000., 9000., 10000., 12500., 15000., 20000., 25100}; -static const Real _weightMask[] = {-75.8, -70.1, -60.8, -52.1, -44.2, -37.5, +static Real _weightMask[] = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; + +static const Real _weights[] = {-75.8, -70.1, -60.8, -52.1, -44.2, -37.5, -31.3, -25.6, -20.9, -16.5, -12.6, -9.6, -7.0, -4.7, -3.0, -1.8, -0.8, -0.2, -0.0, 0.5, 1.6, 3.2, 5.4, 7.8, 8.1, 5.3, -2.4, -11.1, -12.8, - -12.2, -7.4, -17.8, -17.8, -17.8}; + -12.2, -7.4, -17.8, -17.8, -17.8}; // by default the original one is selected + +static const Real _aWeighting[] = {-148.6, -50.4, -44.8, -39.5, -34.5, -30.3, + -26.2, -22.4, -19.1, -16.2, -13.2, -10.8, -8.7, -6.6, -4.8, -3.2, -1.9, + -0.8, 0.0, 0.6, 1.0, 1.2, 1.3, 1.2, 1.0, 0.6, -0.1, -1.1, -1.8, -2.5, + -4.3, -6.0, -9.3, -12.4}; + +static const Real _bWeighting[] = {-96.4, -24.2, -20.5, -17.1, -14.1, -11.6, + -9.4, -7.3, -5.6, -4.2, -2.9, -2.0, -1.4, -0.9, -0.5, -0.3, -0.1, -0.0, + 0.0, 0.0, -0.0, -0.1, -0.2, -0.4, -0.7, -1.2, -1.9, -2.9, -3.6, -4.3, + -6.1, -7.8, -11.2, -14.2}; + +static const Real _cWeighting[] = {-52.5, -6.2, -4.4, -3.0, -2.0, -1.3, -0.8, + -0.5, -0.3, -0.2, -0.1, -0.0, -0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.0, + -0.1, -0.2, -0.3, -0.5, -0.8, -1.3, -2.0, -3.0, -3.7, -4.4, -6.2, + -7.9, -11.3, -14.3}; + +static const Real _dWeighting[] = {-46.6, -20.6, -18.7, -16.7, -14.7, -12.8, + -10.9, -8.9, -7.2, -5.6, -3.9, -2.6, -1.6, -0.8, -0.4, -0.3, -0.5, -0.6, + 0.0, 1.9, 5.0, 7.9, 10.3, 11.5, 11.1, 9.6, 7.6, 5.5, 4.4, 3.4, 1.4, + -0.2, -2.7, -4.7}; + +static const Real _zWeighting[] = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; const char* PitchYinFFT::name = "PitchYinFFT"; const char* PitchYinFFT::category = "Pitch"; @@ -56,13 +84,19 @@ void PitchYinFFT::configure() { _sampleRate = parameter("sampleRate").toReal(); _interpolate = parameter("interpolate").toBool(); _tolerance = parameter("tolerance").toReal(); + _weighting = parameter("weighting").toString(); _sqrMag.resize(_frameSize); _weight.resize(_frameSize/2+1); _yin.resize(_frameSize/2+1); // configure algorithms _fft->configure("size", _frameSize); + + if (_weighting != "default" && _weighting != "A" && _weighting != "B" && _weighting != "C" && _weighting != "D" && _weighting != "Z") { + E_INFO("PitchYinFFT: 'weighting' = "<<_weighting<<"\n"); + throw EssentiaException("PitchYinFFT: Bad 'weighting' parameter"); + } // allocate memory - spectralWeights(); + spectralWeights(_weighting); _tauMax = min(int(ceil(_sampleRate / parameter("minFrequency").toReal())), _frameSize/2); _tauMin = min(int(floor(_sampleRate / parameter("maxFrequency").toReal())), _frameSize/2); @@ -80,9 +114,31 @@ void PitchYinFFT::configure() { "orderBy", "amplitude"); } -void PitchYinFFT::spectralWeights() { +void PitchYinFFT::spectralWeights(std::string weighting) { int i = 0, j = 1; Real freq = 0, a0 = 0, a1 = 0, f0 = 0, f1 = 0; + int _maskSize = 34; + if (weighting == "default") { + for (int n=0; n<_maskSize; n++) + _weightMask[n] = _weights[n]; + } + else if (weighting == "A") { + for (int n=0; n<_maskSize; n++) + _weightMask[n] = _aWeighting[n]; + } + else if (weighting == "B") { + for (int n=0; n<_maskSize; n++) + _weightMask[n] = _bWeighting[n]; + } + else if (weighting == "C") { + for (int n=0; n<_maskSize; n++) + _weightMask[n] = _cWeighting[n]; + } + else if (weighting == "D") { + for (int n=0; n<_maskSize; n++) + _weightMask[n] = _dWeighting[n]; + } + for (i=0; i < int(_weight.size()); ++i) { freq = (Real)i/(Real)_frameSize*_sampleRate; while (freq > _freqsMask[j]) { @@ -189,8 +245,9 @@ void PitchYinFFT::compute() { yinMin = -_amplitudes[0]; } else { - // TODO this should never happen, but some people reported it happening in their real time applications. - throw EssentiaException("PitchYinFFT: it appears that no peaks were found by PeakDetection. If you read this message, PLEASE, report this issue to the developers with an example of audio on which it happened."); + tau = 0.0; // it will provide zero-pitch and zero-pitch confidence. + // launch warning message for user feedbacking + E_WARNING("PitchYinFFT: it appears that no peaks were found by PeakDetection algorithm. So, pitch and confidence will be set to zero."); } } else { diff --git a/src/algorithms/tonal/pitchyinfft.h b/src/algorithms/tonal/pitchyinfft.h index 6059c6b18..237a65431 100644 --- a/src/algorithms/tonal/pitchyinfft.h +++ b/src/algorithms/tonal/pitchyinfft.h @@ -58,6 +58,7 @@ class PitchYinFFT : public Algorithm { int _tauMin; int _tauMax; Real _tolerance; + std::string _weighting; public: PitchYinFFT() { @@ -83,12 +84,13 @@ class PitchYinFFT : public Algorithm { declareParameter("maxFrequency", "the maximum allowed frequency [Hz]", "(0,inf)", 22050.0); declareParameter("interpolate", "boolean flag to enable interpolation", "{true,false}", true); declareParameter("tolerance", "tolerance for peak detection", "[0,1]", 1.0); + declareParameter("weighting", "string to assign a weighting function", "{default,A,B,C,D,Z}", "default"); } void configure(); void compute(); - void spectralWeights(); + void spectralWeights(std::string weighting); static const char* name; static const char* category; diff --git a/test/src/unittests/all_tests.py b/test/src/unittests/all_tests.py index deb354837..2e3d34dcf 100755 --- a/test/src/unittests/all_tests.py +++ b/test/src/unittests/all_tests.py @@ -42,7 +42,7 @@ # Add sys path to make python recognize tests/src/unittests as a module parent_dir = os.path.abspath(os.path.dirname(tests_dir)) sys.path.insert(0, parent_dir) - + # Chdir into the tests dir so that the paths work out right os.chdir(tests_dir) @@ -109,7 +109,8 @@ def computeResetCompute(algo, *args, **kwargs): 'BandReject', 'EqualLoudness', 'MovingAverage' ] special = [ 'FrameCutter', 'OverlapAdd', 'TempoScaleBands', 'TempoTap', 'TempoTapTicks', 'Panning','OnsetDetection', 'MonoWriter', 'Flux', 'StartStopSilence', - 'LogSpectrum', 'ClickDetector', 'SNR', 'SaturationDetector', 'Welch' ] + 'LogSpectrum', 'ClickDetector', 'SNR', 'SaturationDetector', 'Welch', + 'FrameBuffer'] if algo.name() in audioLoaders + filters + special: return algo.normalCompute(*args, **kwargs) diff --git a/test/src/unittests/standard/test_framebuffer.py b/test/src/unittests/standard/test_framebuffer.py new file mode 100644 index 000000000..94110094a --- /dev/null +++ b/test/src/unittests/standard/test_framebuffer.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python + +# Copyright (C) 2006-2021 Music Technology Group - Universitat Pompeu Fabra +# +# This file is part of Essentia +# +# Essentia is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation (FSF), either version 3 of the License, or (at your +# option) any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the Affero GNU General Public License +# version 3 along with this program. If not, see http://www.gnu.org/licenses/ + +from essentia_test import * + + +class TestFrameBuffer(TestCase): + + def testEmpty(self): + with self.assertRaises(RuntimeError): + FrameBuffer()([]) + + def testBufferZeroPadding(self): + buffer = FrameBuffer(bufferSize=8, zeroPadding=True) + self.assertEqualVector(buffer([1, 2]), [0., 0., 0., 0., 0., 0., 1., 2.]) + self.assertEqualVector(buffer([3, 4]), [0., 0., 0., 0., 1., 2., 3., 4.]) + self.assertEqualVector(buffer([5, 6]), [0., 0., 1., 2., 3., 4., 5., 6.]) + self.assertEqualVector(buffer([7, 8]), [1., 2., 3., 4., 5., 6., 7., 8.]) + self.assertEqualVector(buffer([9, 10]), [3., 4., 5., 6., 7., 8., 9., 10.]) + + def testBufferNoZeroPadding(self): + buffer = FrameBuffer(bufferSize=8, zeroPadding=False) + self.assertEqualVector(buffer([1, 2]), []) + self.assertEqualVector(buffer([3, 4]), []) + self.assertEqualVector(buffer([5, 6]), []) + self.assertEqualVector(buffer([7, 8]), [1., 2., 3., 4., 5., 6., 7., 8.]) + + def testFrameSizeEqualsBufferSize(self): + buffer = FrameBuffer(bufferSize=8) + self.assertEqualVector(buffer([1, 2, 3, 4, 5, 6, 7, 8]), [1., 2., 3., 4., 5., 6., 7., 8.]) + + def testFrameSizeLargerBufferSize(self): + buffer = FrameBuffer(bufferSize=8) + self.assertEqualVector(buffer([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), [3., 4., 5., 6., 7., 8., 9., 10.]) + + def testResetZeroPadding(self): + buffer = FrameBuffer(bufferSize=8, zeroPadding=True) + buffer([1, 2, 3, 4, 5, 6]) # Results in [0., 0., 1., 2., 3., 4., 5., 6.] + buffer.reset() # Sets the buffer to zero vector. + self.assertEqualVector(buffer([1, 2]), [0., 0., 0., 0., 0., 0., 1., 2.]) + + def testResetNoZeroPadding(self): + buffer = FrameBuffer(bufferSize=8, zeroPadding=False) + buffer([1, 2, 3, 4, 5, 6, 7, 8]) + buffer.reset() + self.assertEqualVector(buffer([1, 2]), []) + + +suite = allTests(TestFrameBuffer) + +if __name__ == '__main__': + TextTestRunner(verbosity=2).run(suite)