2013-03-06 02:40:17 -08:00
|
|
|
/********************************************************************************
|
|
|
|
Copyright (C) 2013 Lucas Murray <lmurray@undefinedfire.com>
|
|
|
|
|
|
|
|
This program is free software; you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU General Public License as published by
|
|
|
|
the Free Software Foundation; either version 2 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 GNU General Public License
|
|
|
|
along with this program; if not, write to the Free Software
|
|
|
|
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
|
|
|
|
********************************************************************************/
|
|
|
|
|
|
|
|
#include "NoiseGate.h"
|
2013-03-10 01:54:54 -08:00
|
|
|
#include "resource.h"
|
2013-03-06 02:40:17 -08:00
|
|
|
|
|
|
|
//============================================================================
|
|
|
|
// NoiseGateFilter class
|
|
|
|
|
|
|
|
NoiseGateFilter::NoiseGateFilter(NoiseGate *parent)
|
2013-03-09 22:39:02 -08:00
|
|
|
: parent(parent)
|
|
|
|
, attenuation(0.0f)
|
|
|
|
, level(0.0f)
|
|
|
|
, heldTime(0.0f)
|
|
|
|
, isOpen(false)
|
2013-03-06 02:40:17 -08:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
NoiseGateFilter::~NoiseGateFilter()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
AudioSegment *NoiseGateFilter::Process(AudioSegment *segment)
|
|
|
|
{
|
2013-03-14 07:07:23 -07:00
|
|
|
// We process even if the filter is disabled so that it's state is updated for when it gets enabled again
|
2013-03-06 02:40:17 -08:00
|
|
|
ApplyNoiseGate(segment->audioData.Array(), segment->audioData.Num());
|
|
|
|
return segment;
|
|
|
|
}
|
|
|
|
|
|
|
|
void NoiseGateFilter::ApplyNoiseGate(float *buffer, int totalFloats)
|
|
|
|
{
|
|
|
|
// We assume stereo input
|
|
|
|
if(totalFloats % 2)
|
|
|
|
return; // Odd number of samples
|
|
|
|
|
|
|
|
// OBS is currently hard-coded to 44.1ksps
|
|
|
|
const float SAMPLE_RATE_F = 44100.0f;
|
|
|
|
const float dtPerSample = 1.0f / SAMPLE_RATE_F;
|
|
|
|
|
|
|
|
// Convert configuration times into per-sample amounts
|
2013-03-09 22:39:02 -08:00
|
|
|
const float attackRate = 1.0f / (parent->attackTime * SAMPLE_RATE_F);
|
|
|
|
const float releaseRate = 1.0f / (parent->releaseTime * SAMPLE_RATE_F);
|
2013-03-06 02:40:17 -08:00
|
|
|
|
|
|
|
// Determine level decay rate. We don't want human voice (75-300Hz) to cross the close
|
|
|
|
// threshold if the previous peak crosses the open threshold. 75Hz at 44.1ksps is ~590
|
|
|
|
// samples between peaks.
|
2013-03-09 22:39:02 -08:00
|
|
|
const float thresholdDiff = parent->openThreshold - parent->closeThreshold;
|
2013-03-06 02:40:17 -08:00
|
|
|
const float minDecayPeriod = (1.0f / 75.0f) * SAMPLE_RATE_F; // ~590 samples
|
|
|
|
const float decayRate = thresholdDiff / minDecayPeriod;
|
|
|
|
|
|
|
|
// We can't use SSE as the processing of each sample depends on the processed
|
|
|
|
// result of the previous sample.
|
|
|
|
for(int i = 0; i < totalFloats; i += 2)
|
|
|
|
{
|
|
|
|
// Get current input level
|
|
|
|
float curLvl = abs(buffer[i] + buffer[i+1]) * 0.5f;
|
|
|
|
|
|
|
|
// Test thresholds
|
2013-03-09 22:39:02 -08:00
|
|
|
if(curLvl > parent->openThreshold && !isOpen)
|
|
|
|
isOpen = true;
|
|
|
|
if(level < parent->closeThreshold && isOpen)
|
2013-03-06 02:40:17 -08:00
|
|
|
{
|
2013-03-09 22:39:02 -08:00
|
|
|
heldTime = 0.0f;
|
|
|
|
isOpen = false;
|
2013-03-06 02:40:17 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Decay level slowly so human voice (75-300Hz) doesn't cross the close threshold
|
|
|
|
// (Essentially a peak detector with very fast decay)
|
2013-03-09 22:39:02 -08:00
|
|
|
level = max(level, curLvl) - decayRate;
|
2013-03-06 02:40:17 -08:00
|
|
|
|
|
|
|
// Apply gate state to attenuation
|
2013-03-09 22:39:02 -08:00
|
|
|
if(isOpen)
|
|
|
|
attenuation = min(1.0f, attenuation + attackRate);
|
2013-03-06 02:40:17 -08:00
|
|
|
else
|
|
|
|
{
|
2013-03-09 22:39:02 -08:00
|
|
|
heldTime += dtPerSample;
|
|
|
|
if(heldTime > parent->holdTime)
|
|
|
|
attenuation = max(0.0f, attenuation - releaseRate);
|
2013-03-06 02:40:17 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Multiple input by gate multiplier (0.0f if fully closed, 1.0f if fully open)
|
2013-03-14 07:07:23 -07:00
|
|
|
if(parent->isEnabled) {
|
|
|
|
buffer[i] *= attenuation;
|
|
|
|
buffer[i+1] *= attenuation;
|
|
|
|
}
|
2013-03-06 02:40:17 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-03-10 01:54:54 -08:00
|
|
|
//============================================================================
|
|
|
|
// NoiseGateConfigWindow class
|
|
|
|
|
|
|
|
NoiseGateConfigWindow::NoiseGateConfigWindow(NoiseGate *parent, HWND parentHwnd)
|
|
|
|
: parent(parent)
|
|
|
|
, parentHwnd(parentHwnd)
|
|
|
|
, hwnd(NULL)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
NoiseGateConfigWindow::~NoiseGateConfigWindow()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
INT_PTR CALLBACK NoiseGateConfigWindow::DialogProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|
|
|
{
|
|
|
|
// Get the pointer to our class instance
|
|
|
|
NoiseGateConfigWindow *window = (NoiseGateConfigWindow *)GetWindowLongPtr(hwnd, DWLP_USER);
|
|
|
|
|
|
|
|
switch(message)
|
|
|
|
{
|
|
|
|
default:
|
|
|
|
// Unhandled
|
|
|
|
break;
|
|
|
|
case WM_INITDIALOG:
|
|
|
|
SetWindowLongPtr(hwnd, DWLP_USER, (LONG_PTR)lParam);
|
|
|
|
window = (NoiseGateConfigWindow *)lParam;
|
|
|
|
window->hwnd = hwnd;
|
|
|
|
window->MsgInitDialog();
|
|
|
|
return TRUE;
|
|
|
|
case WM_COMMAND:
|
|
|
|
if(window != NULL)
|
2013-03-14 07:07:23 -07:00
|
|
|
return window->MsgClicked(LOWORD(wParam), (HWND)lParam);
|
|
|
|
break;
|
|
|
|
case WM_VSCROLL:
|
|
|
|
case WM_HSCROLL:
|
|
|
|
if(window != NULL)
|
|
|
|
return window->MsgScroll(message == WM_VSCROLL, wParam, lParam);
|
|
|
|
break;
|
|
|
|
case WM_TIMER:
|
|
|
|
if(window != NULL)
|
|
|
|
return window->MsgTimer((int)wParam);
|
2013-03-10 01:54:54 -08:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Display the dialog and begin its message loop. Returns \t{true} if the
|
|
|
|
* user clicked the "OK" button.
|
|
|
|
*/
|
|
|
|
bool NoiseGateConfigWindow::Process()
|
|
|
|
{
|
|
|
|
INT_PTR res = DialogBoxParam(parent->hinstDLL, MAKEINTRESOURCE(IDD_CONFIGURENOISEGATE), parentHwnd, (DLGPROC)DialogProc, (LPARAM)this);
|
|
|
|
if(res == IDOK)
|
|
|
|
return true;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2013-03-14 07:07:23 -07:00
|
|
|
/**
|
|
|
|
* Sets the caption of the open and close threshold trackbars ("-24 dB")
|
|
|
|
*/
|
|
|
|
void NoiseGateConfigWindow::SetTrackbarCaption(int controlId, int db)
|
|
|
|
{
|
|
|
|
SetWindowText(GetDlgItem(hwnd, controlId), FormattedString(TEXT("%d dB"), db));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Updates the current audio volume control.
|
|
|
|
*/
|
|
|
|
void NoiseGateConfigWindow::RepaintVolume()
|
|
|
|
{
|
|
|
|
float rms, max, peak;
|
|
|
|
OBSGetCurMicVolumeStats(&rms, &max, &peak);
|
|
|
|
//SetWindowText(GetDlgItem(hwnd, IDC_OPENTHRES_DB), FormattedString(TEXT("%.3f"), rms));
|
|
|
|
//SetWindowText(GetDlgItem(hwnd, IDC_CLOSETHRES_DB), FormattedString(TEXT("%d"), (int)((rms + 96.0f) * 4.0f)));
|
|
|
|
SendMessage(GetDlgItem(hwnd, IDC_CURVOL), PBM_SETPOS, (int)((rms + 96.0f) * 4.0f), 0);
|
|
|
|
}
|
|
|
|
|
2013-03-10 01:54:54 -08:00
|
|
|
void NoiseGateConfigWindow::MsgInitDialog()
|
|
|
|
{
|
2013-03-14 07:07:23 -07:00
|
|
|
HWND ctrlHwnd;
|
|
|
|
|
2013-03-10 01:54:54 -08:00
|
|
|
LocalizeWindow(hwnd);
|
2013-03-14 07:07:23 -07:00
|
|
|
|
|
|
|
// Volume preview
|
|
|
|
ctrlHwnd = GetDlgItem(hwnd, IDC_CURVOL);
|
|
|
|
SendMessage(ctrlHwnd, PBM_SETRANGE32, 0, CURVOL_RESOLUTION); // Bottom = 0, top = CURVOL_RESOLUTION
|
|
|
|
RepaintVolume(); // Repaint immediately
|
|
|
|
SetTimer(hwnd, REPAINT_TIMER_ID, 16, NULL); // Repaint every 16ms ~= 60fps
|
|
|
|
|
|
|
|
// Close threshold trackbar (Control uses positive values)
|
|
|
|
SetTrackbarCaption(IDC_CLOSETHRES_DB, -10);
|
|
|
|
ctrlHwnd = GetDlgItem(hwnd, IDC_CLOSETHRES_SLIDER);
|
|
|
|
SendMessage(ctrlHwnd, TBM_SETRANGEMIN, FALSE, 0);
|
|
|
|
SendMessage(ctrlHwnd, TBM_SETRANGEMAX, FALSE, 96);
|
|
|
|
SendMessage(ctrlHwnd, TBM_SETPOS, TRUE, 10);
|
|
|
|
|
|
|
|
// Open threshold trackbar (Control uses positive values)
|
|
|
|
SetTrackbarCaption(IDC_OPENTHRES_DB, -10);
|
|
|
|
ctrlHwnd = GetDlgItem(hwnd, IDC_OPENTHRES_SLIDER);
|
|
|
|
SendMessage(ctrlHwnd, TBM_SETRANGEMIN, FALSE, 0);
|
|
|
|
SendMessage(ctrlHwnd, TBM_SETRANGEMAX, FALSE, 96);
|
|
|
|
SendMessage(ctrlHwnd, TBM_SETPOS, TRUE, 10);
|
|
|
|
|
|
|
|
// Enable/disable stream button
|
|
|
|
ctrlHwnd = GetDlgItem(hwnd, IDC_PREVIEWON);
|
|
|
|
if(OBSGetStreaming())
|
|
|
|
{
|
|
|
|
// If OBS is already streaming don't let the user stop from this window
|
|
|
|
SetWindowText(ctrlHwnd, Str("Plugins.NoiseGate.DisablePreview"));
|
|
|
|
EnableWindow(ctrlHwnd, FALSE);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
SetWindowText(ctrlHwnd, Str("Plugins.NoiseGate.EnablePreview"));
|
|
|
|
EnableWindow(ctrlHwnd, TRUE);
|
|
|
|
}
|
2013-03-10 01:54:54 -08:00
|
|
|
}
|
|
|
|
|
2013-03-14 07:07:23 -07:00
|
|
|
INT_PTR NoiseGateConfigWindow::MsgClicked(int controlId, HWND controlHwnd)
|
2013-03-10 01:54:54 -08:00
|
|
|
{
|
2013-03-14 07:07:23 -07:00
|
|
|
switch(controlId)
|
2013-03-10 01:54:54 -08:00
|
|
|
{
|
2013-03-14 07:07:23 -07:00
|
|
|
default:
|
|
|
|
// Unknown button
|
|
|
|
break;
|
2013-03-10 01:54:54 -08:00
|
|
|
case IDOK:
|
|
|
|
case IDCANCEL:
|
2013-03-14 07:07:23 -07:00
|
|
|
EndDialog(hwnd, controlId); // Return IDOK (1) or IDCANCEL (2)
|
2013-03-10 01:54:54 -08:00
|
|
|
return TRUE;
|
2013-03-14 07:07:23 -07:00
|
|
|
case IDC_PREVIEWON:
|
|
|
|
// Toggle stream preview
|
|
|
|
if(OBSGetStreaming())
|
|
|
|
{
|
|
|
|
OBSStartStopPreview();
|
|
|
|
parent->isEnabled = true;
|
|
|
|
SetWindowText(controlHwnd, Str("Plugins.NoiseGate.EnablePreview"));
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
OBSStartStopPreview();
|
|
|
|
parent->isEnabled = false;
|
|
|
|
SetWindowText(controlHwnd, Str("Plugins.NoiseGate.DisablePreview"));
|
|
|
|
}
|
|
|
|
return TRUE;
|
|
|
|
}
|
|
|
|
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
INT_PTR NoiseGateConfigWindow::MsgScroll(bool vertical, WPARAM wParam, LPARAM lParam)
|
|
|
|
{
|
|
|
|
if(lParam == NULL)
|
|
|
|
return FALSE;
|
|
|
|
HWND barHwnd = (HWND)lParam;
|
|
|
|
|
|
|
|
// Determine the current value of the trackbar
|
|
|
|
int pos;
|
|
|
|
switch(LOWORD(wParam))
|
|
|
|
{
|
|
|
|
case SB_THUMBPOSITION:
|
|
|
|
case SB_THUMBTRACK: // The user dragged the slider
|
|
|
|
pos = -HIWORD(wParam);
|
|
|
|
break;
|
|
|
|
default: // Everything else
|
|
|
|
pos = -(int)SendMessage(barHwnd, TBM_GETPOS, 0, 0);
|
|
|
|
break;
|
2013-03-10 01:54:54 -08:00
|
|
|
}
|
|
|
|
|
2013-03-14 07:07:23 -07:00
|
|
|
// Figure out which trackbar it was
|
|
|
|
if(barHwnd == GetDlgItem(hwnd, IDC_CLOSETHRES_SLIDER))
|
|
|
|
{
|
|
|
|
// User modified the close threshold trackbar
|
|
|
|
SetTrackbarCaption(IDC_CLOSETHRES_DB, pos);
|
|
|
|
return TRUE;
|
|
|
|
}
|
|
|
|
else if(barHwnd == GetDlgItem(hwnd, IDC_OPENTHRES_SLIDER))
|
|
|
|
{
|
|
|
|
// User modified the open threshold trackbar
|
|
|
|
SetTrackbarCaption(IDC_OPENTHRES_DB, pos);
|
|
|
|
return TRUE;
|
|
|
|
}
|
|
|
|
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
|
|
|
|
INT_PTR NoiseGateConfigWindow::MsgTimer(int timerId)
|
|
|
|
{
|
|
|
|
if(timerId == REPAINT_TIMER_ID)
|
|
|
|
{
|
|
|
|
RepaintVolume();
|
|
|
|
return TRUE;
|
|
|
|
}
|
2013-03-10 01:54:54 -08:00
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
|
2013-03-06 02:40:17 -08:00
|
|
|
//============================================================================
|
|
|
|
// NoiseGate class
|
|
|
|
|
|
|
|
// Statics
|
2013-03-09 22:45:18 -08:00
|
|
|
HINSTANCE NoiseGate::hinstDLL = NULL;
|
|
|
|
NoiseGate *NoiseGate::instance = NULL;
|
2013-03-06 02:40:17 -08:00
|
|
|
|
|
|
|
NoiseGate::NoiseGate()
|
2013-03-09 22:39:02 -08:00
|
|
|
: micSource(NULL)
|
|
|
|
, filter(NULL)
|
2013-03-14 07:07:23 -07:00
|
|
|
, isEnabled(true)
|
2013-03-09 22:39:02 -08:00
|
|
|
, openThreshold(0.05f) // FIXME: Configurable
|
|
|
|
, closeThreshold(0.005f)
|
|
|
|
, attackTime(0.1f)
|
|
|
|
, holdTime(0.2f)
|
|
|
|
, releaseTime(0.2f)
|
2013-03-06 02:40:17 -08:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
NoiseGate::~NoiseGate()
|
|
|
|
{
|
|
|
|
// Delete our filter if it exists
|
|
|
|
StreamStopped();
|
|
|
|
}
|
|
|
|
|
|
|
|
void NoiseGate::StreamStarted()
|
|
|
|
{
|
2013-03-09 22:39:02 -08:00
|
|
|
micSource = OBSGetMicAudioSource();
|
|
|
|
if(micSource == NULL)
|
2013-03-06 02:40:17 -08:00
|
|
|
return; // No microphone
|
2013-03-09 22:39:02 -08:00
|
|
|
filter = new NoiseGateFilter(this);
|
|
|
|
micSource->AddAudioFilter(filter);
|
2013-03-06 02:40:17 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
void NoiseGate::StreamStopped()
|
|
|
|
{
|
2013-03-09 22:39:02 -08:00
|
|
|
if(filter) {
|
|
|
|
micSource->RemoveAudioFilter(filter);
|
|
|
|
delete filter;
|
|
|
|
filter = NULL;
|
2013-03-06 02:40:17 -08:00
|
|
|
}
|
2013-03-09 22:39:02 -08:00
|
|
|
micSource = NULL;
|
2013-03-06 02:40:17 -08:00
|
|
|
}
|
|
|
|
|
2013-03-10 01:54:54 -08:00
|
|
|
void NoiseGate::ShowConfigDialog(HWND parentHwnd)
|
|
|
|
{
|
|
|
|
NoiseGateConfigWindow dialog(this, parentHwnd);
|
2013-03-14 07:07:23 -07:00
|
|
|
if(OBSGetStreaming())
|
|
|
|
isEnabled = false; // Disable the filter if we are currently streaming
|
2013-03-10 01:54:54 -08:00
|
|
|
dialog.Process();
|
2013-03-14 07:07:23 -07:00
|
|
|
isEnabled = true; // Force enable
|
2013-03-10 01:54:54 -08:00
|
|
|
}
|
|
|
|
|
2013-03-06 02:40:17 -08:00
|
|
|
//============================================================================
|
|
|
|
// Plugin entry points
|
|
|
|
|
|
|
|
bool LoadPlugin()
|
|
|
|
{
|
2013-03-09 22:45:18 -08:00
|
|
|
if(NoiseGate::instance != NULL)
|
2013-03-06 02:40:17 -08:00
|
|
|
return false;
|
2013-03-09 22:45:18 -08:00
|
|
|
NoiseGate::instance = new NoiseGate();
|
2013-03-06 02:40:17 -08:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void UnloadPlugin()
|
|
|
|
{
|
2013-03-09 22:45:18 -08:00
|
|
|
if(NoiseGate::instance == NULL)
|
2013-03-06 02:40:17 -08:00
|
|
|
return;
|
2013-03-09 22:45:18 -08:00
|
|
|
delete NoiseGate::instance;
|
|
|
|
NoiseGate::instance = NULL;
|
2013-03-06 02:40:17 -08:00
|
|
|
}
|
|
|
|
|
2013-03-10 01:54:54 -08:00
|
|
|
void ConfigPlugin(HWND parentHwnd)
|
|
|
|
{
|
|
|
|
if(NoiseGate::instance == NULL)
|
|
|
|
return;
|
|
|
|
NoiseGate::instance->ShowConfigDialog(parentHwnd);
|
|
|
|
}
|
|
|
|
|
2013-03-06 02:40:17 -08:00
|
|
|
void OnStartStream()
|
|
|
|
{
|
2013-03-09 22:45:18 -08:00
|
|
|
if(NoiseGate::instance == NULL)
|
2013-03-06 02:40:17 -08:00
|
|
|
return;
|
2013-03-09 22:45:18 -08:00
|
|
|
NoiseGate::instance->StreamStarted();
|
2013-03-06 02:40:17 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
void OnStopStream()
|
|
|
|
{
|
2013-03-09 22:45:18 -08:00
|
|
|
if(NoiseGate::instance == NULL)
|
2013-03-06 02:40:17 -08:00
|
|
|
return;
|
2013-03-09 22:45:18 -08:00
|
|
|
NoiseGate::instance->StreamStopped();
|
2013-03-06 02:40:17 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
CTSTR GetPluginName()
|
|
|
|
{
|
|
|
|
return Str("Plugins.NoiseGate.PluginName");
|
|
|
|
}
|
|
|
|
|
|
|
|
CTSTR GetPluginDescription()
|
|
|
|
{
|
|
|
|
return Str("Plugins.NoiseGate.PluginDescription");
|
|
|
|
}
|
|
|
|
|
|
|
|
BOOL CALLBACK DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
|
|
|
|
{
|
|
|
|
if(fdwReason == DLL_PROCESS_ATTACH)
|
2013-03-09 22:45:18 -08:00
|
|
|
NoiseGate::hinstDLL = hinstDLL;
|
2013-03-06 02:40:17 -08:00
|
|
|
return TRUE;
|
|
|
|
}
|