From 481108ecd1f14db4d8dc71fc37a7021c64598493 Mon Sep 17 00:00:00 2001 From: Craig Hansen-Sturm Date: Sun, 10 Aug 2014 22:47:27 -0700 Subject: [PATCH] added support for biquad, parametric, and multi-band filter eq --- interface/src/Audio.cpp | 44 ++++- interface/src/Audio.h | 17 +- interface/src/Menu.cpp | 42 ++++ interface/src/Menu.h | 5 + libraries/audio/src/AudioFilter.cpp | 23 +++ libraries/audio/src/AudioFilter.h | 295 ++++++++++++++++++++++++++++ 6 files changed, 421 insertions(+), 5 deletions(-) create mode 100644 libraries/audio/src/AudioFilter.cpp create mode 100644 libraries/audio/src/AudioFilter.h diff --git a/interface/src/Audio.cpp b/interface/src/Audio.cpp index 2cba22b630..0484860c65 100644 --- a/interface/src/Audio.cpp +++ b/interface/src/Audio.cpp @@ -56,7 +56,6 @@ static const int MUTE_ICON_SIZE = 24; static const int RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES = 100; - Audio::Audio(QObject* parent) : AbstractAudioInterface(parent), _audioInput(NULL), @@ -83,6 +82,7 @@ Audio::Audio(QObject* parent) : _noiseGateSampleCounter(0), _noiseGateOpen(false), _noiseGateEnabled(true), + _peqEnabled(false), _toneInjectionEnabled(false), _noiseGateFramesToClose(0), _totalInputAudioSamples(0), @@ -132,6 +132,7 @@ void Audio::init(QGLWidget *parent) { void Audio::reset() { _receivedAudioStream.reset(); resetStats(); + _peq.reset(); } void Audio::resetStats() { @@ -418,9 +419,15 @@ void Audio::start() { if (!outputFormatSupported) { qDebug() << "Unable to set up audio output because of a problem with output format."; } + + _peq.initialize( _inputFormat.sampleRate(), _audioInput->bufferSize() ); + } void Audio::stop() { + + _peq.finalize(); + // "switch" to invalid devices in order to shut down the state switchInputToAudioDevice(QAudioDeviceInfo()); switchOutputToAudioDevice(QAudioDeviceInfo()); @@ -462,7 +469,15 @@ void Audio::handleAudioInput() { int inputSamplesRequired = (int)((float)NETWORK_BUFFER_LENGTH_SAMPLES_PER_CHANNEL * inputToNetworkInputRatio); QByteArray inputByteArray = _inputDevice->readAll(); - + + if (_peqEnabled && !_muted) { + // we wish to pre-filter our captured input, prior to loopback + + int16_t* ioBuffer = (int16_t*)inputByteArray.data(); + + _peq.render( ioBuffer, ioBuffer, inputByteArray.size() / sizeof(int16_t) ); + } + if (Menu::getInstance()->isOptionChecked(MenuOption::EchoLocalAudio) && !_muted && _audioOutput) { // if this person wants local loopback add that to the locally injected audio @@ -1172,6 +1187,31 @@ void Audio::renderToolBox(int x, int y, bool boxed) { glDisable(GL_TEXTURE_2D); } +void Audio::toggleAudioFilter() { + _peqEnabled = !_peqEnabled; +} + +void Audio::selectAudioFilterFlat() { + if (Menu::getInstance()->isOptionChecked(MenuOption::AudioFilterFlat)) { + _peq.loadProfile(0); + } +} +void Audio::selectAudioFilterTrebleCut() { + if (Menu::getInstance()->isOptionChecked(MenuOption::AudioFilterTrebleCut)) { + _peq.loadProfile(1); + } +} +void Audio::selectAudioFilterBassCut() { + if (Menu::getInstance()->isOptionChecked(MenuOption::AudioFilterBassCut)) { + _peq.loadProfile(2); + } +} +void Audio::selectAudioFilterSmiley() { + if (Menu::getInstance()->isOptionChecked(MenuOption::AudioFilterSmiley)) { + _peq.loadProfile(3); + } +} + void Audio::toggleScope() { _scopeEnabled = !_scopeEnabled; if (_scopeEnabled) { diff --git a/interface/src/Audio.h b/interface/src/Audio.h index 8fae6f3bdd..4fb54218af 100644 --- a/interface/src/Audio.h +++ b/interface/src/Audio.h @@ -19,6 +19,7 @@ #include "AudioStreamStats.h" #include "RingBufferHistory.h" #include "MovingMinMaxAvg.h" +#include "AudioFilter.h" #include #include @@ -125,7 +126,12 @@ public slots: void selectAudioScopeFiveFrames(); void selectAudioScopeTwentyFrames(); void selectAudioScopeFiftyFrames(); - + void toggleAudioFilter(); + void selectAudioFilterFlat(); + void selectAudioFilterTrebleCut(); + void selectAudioFilterBassCut(); + void selectAudioFilterSmiley(); + virtual void handleAudioByteArray(const QByteArray& audioByteArray); void sendDownstreamAudioStatsPacket(); @@ -252,12 +258,12 @@ private: // Audio scope methods for rendering void renderBackground(const float* color, int x, int y, int width, int height); void renderGrid(const float* color, int x, int y, int width, int height, int rows, int cols); - void renderLineStrip(const float* color, int x, int y, int n, int offset, const QByteArray* byteArray); + void renderLineStrip(const float* color, int x, int y, int n, int offset, const QByteArray* byteArray); // audio stats methods for rendering void renderAudioStreamStats(const AudioStreamStats& streamStats, int horizontalOffset, int& verticalOffset, float scale, float rotation, int font, const float* color, bool isDownstreamStats = false); - + // Audio scope data static const unsigned int NETWORK_SAMPLES_PER_FRAME = NETWORK_BUFFER_LENGTH_SAMPLES_PER_CHANNEL; static const unsigned int DEFAULT_FRAMES_PER_SCOPE = 5; @@ -270,6 +276,11 @@ private: int _scopeOutputOffset; int _framesPerScope; int _samplesPerScope; + + // Multi-band parametric EQ + bool _peqEnabled; + AudioFilterPEQ3 _peq; + QMutex _guard; QByteArray* _scopeInput; QByteArray* _scopeOutputLeft; diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 43d9fde01a..e6850aac6d 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -486,6 +486,48 @@ Menu::Menu() : true, appInstance->getAudio(), SLOT(toggleAudioNoiseReduction())); + + addCheckableActionToQMenuAndActionHash(audioDebugMenu, MenuOption::AudioFilter, + 0, + false, + appInstance->getAudio(), + SLOT(toggleAudioFilter())); + + QMenu* audioFilterMenu = audioDebugMenu->addMenu("Audio Filter Options"); + addDisabledActionAndSeparator(audioFilterMenu, "Filter Response"); + { + QAction *flat = addCheckableActionToQMenuAndActionHash(audioFilterMenu, MenuOption::AudioFilterFlat, + 0, + true, + appInstance->getAudio(), + SLOT(selectAudioFilterFlat())); + + QAction *trebleCut = addCheckableActionToQMenuAndActionHash(audioFilterMenu, MenuOption::AudioFilterTrebleCut, + 0, + false, + appInstance->getAudio(), + SLOT(selectAudioFilterTrebleCut())); + + QAction *bassCut = addCheckableActionToQMenuAndActionHash(audioFilterMenu, MenuOption::AudioFilterBassCut, + 0, + false, + appInstance->getAudio(), + SLOT(selectAudioFilterBassCut())); + + QAction *smiley = addCheckableActionToQMenuAndActionHash(audioFilterMenu, MenuOption::AudioFilterSmiley, + 0, + false, + appInstance->getAudio(), + SLOT(selectAudioFilterSmiley())); + + + QActionGroup* audioFilterGroup = new QActionGroup(audioFilterMenu); + audioFilterGroup->addAction(flat); + audioFilterGroup->addAction(trebleCut); + audioFilterGroup->addAction(bassCut); + audioFilterGroup->addAction(smiley); + } + addCheckableActionToQMenuAndActionHash(audioDebugMenu, MenuOption::EchoServerAudio); addCheckableActionToQMenuAndActionHash(audioDebugMenu, MenuOption::EchoLocalAudio); addCheckableActionToQMenuAndActionHash(audioDebugMenu, MenuOption::StereoAudio, 0, false, diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 3bef306bef..7ef744e62e 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -311,6 +311,11 @@ namespace MenuOption { const QString Animations = "Animations..."; const QString Atmosphere = "Atmosphere"; const QString Attachments = "Attachments..."; + const QString AudioFilter = "Audio Filter Bank"; + const QString AudioFilterFlat = "Flat Response"; + const QString AudioFilterTrebleCut= "Treble Cut"; + const QString AudioFilterBassCut = "Bass Cut"; + const QString AudioFilterSmiley = "Smiley Curve"; const QString AudioNoiseReduction = "Audio Noise Reduction"; const QString AudioScope = "Audio Scope"; const QString AudioScopeFiftyFrames = "Fifty"; diff --git a/libraries/audio/src/AudioFilter.cpp b/libraries/audio/src/AudioFilter.cpp new file mode 100644 index 0000000000..61e5e20171 --- /dev/null +++ b/libraries/audio/src/AudioFilter.cpp @@ -0,0 +1,23 @@ +// +// AudioFilter.cpp +// hifi +// +// Created by Craig Hansen-Sturm on 8/10/14. +// +// + +#include +#include +#include +#include "AudioRingBuffer.h" +#include "AudioFilter.h" + +template<> +AudioFilterPEQ3::FilterParameter AudioFilterPEQ3::_profiles[ AudioFilterPEQ3::_profileCount ][ AudioFilterPEQ3::_filterCount ] = { + + { { 300., 1., 1. }, { 1000., 1., 1. }, { 4000., 1., 1. } }, // flat response (default) + { { 300., 1., 1. }, { 1000., 1., 1. }, { 4000., .1, 1. } }, // treble cut + { { 300., .1, 1. }, { 1000., 1., 1. }, { 4000., 1., 1. } }, // bass cut + { { 300., 1.5, 0.71 }, { 1000., .5, 1. }, { 4000., 1.5, 0.71 } } // smiley curve +}; + diff --git a/libraries/audio/src/AudioFilter.h b/libraries/audio/src/AudioFilter.h new file mode 100644 index 0000000000..44dcd261d6 --- /dev/null +++ b/libraries/audio/src/AudioFilter.h @@ -0,0 +1,295 @@ +// +// AudioFilter.h +// hifi +// +// Created by Craig Hansen-Sturm on 8/9/14. +// +// + +#ifndef hifi_AudioFilter_h +#define hifi_AudioFilter_h + +//////////////////////////////////////////////////////////////////////////////////////////// +// Implements a standard biquad filter in "Direct Form 1" +// Reference http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt +// +class AudioBiquad { + + // + // private data + // + float _a0; // gain + float _a1; // feedforward 1 + float _a2; // feedforward 2 + float _b1; // feedback 1 + float _b2; // feedback 2 + + float _xm1; + float _xm2; + float _ym1; + float _ym2; + +public: + + // + // ctor/dtor + // + AudioBiquad() + : _xm1(0.) + , _xm2(0.) + , _ym1(0.) + , _ym2(0.) { + setParameters(0.,0.,0.,0.,0.); + } + + ~AudioBiquad() { + } + + // + // public interface + // + void setParameters( const float a0, const float a1, const float a2, const float b1, const float b2 ) { + _a0 = a0; _a1 = a1; _a2 = a2; _b1 = b1; _b2 = b2; + } + + void getParameters( float& a0, float& a1, float& a2, float& b1, float& b2 ) { + a0 = _a0; a1 = _a1; a2 = _a2; b1 = _b1; b2 = _b2; + } + + void render( const float* in, float* out, const int frames) { + + float x; + float y; + + for (int i = 0; i < frames; ++i) { + + x = *in++; + + // biquad + y = (_a0 * x) + + (_a1 * _xm1) + + (_a2 * _xm2) + - (_b1 * _ym1) + - (_b2 * _ym2); + + // update delay line + _xm2 = _xm1; + _xm1 = x; + _ym2 = _ym1; + _ym1 = y; + + *out++ = y; + } + } + + void reset() { + _xm1 = _xm2 = _ym1 = _ym2 = 0.; + } +}; + +//////////////////////////////////////////////////////////////////////////////////////////// +// Implements a single-band parametric EQ using a biquad "peaking EQ" configuration +// +// gain > 1.0 boosts the center frequency +// gain < 1.0 cuts the center frequency +// +class AudioParametricEQ { + + // + // private data + // + AudioBiquad _kernel; + float _sampleRate; + float _frequency; + float _gain; + float _slope; + + // helpers + void updateKernel() { + + /* + a0 = 1 + alpha*A + a1 = -2*cos(w0) + a2 = 1 - alpha*A + b1 = -2*cos(w0) + b2 = 1 - alpha/A + */ + + const float a = _gain; + const float omega = TWO_PI * _frequency / _sampleRate; + const float alpha = 0.5 * sinf(omega) / _slope; + const float gamma = 1.0 / ( 1.0 + (alpha/a) ); + + const float a0 = 1.0 + (alpha*a); + const float a1 = -2.0 * cosf(omega); + const float a2 = 1.0 - (alpha*a); + const float b1 = a1; + const float b2 = 1.0 - (alpha/a); + + _kernel.setParameters( a0*gamma,a1*gamma,a2*gamma,b1*gamma,b2*gamma ); + } + +public: + // + // ctor/dtor + // + AudioParametricEQ() { + + setParameters(0.,0.,0.,0.); + updateKernel(); + } + + ~AudioParametricEQ() { + } + + // + // public interface + // + void setParameters( const float sampleRate, const float frequency, const float gain, const float slope ) { + + _sampleRate = std::max(sampleRate,1.0f); + _frequency = std::max(frequency,2.0f); + _gain = std::max(gain,0.0f); + _slope = std::max(slope,0.00001f); + + updateKernel(); + } + + void getParameters( float& sampleRate, float& frequency, float& gain, float& slope ) { + sampleRate = _sampleRate; frequency = _frequency; gain = _gain; slope = _slope; + } + + void render(const float* in, float* out, const int frames ) { + _kernel.render(in,out,frames); + } + + void reset() { + _kernel.reset(); + } +}; + +//////////////////////////////////////////////////////////////////////////////////////////// +// Helper/convenience class that implements a bank of EQ objects +// +template< typename T, const int N> +class AudioFilterBank { + + // + // types + // + struct FilterParameter { + float _p1; + float _p2; + float _p3; + }; + + // + // private static data + // + static const int _filterCount = N; + static const int _profileCount = 4; + + static FilterParameter _profiles[_profileCount][_filterCount]; + + // + // private data + // + T _filters[ _filterCount ]; + float* _buffer; + float _sampleRate; + uint16_t _frameCount; + +public: + + // + // ctor/dtor + // + AudioFilterBank() + : _buffer(NULL) + , _sampleRate(0.) + , _frameCount(0) { + } + + ~AudioFilterBank() { + finalize(); + } + + // + // public interface + // + void initialize( const float sampleRate, const int frameCount ) { + finalize(); + + _buffer = (float*)malloc( frameCount * sizeof(float) ); + if(!_buffer) { + return; + } + + _sampleRate = sampleRate; + _frameCount = frameCount; + + reset(); + loadProfile(0); // load default profile "flat response" into the bank (see AudioFilter.cpp) + } + + void finalize() { + if (_buffer ) { + free (_buffer); + _buffer = NULL; + } + } + + void loadProfile( int profileIndex ) { + if (profileIndex >= 0 && profileIndex < _profileCount) { + + for (int i = 0; i < _filterCount; ++i) { + FilterParameter p = _profiles[profileIndex][i]; + + _filters[i].setParameters(_sampleRate,p._p1,p._p2,p._p3); + } + } + } + + void render( const float* in, float* out, const int frameCount ) { + for (int i = 0; i < _filterCount; ++i) { + _filters[i].render( in, out, frameCount ); + } + } + + void render( const int16_t* in, int16_t* out, const int frameCount ) { + if( !_buffer || ( frameCount > _frameCount ) ) + return; + + const int scale = (2 << ((8*sizeof(int16_t))-1)); + + // convert int16_t to float32 (normalized to -1. ... 1.) + for (int i = 0; i < frameCount; ++i) { + _buffer[i] = ((float)(*in++)) / scale; + } + // for this filter, we share input/output buffers at each stage, but our design does not mandate this + render( _buffer, _buffer, frameCount ); + + // convert float32 to int16_t + for (int i = 0; i < frameCount; ++i) { + *out++ = (int16_t)(_buffer[i] * scale); + } + } + + void reset() { + for (int i = 0; i < _filterCount; ++i ) { + _filters[i].reset(); + } + } + +}; + +//////////////////////////////////////////////////////////////////////////////////////////// +// Specializations of AudioFilterBank +// +typedef AudioFilterBank< AudioParametricEQ, 1> AudioFilterPEQ1; // bank with one band of PEQ +typedef AudioFilterBank< AudioParametricEQ, 2> AudioFilterPEQ2; // bank with two bands of PEQ +typedef AudioFilterBank< AudioParametricEQ, 3> AudioFilterPEQ3; // bank with three bands of PEQ +// etc.... + + +#endif // hifi_AudioFilter_h