Skip to content

Commit ff90ffa

Browse files
committed
Viewer: volume slider, mute button, and seek-range fix
Adds the volume / mute UI to the audio playback already in master: - Compact volume bar in the progress band, only when a video has audio. Layout is reserved against the maximum frame number so the bar does not shift as the counter advances during playback. - Small speaker-icon mute button drawn from GDI primitives so it does not depend on any specific Unicode glyph being present in the font. - Click or drag in the volume bar sets the level and unmutes. Click the mute button, right-click on the volume bar or mute button, or press 'M' to toggle mute. AudioPlayer gains SetVolume / SetMuted forwarded to the XAudio2 source voice. - Progress-band seeks now use the actual progress track rectangle, so clicks over the frame counter or volume area no longer trigger wrong seeks.
1 parent 055fda9 commit ff90ffa

4 files changed

Lines changed: 214 additions & 17 deletions

File tree

Viewer/AudioPlayer.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,30 @@ bool AudioPlayer::Open(const wchar_t* filePath)
8989
mSourceVoice = src;
9090
mWfx = wfx;
9191
mHasAudio = true;
92+
ApplyVolume();
9293
return true;
9394
}
9495

96+
void AudioPlayer::SetVolume(float volume)
97+
{
98+
if (volume < 0.0f) volume = 0.0f;
99+
if (volume > 1.0f) volume = 1.0f;
100+
mVolume = volume;
101+
ApplyVolume();
102+
}
103+
104+
void AudioPlayer::SetMuted(bool muted)
105+
{
106+
mMuted = muted;
107+
ApplyVolume();
108+
}
109+
110+
void AudioPlayer::ApplyVolume()
111+
{
112+
if (mSourceVoice)
113+
mSourceVoice->SetVolume(mMuted ? 0.0f : mVolume);
114+
}
115+
95116
void AudioPlayer::Close()
96117
{
97118
Pause();

Viewer/AudioPlayer.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,15 @@ class AudioPlayer {
3232
// Pause playback. Call Play() to resume from a new position.
3333
void Pause();
3434

35+
// Volume in [0.0, 1.0]; values outside the range are clamped.
36+
void SetVolume(float volume);
37+
void SetMuted(bool muted);
38+
float GetVolume() const { return mVolume; }
39+
bool IsMuted() const { return mMuted; }
40+
3541
private:
42+
void ApplyVolume();
43+
3644
bool SeekReader(double timeSec);
3745
void FeedLoop();
3846

@@ -44,6 +52,8 @@ class AudioPlayer {
4452

4553
std::wstring mOpenedPath;
4654
bool mHasAudio = false;
55+
float mVolume = 1.0f;
56+
bool mMuted = false;
4757

4858
std::thread mFeedThread;
4959
std::atomic<bool> mFeedRunning{ false };

Viewer/ViewerView.cpp

Lines changed: 175 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,11 @@ CViewerView::CViewerView()
160160
memset(&mStableRgbBufferInfo, 0, sizeof(BufferInfo));
161161

162162
mRcProgress.SetRectEmpty();
163+
mRcVolume.SetRectEmpty();
164+
mRcMute.SetRectEmpty();
165+
mVolume = 1.0f;
166+
mVolumeMuted = false;
167+
mVolumeDragging = false;
163168

164169
mMouseMenu.CreatePopupMenu();
165170
CString str;
@@ -293,39 +298,133 @@ void CViewerView::Initialize(int nFrame, size_t rgbStride, int w, int h, bool pr
293298
void CViewerView::ProgressiveDraw(CDC *pDC, CViewerDoc* pDoc, int frameID)
294299
{
295300
const int barMargin = MARGIN_PROGESS_BAR;
301+
const int volWidth = 64;
302+
const int volGap = 6;
303+
const int muteSize = PROGRESS_BAR_H - barMargin * 2; // square button inside the band
304+
const int muteGap = 6;
296305

297306
int frameMax = pDoc->mFrames - 1;
298-
int limit =
299-
ROUND2I((mWClient - (barMargin << 1)) * frameID / (float)frameMax);
300307

301308
CString str;
302309
str.Format(_T("%d / %d"), frameID, frameMax);
303310

304311
CRect progressBand(0, mHCanvas, mWClient, mHClient);
305312
pDC->FillSolidRect(progressBand, Q1UI_COLOR_SURFACE_ALT);
306313

307-
CRect barTextRect, barRect;
308-
CRect trackRect;
314+
pDC->SelectObject(&mProgressFont);
315+
pDC->SetTextColor(COLOR_PROGRESS_TEXT);
316+
pDC->SetBkMode(TRANSPARENT);
317+
318+
// Reserve a stable slot on the right based on the maximum frame number
319+
// (not the current one) so the volume/mute layout does not shift as the
320+
// counter advances during playback.
321+
CString maxStr;
322+
maxStr.Format(_T("%d / %d"), frameMax, frameMax);
323+
int textW = pDC->GetTextExtent(maxStr).cx;
324+
int rightEnd = mWClient - barMargin;
325+
int textLeft = rightEnd - textW;
326+
327+
bool showVolume = mAudioPlayer.IsOpen();
328+
int progressRight = textLeft - barMargin;
329+
if (showVolume)
330+
progressRight -= muteSize + muteGap + volWidth + volGap;
309331

310-
trackRect.top = mHCanvas + barMargin;
311-
trackRect.left = barMargin;
332+
if (progressRight < barMargin + 4)
333+
progressRight = barMargin + 4;
334+
335+
CRect trackRect;
336+
trackRect.top = mHCanvas + barMargin;
337+
trackRect.left = barMargin;
312338
trackRect.bottom = mHClient - barMargin;
313-
trackRect.right = mWClient - barMargin;
339+
trackRect.right = progressRight;
314340
pDC->FillSolidRect(trackRect, Q1UI_COLOR_BORDER_SOFT);
315341

316-
barRect = trackRect;
342+
// Hit-test region for seeks. Extends vertically over the whole band so
343+
// clicks just above/below the slim track still seek, but horizontally
344+
// stays inside the track so frame-counter and volume areas are excluded.
345+
mRcProgress.SetRect(trackRect.left, mHCanvas, trackRect.right, mHClient);
346+
347+
int limit = ROUND2I((trackRect.Width()) * frameID / (float)frameMax);
348+
CRect barRect = trackRect;
317349
barRect.right = barRect.left + limit;
318350
pDC->FillSolidRect(&barRect, mBarColor);
319351

320-
barTextRect.top = mHCanvas;
321-
barTextRect.left = 0;
352+
if (showVolume) {
353+
// Mute button (small square) just before the volume bar.
354+
CRect muteRect;
355+
muteRect.top = trackRect.top;
356+
muteRect.bottom = trackRect.bottom;
357+
muteRect.right = textLeft - barMargin - volWidth - volGap;
358+
muteRect.left = muteRect.right - muteSize;
359+
DrawMuteButton(pDC, muteRect);
360+
mRcMute = muteRect;
361+
362+
// Volume bar
363+
CRect volTrack;
364+
volTrack.top = trackRect.top;
365+
volTrack.bottom = trackRect.bottom;
366+
volTrack.right = textLeft - barMargin;
367+
volTrack.left = volTrack.right - volWidth;
368+
pDC->FillSolidRect(volTrack, Q1UI_COLOR_BORDER_SOFT);
369+
370+
COLORREF fillColor = mVolumeMuted ? Q1UI_COLOR_WARNING : mBarColor;
371+
float fillLevel = mVolumeMuted ? 1.0f : mVolume;
372+
int fillW = ROUND2I(volTrack.Width() * fillLevel);
373+
CRect volFill = volTrack;
374+
volFill.right = volFill.left + fillW;
375+
pDC->FillSolidRect(&volFill, fillColor);
376+
377+
mRcVolume = volTrack;
378+
} else {
379+
mRcVolume.SetRectEmpty();
380+
mRcMute.SetRectEmpty();
381+
}
382+
383+
CRect barTextRect;
384+
barTextRect.top = mHCanvas;
322385
barTextRect.bottom = mHClient;
323-
barTextRect.right = mWClient - barMargin;
386+
barTextRect.right = rightEnd;
387+
barTextRect.left = 0;
388+
pDC->DrawText(str, &barTextRect, DT_SINGLELINE | DT_RIGHT | DT_VCENTER);
389+
}
324390

325-
pDC->SelectObject(&mProgressFont);
326-
pDC->SetTextColor(COLOR_PROGRESS_TEXT);
327-
pDC->SetBkMode(TRANSPARENT);
328-
pDC->DrawText(str, &barTextRect, DT_SINGLELINE | DT_RIGHT | DT_VCENTER);
391+
// Draws a small speaker icon centered in |rect|. The icon is rendered from
392+
// GDI primitives (rectangle + triangle) so it works without depending on
393+
// any specific Unicode glyph being available in the progress-band font.
394+
void CViewerView::DrawMuteButton(CDC *pDC, const CRect &rect)
395+
{
396+
COLORREF accent = mVolumeMuted ? Q1UI_COLOR_WARNING : mBarColor;
397+
398+
// Speaker base (small rectangle on the left) + cone (triangle to the right).
399+
int cx = rect.left + rect.Width() / 2;
400+
int cy = rect.top + rect.Height() / 2;
401+
int half = rect.Height() / 2;
402+
int baseW = max(half / 2, 2);
403+
404+
CRect baseRect(cx - baseW, cy - half / 2, cx, cy + half / 2);
405+
pDC->FillSolidRect(baseRect, accent);
406+
407+
CBrush brush(accent);
408+
HGDIOBJ oldBrush = pDC->SelectObject(brush);
409+
HGDIOBJ oldPen = pDC->SelectObject(::GetStockObject(NULL_PEN));
410+
POINT cone[3] = {
411+
{ cx, cy - half + 1 },
412+
{ cx + half, cy - half - 1 },
413+
{ cx + half, cy + half + 1 },
414+
};
415+
pDC->Polygon(cone, 3);
416+
pDC->SelectObject(oldBrush);
417+
418+
// When muted, draw a slash through the speaker.
419+
if (mVolumeMuted) {
420+
CPen slashPen(PS_SOLID, 2, Q1UI_COLOR_DANGER);
421+
HGDIOBJ oldSlash = pDC->SelectObject(slashPen);
422+
pDC->MoveTo(rect.left + 1, rect.bottom - 1);
423+
pDC->LineTo(rect.right - 1, rect.top + 1);
424+
pDC->SelectObject(oldSlash);
425+
}
426+
427+
pDC->SelectObject(oldPen);
329428
}
330429

331430
void CViewerView::_ScaleRgb(BYTE *src, BYTE *dst, int sDst, q1::GridInfo &gi)
@@ -484,8 +583,11 @@ void CViewerView::SetPlayTimer(CViewerDoc* pDoc)
484583
}
485584

486585
double startSec = pDoc->mFps > 0.0 ? pDoc->mCurFrameID / pDoc->mFps : 0.0;
487-
if (mAudioPlayer.Open(pDoc->mPathName.GetString()))
586+
if (mAudioPlayer.Open(pDoc->mPathName.GetString())) {
587+
mAudioPlayer.SetVolume(mVolume);
588+
mAudioPlayer.SetMuted(mVolumeMuted);
488589
mAudioPlayer.Play(startSec);
590+
}
489591
}
490592

491593
// Timer callbacks only post clock ticks, so pausing does not need to wait for
@@ -763,6 +865,7 @@ void CViewerView::DrawHelpMenu(CDC *pDC)
763865
"B Selected box size\n"
764866
"I Interpolate pixels\n"
765867
"N Next color space\n"
868+
"M Mute or unmute video audio\n"
766869
);
767870
pDC->DrawText(manual, &manualRect, DT_LEFT | DT_TOP);
768871
}
@@ -1198,8 +1301,41 @@ BOOL CViewerView::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt)
11981301
return CView::OnMouseWheel(nFlags, zDelta, pt);
11991302
}
12001303

