Merge pull request #3270 from chansensturm/audio-filter

Added support for biquad, parametric, and multi-band filter eq
This commit is contained in:
AndrewMeadows 2014-08-11 12:22:40 -07:00
commit 0650169c25
6 changed files with 427 additions and 5 deletions

View file

@ -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) {

View file

@ -19,6 +19,7 @@
#include "AudioStreamStats.h"
#include "RingBufferHistory.h"
#include "MovingMinMaxAvg.h"
#include "AudioFilter.h"
#include <QAudio>
#include <QAudioInput>
@ -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;

View file

@ -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,

View file

@ -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";

View file

@ -0,0 +1,26 @@
//
// AudioFilter.cpp
// hifi
//
// Created by Craig Hansen-Sturm on 8/10/14.
// Copyright 2014 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <math.h>
#include <vector>
#include <SharedUtil.h>
#include "AudioRingBuffer.h"
#include "AudioFilter.h"
template<>
AudioFilterPEQ3::FilterParameter AudioFilterPEQ3::_profiles[ AudioFilterPEQ3::_profileCount ][ AudioFilterPEQ3::_filterCount ] = {
// Freq Gain Q Freq Gain Q Freq Gain Q
{ { 300.0f, 1.0f, 1.0f }, { 1000.0f, 1.0f, 1.0f }, { 4000.0f, 1.0f, 1.0f } }, // flat response (default)
{ { 300.0f, 1.0f, 1.0f }, { 1000.0f, 1.0f, 1.0f }, { 4000.0f, 0.1f, 1.0f } }, // treble cut
{ { 300.0f, 0.1f, 1.0f }, { 1000.0f, 1.0f, 1.0f }, { 4000.0f, 1.0f, 1.0f } }, // bass cut
{ { 300.0f, 1.5f, 0.71f }, { 1000.0f, 0.5f, 1.0f }, { 4000.0f, 1.50f, 0.71f } } // smiley curve
};

View file

@ -0,0 +1,298 @@
//
// AudioFilter.h
// hifi
//
// Created by Craig Hansen-Sturm on 8/9/14.
// Copyright 2014 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#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.5f * sinf(omega) / _slope;
const float gamma = 1.0f / ( 1.0f + (alpha/a) );
const float a0 = 1.0f + (alpha*a);
const float a1 = -2.0f * cosf(omega);
const float a2 = 1.0f - (alpha*a);
const float b1 = a1;
const float b2 = 1.0f - (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