1
0
mirror of https://github.com/cookiengineer/audacity synced 2025-06-17 08:30:06 +02:00
audacity/src/widgets/Meter.cpp
2014-10-24 22:34:47 +00:00

1570 lines
44 KiB
C++

/**********************************************************************
Audacity: A Digital Audio Editor
Meter.cpp
Dominic Mazzoni
Vaughan Johnson
2004.06.25 refresh rate limited to 30mS, by ChackoN
*******************************************************************//**
\class Meter
\brief VU Meter, for displaying recording/playback level
This is a bunch of common code that can display many different
forms of VU meters and other displays.
But note that a lot of later code here assumes these are
MeterToolBar meters, e.g., Meter::StartMonitoring,
so these are not as generic/common as originally intended.
*//****************************************************************//**
\class MeterBar
\brief A struct used by Meter to hold the position of one bar.
*//****************************************************************//**
\class MeterUpdateMsg
\brief Message used to update the Meter
*//****************************************************************//**
\class MeterUpdateQueue
\brief Queue of MeterUpdateMsg used to feed the Meter.
*//******************************************************************/
#include "../Audacity.h"
#include "../AudacityApp.h"
#include <wx/defs.h>
#include <wx/dialog.h>
#include <wx/dcbuffer.h>
#include <wx/dcmemory.h>
#include <wx/image.h>
#include <wx/intl.h>
#include <wx/menu.h>
#include <wx/settings.h>
#include <wx/textdlg.h>
#include <wx/numdlg.h>
#include <wx/radiobut.h>
#include <wx/tooltip.h>
#include <wx/msgdlg.h>
#include <math.h>
#include "Meter.h"
#include "../AudioIO.h"
#include "../AColor.h"
#include "../ImageManipulation.h"
//#include "../../images/MixerImages.h"
#include "../Project.h"
#include "../toolbars/MeterToolBar.h"
#include "../toolbars/ControlToolBar.h"
#include "../Prefs.h"
#include "../Theme.h"
#include "../AllThemeResources.h"
#include "../Experimental.h"
#include "../widgets/valnum.h"
// Event used to notify all meters of preference changes
DEFINE_EVENT_TYPE(EVT_METER_PREFERENCES_CHANGED);
/* Updates to the meter are passed accross via meter updates, each contained in
* a MeterUpdateMsg object */
wxString MeterUpdateMsg::toString()
{
wxString output; // somewhere to build up a string in
output = wxString::Format(wxT("Meter update msg: %i channels, %i samples\n"), \
kMaxMeterBars, numFrames);
for (int i = 0; i<kMaxMeterBars; i++)
{ // for each channel of the meters
output += wxString::Format(wxT("%f peak, %f rms "), peak[i], rms[i]);
if (clipping[i])
output += wxString::Format(wxT("clipped "));
else
output += wxString::Format(wxT("no clip "));
output += wxString::Format(wxT("%i head, %i tail\n"), headPeakCount[i], tailPeakCount[i]);
}
return output;
}
wxString MeterUpdateMsg::toStringIfClipped()
{
for (int i = 0; i<kMaxMeterBars; i++)
{
if (clipping[i] || (headPeakCount[i] > 0) || (tailPeakCount[i] > 0))
return toString();
}
return wxT("");
}
//
// The Meter passes itself messages via this queue so that it can
// communicate between the audio thread and the GUI thread.
// This class is as simple as possible in order to be thread-safe
// without needing mutexes.
//
MeterUpdateQueue::MeterUpdateQueue(int maxLen):
mBufferSize(maxLen)
{
mBuffer = new MeterUpdateMsg[mBufferSize];
Clear();
}
// destructor
MeterUpdateQueue::~MeterUpdateQueue()
{
delete[] mBuffer;
}
void MeterUpdateQueue::Clear()
{
mStart = 0;
mEnd = 0;
}
// Add a message to the end of the queue. Return false if the
// queue was full.
bool MeterUpdateQueue::Put(MeterUpdateMsg &msg)
{
int len = (mEnd + mBufferSize - mStart) % mBufferSize;
// Never completely fill the queue, because then the
// state is ambiguous (mStart==mEnd)
if (len >= mBufferSize-1)
return false;
//wxLogDebug(wxT("Put: %s"), msg.toString().c_str());
mBuffer[mEnd] = msg;
mEnd = (mEnd+1)%mBufferSize;
return true;
}
// Get the next message from the start of the queue.
// Return false if the queue was empty.
bool MeterUpdateQueue::Get(MeterUpdateMsg &msg)
{
int len = (mEnd + mBufferSize - mStart) % mBufferSize;
if (len == 0)
return false;
msg = mBuffer[mStart];
mStart = (mStart+1)%mBufferSize;
return true;
}
//
// Meter class
//
enum {
OnMeterUpdateID = 6000,
OnDisableMeterID,
OnMonitorID,
OnHorizontalID,
OnAutomatedInputLevelAdjustmentID,
OnVerticalID,
OnMultiID,
OnEqualizerID,
OnWaveformID,
OnLinearID,
OnDBID,
OnClipID,
OnFloatID,
OnPreferencesID
};
BEGIN_EVENT_TABLE(Meter, wxPanel)
EVT_TIMER(OnMeterUpdateID, Meter::OnMeterUpdate)
EVT_MOUSE_EVENTS(Meter::OnMouse)
EVT_ERASE_BACKGROUND(Meter::OnErase)
EVT_PAINT(Meter::OnPaint)
EVT_SIZE(Meter::OnSize)
EVT_MENU(OnDisableMeterID, Meter::OnDisableMeter)
EVT_MENU(OnHorizontalID, Meter::OnHorizontal)
EVT_MENU(OnVerticalID, Meter::OnVertical)
EVT_MENU(OnMultiID, Meter::OnMulti)
EVT_MENU(OnEqualizerID, Meter::OnEqualizer)
EVT_MENU(OnWaveformID, Meter::OnWaveform)
EVT_MENU(OnLinearID, Meter::OnLinear)
EVT_MENU(OnDBID, Meter::OnDB)
EVT_MENU(OnClipID, Meter::OnClip)
EVT_MENU(OnMonitorID, Meter::OnMonitor)
#ifdef AUTOMATED_INPUT_LEVEL_ADJUSTMENT
EVT_MENU(OnAutomatedInputLevelAdjustmentID, Meter::OnAutomatedInputLevelAdjustment)
#endif
EVT_MENU(OnFloatID, Meter::OnFloat)
EVT_MENU(OnPreferencesID, Meter::OnPreferences)
END_EVENT_TABLE()
IMPLEMENT_CLASS(Meter, wxPanel)
Meter::Meter(wxWindow* parent, wxWindowID id,
bool isInput,
const wxPoint& pos /*= wxDefaultPosition*/,
const wxSize& size /*= wxDefaultSize*/,
Style style /*= HorizontalStereo*/,
float fDecayRate /*= 60.0f*/)
: wxPanel(parent, id, pos, size),
mQueue(1024),
mWidth(size.x), mHeight(size.y),
mIsInput(isInput),
mStyle(style),
mGradient(true),
mDB(true),
mDBRange(ENV_DB_RANGE),
mDecay(true),
mDecayRate(fDecayRate),
mClip(true),
mNumPeakSamplesToClip(3),
mPeakHoldDuration(3),
mT(0),
mRate(0),
mNumBars(0),
mLayoutValid(false),
mBitmap(NULL),
mIcon(NULL)
{
int i;
wxColour backgroundColour =
wxSystemSettings::GetColour(wxSYS_COLOUR_3DFACE);
mBkgndBrush = wxBrush(backgroundColour, wxSOLID);
UpdatePrefs();
mPeakPeakPen = wxPen(theTheme.Colour( clrMeterPeak), 1, wxSOLID);
mDisabledPen = wxPen(theTheme.Colour( clrMeterDisabledPen), 1, wxSOLID);
mLeftSize = wxSize(0, 0);
mRightSize = wxSize(0, 0);
if (mIsInput) {
mPen = wxPen( theTheme.Colour( clrMeterInputPen ), 1, wxSOLID);
mBrush = wxBrush( theTheme.Colour( clrMeterInputBrush ), wxSOLID);
mRMSBrush = wxBrush( theTheme.Colour( clrMeterInputRMSBrush ), wxSOLID);
mClipBrush = wxBrush( theTheme.Colour( clrMeterInputClipBrush ), wxSOLID);
mLightPen = wxPen( theTheme.Colour( clrMeterInputLightPen ), 1, wxSOLID);
mDarkPen = wxPen( theTheme.Colour( clrMeterInputDarkPen ), 1, wxSOLID);
}
else {
mPen = wxPen( theTheme.Colour( clrMeterOutputPen ), 1, wxSOLID);
mBrush = wxBrush( theTheme.Colour( clrMeterOutputBrush ), wxSOLID);
mRMSBrush = wxBrush( theTheme.Colour( clrMeterOutputRMSBrush ), wxSOLID);
mClipBrush = wxBrush( theTheme.Colour( clrMeterOutputClipBrush ), wxSOLID);
mLightPen = wxPen( theTheme.Colour( clrMeterOutputLightPen ), 1, wxSOLID);
mDarkPen = wxPen( theTheme.Colour( clrMeterOutputDarkPen ), 1, wxSOLID);
}
mDisabledBkgndBrush = wxBrush(theTheme.Colour( clrMeterDisabledBrush), wxSOLID);
// mDisabledBkgndBrush = wxBrush(
// wxSystemSettings::GetSystemColour(wxSYS_COLOUR_3DSHADOW), wxSOLID);
// wxSystemSettings::GetSystemColour(wxSYS_COLOUR_3DLIGHT), wxSOLID);
if (mMeterDisabled) {
mSavedBkgndBrush = mBkgndBrush;
mSavedBrush = mBrush;
mSavedRMSBrush = mRMSBrush;
mBkgndBrush = mDisabledBkgndBrush;
mBrush = mDisabledBkgndBrush;
mRMSBrush = mDisabledBkgndBrush;
}
// MixerTrackCluster style has no menu, so disallows SetStyle, so never needs icon.
if (mStyle != MixerTrackCluster)
CreateIcon(2);
mRuler.SetFonts(GetFont(), GetFont(), GetFont());
mTimer.SetOwner(this, OnMeterUpdateID);
// TODO: Yikes. Hard coded sample rate.
// JKC: I've looked at this, and it's benignish. It just means that the meter
// balistics are right for 44KHz and a bit more frisky than they should be
// for higher sample rates.
Reset(44100.0, true);
for(i=0; i<kMaxMeterBars; i++) {
mBar[i].clipping = false;
mBar[i].isclipping = false;
}
}
void Meter::Clear()
{
mQueue.Clear();
}
void Meter::CreateIcon(int WXUNUSED(aquaOffset))
{
/// \todo Remove wasteful delete/new pair. It is done in every call to layout.
if (mIcon) {
delete mIcon;
mIcon = NULL;
}
if(mIsInput)
{
/// JKC: !!!! If you pass theTheme.Bitmap(bmpMic) you get a white rather than a black mic.
/// Weird behaviour in wxWidgets, I guess.
mIcon = new wxBitmap(theTheme.Image( bmpMic ));
}
else
{
mIcon = new wxBitmap(theTheme.Image( bmpSpeaker ));
}
}
Meter::~Meter()
{
// LLL: This prevents a crash during termination if monitoring
// is active.
if (gAudioIO->IsMonitoring())
gAudioIO->StopStream();
if (mIcon)
delete mIcon;
if (mBitmap)
delete mBitmap;
}
void Meter::UpdatePrefs()
{
mDBRange = gPrefs->Read(wxT("/GUI/EnvdBRange"), ENV_DB_RANGE);
mMeterRefreshRate = gPrefs->Read(wxT("/Meter/MeterRefreshRate"), 30);
// MixerTrackCluster style has no menu, so disallows disabling the meter.
if (mStyle == MixerTrackCluster)
{
mMeterDisabled = 0L;
}
else
{
mStyle = gPrefs->Read(wxT("/Meter/MeterStyle"), wxT("HorizontalStereo")) == wxT("HorizontalStereo") ?
HorizontalStereo :
VerticalStereo;
mGradient = gPrefs->Read(wxT("/Meter/MeterBars"), wxT("Gradient")) == wxT("Gradient");
mDB = gPrefs->Read(wxT("/Meter/MeterType"), wxT("Linear")) == wxT("dB");
if (mIsInput)
{
mMeterDisabled = gPrefs->Read(wxT("/Meter/MeterInputDisabled"), (long)0);
}
else
{
mMeterDisabled = gPrefs->Read(wxT("/Meter/MeterOutputDisabled"), (long)0);
}
}
mTimer.Start(1000 / mMeterRefreshRate);
mLayoutValid = false;
Refresh(true);
}
void Meter::OnErase(wxEraseEvent & WXUNUSED(event))
{
// Ignore it to prevent flashing
}
void Meter::OnPaint(wxPaintEvent & WXUNUSED(event))
{
mLayoutValid = false;
wxPaintDC dc(this);
HandlePaint(dc);
}
void Meter::OnSize(wxSizeEvent & WXUNUSED(event))
{
GetClientSize(&mWidth, &mHeight);
mLayoutValid = false;
}
void Meter::OnMouse(wxMouseEvent &evt)
{
if (mStyle == MixerTrackCluster) // MixerTrackCluster style has no menu.
return;
#if wxUSE_TOOLTIPS // Not available in wxX11
if (evt.Leaving()){
GetActiveProject()->TP_DisplayStatusMessage(wxT(""));
}
else if (evt.Entering()) {
// Display the tooltip in the status bar
wxToolTip * pTip = this->GetToolTip();
if( pTip ) {
wxString tipText = pTip->GetTip();
GetActiveProject()->TP_DisplayStatusMessage(tipText);
}
}
#endif
if (evt.RightDown() ||
(evt.ButtonDown() && mMenuRect.Contains(evt.m_x, evt.m_y)))
{
wxMenu *menu = new wxMenu();
// Note: these should be kept in the same order as the enum
if (mMeterDisabled)
menu->Append(OnDisableMeterID, _("Enable Meter"));
else
menu->Append(OnDisableMeterID, _("Disable Meter"));
if (mIsInput) {
if (gAudioIO->IsMonitoring())
menu->Append(OnMonitorID, _("Stop Monitoring"));
else
menu->Append(OnMonitorID, _("Start Monitoring"));
#ifdef AUTOMATED_INPUT_LEVEL_ADJUSTMENT
if (gAudioIO->AILAIsActive())
menu->Append(OnAutomatedInputLevelAdjustmentID, _("Stop Automated Recording Level Adjustment"));
else
menu->Append(OnAutomatedInputLevelAdjustmentID, _("Start Automated Recording Level Adjustment"));
bool AVActive;
gPrefs->Read(wxT("/AudioIO/AutomatedInputLevelAdjustment"), &AVActive, false);
if (!AVActive || !GetActiveProject()->GetControlToolBar()->IsRecordDown())
menu->Enable(OnAutomatedInputLevelAdjustmentID, false);
#endif
}
menu->AppendSeparator();
//menu->Append(OnHorizontalID, _("Horizontal Stereo"));
//menu->Append(OnVerticalID, _("Vertical Stereo"));
//menu->Append(OnMultiID, _("Vertical Multichannel"));
//menu->Append(OnEqualizerID, _("Equalizer"));
//menu->Append(OnWaveformID, _("Waveform"));
//menu->Enable(OnHorizontalID + mStyle, false);
//menu->Enable(mStyle==VerticalStereo? OnVerticalID: OnHorizontalID, false);
//menu->AppendSeparator();
//menu->Append(OnLinearID, _("Linear"));
//menu->Append(OnDBID, _("dB"));
//menu->Enable(mDB? OnDBID: OnLinearID, false);
//menu->AppendSeparator();
//menu->Append(OnClipID, _("Turn on clipping"));
//menu->AppendSeparator();
//menu->Append(OnFloatID, _("Float Window"));
//menu->AppendSeparator();
menu->Append(OnPreferencesID, _("Preferences..."));
if (evt.RightDown())
PopupMenu(menu, evt.m_x, evt.m_y);
else
PopupMenu(menu, mMenuRect.x + 1, mMenuRect.y + mMenuRect.height + 1);
delete menu;
}
else if (evt.ButtonDown()) {
if (mIsInput)
StartMonitoring();
else {
Reset(mRate, true);
}
}
}
void Meter::SetStyle(Meter::Style newStyle)
{
// MixerTrackCluster disallows style change.
if (mStyle == MixerTrackCluster)
return;
mStyle = newStyle;
mLayoutValid = false;
Refresh(true);
}
void Meter::Reset(double sampleRate, bool resetClipping)
{
int j;
mT = 0;
mRate = sampleRate;
for(j=0; j<kMaxMeterBars; j++)
ResetBar(&mBar[j], resetClipping);
// wxTimers seem to be a little unreliable - sometimes they stop for
// no good reason, so this "primes" it every now and then...
mTimer.Stop();
// While it's stopped, empty the queue
mQueue.Clear();
mLayoutValid = false;
mTimer.Start(1000 / mMeterRefreshRate);
Refresh(false);
}
static float floatMax(float a, float b)
{
return a>b? a: b;
}
static int intmin(int a, int b)
{
return a<b? a: b;
}
static int intmax(int a, int b)
{
return a>b? a: b;
}
static float ClipZeroToOne(float z)
{
if (z > 1.0)
return 1.0;
else if (z < 0.0)
return 0.0;
else
return z;
}
static float ToDB(float v, float range)
{
double db;
if (v > 0)
db = 20 * log10(fabs(v));
else
db = -999;
return ClipZeroToOne((db + range) / range);
}
void Meter::UpdateDisplay(int numChannels, int numFrames, float *sampleData)
{
int i, j;
float *sptr = sampleData;
int num = intmin(numChannels, mNumBars);
MeterUpdateMsg msg;
msg.numFrames = numFrames;
for(j=0; j<mNumBars; j++) {
msg.peak[j] = 0;
msg.rms[j] = 0;
msg.clipping[j] = false;
msg.headPeakCount[j] = 0;
msg.tailPeakCount[j] = 0;
}
for(i=0; i<numFrames; i++) {
for(j=0; j<num; j++) {
msg.peak[j] = floatMax(msg.peak[j], fabs(sptr[j]));
msg.rms[j] += sptr[j]*sptr[j];
// In addition to looking for mNumPeakSamplesToClip peaked
// samples in a row, also send the number of peaked samples
// at the head and tail, in case there's a run of peaked samples
// that crosses block boundaries
if (fabs(sptr[j])>=MAX_AUDIO) {
if (msg.headPeakCount[j]==i)
msg.headPeakCount[j]++;
msg.tailPeakCount[j]++;
if (msg.tailPeakCount[j] > mNumPeakSamplesToClip)
msg.clipping[j] = true;
}
else
msg.tailPeakCount[j] = 0;
}
sptr += numChannels;
}
for(j=0; j<mNumBars; j++)
msg.rms[j] = sqrt(msg.rms[j]/numFrames);
mQueue.Put(msg);
}
// Vaughan, 2010-11-29: This not currently used. See comments in MixerTrackCluster::UpdateMeter().
//void Meter::UpdateDisplay(int numChannels, int numFrames,
// // Need to make these double-indexed arrays if we handle more than 2 channels.
// float* maxLeft, float* rmsLeft,
// float* maxRight, float* rmsRight,
// const sampleCount kSampleCount)
//{
// int i, j;
// int num = intmin(numChannels, mNumBars);
// MeterUpdateMsg msg;
//
// msg.numFrames = kSampleCount;
// for(j=0; j<mNumBars; j++) {
// msg.peak[j] = 0.0;
// msg.rms[j] = 0.0;
// msg.clipping[j] = false;
// msg.headPeakCount[j] = 0;
// msg.tailPeakCount[j] = 0;
// }
//
// for(i=0; i<numFrames; i++) {
// for(j=0; j<num; j++) {
// msg.peak[j] = floatMax(msg.peak[j], ((j == 0) ? maxLeft[i] : maxRight[i]));
// msg.rms[j] = floatMax(msg.rms[j], ((j == 0) ? rmsLeft[i] : rmsRight[i]));
//
// // In addition to looking for mNumPeakSamplesToClip peaked
// // samples in a row, also send the number of peaked samples
// // at the head and tail, in case there's a run
// // of peaked samples that crosses block boundaries.
// if (fabs((j == 0) ? maxLeft[i] : maxRight[i]) >= MAX_AUDIO)
// {
// if (msg.headPeakCount[j]==i)
// msg.headPeakCount[j]++;
// msg.tailPeakCount[j]++;
// if (msg.tailPeakCount[j] > mNumPeakSamplesToClip)
// msg.clipping[j] = true;
// }
// else
// msg.tailPeakCount[j] = 0;
// }
// }
//
// mQueue.Put(msg);
//}
void Meter::OnMeterUpdate(wxTimerEvent & WXUNUSED(event))
{
MeterUpdateMsg msg;
int numChanges = 0;
#ifdef AUTOMATED_INPUT_LEVEL_ADJUSTMENT
double maxPeak = 0.0;
bool discarded = false;
#endif
// There may have been several update messages since the last
// time we got to this function. Catch up to real-time by
// popping them off until there are none left. It is necessary
// to process all of them, otherwise we won't handle peaks and
// peak-hold bars correctly.
while(mQueue.Get(msg)) {
numChanges++;
double deltaT = msg.numFrames / mRate;
int j;
// <Why is this in the loop, rather than top of the method?
// Or just a condition on the following, so we pop all the msgs while disabled?>
if (mMeterDisabled)
return;
//wxLogDebug(wxT("Pop: %s"), msg.toString().c_str());
mT += deltaT;
for(j=0; j<mNumBars; j++) {
mBar[j].isclipping = false;
//
if (mDB) {
msg.peak[j] = ToDB(msg.peak[j], mDBRange);
msg.rms[j] = ToDB(msg.rms[j], mDBRange);
}
if (mDecay) {
if (mDB) {
float decayAmount = mDecayRate * deltaT / mDBRange;
mBar[j].peak = floatMax(msg.peak[j],
mBar[j].peak - decayAmount);
}
else {
double decayAmount = mDecayRate * deltaT;
double decayFactor = pow(10.0, -decayAmount/20);
mBar[j].peak = floatMax(msg.peak[j],
mBar[j].peak * decayFactor);
}
}
else
mBar[j].peak = msg.peak[j];
// This smooths out the RMS signal
float smooth = pow(0.9, (double)msg.numFrames/1024.0);
mBar[j].rms = mBar[j].rms * smooth + msg.rms[j] * (1.0 - smooth);
if (mT - mBar[j].peakHoldTime > mPeakHoldDuration ||
mBar[j].peak > mBar[j].peakHold) {
mBar[j].peakHold = mBar[j].peak;
mBar[j].peakHoldTime = mT;
}
if (mBar[j].peak > mBar[j].peakPeakHold )
mBar[j].peakPeakHold = mBar[j].peak;
if (msg.clipping[j] ||
mBar[j].tailPeakCount+msg.headPeakCount[j] >=
mNumPeakSamplesToClip){
mBar[j].clipping = true;
mBar[j].isclipping = true;
}
mBar[j].tailPeakCount = msg.tailPeakCount[j];
#ifdef AUTOMATED_INPUT_LEVEL_ADJUSTMENT
if (mT > gAudioIO->AILAGetLastDecisionTime()) {
discarded = false;
maxPeak = msg.peak[j] > maxPeak ? msg.peak[j] : maxPeak;
printf("%f@%f ", msg.peak[j], mT);
}
else {
discarded = true;
printf("%f@%f discarded\n", msg.peak[j], mT);
}
#endif
}
} // while
if (numChanges > 0) {
#ifdef AUTOMATED_INPUT_LEVEL_ADJUSTMENT
if (gAudioIO->AILAIsActive() && mIsInput && !discarded) {
gAudioIO->AILAProcess(maxPeak);
putchar('\n');
}
#endif
RepaintBarsNow();
}
}
float Meter::GetMaxPeak()
{
int j;
float maxPeak = 0.;
for(j=0; j<mNumBars; j++)
maxPeak = mBar[j].peak > maxPeak ? mBar[j].peak : maxPeak;
return(maxPeak);
}
double Meter::ToLinearIfDB(double value)
{
if (mDB)
value = pow(10.0, (-(1.0-value)*mDBRange)/20.0);
return value;
}
wxFont Meter::GetFont()
{
int fontSize = 10;
#if defined __WXMSW__
fontSize = 8;
#endif
return wxFont(fontSize, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL);
}
void Meter::ResetBar(MeterBar *b, bool resetClipping)
{
b->peak = 0.0;
b->rms = 0.0;
b->peakHold = 0.0;
b->peakHoldTime = 0.0;
if (resetClipping)
{
b->clipping = false;
b->peakPeakHold =0.0;
}
b->isclipping = false;
b->tailPeakCount = 0;
}
bool Meter::IsClipping()
{
for (int c = 0; c < kMaxMeterBars; c++)
if (mBar[c].isclipping)
return true;
return false;
}
void Meter::HandleLayout(wxDC &dc)
{
// Refresh to reflect any language changes
/* i18n-hint: One-letter abbreviation for Left, in VU Meter */
mLeftText = _("L"); // used in a couple of places in this method.
/* i18n-hint: One-letter abbreviation for Right, in VU Meter */
mRightText = _("R");
dc.SetFont(GetFont());
if (mLeftSize.x == 0) { // Not yet initialized to dc.
if (mStyle != MixerTrackCluster) // MixerTrackCluster style has no L/R labels.
{
dc.GetTextExtent(mLeftText, &mLeftSize.x, &mLeftSize.y);
dc.GetTextExtent(mRightText, &mRightSize.x, &mRightSize.y);
}
if ((mStyle == VerticalStereo) || (mStyle == MixerTrackCluster)) // VerticalMulti?
{
// For any vertical style, also need mRightSize big enough for Ruler width.
int rulerWidth;
int rulerHeight;
dc.GetTextExtent(wxT("-888"), &rulerWidth, &rulerHeight); // -888 is nice and wide.
if (mRightSize.x < rulerWidth)
mRightSize.x = rulerWidth;
}
}
int iconWidth = 0;
int iconHeight = 0;
int menuWidth = 0;
int menuHeight = 0;
if (mStyle != MixerTrackCluster)
{
iconWidth = mIcon->GetWidth();
iconHeight = mIcon->GetHeight();
menuWidth = 17;
menuHeight = 14;
}
int width = mWidth;
int height = mHeight;
int left = 0, top = 0;
int right, bottom;
int barw, barh;
int i;
mRuler.SetFlip(true);
mRuler.SetLabelEdges(true);
switch(mStyle) {
default:
wxPrintf(wxT("Style not handled yet!\n"));
break;
case VerticalStereo:
mMenuRect = wxRect(mWidth - menuWidth - 5, mHeight - menuHeight - 2,
menuWidth, menuHeight);
if (mHeight < (menuHeight + iconHeight + 8))
mIconPos = wxPoint(-999, -999); // Don't display
else
mIconPos = wxPoint(mWidth - iconWidth - 1, 1);
width = intmin(mWidth-(iconWidth+2), mWidth-(menuWidth+3));
// No break. Fall-through to MixerTrackCluster is intentional.
case MixerTrackCluster:
// Doesn't show menu, icon, or L/R labels,
// but otherwise like VerticalStereo.
if (width >= mLeftSize.x + mRightSize.x + 24) {
if (mStyle != MixerTrackCluster)
{
mLeftTextPos = wxPoint(2, height-2-mLeftSize.y);
left += mLeftSize.x+4;
}
mRightTextPos = wxPoint(width-mLeftSize.x, height-2-mLeftSize.y);
width -= mLeftSize.x + mRightSize.x + 8; //v ...but then -8 in UmixIt? -- for vertical only?
}
barw = (width-2)/2;
barh = height - 4;
mNumBars = 2;
mBar[0].vert = true;
ResetBar(&mBar[0], false);
mBar[0].r = wxRect(left + width/2 - barw - 1, 2, barw, barh);
if (mClip) {
mBar[0].rClip = mBar[0].r;
mBar[0].rClip.height = 3;
mBar[0].r.y += 4;
mBar[0].r.height -= 4;
}
mBar[1].vert = true;
ResetBar(&mBar[1], false);
mBar[1].r = wxRect(left + width/2 + 1, 2, barw, barh);
if (mClip) {
mBar[1].rClip = mBar[1].r;
mBar[1].rClip.height = 3;
mBar[1].r.y += 4;
mBar[1].r.height -= 4;
}
mRuler.SetOrientation(wxVERTICAL);
mRuler.SetBounds(mBar[1].r.x + mBar[1].r.width + 1,
mBar[1].r.y + 1,
mBar[1].r.x + width,
mBar[1].r.y + mBar[1].r.height - 1);
if (mDB) {
mRuler.SetRange(0, -mDBRange);
mRuler.SetFormat(Ruler::LinearDBFormat);
}
else {
mRuler.SetRange(1, 0);
mRuler.SetFormat(Ruler::RealFormat);
}
if (mStyle != MixerTrackCluster) // MixerTrackCluster style has no menu.
mRuler.OfflimitsPixels(mMenuRect.y-mBar[1].r.y, mBar[1].r.height);
break;
case HorizontalStereo:
if (mWidth < menuWidth + iconWidth + 8) {
mIconPos = wxPoint(-999, -999); // Don't display icon
mMenuRect = wxRect(2, mHeight - menuHeight - 2,
menuWidth, menuHeight);
}
else {
mIconPos = wxPoint(2, mHeight - iconHeight);
mMenuRect = wxRect(iconWidth + 2, mHeight - menuHeight - 5,
menuWidth, menuHeight);
}
height = intmin(height-(menuHeight+3), height-iconHeight) - 2;
left = 2 + intmax(mLeftSize.x, mRightSize.x);
width -= left;
mLeftTextPos = wxPoint(2, (height)/4 - mLeftSize.y/2);
mRightTextPos = wxPoint(2, (height*3)/4 - mLeftSize.y/2);
barw = width - 4;
barh = (height-2)/2;
mNumBars = 2;
mBar[0].vert = false;
ResetBar(&mBar[0], false);
mBar[0].r = wxRect(left+2, height/2 - barh - 1, barw, barh);
if (mClip) {
mBar[0].rClip = mBar[0].r;
mBar[0].rClip.x += mBar[0].rClip.width-3;
mBar[0].rClip.width = 3;
mBar[0].r.width -= 4;
}
mBar[1].vert = false;
ResetBar(&mBar[1], false);
mBar[1].r = wxRect(left+2, height/2 + 1, barw, barh);
if (mClip) {
mBar[1].rClip = mBar[1].r;
mBar[1].rClip.x += mBar[1].rClip.width-3;
mBar[1].rClip.width = 3;
mBar[1].r.width -= 4;
}
mRuler.SetOrientation(wxHORIZONTAL);
mRuler.SetBounds(mBar[1].r.x,
mBar[1].r.y + mBar[1].r.height + 1,
mBar[1].r.x + mBar[1].r.width,
mWidth);
if (mDB) {
mRuler.SetRange(-mDBRange, 0);
mRuler.SetFormat(Ruler::LinearDBFormat);
}
else {
mRuler.SetRange(0, 1);
mRuler.SetFormat(Ruler::RealFormat);
}
mRuler.OfflimitsPixels(0, mMenuRect.x+mMenuRect.width-4);
break;
case Waveform:
mNumBars = 0;
break;
}
if (mNumBars > 0) {
// Compute bounding rectangle of all bars (to save time when
// blitting just the bars to the screen)
left = mBar[0].r.x;
top = mBar[0].r.y;
right = mBar[0].r.x + mBar[0].r.width;
bottom = mBar[0].r.y + mBar[0].r.height;
for(i=1; i<mNumBars; i++) {
left = intmin(left, mBar[i].r.x);
top = intmin(top, mBar[i].r.y);
right = intmax(right, mBar[i].r.x + mBar[i].r.width);
bottom = intmax(bottom, mBar[i].r.y + mBar[i].r.height);
left = intmin(left, mBar[i].rClip.x);
top = intmin(top, mBar[i].rClip.y);
right = intmax(right, mBar[i].rClip.x + mBar[i].rClip.width);
bottom = intmax(bottom, mBar[i].rClip.y + mBar[i].rClip.height);
}
mAllBarsRect = wxRect(left, top, right-left+1, bottom-top+1);
}
// MixerTrackCluster style has no popup, so disallows SetStyle, so never needs icon.
if (mStyle != MixerTrackCluster)
CreateIcon(mIconPos.y % 4);
mLayoutValid = true;
}
void Meter::HandlePaint(wxDC &destDC)
{
if (!mLayoutValid)
{
// Get rid of previous predrawn bitmap
if (mBitmap)
{
delete mBitmap;
}
// Create a new one using current size and select into the DC
mBitmap = new wxBitmap(mWidth, mHeight);
wxMemoryDC dc;
dc.SelectObject(*mBitmap);
// Go calculate all of the layout metrics
HandleLayout(dc);
// Start with a clean background
// LLL: Should research USE_AQUA_THEME usefulness...
#ifndef USE_AQUA_THEME
#ifdef EXPERIMENTAL_THEMING
if( !mMeterDisabled )
{
mBkgndBrush.SetColour( GetParent()->GetBackgroundColour() );
}
#endif
dc.SetPen(*wxTRANSPARENT_PEN);
dc.SetBrush(mBkgndBrush);
dc.DrawRectangle(0, 0, mWidth, mHeight);
#endif
// MixerTrackCluster style has no icon or menu.
if (mStyle != MixerTrackCluster)
{
dc.DrawBitmap(*mIcon, mIconPos.x, mIconPos.y, true);
// Draws a beveled button and a down pointing triangle.
// The style and sizing matches the ones in the title
// bar of the waveform left-hand-side panels.
wxRect r = mMenuRect;
AColor::Bevel(dc, true, r);
dc.SetBrush(*wxBLACK_BRUSH);
dc.SetPen(*wxBLACK_PEN);
AColor::Arrow(dc,
r.x + 3,
r.y + 5,
10);
}
// Draw the ruler
// LLL: Why test the number of bars? Not a very good meter without bars...
if (mNumBars > 0)
{
mRuler.Draw(dc);
}
// MixerTrackCluster style has no L/R labels.
if (mStyle != MixerTrackCluster)
{
dc.SetFont(GetFont());
dc.DrawText(mLeftText, mLeftTextPos.x, mLeftTextPos.y);
dc.DrawText(mRightText, mRightTextPos.x, mRightTextPos.y);
}
// Setup the colors for the 3 sections of the meter bars
wxColor green(117, 215, 112);
wxColor yellow(255, 255, 0);
wxColor red(255, 0, 0);
// Draw the meter bars at maximum levels
for (int i = 0; i < mNumBars; i++)
{
// Cache bar rect
wxRect r = mBar[i].r;
if (mGradient)
{
// Calculate the size of the two gradiant segments of the meter
double gradw;
double gradh;
if (mDB)
{
gradw = (double) r.GetWidth() / mDBRange * 6.0;
gradh = (double) r.GetHeight() / mDBRange * 6.0;
}
else
{
gradw = (double) r.GetWidth() / 100 * 25;
gradh = (double) r.GetHeight() / 100 * 25;
}
if (mBar[i].vert)
{
// Draw the "critical" segment (starts at top of meter and works down)
r.SetHeight(gradh);
dc.GradientFillLinear(r, red, yellow, wxSOUTH);
// Draw the "warning" segment
r.SetTop(r.GetBottom());
dc.GradientFillLinear(r, yellow, green, wxSOUTH);
// Draw the "safe" segment
r.SetTop(r.GetBottom());
r.SetBottom(mBar[i].r.GetBottom());
dc.SetPen(*wxTRANSPARENT_PEN);
dc.SetBrush(green);
dc.DrawRectangle(r);
}
else
{
// Draw the "safe" segment
r.SetWidth(r.GetWidth() - (int) (gradw + gradw + 0.5));
dc.SetPen(*wxTRANSPARENT_PEN);
dc.SetBrush(green);
dc.DrawRectangle(r);
// Draw the "warning" segment
r.SetLeft(r.GetRight() + 1);
r.SetWidth(floor(gradw));
dc.GradientFillLinear(r, green, yellow);
// Draw the "critical" segment
r.SetLeft(r.GetRight() + 1);
r.SetRight(mBar[i].r.GetRight());
dc.GradientFillLinear(r, yellow, red);
}
}
// Give it a recessed look
AColor::Bevel(dc, false, mBar[i].r);
}
// Copy predrawn bitmap to the dest DC
destDC.Blit(0, 0, mWidth, mHeight, &dc, 0, 0);
// And unselect it from the source DC
dc.SelectObject(wxNullBitmap);
}
// Go draw the meter bars using current levels
for (int i = 0; i < mNumBars; i++)
{
DrawMeterBar(destDC, &mBar[i]);
}
}
void Meter::RepaintBarsNow()
{
if (mLayoutValid)
{
wxDC *dc;
wxClientDC clientDC(this);
#if defined(__WXMAC__)
// OSX is already double buffered and using our own buffer
// will cause text to be rendered incorrectly.
dc = &clientDC;
#else
wxBufferedDC bufDC(&clientDC, *mBitmap);
dc = &bufDC;
#endif
for (int i = 0; i < mNumBars; i++)
{
DrawMeterBar(*dc, &mBar[i]);
}
}
}
void Meter::DrawMeterBar(wxDC &dc, MeterBar *meterBar)
{
// Cache some metrics (and adjust to be inside the bevel)
wxRect r = meterBar->r;
wxCoord x = r.GetLeft() + 1;
wxCoord y = r.GetTop() + 1;
wxCoord w = r.GetWidth() - 1;
wxCoord h = r.GetHeight() - 1;
// Map the predrawn bitmap into the source DC
wxMemoryDC srcDC;
srcDC.SelectObject(*mBitmap);
// Setup for erasing the background
dc.SetPen(*wxTRANSPARENT_PEN);
dc.SetBrush(mBkgndBrush);
int ht;
int wd;
if (mGradient)
{
if (meterBar->vert)
{
// Copy as much of the predrawn meter bar as is required for the
// current peak.
ht = (int)(meterBar->peak * h + 0.5);
dc.Blit(x, y + h - ht, w, ht, &srcDC, x, y + h - ht);
// Blank out the rest
dc.DrawRectangle(x, y, w, y + h - ht);
// Draw the "recent" peak hold line using the predrawn meter bar so that
// it will be the same color as the original level.
ht = (int)(meterBar->peakHold * h + 0.5);
dc.Blit(x, y + h - ht - 1, w, 2, &srcDC, x, y + h - ht - 1);
// Draw the "maximum" peak hold line
dc.SetPen(mPeakPeakPen);
ht = (int)(meterBar->peakPeakHold * h + 0.5);
AColor::Line(dc, x, y + h - ht - 1, x + w - 1, y + h - ht - 1);
if (ht > 1)
{
AColor::Line(dc, x, y + h - ht, x + w - 1, y + h - ht);
}
}
else
{
// Copy as much of the predrawn meter bar as is required for the
// current peak. But, only blit() if there's something to copy
// to prevent display corruption.
wd = (int)(meterBar->peak * w + 0.5);
if (wd)
dc.Blit(x, y, wd, h, &srcDC, x, y);
// Blank out the rest
if (w - wd)
dc.DrawRectangle(x + wd, y, w - wd, h);
// Draw the "recent" peak hold line using the predrawn meter bar so that
// it will be the same color as the original level.
wd = (int)(meterBar->peakHold * w + 0.5);
dc.Blit(wd, y, 2, h, &srcDC, wd, y);
// Draw the "maximum" peak hold line using a themed color.
dc.SetPen(mPeakPeakPen);
wd = (int)(meterBar->peakPeakHold * w + 0.5);
AColor::Line(dc, x + wd, y, x + wd, y + h - 1);
if (wd > 1)
{
AColor::Line(dc, x + wd - 1, y, x + wd - 1, y + h - 1);
}
}
}
else
{
wxRect rRMS;
if (meterBar->vert)
{
// Calculate the peak and rms rectangles
ht = (int)(meterBar->peak * h + 0.5);
r = wxRect(x, y + h - ht, w, ht);
ht = (int)(meterBar->rms * h + 0.5);
rRMS = wxRect(x, y + h - ht, w, ht);
// Blank out the rest
dc.DrawRectangle(x, y, w, y + h - ht);
// Reset the colors
dc.SetBrush(*wxTRANSPARENT_BRUSH);
dc.SetPen(mPen);
// Draw the "recent" peak hold line
int ht = (int)(meterBar->peakHold * h + 0.5);
AColor::Line(dc, x, y + h - ht - 1, x + w - 1, y + h - ht - 1);
if (ht > 1)
{
AColor::Line(dc, x, y + h - ht - 1, x + w - 1, y + h - ht);
}
// Draw the "maximum" peak hold line
dc.SetPen(mPeakPeakPen);
ht = (int)(meterBar->peakPeakHold * h + 0.5);
AColor::Line(dc, x, y + h - ht - 1, x + w - 1, y + h - ht - 1);
if (ht > 1)
{
AColor::Line(dc, x, y + h - ht, x + w - 1, y + h - ht);
}
}
else
{
// Calculate the peak and rms rectangles
wd = (int)(meterBar->peak * w + 0.5);
r = wxRect(x, y, wd, h);
wd = (int)(meterBar->rms * w + 0.5);
rRMS = wxRect(x, y, wd, h);
// Blank out the rest
dc.DrawRectangle(x + wd, y, w, h);
// Reset the colors
dc.SetBrush(*wxTRANSPARENT_BRUSH);
dc.SetPen(mPen);
// Draw the "recent" peak hold line
wd = (int)(meterBar->peakHold * w + 0.5);
AColor::Line(dc, x + wd, y, x + wd, y + h - 1);
if (wd > 1)
{
AColor::Line(dc, x + wd - 1, y, x + wd - 1, y + h - 1);
}
// Draw the "maximum" peak hold line using a themed color
dc.SetPen(mPeakPeakPen);
wd = (int)(meterBar->peakPeakHold * w + 0.5);
AColor::Line(dc, x + wd, y, x + wd, y + h - 1);
if (wd > 1)
{
AColor::Line(dc, x + wd - 1, y, x + wd - 1, y + h - 1);
}
}
// Draw the peak and rms levels
dc.SetPen(*wxTRANSPARENT_PEN);
dc.SetBrush(mBrush);
dc.DrawRectangle(r);
dc.SetBrush(mRMSBrush);
dc.DrawRectangle(rRMS);
}
// If meter had a clipping indicator, draw or erase it
// LLL: At least I assume that's what "mClip" is supposed to be for as
// it is always "true".
if (mClip)
{
if (meterBar->clipping)
{
dc.SetBrush(mClipBrush);
}
else
{
dc.SetBrush(mBkgndBrush);
}
dc.SetPen(*wxTRANSPARENT_PEN);
dc.DrawRectangle(meterBar->rClip);
dc.SetBrush(*wxTRANSPARENT_BRUSH);
AColor::Bevel(dc, false, meterBar->rClip);
}
// No longer need the source DC, so unselect the predrawn bitmap
srcDC.SelectObject(wxNullBitmap);
}
bool Meter::IsMeterDisabled()
{
return mMeterDisabled != 0;
}
void Meter::StartMonitoring()
{
if (gAudioIO->IsMonitoring())
gAudioIO->StopStream();
else {
if (mMeterDisabled){
wxCommandEvent dummy;
OnDisableMeter(dummy);
}
AudacityProject *p = GetActiveProject();
if (p) {
gAudioIO->StartMonitoring(p->GetRate());
Meter *play, *record;
MeterToolBars::GetMeters( &play, &record );
gAudioIO->SetMeters(record, play);
}
}
}
//
// Pop-up menu handlers
//
void Meter::OnDisableMeter(wxCommandEvent & WXUNUSED(event))
{
if (mMeterDisabled) //Enable
{
mLightPen = mSavedLightPen;
mDarkPen = mSavedDarkPen;
mBkgndBrush = mSavedBkgndBrush;
mBrush = mSavedBrush;
mRMSBrush = mSavedRMSBrush;
mBkgndBrush = mSavedBkgndBrush;
mLightPen = mSavedLightPen;
Refresh(false);
mMeterDisabled = false;
}
else
{
if (mIsInput)
{
if (gAudioIO->IsMonitoring())
{
gAudioIO->StopStream();
}
}
mSavedLightPen = mLightPen;
mSavedDarkPen = mDarkPen;
mSavedBkgndBrush = mBkgndBrush;
mSavedBrush = mBrush;
mSavedRMSBrush = mRMSBrush;
mLightPen = mDisabledPen;
mDarkPen = mDisabledPen;
mBkgndBrush = mDisabledBkgndBrush;
mBrush = mDisabledBkgndBrush;
mRMSBrush = mDisabledBkgndBrush;
mLayoutValid = false;
Refresh(false);
mMeterDisabled = true;
}
if (mIsInput)
{
gPrefs->Write(wxT("/Meter/MeterInputDisabled"), mMeterDisabled);
}
else
{
gPrefs->Write(wxT("/Meter/MeterOutputDisabled"), mMeterDisabled);
}
gPrefs->Flush();
}
void Meter::OnHorizontal(wxCommandEvent & WXUNUSED(event))
{
SetStyle(HorizontalStereo);
}
void Meter::OnVertical(wxCommandEvent & WXUNUSED(event))
{
SetStyle(VerticalStereo);
}
void Meter::OnMulti(wxCommandEvent & WXUNUSED(event))
{
SetStyle(VerticalMulti);
}
void Meter::OnEqualizer(wxCommandEvent & WXUNUSED(event))
{
SetStyle(Equalizer);
}
void Meter::OnWaveform(wxCommandEvent & WXUNUSED(event))
{
SetStyle(Waveform);
}
void Meter::OnLinear(wxCommandEvent & WXUNUSED(event))
{
mDB = false;
mLayoutValid = false;
Refresh(false);
}
void Meter::OnDB(wxCommandEvent & WXUNUSED(event))
{
mDB = true;
mLayoutValid = false;
Refresh(false);
}
void Meter::OnClip(wxCommandEvent & WXUNUSED(event))
{
}
void Meter::OnMonitor(wxCommandEvent & WXUNUSED(event))
{
StartMonitoring();
}
#ifdef AUTOMATED_INPUT_LEVEL_ADJUSTMENT
void Meter::OnAutomatedInputLevelAdjustment(wxCommandEvent &evt)
{
if (gAudioIO->AILAIsActive()) {
gAudioIO->AILADisable();
AudacityProject *p = GetActiveProject();
if (p) p->TP_DisplayStatusMessage(_("Automated Recording Level Adjustment stopped as requested by user."));
}
else
gAudioIO->AILAInitialize();
}
#endif
void Meter::OnFloat(wxCommandEvent & WXUNUSED(event))
{
}
void Meter::OnPreferences(wxCommandEvent & WXUNUSED(event))
{
wxTextCtrl *rate;
wxRadioButton *gradient;
wxRadioButton *rms;
wxRadioButton *db;
wxRadioButton *linear;
wxRadioButton *horizontal;
wxRadioButton *vertical;
wxDialog dlg(GetParent(), wxID_ANY, wxString(_("Meter Preferences")));
ShuttleGui S(&dlg, eIsCreating);
S.StartVerticalLay();
{
S.StartStatic(_("Refresh Rate"), 0);
{
S.AddFixedText(_("Higher refresh rates make the meter show more frequent\nchanges. A rate of 30 per second or less should prevent\nthe meter affecting audio quality on slower machines."));
S.StartHorizontalLay();
{
rate = S.AddTextBox(_("Meter refresh rate per second [1-100]: "),
wxString::Format(wxT("%d"), (int) mMeterRefreshRate),
10);
rate->SetName(_("Meter refresh rate per second [1-100]"));
wxIntegerValidator<long> vld(&mMeterRefreshRate);
vld.SetRange(0, 100);
rate->SetValidator(vld);
}
S.EndHorizontalLay();
}
S.EndStatic();
S.StartHorizontalLay();
{
S.StartStatic(_("Meter Style"), 0);
{
S.StartVerticalLay();
{
gradient = S.AddRadioButton(_("Gradient"));
gradient->SetName(_("Gradient"));
gradient->SetValue(mGradient);
rms = S.AddRadioButtonToGroup(_("RMS"));
rms->SetName(_("RMS"));
rms->SetValue(!mGradient);
}
S.EndVerticalLay();
}
S.EndStatic();
S.StartStatic(_("Meter Type"), 0);
{
S.StartVerticalLay();
{
db = S.AddRadioButton(_("dB"));
db->SetName(_("dB"));
db->SetValue(mDB);
linear = S.AddRadioButtonToGroup(_("Linear"));
linear->SetName(_("Linear"));
linear->SetValue(!mDB);
}
S.EndVerticalLay();
}
S.EndStatic();
S.StartStatic(_("Orientation"), 1);
{
S.StartVerticalLay();
{
horizontal = S.AddRadioButton(_("Horizontal"));
horizontal->SetName(_("Horizontal"));
horizontal->SetValue(mStyle == HorizontalStereo);
vertical = S.AddRadioButtonToGroup(_("Vertical"));
vertical->SetName(_("Vertical"));
vertical->SetValue(mStyle == VerticalStereo);
}
S.EndVerticalLay();
}
S.EndStatic();
}
S.EndHorizontalLay();
S.AddStandardButtons();
}
S.EndVerticalLay();
dlg.Layout();
dlg.Fit();
dlg.CenterOnParent();
if (dlg.ShowModal() == wxID_OK)
{
gPrefs->Write(wxT("/Meter/MeterRefreshRate"), mMeterRefreshRate);
gPrefs->Write(wxT("/Meter/MeterStyle"), horizontal->GetValue() ? wxT("HorizontalStereo") : wxT("VerticalStereo"));
gPrefs->Write(wxT("/Meter/MeterBars"), gradient->GetValue() ? wxT("Gradient") : wxT("RMS"));
gPrefs->Write(wxT("/Meter/MeterType"), db->GetValue() ? wxT("dB") : wxT("Linear"));
gPrefs->Flush();
wxCommandEvent e(EVT_METER_PREFERENCES_CHANGED);
e.SetEventObject(this);
GetParent()->GetEventHandler()->ProcessEvent(e);
}
}