1304+
void CViewerView::UpdateVolumeFromPoint(CPoint point)
1305+
{
1306+
if (mRcVolume.IsRectEmpty())
1307+
return;
1308+
int x = point.x - mRcVolume.left;
1309+
int w = mRcVolume.Width();
1310+
if (w <= 0) return;
1311+
if (x < 0) x = 0;
1312+
if (x > w) x = w;
1313+
mVolume = static_cast<float>(x) / static_cast<float>(w);
1314+
mVolumeMuted = false;
1315+
mAudioPlayer.SetMuted(false);
1316+
mAudioPlayer.SetVolume(mVolume);
1317+
Invalidate(FALSE);
1318+
}
1319+
1320+
void CViewerView::ToggleMute()
1321+
{
1322+
mVolumeMuted = !mVolumeMuted;
1323+
mAudioPlayer.SetMuted(mVolumeMuted);
1324+
Invalidate(FALSE);
1325+
}
1326+
12011327
void CViewerView::OnLButtonDown(UINT nFlags, CPoint point)
12021328
{
1329+
if (!mRcMute.IsRectEmpty() && mRcMute.PtInRect(point)) {
1330+
ToggleMute();
1331+
return;
1332+
}
1333+
if (!mRcVolume.IsRectEmpty() && mRcVolume.PtInRect(point)) {
1334+
SetCapture();
1335+
mVolumeDragging = true;
1336+
UpdateVolumeFromPoint(point);
1337+
return;
1338+
}
12031339
if (mRcProgress.PtInRect(point)) {
12041340
CViewerDoc* pDoc = GetDocument();
12051341
CMainFrame* pMainFrm = static_cast<CMainFrame*>(AfxGetMainWnd());
@@ -1210,8 +1346,12 @@ void CViewerView::OnLButtonDown(UINT nFlags, CPoint point)
12101346
KillPlayTimerSafe();
12111347
}
12121348

1213-
double R = (point.x + 1) / double(mWCanvas);
1349+
int trackW = mRcProgress.Width();
1350+
double R = trackW > 0
1351+
? (point.x - mRcProgress.left + 1) / double(trackW) : 0.0;
12141352
int frameID = max(ROUND2I(R * pDoc->mFrames) - 1, 0);
1353+
int frameMax = static_cast<int>(pDoc->mFrames) - 1;
1354+
if (frameID > frameMax) frameID = frameMax;
12151355
if (pDoc->SeekScene(frameID) >= 0) {
12161356
mKeyProcessing = true;
12171357
Invalidate(FALSE);
@@ -1264,6 +1404,11 @@ void CViewerView::OnLButtonDown(UINT nFlags, CPoint point)
12641404

12651405
void CViewerView::OnLButtonUp(UINT nFlags, CPoint point)
12661406
{
1407+
if (mVolumeDragging) {
1408+
mVolumeDragging = false;
1409+
::ReleaseCapture();
1410+
return;
1411+
}
12671412
if (mSelMode && !(nFlags & MK_CONTROL) && mIsClicked) {
12681413
CPoint transPointS, transPointE;
12691414
QSelRegion selRegion;
@@ -1330,6 +1475,10 @@ void CViewerView::OnLButtonUp(UINT nFlags, CPoint point)
13301475

13311476
void CViewerView::OnMouseMove(UINT nFlags, CPoint point)
13321477
{
1478+
if (mVolumeDragging) {
1479+
UpdateVolumeFromPoint(point);
1480+
return;
1481+
}
13331482
if (mSelMode && !(nFlags & MK_CONTROL)) {
13341483
CPoint transPointS(int((-mXDst + mPointS.x) / mN), int((-mYDst + mPointS.y) / mN));
13351484
CPoint transPointE(int((-mXDst + point.x) / mN), int((-mYDst + point.y) / mN));
@@ -1886,6 +2035,9 @@ void CViewerView::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
18862035
mInterpol = !mInterpol;
18872036
BroadcastDisplayOptions();
18882037
break;
2038+
case 'M':
2039+
ToggleMute();
2040+
break;
18892041
case 'R':
18902042
pDoc->Rotate90();
18912043
{
@@ -1969,6 +2121,12 @@ void CViewerView::OnSize(UINT nType, int cx, int cy)
19692121

19702122
void CViewerView::OnRButtonUp(UINT nFlags, CPoint point)
19712123
{
2124+
if ((!mRcVolume.IsRectEmpty() && mRcVolume.PtInRect(point)) ||
2125+
(!mRcMute.IsRectEmpty() && mRcMute.PtInRect(point))) {
2126+
ToggleMute();
2127+
return;
2128+
}
2129+
19722130
CPoint screenPoint = point;
19732131

19742132
ClientToScreen(&screenPoint);

Viewer/ViewerView.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,11 @@ class CViewerView : public CView
153153
bool mKeyProcessing;
154154

155155
CRect mRcProgress;
156+
CRect mRcVolume;
157+
CRect mRcMute;
158+
float mVolume;
159+
bool mVolumeMuted;
160+
bool mVolumeDragging;
156161
CMenu mMouseMenu;
157162

158163
bool mSelMode;
@@ -177,6 +182,9 @@ class CViewerView : public CView
177182
public:
178183
void AdjustWindowSize();
179184
void ProgressiveDraw(CDC *pDC, CViewerDoc* pDoc, int frameID);
185+
void UpdateVolumeFromPoint(CPoint point);
186+
void DrawMuteButton(CDC *pDC, const CRect &rect);
187+
void ToggleMute();
180188
void SetPlayTimer(CViewerDoc* pDoc);
181189
void KillPlayTimer();
182190
void KillPlayTimerSafe();

0 commit comments

Comments
 (0)