1 /**********************************************************************
2
3 Audacity: A Digital Audio Editor
4
5 MeterPanel.cpp
6
7 Dominic Mazzoni
8 Vaughan Johnson
9
10 2004.06.25 refresh rate limited to 30mS, by ChackoN
11
12 *******************************************************************//**
13
14 \class MeterPanel
15 \brief VU Meter, for displaying recording/playback level
16
17 This is a bunch of common code that can display many different
18 forms of VU meters and other displays.
19
20 But note that a lot of later code here assumes these are
21 MeterToolBar meters, e.g., MeterPanel::StartMonitoring,
22 so these are not as generic/common as originally intended.
23
24 *//****************************************************************//**
25
26 \class MeterBar
27 \brief A struct used by MeterPanel to hold the position of one bar.
28
29 *//****************************************************************//**
30
31 \class MeterUpdateMsg
32 \brief Message used to update the MeterPanel
33
34 *//****************************************************************//**
35
36 \class MeterUpdateQueue
37 \brief Queue of MeterUpdateMsg used to feed the MeterPanel.
38
39 *//******************************************************************/
40
41 #include "MeterPanel.h"
42
43 #include <algorithm>
44 #include <wx/setup.h> // for wxUSE_* macros
45 #include <wx/wxcrtvararg.h>
46 #include <wx/app.h>
47 #include <wx/defs.h>
48 #include <wx/dialog.h>
49 #include <wx/dcbuffer.h>
50 #include <wx/frame.h>
51 #include <wx/image.h>
52 #include <wx/intl.h>
53 #include <wx/menu.h>
54 #include <wx/settings.h>
55 #include <wx/textdlg.h>
56 #include <wx/numdlg.h>
57 #include <wx/radiobut.h>
58 #include <wx/tooltip.h>
59
60 #include <math.h>
61
62 #include "../AudioIO.h"
63 #include "AColor.h"
64 #include "../widgets/BasicMenu.h"
65 #include "ImageManipulation.h"
66 #include "Decibels.h"
67 #include "Project.h"
68 #include "../ProjectAudioManager.h"
69 #include "ProjectStatus.h"
70 #include "../ProjectWindows.h"
71 #include "Prefs.h"
72 #include "../ShuttleGui.h"
73 #include "Theme.h"
74 #include "../widgets/wxWidgetsWindowPlacement.h"
75
76 #include "AllThemeResources.h"
77 #include "../widgets/valnum.h"
78
79 #if wxUSE_ACCESSIBILITY
80 #include "WindowAccessible.h"
81
82 class MeterAx final : public WindowAccessible
83 {
84 public:
85 MeterAx(wxWindow * window);
86
87 virtual ~ MeterAx();
88
89 // Performs the default action. childId is 0 (the action for this object)
90 // or > 0 (the action for a child).
91 // Return wxACC_NOT_SUPPORTED if there is no default action for this
92 // window (e.g. an edit control).
93 wxAccStatus DoDefaultAction(int childId) override;
94
95 // Retrieves the address of an IDispatch interface for the specified child.
96 // All objects must support this property.
97 wxAccStatus GetChild(int childId, wxAccessible** child) override;
98
99 // Gets the number of children.
100 wxAccStatus GetChildCount(int* childCount) override;
101
102 // Gets the default action for this object (0) or > 0 (the action for a child).
103 // Return wxACC_OK even if there is no action. actionName is the action, or the empty
104 // string if there is no action.
105 // The retrieved string describes the action that is performed on an object,
106 // not what the object does as a result. For example, a toolbar button that prints
107 // a document has a default action of "Press" rather than "Prints the current document."
108 wxAccStatus GetDefaultAction(int childId, wxString *actionName) override;
109
110 // Returns the description for this object or a child.
111 wxAccStatus GetDescription(int childId, wxString *description) override;
112
113 // Gets the window with the keyboard focus.
114 // If childId is 0 and child is NULL, no object in
115 // this subhierarchy has the focus.
116 // If this object has the focus, child should be 'this'.
117 wxAccStatus GetFocus(int *childId, wxAccessible **child) override;
118
119 // Returns help text for this object or a child, similar to tooltip text.
120 wxAccStatus GetHelpText(int childId, wxString *helpText) override;
121
122 // Returns the keyboard shortcut for this object or child.
123 // Return e.g. ALT+K
124 wxAccStatus GetKeyboardShortcut(int childId, wxString *shortcut) override;
125
126 // Returns the rectangle for this object (id = 0) or a child element (id > 0).
127 // rect is in screen coordinates.
128 wxAccStatus GetLocation(wxRect& rect, int elementId) override;
129
130 // Gets the name of the specified object.
131 wxAccStatus GetName(int childId, wxString *name) override;
132
133 // Returns a role constant.
134 wxAccStatus GetRole(int childId, wxAccRole *role) override;
135
136 // Gets a variant representing the selected children
137 // of this object.
138 // Acceptable values:
139 // - a null variant (IsNull() returns TRUE)
140 // - a list variant (GetType() == wxT("list"))
141 // - an integer representing the selected child element,
142 // or 0 if this object is selected (GetType() == wxT("long"))
143 // - a "void*" pointer to a wxAccessible child object
144 wxAccStatus GetSelections(wxVariant *selections) override;
145
146 // Returns a state constant.
147 wxAccStatus GetState(int childId, long* state) override;
148
149 // Returns a localized string representing the value for the object
150 // or child.
151 wxAccStatus GetValue(int childId, wxString* strValue) override;
152
153 };
154
155 #endif // wxUSE_ACCESSIBILITY
156
157 static const long MIN_REFRESH_RATE = 1;
158 static const long MAX_REFRESH_RATE = 100;
159
160 /* Updates to the meter are passed across via meter updates, each contained in
161 * a MeterUpdateMsg object */
toString()162 wxString MeterUpdateMsg::toString()
163 {
164 wxString output; // somewhere to build up a string in
165 output = wxString::Format(wxT("Meter update msg: %i channels, %i samples\n"), \
166 kMaxMeterBars, numFrames);
167 for (int i = 0; i<kMaxMeterBars; i++)
168 { // for each channel of the meters
169 output += wxString::Format(wxT("%f peak, %f rms "), peak[i], rms[i]);
170 if (clipping[i])
171 output += wxString::Format(wxT("clipped "));
172 else
173 output += wxString::Format(wxT("no clip "));
174 output += wxString::Format(wxT("%i head, %i tail\n"), headPeakCount[i], tailPeakCount[i]);
175 }
176 return output;
177 }
178
toStringIfClipped()179 wxString MeterUpdateMsg::toStringIfClipped()
180 {
181 for (int i = 0; i<kMaxMeterBars; i++)
182 {
183 if (clipping[i] || (headPeakCount[i] > 0) || (tailPeakCount[i] > 0))
184 return toString();
185 }
186 return wxT("");
187 }
188
189 //
190 // The MeterPanel passes itself messages via this queue so that it can
191 // communicate between the audio thread and the GUI thread.
192 // This class is as simple as possible in order to be thread-safe
193 // without needing mutexes.
194 //
195
MeterUpdateQueue(size_t maxLen)196 MeterUpdateQueue::MeterUpdateQueue(size_t maxLen):
197 mBufferSize(maxLen)
198 {
199 Clear();
200 }
201
202 // destructor
~MeterUpdateQueue()203 MeterUpdateQueue::~MeterUpdateQueue()
204 {
205 }
206
Clear()207 void MeterUpdateQueue::Clear()
208 {
209 mStart = 0;
210 mEnd = 0;
211 }
212
213 // Add a message to the end of the queue. Return false if the
214 // queue was full.
Put(MeterUpdateMsg & msg)215 bool MeterUpdateQueue::Put(MeterUpdateMsg &msg)
216 {
217 // mStart can be greater than mEnd because it is all mod mBufferSize
218 wxASSERT( (mEnd + mBufferSize - mStart) >= 0 );
219 int len = (mEnd + mBufferSize - mStart) % mBufferSize;
220
221 // Never completely fill the queue, because then the
222 // state is ambiguous (mStart==mEnd)
223 if (len + 1 >= (int)(mBufferSize))
224 return false;
225
226 //wxLogDebug(wxT("Put: %s"), msg.toString());
227
228 mBuffer[mEnd] = msg;
229 mEnd = (mEnd+1)%mBufferSize;
230
231 return true;
232 }
233
234 // Get the next message from the start of the queue.
235 // Return false if the queue was empty.
Get(MeterUpdateMsg & msg)236 bool MeterUpdateQueue::Get(MeterUpdateMsg &msg)
237 {
238 int len = (mEnd + mBufferSize - mStart) % mBufferSize;
239
240 if (len == 0)
241 return false;
242
243 msg = mBuffer[mStart];
244 mStart = (mStart+1)%mBufferSize;
245
246 return true;
247 }
248
249 //
250 // MeterPanel class
251 //
252
253 #include "../../images/SpeakerMenu.xpm"
254 #include "../../images/MicMenu.xpm"
255
256 // How many pixels between items?
257 const static int gap = 2;
258
259 const static wxChar *PrefStyles[] =
260 {
261 wxT("AutomaticStereo"),
262 wxT("HorizontalStereo"),
263 wxT("VerticalStereo")
264 };
265
266 enum {
267 OnMeterUpdateID = 6000,
268 OnMonitorID,
269 OnPreferencesID
270 };
271
BEGIN_EVENT_TABLE(MeterPanel,MeterPanelBase)272 BEGIN_EVENT_TABLE(MeterPanel, MeterPanelBase)
273 EVT_TIMER(OnMeterUpdateID, MeterPanel::OnMeterUpdate)
274 EVT_MOUSE_EVENTS(MeterPanel::OnMouse)
275 EVT_CONTEXT_MENU(MeterPanel::OnContext)
276 EVT_KEY_DOWN(MeterPanel::OnKeyDown)
277 EVT_KEY_UP(MeterPanel::OnKeyUp)
278 EVT_SET_FOCUS(MeterPanel::OnSetFocus)
279 EVT_KILL_FOCUS(MeterPanel::OnKillFocus)
280 EVT_ERASE_BACKGROUND(MeterPanel::OnErase)
281 EVT_PAINT(MeterPanel::OnPaint)
282 EVT_SIZE(MeterPanel::OnSize)
283 EVT_MENU(OnMonitorID, MeterPanel::OnMonitor)
284 EVT_MENU(OnPreferencesID, MeterPanel::OnPreferences)
285 END_EVENT_TABLE()
286
287 IMPLEMENT_CLASS(MeterPanel, wxPanelWrapper)
288
289 MeterPanel::MeterPanel(AudacityProject *project,
290 wxWindow* parent, wxWindowID id,
291 bool isInput,
292 const wxPoint& pos /*= wxDefaultPosition*/,
293 const wxSize& size /*= wxDefaultSize*/,
294 Style style /*= HorizontalStereo*/,
295 float fDecayRate /*= 60.0f*/)
296 : MeterPanelBase(parent, id, pos, size, wxTAB_TRAVERSAL | wxNO_BORDER | wxWANTS_CHARS),
297 mProject(project),
298 mQueue(1024),
299 mWidth(size.x),
300 mHeight(size.y),
301 mIsInput(isInput),
302 mDesiredStyle(style),
303 mGradient(true),
304 mDB(true),
305 mDBRange(DecibelScaleCutoff.GetDefault()),
306 mDecay(true),
307 mDecayRate(fDecayRate),
308 mClip(true),
309 mNumPeakSamplesToClip(3),
310 mPeakHoldDuration(3),
311 mT(0),
312 mRate(0),
313 mMonitoring(false),
314 mActive(false),
315 mNumBars(0),
316 mLayoutValid(false),
317 mBitmap{},
318 mIcon{},
319 mAccSilent(false)
320 {
321 // i18n-hint: Noun (the meter is used for playback or record level monitoring)
322 SetName( XO("Meter") );
323 // Suppress warnings about the header file
324 wxUnusedVar(SpeakerMenu_xpm);
325 wxUnusedVar(MicMenu_xpm);
326 wxUnusedVar(PrefStyles);
327
328 mStyle = mDesiredStyle;
329
330 mIsFocused = false;
331
332 #if wxUSE_ACCESSIBILITY
333 SetAccessible(safenew MeterAx(this));
334 #endif
335
336 // Do this BEFORE UpdatePrefs()!
337 mRuler.SetFonts(GetFont(), GetFont(), GetFont());
338 mRuler.SetFlip(mStyle != MixerTrackCluster);
339 mRuler.SetLabelEdges(true);
340 //mRuler.SetTickColour( wxColour( 0,0,255 ) );
341
342 UpdatePrefs();
343
344 wxColour backgroundColour = theTheme.Colour( clrMedium);
345 mBkgndBrush = wxBrush(backgroundColour, wxBRUSHSTYLE_SOLID);
346 SetBackgroundColour( backgroundColour );
347
348 mPeakPeakPen = wxPen(theTheme.Colour( clrMeterPeak), 1, wxPENSTYLE_SOLID);
349 mDisabledPen = wxPen(theTheme.Colour( clrMeterDisabledPen), 1, wxPENSTYLE_SOLID);
350
351 if (mIsInput) {
352 wxTheApp->Bind(EVT_AUDIOIO_MONITOR,
353 &MeterPanel::OnAudioIOStatus,
354 this);
355 wxTheApp->Bind(EVT_AUDIOIO_CAPTURE,
356 &MeterPanel::OnAudioIOStatus,
357 this);
358
359 mPen = wxPen( theTheme.Colour( clrMeterInputPen ), 1, wxPENSTYLE_SOLID);
360 mBrush = wxBrush( theTheme.Colour( clrMeterInputBrush ), wxBRUSHSTYLE_SOLID);
361 mRMSBrush = wxBrush( theTheme.Colour( clrMeterInputRMSBrush ), wxBRUSHSTYLE_SOLID);
362 mClipBrush = wxBrush( theTheme.Colour( clrMeterInputClipBrush ), wxBRUSHSTYLE_SOLID);
363 // mLightPen = wxPen( theTheme.Colour( clrMeterInputLightPen ), 1, wxSOLID);
364 // mDarkPen = wxPen( theTheme.Colour( clrMeterInputDarkPen ), 1, wxSOLID);
365 }
366 else {
367 // Register for AudioIO events
368 wxTheApp->Bind(EVT_AUDIOIO_PLAYBACK,
369 &MeterPanel::OnAudioIOStatus,
370 this);
371
372 mPen = wxPen( theTheme.Colour( clrMeterOutputPen ), 1, wxPENSTYLE_SOLID);
373 mBrush = wxBrush( theTheme.Colour( clrMeterOutputBrush ), wxBRUSHSTYLE_SOLID);
374 mRMSBrush = wxBrush( theTheme.Colour( clrMeterOutputRMSBrush ), wxBRUSHSTYLE_SOLID);
375 mClipBrush = wxBrush( theTheme.Colour( clrMeterOutputClipBrush ), wxBRUSHSTYLE_SOLID);
376 // mLightPen = wxPen( theTheme.Colour( clrMeterOutputLightPen ), 1, wxSOLID);
377 // mDarkPen = wxPen( theTheme.Colour( clrMeterOutputDarkPen ), 1, wxSOLID);
378 }
379
380 // mDisabledBkgndBrush = wxBrush(theTheme.Colour( clrMeterDisabledBrush), wxSOLID);
381 // No longer show a difference in the background colour when not monitoring.
382 // We have the tip instead.
383 mDisabledBkgndBrush = mBkgndBrush;
384
385 // MixerTrackCluster style has no menu, so disallows SetStyle, so never needs icon.
386 if (mStyle != MixerTrackCluster)
387 {
388 if(mIsInput)
389 {
390 //mIcon = NEW wxBitmap(MicMenuNarrow_xpm);
391 mIcon = std::make_unique<wxBitmap>(wxBitmap(theTheme.Bitmap(bmpMic)));
392 }
393 else
394 {
395 //mIcon = NEW wxBitmap(SpeakerMenuNarrow_xpm);
396 mIcon = std::make_unique<wxBitmap>(wxBitmap(theTheme.Bitmap(bmpSpeaker)));
397 }
398 }
399
400 mTimer.SetOwner(this, OnMeterUpdateID);
401 // TODO: Yikes. Hard coded sample rate.
402 // JKC: I've looked at this, and it's benignish. It just means that the meter
403 // balistics are right for 44KHz and a bit more frisky than they should be
404 // for higher sample rates.
405 Reset(44100.0, true);
406 }
407
Clear()408 void MeterPanel::Clear()
409 {
410 mQueue.Clear();
411 }
412
UpdatePrefs()413 void MeterPanel::UpdatePrefs()
414 {
415 mDBRange = DecibelScaleCutoff.Read();
416
417 mMeterRefreshRate =
418 std::max(MIN_REFRESH_RATE, std::min(MAX_REFRESH_RATE,
419 gPrefs->Read(Key(wxT("RefreshRate")), 30)));
420 mGradient = gPrefs->Read(Key(wxT("Bars")), wxT("Gradient")) == wxT("Gradient");
421 mDB = gPrefs->Read(Key(wxT("Type")), wxT("dB")) == wxT("dB");
422 mMeterDisabled = gPrefs->Read(Key(wxT("Disabled")), (long)0);
423
424 if (mDesiredStyle != MixerTrackCluster)
425 {
426 wxString style = gPrefs->Read(Key(wxT("Style")));
427 if (style == wxT("AutomaticStereo"))
428 {
429 mDesiredStyle = AutomaticStereo;
430 }
431 else if (style == wxT("HorizontalStereo"))
432 {
433 mDesiredStyle = HorizontalStereo;
434 }
435 else if (style == wxT("VerticalStereo"))
436 {
437 mDesiredStyle = VerticalStereo;
438 }
439 else
440 {
441 mDesiredStyle = AutomaticStereo;
442 }
443 }
444
445 // Set the desired orientation (resets ruler orientation)
446 SetActiveStyle(mDesiredStyle);
447
448 // Reset to ensure NEW size is retrieved when language changes
449 mLeftSize = wxSize(0, 0);
450 mRightSize = wxSize(0, 0);
451
452 Reset(mRate, false);
453
454 mLayoutValid = false;
455
456 Refresh(false);
457 }
458
MeterPrefsID()459 static int MeterPrefsID()
460 {
461 static int value = wxNewId();
462 return value;
463 }
464
UpdateSelectedPrefs(int id)465 void MeterPanel::UpdateSelectedPrefs(int id)
466 {
467 if (id == MeterPrefsID())
468 UpdatePrefs();
469 }
470
OnErase(wxEraseEvent & WXUNUSED (event))471 void MeterPanel::OnErase(wxEraseEvent & WXUNUSED(event))
472 {
473 // Ignore it to prevent flashing
474 }
475
OnPaint(wxPaintEvent & WXUNUSED (event))476 void MeterPanel::OnPaint(wxPaintEvent & WXUNUSED(event))
477 {
478 #if defined(__WXMAC__)
479 auto paintDC = std::make_unique<wxPaintDC>(this);
480 #else
481 std::unique_ptr<wxDC> paintDC{ wxAutoBufferedPaintDCFactory(this) };
482 #endif
483 wxDC & destDC = *paintDC;
484 wxColour clrText = theTheme.Colour( clrTrackPanelText );
485 wxColour clrBoxFill = theTheme.Colour( clrMedium );
486
487 if (mLayoutValid == false || (mStyle == MixerTrackCluster ))
488 {
489 // Create a NEW one using current size and select into the DC
490 mBitmap = std::make_unique<wxBitmap>();
491 mBitmap->Create(mWidth, mHeight, destDC);
492 wxMemoryDC dc;
493 dc.SelectObject(*mBitmap);
494
495 // Go calculate all of the layout metrics
496 HandleLayout(dc);
497
498 // Start with a clean background
499 // LLL: Should research USE_AQUA_THEME usefulness...
500 //#ifndef USE_AQUA_THEME
501 #ifdef EXPERIMENTAL_THEMING
502 //if( !mMeterDisabled )
503 //{
504 // mBkgndBrush.SetColour( GetParent()->GetBackgroundColour() );
505 //}
506 #endif
507
508 mBkgndBrush.SetColour( GetBackgroundColour() );
509 dc.SetPen(*wxTRANSPARENT_PEN);
510 dc.SetBrush(mBkgndBrush);
511 dc.DrawRectangle(0, 0, mWidth, mHeight);
512 //#endif
513
514 // MixerTrackCluster style has no icon or L/R labels
515 if (mStyle != MixerTrackCluster)
516 {
517 bool highlight = InIcon();
518 dc.DrawBitmap( theTheme.Bitmap( highlight ?
519 bmpHiliteUpButtonSmall : bmpUpButtonSmall ),
520 mIconRect.GetPosition(), false );
521
522 dc.DrawBitmap(*mIcon, mIconRect.GetPosition(), true);
523 dc.SetFont(GetFont());
524 dc.SetTextForeground( clrText );
525 dc.SetTextBackground( clrBoxFill );
526 dc.DrawText(mLeftText, mLeftTextPos.x, mLeftTextPos.y);
527 dc.DrawText(mRightText, mRightTextPos.x, mRightTextPos.y);
528 }
529
530 // Setup the colors for the 3 sections of the meter bars
531 wxColor green(117, 215, 112);
532 wxColor yellow(255, 255, 0);
533 wxColor red(255, 0, 0);
534
535 // Bug #2473 - (Sort of) Hack to make text on meters more
536 // visible with darker backgrounds. It would be better to have
537 // different colors entirely and as part of the theme.
538 if (GetBackgroundColour().GetLuminance() < 0.25)
539 {
540 green = wxColor(117-100, 215-100, 112-100);
541 yellow = wxColor(255-100, 255-100, 0);
542 red = wxColor(255-100, 0, 0);
543 }
544 else if (GetBackgroundColour().GetLuminance() < 0.50)
545 {
546 green = wxColor(117-50, 215-50, 112-50);
547 yellow = wxColor(255-50, 255-50, 0);
548 red = wxColor(255-50, 0, 0);
549 }
550
551 // Draw the meter bars at maximum levels
552 for (unsigned int i = 0; i < mNumBars; i++)
553 {
554 // Give it a recessed look
555 AColor::Bevel(dc, false, mBar[i].b);
556
557 // Draw the clip indicator bevel
558 if (mClip)
559 {
560 AColor::Bevel(dc, false, mBar[i].rClip);
561 }
562
563 // Cache bar rect
564 wxRect r = mBar[i].r;
565
566 if (mGradient)
567 {
568 // Calculate the size of the two gradiant segments of the meter
569 double gradw;
570 double gradh;
571 if (mDB)
572 {
573 gradw = (double) r.GetWidth() / mDBRange * 6.0;
574 gradh = (double) r.GetHeight() / mDBRange * 6.0;
575 }
576 else
577 {
578 gradw = (double) r.GetWidth() / 100 * 25;
579 gradh = (double) r.GetHeight() / 100 * 25;
580 }
581
582 if (mBar[i].vert)
583 {
584 // Draw the "critical" segment (starts at top of meter and works down)
585 r.SetHeight(gradh);
586 dc.GradientFillLinear(r, red, yellow, wxSOUTH);
587
588 // Draw the "warning" segment
589 r.SetTop(r.GetBottom());
590 dc.GradientFillLinear(r, yellow, green, wxSOUTH);
591
592 // Draw the "safe" segment
593 r.SetTop(r.GetBottom());
594 r.SetBottom(mBar[i].r.GetBottom());
595 dc.SetPen(*wxTRANSPARENT_PEN);
596 dc.SetBrush(green);
597 dc.DrawRectangle(r);
598 }
599 else
600 {
601 // Draw the "safe" segment
602 r.SetWidth(r.GetWidth() - (int) (gradw + gradw + 0.5));
603 dc.SetPen(*wxTRANSPARENT_PEN);
604 dc.SetBrush(green);
605 dc.DrawRectangle(r);
606
607 // Draw the "warning" segment
608 r.SetLeft(r.GetRight() + 1);
609 r.SetWidth(floor(gradw));
610 dc.GradientFillLinear(r, green, yellow);
611
612 // Draw the "critical" segment
613 r.SetLeft(r.GetRight() + 1);
614 r.SetRight(mBar[i].r.GetRight());
615 dc.GradientFillLinear(r, yellow, red);
616 }
617 #ifdef EXPERIMENTAL_METER_LED_STYLE
618 if (!mBar[i].vert)
619 {
620 wxRect r = mBar[i].r;
621 wxPen BackgroundPen;
622 BackgroundPen.SetColour( wxSystemSettings::GetColour(wxSYS_COLOUR_3DFACE) );
623 dc.SetPen( BackgroundPen );
624 int i;
625 for(i=0;i<r.width;i++)
626 {
627 // 2 pixel spacing between the LEDs
628 if( (i%7)<2 ){
629 AColor::Line( dc, i+r.x, r.y, i+r.x, r.y+r.height );
630 } else {
631 // The LEDs have triangular ends.
632 // This code shapes the ends.
633 int j = abs( (i%7)-4);
634 AColor::Line( dc, i+r.x, r.y, i+r.x, r.y+j +1);
635 AColor::Line( dc, i+r.x, r.y+r.height-j, i+r.x, r.y+r.height );
636 }
637 }
638 }
639 #endif
640 }
641 }
642 mRuler.SetTickColour( clrText );
643 dc.SetTextForeground( clrText );
644 // Draw the ruler
645 #ifndef EXPERIMENTAL_DA
646 mRuler.Draw(dc);
647 #endif
648
649 // Bitmap created...unselect
650 dc.SelectObject(wxNullBitmap);
651 }
652
653 // Copy predrawn bitmap to the dest DC
654 destDC.DrawBitmap(*mBitmap, 0, 0);
655
656 // Go draw the meter bars, Left & Right channels using current levels
657 for (unsigned int i = 0; i < mNumBars; i++)
658 {
659 DrawMeterBar(destDC, &mBar[i]);
660 }
661
662 destDC.SetTextForeground( clrText );
663
664 #ifndef EXPERIMENTAL_DA
665 // We can have numbers over the bars, in which case we have to draw them each time.
666 if (mStyle == HorizontalStereoCompact || mStyle == VerticalStereoCompact)
667 {
668 mRuler.SetTickColour( clrText );
669 // If the text colour is too similar to the meter colour, then we need a background
670 // for the text. We require a total of at least one full-scale RGB difference.
671 int d = theTheme.ColourDistance( clrText, theTheme.Colour( clrMeterOutputRMSBrush ) );
672 if( d < 256 )
673 {
674 destDC.SetBackgroundMode( wxSOLID );
675 destDC.SetTextBackground( clrBoxFill );
676 }
677 mRuler.Draw(destDC);
678 }
679 #endif
680
681 // Let the user know they can click to start monitoring
682 if( mIsInput && !mActive )
683 {
684 destDC.SetFont( GetFont() );
685
686 wxArrayStringEx texts{
687 _("Click to Start Monitoring") ,
688 _("Click for Monitoring") ,
689 _("Click to Start") ,
690 _("Click") ,
691 };
692
693 for( size_t i = 0, cnt = texts.size(); i < cnt; i++ )
694 {
695 wxString Text = wxT(" ") + texts[i] + wxT(" ");
696 wxSize Siz = destDC.GetTextExtent( Text );
697 Siz.SetWidth( Siz.GetWidth() + gap );
698 Siz.SetHeight( Siz.GetHeight() + gap );
699
700 if( mBar[0].vert)
701 {
702 if( Siz.GetWidth() < mBar[0].r.GetHeight() )
703 {
704 wxRect r( mBar[1].b.GetLeft() - (int) (Siz.GetHeight() / 2.0) + 0.5,
705 mBar[0].r.GetTop() + (int) ((mBar[0].r.GetHeight() - Siz.GetWidth()) / 2.0) + 0.5,
706 Siz.GetHeight(),
707 Siz.GetWidth() );
708
709 destDC.SetBrush( wxBrush( clrBoxFill ) );
710 destDC.SetPen( *wxWHITE_PEN );
711 destDC.DrawRectangle( r );
712 destDC.SetBackgroundMode( wxTRANSPARENT );
713 r.SetTop( r.GetBottom() + (gap / 2) );
714 destDC.SetTextForeground( clrText );
715 destDC.DrawRotatedText( Text, r.GetPosition(), 90 );
716 break;
717 }
718 }
719 else
720 {
721 if( Siz.GetWidth() < mBar[0].r.GetWidth() )
722 {
723 wxRect r( mBar[0].r.GetLeft() + (int) ((mBar[0].r.GetWidth() - Siz.GetWidth()) / 2.0) + 0.5,
724 mBar[1].b.GetTop() - (int) (Siz.GetHeight() / 2.0) + 0.5,
725 Siz.GetWidth(),
726 Siz.GetHeight() );
727
728 destDC.SetBrush( wxBrush( clrBoxFill ) );
729 destDC.SetPen( *wxWHITE_PEN );
730 destDC.DrawRectangle( r );
731 destDC.SetBackgroundMode( wxTRANSPARENT );
732 r.SetLeft( r.GetLeft() + (gap / 2) );
733 r.SetTop( r.GetTop() + (gap / 2));
734 destDC.SetTextForeground( clrText );
735 destDC.DrawText( Text, r.GetPosition() );
736 break;
737 }
738 }
739 }
740 }
741
742 if (mIsFocused)
743 {
744 wxRect r = mIconRect;
745 AColor::DrawFocus(destDC, r.Inflate(1, 1));
746 }
747 }
748
OnSize(wxSizeEvent & WXUNUSED (event))749 void MeterPanel::OnSize(wxSizeEvent & WXUNUSED(event))
750 {
751 GetClientSize(&mWidth, &mHeight);
752
753 mLayoutValid = false;
754 Refresh();
755 }
756
InIcon(wxMouseEvent * pEvent) const757 bool MeterPanel::InIcon(wxMouseEvent *pEvent) const
758 {
759 auto point = pEvent ? pEvent->GetPosition() : ScreenToClient(::wxGetMousePosition());
760 return mIconRect.Contains(point);
761 }
762
OnMouse(wxMouseEvent & evt)763 void MeterPanel::OnMouse(wxMouseEvent &evt)
764 {
765 bool shouldHighlight = InIcon(&evt);
766 if ((evt.GetEventType() == wxEVT_MOTION || evt.Entering() || evt.Leaving()) &&
767 (mHighlighted != shouldHighlight)) {
768 mHighlighted = shouldHighlight;
769 mLayoutValid = false;
770 Refresh();
771 }
772
773 if (mStyle == MixerTrackCluster) // MixerTrackCluster style has no menu.
774 return;
775
776 #if wxUSE_TOOLTIPS // Not available in wxX11
777 if (evt.Leaving()){
778 ProjectStatus::Get( *mProject ).Set({});
779 }
780 else if (evt.Entering()) {
781 // Display the tooltip in the status bar
782 wxToolTip * pTip = this->GetToolTip();
783 if( pTip ) {
784 auto tipText = Verbatim( pTip->GetTip() );
785 ProjectStatus::Get( *mProject ).Set(tipText);
786 }
787 }
788 #endif
789
790 if (evt.RightDown() ||
791 (evt.ButtonDown() && InIcon(&evt)))
792 {
793 wxMenu menu;
794 // Note: these should be kept in the same order as the enum
795 if (mIsInput) {
796 wxMenuItem *mi;
797 if (mMonitoring)
798 mi = menu.Append(OnMonitorID, _("Stop Monitoring"));
799 else
800 mi = menu.Append(OnMonitorID, _("Start Monitoring"));
801 mi->Enable(!mActive || mMonitoring);
802 }
803
804 menu.Append(OnPreferencesID, _("Options..."));
805
806 if (evt.RightDown()) {
807 ShowMenu(evt.GetPosition());
808 }
809 else {
810 ShowMenu(wxPoint(mIconRect.x + 1, mIconRect.y + mIconRect.height + 1));
811 }
812 }
813 else if (evt.LeftDown()) {
814 if (mIsInput) {
815 if (mActive && !mMonitoring) {
816 Reset(mRate, true);
817 }
818 else {
819 StartMonitoring();
820 }
821 }
822 else {
823 Reset(mRate, true);
824 }
825 }
826 }
827
OnContext(wxContextMenuEvent & evt)828 void MeterPanel::OnContext(wxContextMenuEvent &evt)
829 {
830 #if defined(__WXMSW__)
831 if (mHadKeyDown)
832 #endif
833 if (mStyle != MixerTrackCluster) // MixerTrackCluster style has no menu.
834 {
835 ShowMenu(wxPoint(mIconRect.x + 1, mIconRect.y + mIconRect.height + 1));
836 }
837 else
838 {
839 evt.Skip();
840 }
841
842 #if defined(__WXMSW__)
843 mHadKeyDown = false;
844 #endif
845 }
846
OnKeyDown(wxKeyEvent & evt)847 void MeterPanel::OnKeyDown(wxKeyEvent &evt)
848 {
849 switch (evt.GetKeyCode())
850 {
851 // These are handled in the OnKeyUp handler because, on Windows at least, the
852 // key up event will be passed on to the menu if we show it here. This causes
853 // the default sound to be heard if assigned.
854 //
855 // But, again on Windows, when the user selects a menu item, it is handled by
856 // the menu and the key up event is passed along to our OnKeyUp() handler, so
857 // we have to ignore it, otherwise we'd just show the menu again.
858 case WXK_RETURN:
859 case WXK_NUMPAD_ENTER:
860 case WXK_WINDOWS_MENU:
861 case WXK_MENU:
862 #if defined(__WXMSW__)
863 mHadKeyDown = true;
864 #endif
865 break;
866 case WXK_RIGHT:
867 Navigate(wxNavigationKeyEvent::IsForward);
868 break;
869 case WXK_LEFT:
870 Navigate(wxNavigationKeyEvent::IsBackward);
871 break;
872 case WXK_TAB:
873 if (evt.ShiftDown())
874 Navigate(wxNavigationKeyEvent::IsBackward);
875 else
876 Navigate(wxNavigationKeyEvent::IsForward);
877 break;
878 default:
879 evt.Skip();
880 break;
881 }
882 }
883
OnKeyUp(wxKeyEvent & evt)884 void MeterPanel::OnKeyUp(wxKeyEvent &evt)
885 {
886 switch (evt.GetKeyCode())
887 {
888 case WXK_RETURN:
889 case WXK_NUMPAD_ENTER:
890 #if defined(__WXMSW__)
891 if (mHadKeyDown)
892 #endif
893 if (mStyle != MixerTrackCluster) // MixerTrackCluster style has no menu.
894 {
895 ShowMenu(wxPoint(mIconRect.x + 1, mIconRect.y + mIconRect.height + 1));
896 }
897 #if defined(__WXMSW__)
898 mHadKeyDown = false;
899 #endif
900 break;
901 default:
902 evt.Skip();
903 break;
904 }
905 }
906
OnSetFocus(wxFocusEvent & WXUNUSED (evt))907 void MeterPanel::OnSetFocus(wxFocusEvent & WXUNUSED(evt))
908 {
909 mIsFocused = true;
910 Refresh(false);
911 }
912
OnKillFocus(wxFocusEvent & WXUNUSED (evt))913 void MeterPanel::OnKillFocus(wxFocusEvent & WXUNUSED(evt))
914 {
915 mIsFocused = false;
916 Refresh(false);
917 }
918
SetStyle(Style newStyle)919 void MeterPanel::SetStyle(Style newStyle)
920 {
921 if (mStyle != newStyle && mDesiredStyle == AutomaticStereo)
922 {
923 SetActiveStyle(newStyle);
924
925 mLayoutValid = false;
926
927 Refresh(false);
928 }
929 }
930
Reset(double sampleRate,bool resetClipping)931 void MeterPanel::Reset(double sampleRate, bool resetClipping)
932 {
933 mT = 0;
934 mRate = sampleRate;
935 for (int j = 0; j < kMaxMeterBars; j++)
936 {
937 ResetBar(&mBar[j], resetClipping);
938 }
939
940 // wxTimers seem to be a little unreliable - sometimes they stop for
941 // no good reason, so this "primes" it every now and then...
942 mTimer.Stop();
943
944 // While it's stopped, empty the queue
945 mQueue.Clear();
946
947 mLayoutValid = false;
948
949 mTimer.Start(1000 / mMeterRefreshRate);
950
951 Refresh(false);
952 }
953
floatMax(float a,float b)954 static float floatMax(float a, float b)
955 {
956 return a>b? a: b;
957 }
958
959 /* Unused as yet.
960 static int intmin(int a, int b)
961 {
962 return a<b? a: b;
963 }
964 */
965
intmax(int a,int b)966 static int intmax(int a, int b)
967 {
968 return a>b? a: b;
969 }
970
ClipZeroToOne(float z)971 static float ClipZeroToOne(float z)
972 {
973 if (z > 1.0)
974 return 1.0;
975 else if (z < 0.0)
976 return 0.0;
977 else
978 return z;
979 }
980
ToDB(float v,float range)981 static float ToDB(float v, float range)
982 {
983 double db;
984 if (v > 0)
985 db = LINEAR_TO_DB(fabs(v));
986 else
987 db = -999;
988 return ClipZeroToOne((db + range) / range);
989 }
990
UpdateDisplay(unsigned numChannels,int numFrames,const float * sampleData)991 void MeterPanel::UpdateDisplay(
992 unsigned numChannels, int numFrames, const float *sampleData)
993 {
994 auto sptr = sampleData;
995 auto num = std::min(numChannels, mNumBars);
996 MeterUpdateMsg msg;
997
998 memset(&msg, 0, sizeof(msg));
999 msg.numFrames = numFrames;
1000
1001 for(int i=0; i<numFrames; i++) {
1002 for(unsigned int j=0; j<num; j++) {
1003 msg.peak[j] = floatMax(msg.peak[j], fabs(sptr[j]));
1004 msg.rms[j] += sptr[j]*sptr[j];
1005
1006 // In addition to looking for mNumPeakSamplesToClip peaked
1007 // samples in a row, also send the number of peaked samples
1008 // at the head and tail, in case there's a run of peaked samples
1009 // that crosses block boundaries
1010 if (fabs(sptr[j])>=MAX_AUDIO) {
1011 if (msg.headPeakCount[j]==i)
1012 msg.headPeakCount[j]++;
1013 msg.tailPeakCount[j]++;
1014 if (msg.tailPeakCount[j] > mNumPeakSamplesToClip)
1015 msg.clipping[j] = true;
1016 }
1017 else
1018 msg.tailPeakCount[j] = 0;
1019 }
1020 sptr += numChannels;
1021 }
1022 for(unsigned int j=0; j<mNumBars; j++)
1023 msg.rms[j] = sqrt(msg.rms[j]/numFrames);
1024
1025 mQueue.Put(msg);
1026 }
1027
1028 // Vaughan, 2010-11-29: This not currently used. See comments in MixerTrackCluster::UpdateMeter().
1029 //void MeterPanel::UpdateDisplay(int numChannels, int numFrames,
1030 // // Need to make these double-indexed arrays if we handle more than 2 channels.
1031 // float* maxLeft, float* rmsLeft,
1032 // float* maxRight, float* rmsRight,
1033 // const size_t kSampleCount)
1034 //{
1035 // int i, j;
1036 // int num = intmin(numChannels, mNumBars);
1037 // MeterUpdateMsg msg;
1038 //
1039 // msg.numFrames = kSampleCount;
1040 // for(j=0; j<mNumBars; j++) {
1041 // msg.peak[j] = 0.0;
1042 // msg.rms[j] = 0.0;
1043 // msg.clipping[j] = false;
1044 // msg.headPeakCount[j] = 0;
1045 // msg.tailPeakCount[j] = 0;
1046 // }
1047 //
1048 // for(i=0; i<numFrames; i++) {
1049 // for(j=0; j<num; j++) {
1050 // msg.peak[j] = floatMax(msg.peak[j], ((j == 0) ? maxLeft[i] : maxRight[i]));
1051 // msg.rms[j] = floatMax(msg.rms[j], ((j == 0) ? rmsLeft[i] : rmsRight[i]));
1052 //
1053 // // In addition to looking for mNumPeakSamplesToClip peaked
1054 // // samples in a row, also send the number of peaked samples
1055 // // at the head and tail, in case there's a run
1056 // // of peaked samples that crosses block boundaries.
1057 // if (fabs((j == 0) ? maxLeft[i] : maxRight[i]) >= MAX_AUDIO)
1058 // {
1059 // if (msg.headPeakCount[j]==i)
1060 // msg.headPeakCount[j]++;
1061 // msg.tailPeakCount[j]++;
1062 // if (msg.tailPeakCount[j] > mNumPeakSamplesToClip)
1063 // msg.clipping[j] = true;
1064 // }
1065 // else
1066 // msg.tailPeakCount[j] = 0;
1067 // }
1068 // }
1069 //
1070 // mQueue.Put(msg);
1071 //}
1072
OnMeterUpdate(wxTimerEvent & WXUNUSED (event))1073 void MeterPanel::OnMeterUpdate(wxTimerEvent & WXUNUSED(event))
1074 {
1075 MeterUpdateMsg msg;
1076 int numChanges = 0;
1077 #ifdef EXPERIMENTAL_AUTOMATED_INPUT_LEVEL_ADJUSTMENT
1078 double maxPeak = 0.0;
1079 bool discarded = false;
1080 #endif
1081
1082 // We shouldn't receive any events if the meter is disabled, but clear it to be safe
1083 if (mMeterDisabled) {
1084 mQueue.Clear();
1085 return;
1086 }
1087
1088 auto gAudioIO = AudioIO::Get();
1089
1090 // There may have been several update messages since the last
1091 // time we got to this function. Catch up to real-time by
1092 // popping them off until there are none left. It is necessary
1093 // to process all of them, otherwise we won't handle peaks and
1094 // peak-hold bars correctly.
1095 while(mQueue.Get(msg)) {
1096 numChanges++;
1097 double deltaT = msg.numFrames / mRate;
1098
1099 mT += deltaT;
1100 for(unsigned int j=0; j<mNumBars; j++) {
1101 mBar[j].isclipping = false;
1102
1103 //
1104 if (mDB) {
1105 msg.peak[j] = ToDB(msg.peak[j], mDBRange);
1106 msg.rms[j] = ToDB(msg.rms[j], mDBRange);
1107 }
1108
1109 if (mDecay) {
1110 if (mDB) {
1111 float decayAmount = mDecayRate * deltaT / mDBRange;
1112 mBar[j].peak = floatMax(msg.peak[j],
1113 mBar[j].peak - decayAmount);
1114 }
1115 else {
1116 double decayAmount = mDecayRate * deltaT;
1117 double decayFactor = DB_TO_LINEAR(-decayAmount);
1118 mBar[j].peak = floatMax(msg.peak[j],
1119 mBar[j].peak * decayFactor);
1120 }
1121 }
1122 else
1123 mBar[j].peak = msg.peak[j];
1124
1125 // This smooths out the RMS signal
1126 float smooth = pow(0.9, (double)msg.numFrames/1024.0);
1127 mBar[j].rms = mBar[j].rms * smooth + msg.rms[j] * (1.0 - smooth);
1128
1129 if (mT - mBar[j].peakHoldTime > mPeakHoldDuration ||
1130 mBar[j].peak > mBar[j].peakHold) {
1131 mBar[j].peakHold = mBar[j].peak;
1132 mBar[j].peakHoldTime = mT;
1133 }
1134
1135 if (mBar[j].peak > mBar[j].peakPeakHold )
1136 mBar[j].peakPeakHold = mBar[j].peak;
1137
1138 if (msg.clipping[j] ||
1139 mBar[j].tailPeakCount+msg.headPeakCount[j] >=
1140 mNumPeakSamplesToClip){
1141 mBar[j].clipping = true;
1142 mBar[j].isclipping = true;
1143 }
1144
1145 mBar[j].tailPeakCount = msg.tailPeakCount[j];
1146 #ifdef EXPERIMENTAL_AUTOMATED_INPUT_LEVEL_ADJUSTMENT
1147 if (mT > gAudioIO->AILAGetLastDecisionTime()) {
1148 discarded = false;
1149 maxPeak = msg.peak[j] > maxPeak ? msg.peak[j] : maxPeak;
1150 wxPrintf("%f@%f ", msg.peak[j], mT);
1151 }
1152 else {
1153 discarded = true;
1154 wxPrintf("%f@%f discarded\n", msg.peak[j], mT);
1155 }
1156 #endif
1157 }
1158 } // while
1159
1160 if (numChanges > 0) {
1161 #ifdef EXPERIMENTAL_AUTOMATED_INPUT_LEVEL_ADJUSTMENT
1162 if (gAudioIO->AILAIsActive() && mIsInput && !discarded) {
1163 gAudioIO->AILAProcess(maxPeak);
1164 putchar('\n');
1165 }
1166 #endif
1167 RepaintBarsNow();
1168 }
1169 }
1170
GetMaxPeak() const1171 float MeterPanel::GetMaxPeak() const
1172 {
1173 float maxPeak = 0.;
1174
1175 for(unsigned int j=0; j<mNumBars; j++)
1176 maxPeak = mBar[j].peak > maxPeak ? mBar[j].peak : maxPeak;
1177
1178 return(maxPeak);
1179 }
1180
GetFont() const1181 wxFont MeterPanel::GetFont() const
1182 {
1183 int fontSize = 10;
1184 #if defined __WXMSW__
1185 fontSize = 8;
1186 #endif
1187
1188 return wxFont(fontSize, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL);
1189 }
1190
ResetBar(MeterBar * b,bool resetClipping)1191 void MeterPanel::ResetBar(MeterBar *b, bool resetClipping)
1192 {
1193 b->peak = 0.0;
1194 b->rms = 0.0;
1195 b->peakHold = 0.0;
1196 b->peakHoldTime = 0.0;
1197 if (resetClipping)
1198 {
1199 b->clipping = false;
1200 b->peakPeakHold = 0.0;
1201 }
1202 b->isclipping = false;
1203 b->tailPeakCount = 0;
1204 }
1205
IsClipping() const1206 bool MeterPanel::IsClipping() const
1207 {
1208 for (int c = 0; c < kMaxMeterBars; c++)
1209 if (mBar[c].isclipping)
1210 return true;
1211 return false;
1212 }
1213
SetActiveStyle(Style newStyle)1214 void MeterPanel::SetActiveStyle(Style newStyle)
1215 {
1216 mStyle = newStyle;
1217
1218 // Set dummy ruler bounds so width/height can be retrieved
1219 // NOTE: Make sure the Right and Bottom values are large enough to
1220 // ensure full width/height of digits get calculated.
1221 mRuler.SetBounds(0, 0, 500, 500);
1222
1223 if (mDB)
1224 {
1225 mRuler.SetFormat(Ruler::LinearDBFormat);
1226 if (mStyle == HorizontalStereo || mStyle == HorizontalStereoCompact)
1227 {
1228 mRuler.SetOrientation(wxHORIZONTAL);
1229 mRuler.SetRange(-mDBRange, 0);
1230 }
1231 else
1232 {
1233 mRuler.SetOrientation(wxVERTICAL);
1234 mRuler.SetRange(0, -mDBRange);
1235 }
1236 }
1237 else
1238 {
1239 mRuler.SetFormat(Ruler::RealFormat);
1240 if (mStyle == HorizontalStereo || mStyle == HorizontalStereoCompact)
1241 {
1242 mRuler.SetOrientation(wxHORIZONTAL);
1243 mRuler.SetRange(0, 1);
1244 }
1245 else
1246 {
1247 mRuler.SetOrientation(wxVERTICAL);
1248 mRuler.SetRange(1, 0);
1249 }
1250 }
1251
1252 mRuler.GetMaxSize(&mRulerWidth, &mRulerHeight);
1253 }
1254
SetBarAndClip(int iBar,bool vert)1255 void MeterPanel::SetBarAndClip(int iBar, bool vert)
1256 {
1257 // Save the orientation
1258 mBar[iBar].vert = vert;
1259
1260 // Create the bar rectangle and educe to fit inside the bevel
1261 mBar[iBar].r = mBar[iBar].b;
1262 mBar[iBar].r.x += 1;
1263 mBar[iBar].r.width -= 1;
1264 mBar[iBar].r.y += 1;
1265 mBar[iBar].r.height -= 1;
1266
1267 if (vert)
1268 {
1269 if (mClip)
1270 {
1271 // Create the clip rectangle
1272 mBar[iBar].rClip = mBar[iBar].b;
1273 mBar[iBar].rClip.height = 3;
1274
1275 // Make room for the clipping indicator
1276 mBar[iBar].b.y += 3 + gap;
1277 mBar[iBar].b.height -= 3 + gap;
1278 mBar[iBar].r.y += 3 + gap;
1279 mBar[iBar].r.height -= 3 + gap;
1280 }
1281 }
1282 else
1283 {
1284 if (mClip)
1285 {
1286 // Make room for the clipping indicator
1287 mBar[iBar].b.width -= 4;
1288 mBar[iBar].r.width -= 4;
1289
1290 // Create the indicator rectangle
1291 mBar[iBar].rClip = mBar[iBar].b;
1292 mBar[iBar].rClip.x = mBar[iBar].b.GetRight() + 1 + gap; // +1 for bevel
1293 mBar[iBar].rClip.width = 3;
1294 }
1295 }
1296 }
1297
HandleLayout(wxDC & dc)1298 void MeterPanel::HandleLayout(wxDC &dc)
1299 {
1300 // Refresh to reflect any language changes
1301 /* i18n-hint: One-letter abbreviation for Left, in VU Meter */
1302 mLeftText = _("L");
1303 /* i18n-hint: One-letter abbreviation for Right, in VU Meter */
1304 mRightText = _("R");
1305
1306 dc.SetFont(GetFont());
1307 int iconWidth = 0;
1308 int iconHeight = 0;
1309 int width = mWidth;
1310 int height = mHeight;
1311 int left = 0;
1312 int top = 0;
1313 int barw;
1314 int barh;
1315 int lside;
1316 int rside;
1317
1318 // MixerTrackCluster has no L/R labels or icon
1319 if (mStyle != MixerTrackCluster)
1320 {
1321 if (mDesiredStyle == AutomaticStereo)
1322 {
1323 SetActiveStyle(width > height ? HorizontalStereo : VerticalStereo);
1324 }
1325
1326 if (mStyle == HorizontalStereoCompact || mStyle == HorizontalStereo)
1327 {
1328 SetActiveStyle(height < 50 ? HorizontalStereoCompact : HorizontalStereo);
1329 }
1330 else if (mStyle == VerticalStereoCompact || mStyle == VerticalStereo)
1331 {
1332 SetActiveStyle(width < 100 ? VerticalStereoCompact : VerticalStereo);
1333 }
1334
1335 iconWidth = mIcon->GetWidth();
1336 iconHeight = mIcon->GetHeight();
1337 if (mLeftSize.GetWidth() == 0) // Not yet initialized to dc.
1338 {
1339 dc.GetTextExtent(mLeftText, &mLeftSize.x, &mLeftSize.y);
1340 dc.GetTextExtent(mRightText, &mRightSize.x, &mRightSize.y);
1341 }
1342 }
1343
1344 int ltxtWidth = mLeftSize.GetWidth();
1345 int ltxtHeight = mLeftSize.GetHeight();
1346 int rtxtWidth = mRightSize.GetWidth();
1347 int rtxtHeight = mRightSize.GetHeight();
1348
1349 switch (mStyle)
1350 {
1351 default:
1352 wxPrintf(wxT("Style not handled yet!\n"));
1353 break;
1354 case MixerTrackCluster:
1355 // width is now the entire width of the meter canvas
1356 width -= mRulerWidth + left;
1357
1358 // height is now the entire height of the meter canvas
1359 height -= top + gap;
1360
1361 // barw is half of the canvas while allowing for a gap between meters
1362 barw = (width - gap) / 2;
1363
1364 // barh is now the height of the canvas
1365 barh = height;
1366
1367 // We always have 2 bars
1368 mNumBars = 2;
1369
1370 // Save dimensions of the left bevel
1371 mBar[0].b = wxRect(left, top, barw, barh);
1372
1373 // Save dimensions of the right bevel
1374 mBar[1].b = mBar[0].b;
1375 mBar[1].b.SetLeft(mBar[0].b.GetRight() + 1 + gap); // +1 for right edge
1376
1377 // Set bar and clipping indicator dimensions
1378 SetBarAndClip(0, true);
1379 SetBarAndClip(1, true);
1380
1381 mRuler.SetBounds(mBar[1].r.GetRight() + 1, // +1 for the bevel
1382 mBar[1].r.GetTop(),
1383 mWidth,
1384 mBar[1].r.GetBottom());
1385 mRuler.OfflimitsPixels(0, 0);
1386 break;
1387 case VerticalStereo:
1388 // Determine required width of each side;
1389 lside = intmax(iconWidth, ltxtWidth);
1390 rside = intmax(mRulerWidth, rtxtWidth);
1391
1392 // left is now the right edge of the icon or L label
1393 left = lside;
1394
1395 // Ensure there's a margin between top edge of window and the meters
1396 top = gap;
1397
1398 // Position the icon
1399 mIconRect.SetX(left - iconWidth);
1400 mIconRect.SetY(top);
1401 mIconRect.SetWidth(iconWidth);
1402 mIconRect.SetHeight(iconHeight);
1403
1404 // Position the L/R labels
1405 mLeftTextPos = wxPoint(left - ltxtWidth - gap, height - gap - ltxtHeight);
1406 mRightTextPos = wxPoint(width - rside - gap, height - gap - rtxtHeight);
1407
1408 // left is now left edge of left bar
1409 left += gap;
1410
1411 // width is now the entire width of the meter canvas
1412 width -= gap + rside + gap + left;
1413
1414 // height is now the entire height of the meter canvas
1415 height -= top + gap;
1416
1417 // barw is half of the canvas while allowing for a gap between meters
1418 barw = (width - gap) / 2;
1419
1420 // barh is now the height of the canvas
1421 barh = height;
1422
1423 // We always have 2 bars
1424 mNumBars = 2;
1425
1426 // Save dimensions of the left bevel
1427 mBar[0].b = wxRect(left, top, barw, barh);
1428
1429 // Save dimensions of the right bevel
1430 mBar[1].b = mBar[0].b;
1431 mBar[1].b.SetLeft(mBar[0].b.GetRight() + 1 + gap); // +1 for right edge
1432
1433 // Set bar and clipping indicator dimensions
1434 SetBarAndClip(0, true);
1435 SetBarAndClip(1, true);
1436
1437 mRuler.SetBounds(mBar[1].r.GetRight() + 1, // +1 for the bevel
1438 mBar[1].r.GetTop(),
1439 mWidth,
1440 mBar[1].r.GetBottom());
1441 mRuler.OfflimitsPixels(mRightTextPos.y - gap, mBar[1].r.GetBottom());
1442 break;
1443 case VerticalStereoCompact:
1444 // Ensure there's a margin between top edge of window and the meters
1445 top = gap;
1446
1447 // Position the icon
1448 mIconRect.SetX((width - iconWidth) / 2);
1449 mIconRect.SetY(top);
1450 mIconRect.SetWidth(iconWidth);
1451 mIconRect.SetHeight(iconHeight);
1452
1453 // top is now the top of the bar
1454 top += iconHeight + gap;
1455
1456 // height is now the entire height of the meter canvas
1457 height -= top + gap + ltxtHeight + gap;
1458
1459 // barw is half of the canvas while allowing for a gap between meters
1460 barw = (width / 2) - gap;
1461
1462 // barh is now the height of the canvas
1463 barh = height;
1464
1465 // We always have 2 bars
1466 mNumBars = 2;
1467
1468 // Save dimensions of the left bevel
1469 mBar[0].b = wxRect(left, top, barw, barh);
1470
1471 // Save dimensions of the right bevel
1472 mBar[1].b = mBar[0].b;
1473 mBar[1].b.SetLeft(mBar[0].b.GetRight() + 1 + gap); // +1 for right edge
1474
1475 // Set bar and clipping indicator dimensions
1476 SetBarAndClip(0, true);
1477 SetBarAndClip(1, true);
1478
1479 // L/R is centered horizontally under each bar
1480 mLeftTextPos = wxPoint(mBar[0].b.GetLeft() + ((mBar[0].b.GetWidth() - ltxtWidth) / 2), top + barh + gap);
1481 mRightTextPos = wxPoint(mBar[1].b.GetLeft() + ((mBar[1].b.GetWidth() - rtxtWidth) / 2), top + barh + gap);
1482
1483 mRuler.SetBounds((mWidth - mRulerWidth) / 2,
1484 mBar[1].r.GetTop(),
1485 (mWidth - mRulerWidth) / 2,
1486 mBar[1].r.GetBottom());
1487 mRuler.OfflimitsPixels(0, 0);
1488 break;
1489 case HorizontalStereo:
1490 // Button right next to dragger.
1491 left = 0;
1492
1493 // Add a gap between bottom of icon and bottom of window
1494 height -= gap;
1495
1496 // Create icon rectangle
1497 mIconRect.SetX(left);
1498 mIconRect.SetY(height - iconHeight);
1499 mIconRect.SetWidth(iconWidth);
1500 mIconRect.SetHeight(iconHeight);
1501 left = gap;
1502
1503 // Make sure there's room for icon and gap between the bottom of the meter and icon
1504 height -= iconHeight + gap;
1505
1506 // L/R is centered vertically and to the left of a each bar
1507 mLeftTextPos = wxPoint(left, (height / 4) - ltxtHeight / 2);
1508 mRightTextPos = wxPoint(left, (height * 3 / 4) - rtxtHeight / 2);
1509
1510 // Add width of widest of the L/R characters
1511 left += intmax(ltxtWidth, rtxtWidth); //, iconWidth);
1512
1513 // Add gap between L/R and meter bevel
1514 left += gap;
1515
1516 // width is now the entire width of the meter canvas
1517 width -= left;
1518
1519 // barw is now the width of the canvas minus gap between canvas and right window edge
1520 barw = width - gap;
1521
1522 // barh is half of the canvas while allowing for a gap between meters
1523 barh = (height - gap) / 2;
1524
1525 // We always have 2 bars
1526 mNumBars = 2;
1527
1528 // Save dimensions of the top bevel
1529 mBar[0].b = wxRect(left, top, barw, barh);
1530
1531 // Save dimensions of the bottom bevel
1532 mBar[1].b = mBar[0].b;
1533 mBar[1].b.SetTop(mBar[0].b.GetBottom() + 1 + gap); // +1 for bottom edge
1534
1535 // Set bar and clipping indicator dimensions
1536 SetBarAndClip(0, false);
1537 SetBarAndClip(1, false);
1538
1539 mRuler.SetBounds(mBar[1].r.GetLeft(),
1540 mBar[1].r.GetBottom() + 1, // +1 to fit below bevel
1541 mBar[1].r.GetRight(),
1542 mHeight - mBar[1].r.GetBottom() + 1);
1543 mRuler.OfflimitsPixels(0, mIconRect.GetRight() - 4);
1544 break;
1545 case HorizontalStereoCompact:
1546 // Button right next to dragger.
1547 left = 0;
1548
1549 // Create icon rectangle
1550 mIconRect.SetX(left);
1551 mIconRect.SetY((height - iconHeight) / 2 -1);
1552 mIconRect.SetWidth(iconWidth);
1553 mIconRect.SetHeight(iconHeight);
1554
1555 left = gap;
1556 // Add width of icon and gap between icon and L/R
1557 left += iconWidth + gap;
1558
1559 // L/R is centered vertically and to the left of a each bar
1560 mLeftTextPos = wxPoint(left, (height / 4) - (ltxtHeight / 2));
1561 mRightTextPos = wxPoint(left, (height * 3 / 4) - (ltxtHeight / 2));
1562
1563 // Add width of widest of the L/R characters and a gap between labels and meter bevel
1564 left += intmax(ltxtWidth, rtxtWidth) + gap;
1565
1566 // width is now the entire width of the meter canvas
1567 width -= left;
1568
1569 // barw is now the width of the canvas minus gap between canvas and window edge
1570 barw = width - gap;
1571
1572 // barh is half of the canvas while allowing for a gap between meters
1573 barh = (height - gap) / 2;
1574
1575 // We always have 2 bars
1576 mNumBars = 2;
1577
1578 // Save dimensions of the top bevel
1579 mBar[0].b = wxRect(left, top, barw, barh);
1580
1581 // Save dimensions of the bottom bevel
1582 // Since the bars butt up against the window's top and bottom edges, we need
1583 // to include an extra pixel in the bottom bar when the window height and
1584 // meter height do not exactly match.
1585 mBar[1].b = mBar[0].b;
1586 mBar[1].b.SetTop(mBar[0].b.GetBottom() + 1 + gap); // +1 for bottom bevel
1587 mBar[1].b.SetHeight(mHeight - mBar[1].b.GetTop() - 1); // +1 for bottom bevel
1588
1589 // Add clipping indicators - do after setting bar/bevel dimensions above
1590 SetBarAndClip(0, false);
1591 SetBarAndClip(1, false);
1592
1593 mRuler.SetBounds(mBar[1].r.GetLeft(),
1594 mBar[1].b.GetTop() - (mRulerHeight / 2),
1595 mBar[1].r.GetRight(),
1596 mBar[1].b.GetTop() - (mRulerHeight / 2));
1597 mRuler.OfflimitsPixels(0, 0);
1598 break;
1599 }
1600
1601 mLayoutValid = true;
1602 }
1603
RepaintBarsNow()1604 void MeterPanel::RepaintBarsNow()
1605 {
1606 if (mLayoutValid)
1607 {
1608 // Invalidate the bars so they get redrawn
1609 for (unsigned int i = 0; i < mNumBars; i++)
1610 {
1611 Refresh(false);
1612 }
1613
1614 // Immediate redraw (using wxPaintDC)
1615 Update();
1616
1617 return;
1618 }
1619 }
1620
DrawMeterBar(wxDC & dc,MeterBar * bar)1621 void MeterPanel::DrawMeterBar(wxDC &dc, MeterBar *bar)
1622 {
1623 // Cache some metrics
1624 wxCoord x = bar->r.GetLeft();
1625 wxCoord y = bar->r.GetTop();
1626 wxCoord w = bar->r.GetWidth();
1627 wxCoord h = bar->r.GetHeight();
1628 wxCoord ht;
1629 wxCoord wd;
1630
1631 // Setup for erasing the background
1632 dc.SetPen(*wxTRANSPARENT_PEN);
1633 dc.SetBrush(mMeterDisabled ? mDisabledBkgndBrush : mBkgndBrush);
1634
1635 if (mGradient)
1636 {
1637 // Map the predrawn bitmap into the source DC
1638 wxMemoryDC srcDC;
1639 srcDC.SelectObject(*mBitmap);
1640
1641 if (bar->vert)
1642 {
1643 // Copy as much of the predrawn meter bar as is required for the
1644 // current peak.
1645 // (h - 1) corresponds to the mRuler.SetBounds() in HandleLayout()
1646 ht = (int)(bar->peak * (h - 1) + 0.5);
1647
1648 // Blank out the rest
1649 if (h - ht)
1650 {
1651 // ht includes peak value...not really needed but doesn't hurt
1652 dc.DrawRectangle(x, y, w, h - ht);
1653 }
1654
1655 // Copy as much of the predrawn meter bar as is required for the
1656 // current peak.
1657 // +/-1 to include the peak position
1658 if (ht)
1659 {
1660 dc.Blit(x, y + h - ht - 1, w, ht + 1, &srcDC, x, y + h - ht - 1);
1661 }
1662
1663 // Draw the "recent" peak hold line using the predrawn meter bar so that
1664 // it will be the same color as the original level.
1665 // (h - 1) corresponds to the mRuler.SetBounds() in HandleLayout()
1666 ht = (int)(bar->peakHold * (h - 1) + 0.5);
1667 if (ht > 1)
1668 {
1669 dc.Blit(x, y + h - ht - 1, w, 2, &srcDC, x, y + h - ht - 1);
1670 }
1671
1672 // Draw the "maximum" peak hold line
1673 // (h - 1) corresponds to the mRuler.SetBounds() in HandleLayout()
1674 dc.SetPen(mPeakPeakPen);
1675 ht = (int)(bar->peakPeakHold * (h - 1) + 0.5);
1676 if (ht > 0)
1677 {
1678 AColor::Line(dc, x, y + h - ht - 1, x + w - 1, y + h - ht - 1);
1679 if (ht > 1)
1680 {
1681 AColor::Line(dc, x, y + h - ht, x + w - 1, y + h - ht);
1682 }
1683 }
1684 }
1685 else
1686 {
1687 // Calculate the peak position
1688 // (w - 1) corresponds to the mRuler.SetBounds() in HandleLayout()
1689 wd = (int)(bar->peak * (w - 1) + 0.5);
1690
1691 // Blank out the rest
1692 if (w - wd)
1693 {
1694 // wd includes peak value...not really needed but doesn't hurt
1695 dc.DrawRectangle(x + wd, y, w - wd, h);
1696 }
1697
1698 // Copy as much of the predrawn meter bar as is required for the
1699 // current peak. But, only blit() if there's something to copy
1700 // to prevent display corruption.
1701 // +1 to include peak position
1702 if (wd)
1703 {
1704 dc.Blit(x, y, wd + 1, h, &srcDC, x, y);
1705 }
1706
1707 // Draw the "recent" peak hold line using the predrawn meter bar so that
1708 // it will be the same color as the original level.
1709 // -1 to give a 2 pixel width
1710 wd = (int)(bar->peakHold * (w - 1) + 0.5);
1711 if (wd > 1)
1712 {
1713 dc.Blit(x + wd - 1, y, 2, h, &srcDC, x + wd, y);
1714 }
1715
1716 // Draw the "maximum" peak hold line using a themed color
1717 // (w - 1) corresponds to the mRuler.SetBounds() in HandleLayout()
1718 dc.SetPen(mPeakPeakPen);
1719 wd = (int)(bar->peakPeakHold * (w - 1) + 0.5);
1720 if (wd > 0)
1721 {
1722 AColor::Line(dc, x + wd, y, x + wd, y + h - 1);
1723 if (wd > 1)
1724 {
1725 AColor::Line(dc, x + wd - 1, y, x + wd - 1, y + h - 1);
1726 }
1727 }
1728 }
1729
1730 // No longer need the source DC, so unselect the predrawn bitmap
1731 srcDC.SelectObject(wxNullBitmap);
1732 }
1733 else
1734 {
1735 if (bar->vert)
1736 {
1737 // Calculate the peak position
1738 // (h - 1) corresponds to the mRuler.SetBounds() in HandleLayout()
1739 ht = (int)(bar->peak * (h - 1) + 0.5);
1740
1741 // Blank out the rest
1742 if (h - ht)
1743 {
1744 // ht includes peak value...not really needed but doesn't hurt
1745 dc.DrawRectangle(x, y, w, h - ht);
1746 }
1747
1748 // Draw the peak level
1749 // +/-1 to include the peak position
1750 dc.SetPen(*wxTRANSPARENT_PEN);
1751 dc.SetBrush(mMeterDisabled ? mDisabledBkgndBrush : mBrush);
1752 if (ht)
1753 {
1754 dc.DrawRectangle(x, y + h - ht - 1, w, ht + 1);
1755 }
1756
1757 // Draw the "recent" peak hold line
1758 // (h - 1) corresponds to the mRuler.SetBounds() in HandleLayout()
1759 dc.SetPen(mPen);
1760 ht = (int)(bar->peakHold * (h - 1) + 0.5);
1761 if (ht > 0)
1762 {
1763 AColor::Line(dc, x, y + h - ht - 1, x + w - 1, y + h - ht - 1);
1764 if (ht > 1)
1765 {
1766 AColor::Line(dc, x, y + h - ht, x + w - 1, y + h - ht);
1767 }
1768 }
1769
1770 // Calculate the rms position
1771 // (h - 1) corresponds to the mRuler.SetBounds() in HandleLayout()
1772 // +1 to include the rms position
1773 ht = (int)(bar->rms * (h - 1) + 0.5);
1774
1775 // Draw the RMS level
1776 dc.SetPen(*wxTRANSPARENT_PEN);
1777 dc.SetBrush(mMeterDisabled ? mDisabledBkgndBrush : mRMSBrush);
1778 if (ht)
1779 {
1780 dc.DrawRectangle(x, y + h - ht - 1, w, ht + 1);
1781 }
1782
1783 // Draw the "maximum" peak hold line
1784 // (h - 1) corresponds to the mRuler.SetBounds() in HandleLayout()
1785 dc.SetPen(mPeakPeakPen);
1786 ht = (int)(bar->peakPeakHold * (h - 1) + 0.5);
1787 if (ht > 0)
1788 {
1789 AColor::Line(dc, x, y + h - ht - 1, x + w - 1, y + h - ht - 1);
1790 if (ht > 1)
1791 {
1792 AColor::Line(dc, x, y + h - ht, x + w - 1, y + h - ht);
1793 }
1794 }
1795 }
1796 else
1797 {
1798 // Calculate the peak position
1799 // (w - 1) corresponds to the mRuler.SetBounds() in HandleLayout()
1800 wd = (int)(bar->peak * (w - 1) + 0.5);
1801
1802 // Blank out the rest
1803 if (w - wd)
1804 {
1805 // wd includes peak value...not really needed but doesn't hurt
1806 dc.DrawRectangle(x + wd, y, w - wd, h);
1807 }
1808
1809 // Draw the peak level
1810 // +1 to include peak position
1811 dc.SetPen(*wxTRANSPARENT_PEN);
1812 dc.SetBrush(mMeterDisabled ? mDisabledBkgndBrush : mBrush);
1813 if (wd)
1814 {
1815 dc.DrawRectangle(x, y, wd + 1, h);
1816 }
1817
1818 // Draw the "recent" peak hold line
1819 // (w - 1) corresponds to the mRuler.SetBounds() in HandleLayout()
1820 dc.SetPen(mPen);
1821 wd = (int)(bar->peakHold * (w - 1) + 0.5);
1822 if (wd > 0)
1823 {
1824 AColor::Line(dc, x + wd, y, x + wd, y + h - 1);
1825 if (wd > 1)
1826 {
1827 AColor::Line(dc, x + wd - 1, y, x + wd - 1, y + h - 1);
1828 }
1829 }
1830
1831 // Calculate the rms position
1832 // (w - 1) corresponds to the mRuler.SetBounds() in HandleLayout()
1833 wd = (int)(bar->rms * (w - 1) + 0.5);
1834
1835 // Draw the rms level
1836 // +1 to include the rms position
1837 dc.SetPen(*wxTRANSPARENT_PEN);
1838 dc.SetBrush(mMeterDisabled ? mDisabledBkgndBrush : mRMSBrush);
1839 if (wd)
1840 {
1841 dc.DrawRectangle(x, y, wd + 1, h);
1842 }
1843
1844 // Draw the "maximum" peak hold line using a themed color
1845 // (w - 1) corresponds to the mRuler.SetBounds() in HandleLayout()
1846 dc.SetPen(mPeakPeakPen);
1847 wd = (int)(bar->peakPeakHold * (w - 1) + 0.5);
1848 if (wd > 0)
1849 {
1850 AColor::Line(dc, x + wd, y, x + wd, y + h - 1);
1851 if (wd > 1)
1852 {
1853 AColor::Line(dc, x + wd - 1, y, x + wd - 1, y + h - 1);
1854 }
1855 }
1856 }
1857 }
1858
1859 // If meter had a clipping indicator, draw or erase it
1860 // LLL: At least I assume that's what "mClip" is supposed to be for as
1861 // it is always "true".
1862 if (mClip)
1863 {
1864 if (bar->clipping)
1865 {
1866 dc.SetBrush(mClipBrush);
1867 }
1868 else
1869 {
1870 dc.SetBrush(mMeterDisabled ? mDisabledBkgndBrush : mBkgndBrush);
1871 }
1872 dc.SetPen(*wxTRANSPARENT_PEN);
1873 wxRect r(bar->rClip.GetX() + 1,
1874 bar->rClip.GetY() + 1,
1875 bar->rClip.GetWidth() - 1,
1876 bar->rClip.GetHeight() - 1);
1877 dc.DrawRectangle(r);
1878 }
1879 }
1880
IsMeterDisabled() const1881 bool MeterPanel::IsMeterDisabled() const
1882 {
1883 return mMeterDisabled != 0;
1884 }
1885
StartMonitoring()1886 void MeterPanel::StartMonitoring()
1887 {
1888 bool start = !mMonitoring;
1889
1890 auto gAudioIO = AudioIO::Get();
1891 if (gAudioIO->IsMonitoring()){
1892 gAudioIO->StopStream();
1893 }
1894
1895 if (start && !gAudioIO->IsBusy()){
1896 AudacityProject *p = mProject;
1897 if (p){
1898 gAudioIO->StartMonitoring( DefaultPlayOptions( *p ) );
1899 }
1900
1901 mLayoutValid = false;
1902
1903 Refresh(false);
1904 }
1905 }
1906
StopMonitoring()1907 void MeterPanel::StopMonitoring(){
1908 mMonitoring = false;
1909 auto gAudioIO = AudioIO::Get();
1910 if (gAudioIO->IsMonitoring()){
1911 gAudioIO->StopStream();
1912 }
1913 }
1914
OnAudioIOStatus(wxCommandEvent & evt)1915 void MeterPanel::OnAudioIOStatus(wxCommandEvent &evt)
1916 {
1917 evt.Skip();
1918 AudacityProject *p = (AudacityProject *) evt.GetEventObject();
1919
1920 mActive = (evt.GetInt() != 0) && (p == mProject);
1921
1922 if( mActive ){
1923 mTimer.Start(1000 / mMeterRefreshRate);
1924 if (evt.GetEventType() == EVT_AUDIOIO_MONITOR)
1925 mMonitoring = mActive;
1926 } else {
1927 mTimer.Stop();
1928 mMonitoring = false;
1929 }
1930
1931 // Only refresh is we're the active meter
1932 if (IsShownOnScreen())
1933 Refresh(false);
1934 }
1935
1936 // SaveState() and RestoreState() exist solely for purpose of recreating toolbars
1937 // They should really be querying the project for current audio I/O state, but there
1938 // isn't a clear way of doing that just yet. (It should NOT query AudioIO.)
SaveState()1939 auto MeterPanel::SaveState() -> State
1940 {
1941 return { true, mMonitoring, mActive };
1942 }
1943
RestoreState(const State & state)1944 void MeterPanel::RestoreState(const State &state)
1945 {
1946 if (!state.mSaved)
1947 return;
1948
1949 mMonitoring = state.mMonitoring;
1950 mActive = state.mActive;
1951 //wxLogDebug("Restore state for %p, is %i", this, mActive );
1952
1953 if (mActive)
1954 mTimer.Start(1000 / mMeterRefreshRate);
1955 }
1956
1957 //
1958 // Pop-up menu
1959 //
1960
ShowMenu(const wxPoint & pos)1961 void MeterPanel::ShowMenu(const wxPoint & pos)
1962 {
1963 wxMenu menu;
1964 // Note: these should be kept in the same order as the enum
1965 if (mIsInput) {
1966 wxMenuItem *mi;
1967 if (mMonitoring)
1968 mi = menu.Append(OnMonitorID, _("Stop Monitoring"));
1969 else
1970 mi = menu.Append(OnMonitorID, _("Start Monitoring"));
1971 mi->Enable(!mActive || mMonitoring);
1972 }
1973
1974 menu.Append(OnPreferencesID, _("Options..."));
1975
1976 mAccSilent = true; // temporarily make screen readers say (close to) nothing on focus events
1977
1978 BasicMenu::Handle{ &menu }.Popup(
1979 wxWidgetsWindowPlacement{ this },
1980 { pos.x, pos.y }
1981 );
1982
1983 /* if stop/start monitoring was chosen in the menu, then by this point
1984 OnMonitoring has been called and variables which affect the accessibility
1985 name have been updated so it's now ok for screen readers to read the name of
1986 the button */
1987 mAccSilent = false;
1988 #if wxUSE_ACCESSIBILITY
1989 GetAccessible()->NotifyEvent(wxACC_EVENT_OBJECT_FOCUS,
1990 this,
1991 wxOBJID_CLIENT,
1992 wxACC_SELF);
1993 #endif
1994 }
1995
OnMonitor(wxCommandEvent & WXUNUSED (event))1996 void MeterPanel::OnMonitor(wxCommandEvent & WXUNUSED(event))
1997 {
1998 StartMonitoring();
1999 }
2000
OnPreferences(wxCommandEvent & WXUNUSED (event))2001 void MeterPanel::OnPreferences(wxCommandEvent & WXUNUSED(event))
2002 {
2003 wxTextCtrl *rate;
2004 wxRadioButton *gradient;
2005 wxRadioButton *rms;
2006 wxRadioButton *db;
2007 wxRadioButton *linear;
2008 wxRadioButton *automatic;
2009 wxRadioButton *horizontal;
2010 wxRadioButton *vertical;
2011 int meterRefreshRate = mMeterRefreshRate;
2012
2013 auto title = mIsInput ? XO("Recording Meter Options") : XO("Playback Meter Options");
2014
2015 // Dialog is a child of the project, rather than of the toolbar.
2016 // This determines where it pops up.
2017
2018 wxDialogWrapper dlg( FindProjectFrame( mProject ), wxID_ANY, title);
2019 dlg.SetName();
2020 ShuttleGui S(&dlg, eIsCreating);
2021 S.StartVerticalLay();
2022 {
2023 S.StartStatic(XO("Refresh Rate"), 0);
2024 {
2025 S.AddFixedText(XO(
2026 "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."));
2027 S.StartHorizontalLay();
2028 {
2029 rate = S.Name(XO("Meter refresh rate per second [1-100]"))
2030 .Validator<IntegerValidator<long>>(
2031 &mMeterRefreshRate, NumValidatorStyle::DEFAULT,
2032 MIN_REFRESH_RATE, MAX_REFRESH_RATE)
2033 .AddTextBox(XXO("Meter refresh rate per second [1-100]: "),
2034 wxString::Format(wxT("%d"), meterRefreshRate),
2035 10);
2036 }
2037 S.EndHorizontalLay();
2038 }
2039 S.EndStatic();
2040
2041 S.StartHorizontalLay();
2042 {
2043 S.StartStatic(XO("Meter Style"), 0);
2044 {
2045 S.StartVerticalLay();
2046 {
2047 gradient = S.AddRadioButton(XXO("Gradient"), true, mGradient);
2048 rms = S.AddRadioButtonToGroup(XXO("RMS"), false, mGradient);
2049 }
2050 S.EndVerticalLay();
2051 }
2052 S.EndStatic();
2053
2054 S.StartStatic(XO("Meter Type"), 0);
2055 {
2056 S.StartVerticalLay();
2057 {
2058 db = S.AddRadioButton(XXO("dB"), true, mDB);
2059 linear = S.AddRadioButtonToGroup(XXO("Linear"), false, mDB);
2060 }
2061 S.EndVerticalLay();
2062 }
2063 S.EndStatic();
2064
2065 S.StartStatic(XO("Orientation"), 1);
2066 {
2067 S.StartVerticalLay();
2068 {
2069 automatic = S.AddRadioButton(
2070 XXO("Automatic"), AutomaticStereo, mDesiredStyle);
2071 horizontal = S.AddRadioButtonToGroup(
2072 XXO("Horizontal"), HorizontalStereo, mDesiredStyle);
2073 vertical = S.AddRadioButtonToGroup(
2074 XXO("Vertical"), VerticalStereo, mDesiredStyle);
2075 }
2076 S.EndVerticalLay();
2077 }
2078 S.EndStatic();
2079 }
2080 S.EndHorizontalLay();
2081 S.AddStandardButtons();
2082 }
2083 S.EndVerticalLay();
2084 dlg.Layout();
2085 dlg.Fit();
2086
2087 dlg.CenterOnParent();
2088
2089 if (dlg.ShowModal() == wxID_OK)
2090 {
2091 wxArrayStringEx style{
2092 wxT("AutomaticStereo") ,
2093 wxT("HorizontalStereo") ,
2094 wxT("VerticalStereo") ,
2095 };
2096
2097 int s = 0;
2098 s = automatic->GetValue() ? 0 : s;
2099 s = horizontal->GetValue() ? 1 : s;
2100 s = vertical->GetValue() ? 2 : s;
2101
2102 gPrefs->Write(Key(wxT("Style")), style[s]);
2103 gPrefs->Write(Key(wxT("Bars")), gradient->GetValue() ? wxT("Gradient") : wxT("RMS"));
2104 gPrefs->Write(Key(wxT("Type")), db->GetValue() ? wxT("dB") : wxT("Linear"));
2105 gPrefs->Write(Key(wxT("RefreshRate")), rate->GetValue());
2106
2107 gPrefs->Flush();
2108
2109 // Currently, there are 2 playback meters and 2 record meters and any number of
2110 // mixerboard meters, so we have to send out an preferences updated message to
2111 // ensure they all update themselves.
2112 PrefsListener::Broadcast(MeterPrefsID());
2113 }
2114 }
2115
Key(const wxString & key) const2116 wxString MeterPanel::Key(const wxString & key) const
2117 {
2118 if (mStyle == MixerTrackCluster)
2119 {
2120 return wxT("/Meter/Mixerboard/") + key;
2121 }
2122
2123 if (mIsInput)
2124 {
2125 return wxT("/Meter/Input/") + key;
2126 }
2127
2128 return wxT("/Meter/Output/") + key;
2129 }
2130
2131 // This compensates for a but in wxWidgets 3.0.2 for mac:
2132 // Couldn't set focus from keyboard when AcceptsFocus returns false;
2133 // this bypasses that limitation
SetFocusFromKbd()2134 void MeterPanel::SetFocusFromKbd()
2135 {
2136 auto temp = TemporarilyAllowFocus();
2137 SetFocus();
2138 }
2139
2140
2141 #if wxUSE_ACCESSIBILITY
2142
MeterAx(wxWindow * window)2143 MeterAx::MeterAx(wxWindow *window):
2144 WindowAccessible(window)
2145 {
2146 }
2147
~MeterAx()2148 MeterAx::~MeterAx()
2149 {
2150 }
2151
2152 // Performs the default action. childId is 0 (the action for this object)
2153 // or > 0 (the action for a child).
2154 // Return wxACC_NOT_SUPPORTED if there is no default action for this
2155 // window (e.g. an edit control).
DoDefaultAction(int WXUNUSED (childId))2156 wxAccStatus MeterAx::DoDefaultAction(int WXUNUSED(childId))
2157 {
2158 MeterPanel *m = wxDynamicCast(GetWindow(), MeterPanel);
2159
2160 if (m && m->mIsInput)
2161 m->StartMonitoring();
2162
2163 return wxACC_OK;
2164 }
2165
2166 // Retrieves the address of an IDispatch interface for the specified child.
2167 // All objects must support this property.
GetChild(int childId,wxAccessible ** child)2168 wxAccStatus MeterAx::GetChild(int childId, wxAccessible** child)
2169 {
2170 if (childId == wxACC_SELF)
2171 *child = this;
2172 else
2173 *child = NULL;
2174 return wxACC_OK;
2175 }
2176
2177 // Gets the number of children.
GetChildCount(int * childCount)2178 wxAccStatus MeterAx::GetChildCount(int* childCount)
2179 {
2180 *childCount = 0;
2181 return wxACC_OK;
2182 }
2183
2184 // Gets the default action for this object (0) or > 0 (the action for
2185 // a child). Return wxACC_OK even if there is no action. actionName
2186 // is the action, or the empty string if there is no action. The
2187 // retrieved string describes the action that is performed on an
2188 // object, not what the object does as a result. For example, a
2189 // toolbar button that prints a document has a default action of
2190 // "Press" rather than "Prints the current document."
GetDefaultAction(int WXUNUSED (childId),wxString * actionName)2191 wxAccStatus MeterAx::GetDefaultAction(int WXUNUSED(childId), wxString* actionName)
2192 {
2193 *actionName = _("Press");
2194 return wxACC_OK;
2195 }
2196
2197 // Returns the description for this object or a child.
GetDescription(int WXUNUSED (childId),wxString * description)2198 wxAccStatus MeterAx::GetDescription(int WXUNUSED(childId), wxString *description)
2199 {
2200 description->clear();
2201 return wxACC_NOT_SUPPORTED;
2202 }
2203
2204 // Gets the window with the keyboard focus.
2205 // If childId is 0 and child is NULL, no object in
2206 // this subhierarchy has the focus.
2207 // If this object has the focus, child should be 'this'.
GetFocus(int * childId,wxAccessible ** child)2208 wxAccStatus MeterAx::GetFocus(int* childId, wxAccessible** child)
2209 {
2210 *childId = 0;
2211 *child = this;
2212 return wxACC_OK;
2213 }
2214
2215 // Returns help text for this object or a child, similar to tooltip text.
GetHelpText(int WXUNUSED (childId),wxString * helpText)2216 wxAccStatus MeterAx::GetHelpText(int WXUNUSED(childId), wxString *helpText)
2217 {
2218 helpText->clear();
2219 return wxACC_NOT_SUPPORTED;
2220 }
2221
2222 // Returns the keyboard shortcut for this object or child.
2223 // Return e.g. ALT+K
GetKeyboardShortcut(int WXUNUSED (childId),wxString * shortcut)2224 wxAccStatus MeterAx::GetKeyboardShortcut(int WXUNUSED(childId), wxString *shortcut)
2225 {
2226 shortcut->clear();
2227 return wxACC_OK;
2228 }
2229
2230 // Returns the rectangle for this object (id = 0) or a child element (id > 0).
2231 // rect is in screen coordinates.
GetLocation(wxRect & rect,int WXUNUSED (elementId))2232 wxAccStatus MeterAx::GetLocation(wxRect & rect, int WXUNUSED(elementId))
2233 {
2234 MeterPanel *m = wxDynamicCast(GetWindow(), MeterPanel);
2235
2236 rect = m->mIconRect;
2237 rect.SetPosition(m->ClientToScreen(rect.GetPosition()));
2238
2239 return wxACC_OK;
2240 }
2241
2242 // Gets the name of the specified object.
GetName(int WXUNUSED (childId),wxString * name)2243 wxAccStatus MeterAx::GetName(int WXUNUSED(childId), wxString* name)
2244 {
2245 MeterPanel *m = wxDynamicCast(GetWindow(), MeterPanel);
2246
2247 if (m->mAccSilent)
2248 {
2249 *name = wxT(""); // Jaws reads nothing, and nvda reads "unknown"
2250 }
2251 else
2252 {
2253 *name = m->GetName();
2254 if (name->empty())
2255 *name = m->GetLabel();
2256
2257 if (name->empty())
2258 *name = _("Meter");
2259
2260 if (m->mMonitoring)
2261 // translations of strings such as " Monitoring " did not
2262 // always retain the leading space. Therefore a space has
2263 // been added to ensure at least one space, and stop
2264 // words from being merged
2265 *name += wxT(" ") + _(" Monitoring ");
2266 else if (m->mActive)
2267 *name += wxT(" ") + _(" Active ");
2268
2269 float peak = 0.;
2270 bool clipped = false;
2271 for (unsigned int i = 0; i < m->mNumBars; i++)
2272 {
2273 peak = wxMax(peak, m->mBar[i].peakPeakHold);
2274 if (m->mBar[i].clipping)
2275 clipped = true;
2276 }
2277
2278 if (m->mDB)
2279 *name += wxT(" ") + wxString::Format(_(" Peak %2.f dB"), (peak * m->mDBRange) - m->mDBRange);
2280 else
2281 *name += wxT(" ") + wxString::Format(_(" Peak %.2f "), peak);
2282
2283 if (clipped)
2284 *name += wxT(" ") + _(" Clipped ");
2285 }
2286
2287 return wxACC_OK;
2288 }
2289
2290 // Returns a role constant.
GetRole(int WXUNUSED (childId),wxAccRole * role)2291 wxAccStatus MeterAx::GetRole(int WXUNUSED(childId), wxAccRole* role)
2292 {
2293 MeterPanel *m = wxDynamicCast(GetWindow(), MeterPanel);
2294
2295 if (m->mAccSilent)
2296 *role = wxROLE_NONE; // Jaws and nvda both read nothing
2297 else
2298 *role = wxROLE_SYSTEM_BUTTONDROPDOWN;
2299
2300 return wxACC_OK;
2301 }
2302
2303 // Gets a variant representing the selected children
2304 // of this object.
2305 // Acceptable values:
2306 // - a null variant (IsNull() returns TRUE)
2307 // - a list variant (GetType() == wxT("list"))
2308 // - an integer representing the selected child element,
2309 // or 0 if this object is selected (GetType() == wxT("long"))
2310 // - a "void*" pointer to a wxAccessible child object
GetSelections(wxVariant * WXUNUSED (selections))2311 wxAccStatus MeterAx::GetSelections(wxVariant * WXUNUSED(selections))
2312 {
2313 return wxACC_NOT_IMPLEMENTED;
2314 }
2315
2316 // Returns a state constant.
GetState(int WXUNUSED (childId),long * state)2317 wxAccStatus MeterAx::GetState(int WXUNUSED(childId), long* state)
2318 {
2319 MeterPanel *m = wxDynamicCast( GetWindow(), MeterPanel );
2320
2321 *state = wxACC_STATE_SYSTEM_FOCUSABLE;
2322 *state |= ( m == wxWindow::FindFocus() ? wxACC_STATE_SYSTEM_FOCUSED : 0 );
2323
2324 return wxACC_OK;
2325 }
2326
2327 // Returns a localized string representing the value for the object
2328 // or child.
GetValue(int WXUNUSED (childId),wxString * WXUNUSED (strValue))2329 wxAccStatus MeterAx::GetValue(int WXUNUSED(childId), wxString* WXUNUSED(strValue))
2330 {
2331 return wxACC_NOT_SUPPORTED;
2332 }
2333
2334 #endif
2335