Skip to content

Commit 5637ef8

Browse files
committed
[EXPLORER] Implement volume control
1 parent 43c7348 commit 5637ef8

File tree

5 files changed

+285
-3
lines changed

5 files changed

+285
-3
lines changed

base/shell/explorer/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ list(APPEND SOURCE
88
appbar.cpp
99
desktop.cpp
1010
explorer.cpp
11+
mediabtns.cpp
1112
notifyiconscust.cpp
1213
rshell.cpp
1314
settings.cpp

base/shell/explorer/mediabtns.cpp

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
/*
2+
* PROJECT: ReactOS Explorer
3+
* LICENSE: LGPL-2.1-or-later (https://spdx.org/licenses/LGPL-2.1-or-later)
4+
* PURPOSE: Backend class for media buttons
5+
* COPYRIGHT: Copyright 2026 Vitaly Orekhov <vkvo2000@vivaldi.net>
6+
*/
7+
8+
#include "precomp.h"
9+
#include "mediabtns.h"
10+
11+
CMultimediaBackend::CMultimediaBackend()
12+
{
13+
m_isWinMMAvailable = false;
14+
15+
m_hWinMM = LoadLibraryW(L"winmm.dll");
16+
if (!m_hWinMM)
17+
return;
18+
19+
if (!WINMM_PROC(mixerOpen, pmxOpen))
20+
return;
21+
22+
if (!WINMM_PROC(mixerClose, pmxClose))
23+
return;
24+
25+
if (!WINMM_PROC(mixerSetControlDetails, pmxControlDetails))
26+
return;
27+
28+
if (!WINMM_PROC(mixerGetControlDetailsW, pmxControlDetails))
29+
return;
30+
31+
if (!WINMM_PROC(mixerGetLineInfoW, pmxLine))
32+
return;
33+
34+
if (!WINMM_PROC(mixerGetLineControlsW, pmxLineControls))
35+
return;
36+
37+
if (!WINMM_PROC(waveOutGetErrorTextW, pwoGetErrText))
38+
return;
39+
};
40+
41+
CMultimediaBackend::~CMultimediaBackend()
42+
{
43+
if (m_isWinMMAvailable)
44+
mixerClose(m_hMixer);
45+
46+
CloseHandle(m_hWinMM);
47+
}
48+
49+
void
50+
CMultimediaBackend::GetMasterMixer()
51+
{
52+
if (m_hMixer != NULL)
53+
mixerClose(m_hMixer);
54+
55+
WCHAR pszWinMMErrText[MAXERRORLENGTH] = {0};
56+
57+
MMRESULT mmres = mixerOpen(&m_hMixer, 0, NULL, 0, MIXER_OBJECTF_MIXER);
58+
if (mmres != MMSYSERR_NOERROR)
59+
{
60+
waveOutGetErrorTextW(mmres, pszWinMMErrText, _countof(pszWinMMErrText));
61+
ERR("GetMasterMixer: mixerOpen failed: %lu (%S)\n", mmres, pszWinMMErrText);
62+
return;
63+
}
64+
65+
MIXERLINEW mxln;
66+
mxln.cbStruct = sizeof(mxln);
67+
mxln.dwComponentType = MIXERLINE_COMPONENTTYPE_DST_SPEAKERS;
68+
69+
mmres = mixerGetLineInfoW((HMIXEROBJ)m_hMixer, &mxln, MIXER_OBJECTF_HMIXER | MIXER_GETLINEINFOF_COMPONENTTYPE);
70+
if (mmres != MMSYSERR_NOERROR)
71+
{
72+
waveOutGetErrorTextW(mmres, pszWinMMErrText, _countof(pszWinMMErrText));
73+
ERR("GetMasterMixer: mixerGetLineInfoW failed: %lu (%S)\n", mmres, pszWinMMErrText);
74+
return;
75+
}
76+
77+
m_dwMasterChannels = mxln.cChannels;
78+
79+
MIXERLINECONTROLS mxlctrl;
80+
MIXERCONTROLW mxc;
81+
mxlctrl.cbStruct = sizeof(mxlctrl);
82+
mxlctrl.dwLineID = mxln.dwLineID;
83+
mxlctrl.dwControlID = MIXERCONTROL_CONTROLTYPE_MUTE;
84+
mxlctrl.cControls = 1;
85+
mxlctrl.cbmxctrl = sizeof(mxc);
86+
mxlctrl.pamxctrl = &mxc;
87+
88+
mmres = mixerGetLineControlsW((HMIXEROBJ)m_hMixer, &mxlctrl, MIXER_OBJECTF_HMIXER | MIXER_GETLINECONTROLSF_ONEBYTYPE);
89+
if (mmres != MMSYSERR_NOERROR)
90+
{
91+
waveOutGetErrorTextW(mmres, pszWinMMErrText, _countof(pszWinMMErrText));
92+
ERR("GetMasterMixer: mixerGetLineControlsW failed retrieving Master Mute control: %lu (%S)\n", mmres, pszWinMMErrText);
93+
return;
94+
}
95+
96+
m_dwMasterMuteControlID = mxc.dwControlID;
97+
98+
mxlctrl.dwControlID = MIXERCONTROL_CONTROLTYPE_VOLUME;
99+
100+
mmres = mixerGetLineControlsW((HMIXEROBJ)m_hMixer, &mxlctrl, MIXER_OBJECTF_HMIXER | MIXER_GETLINECONTROLSF_ONEBYTYPE);
101+
if (mmres != MMSYSERR_NOERROR)
102+
{
103+
waveOutGetErrorTextW(mmres, pszWinMMErrText, _countof(pszWinMMErrText));
104+
ERR("GetMasterMixer: mixerGetLineControlsW failed retrieving Master Volume control: %lu (%S)\n", mmres, pszWinMMErrText);
105+
return;
106+
}
107+
108+
m_dwMasterVolumeID = mxc.dwControlID;
109+
m_dwMasterRanges[0] = mxc.Bounds.dwMinimum;
110+
m_dwMasterRanges[1] = mxc.Bounds.dwMaximum;
111+
m_dwVolumeStep = (m_dwMasterRanges[1] - m_dwMasterRanges[0]) / VOLUME_STEPS_COUNT;
112+
113+
TRACE("GetMasterMixer: Master controls configured successfully:\n"
114+
" Master Mute dwControlID: %lu; Master Volume dwControlID %lu\n"
115+
" Master Volume channels: %lu; Master Volume value range: %lu-%lu\n"
116+
" Master Volume update step: %lu\n",
117+
m_dwMasterMuteControlID, m_dwMasterVolumeID,
118+
m_dwMasterChannels, m_dwMasterRanges[0], m_dwMasterRanges[1],
119+
m_dwVolumeStep);
120+
121+
m_isWinMMAvailable = true;
122+
}
123+
124+
bool
125+
CMultimediaBackend::Mute()
126+
{
127+
if (!m_isWinMMAvailable)
128+
{
129+
WARN("CMultimediaBackend: Mixer is not available; cannot (un)mute sound. Check the prior class constructor calls\n");
130+
return false;
131+
}
132+
133+
MIXERCONTROLDETAILS mxcd;
134+
MIXERCONTROLDETAILS_BOOLEAN mxcdMute;
135+
136+
mxcd.cbStruct = sizeof(mxcd);
137+
mxcd.dwControlID = m_dwMasterMuteControlID;
138+
mxcd.cChannels = 1;
139+
mxcd.cMultipleItems = 0;
140+
mxcd.cbDetails = sizeof(mxcdMute);
141+
mxcd.paDetails = &mxcdMute;
142+
143+
mixerGetControlDetailsW((HMIXEROBJ)m_hMixer, &mxcd, MIXER_OBJECTF_HMIXER | MIXER_GETCONTROLDETAILSF_VALUE);
144+
145+
mxcdMute.fValue = !mxcdMute.fValue;
146+
147+
mixerSetControlDetails((HMIXEROBJ)m_hMixer, &mxcd, MIXER_OBJECTF_HMIXER | MIXER_SETCONTROLDETAILSF_VALUE);
148+
149+
return true;
150+
}
151+
152+
bool
153+
CMultimediaBackend::AdjustVolume(_In_ UINT direction, _In_ HANDLE hProcessHeap)
154+
{
155+
if (!m_isWinMMAvailable)
156+
{
157+
WARN("CMultimediaBackend: Mixer is not available; cannot %s volume. Check the prior class constructor calls\n",
158+
direction == APPCOMMAND_VOLUME_DOWN ? "decrease" : "increase");
159+
return FALSE;
160+
}
161+
162+
MIXERCONTROLDETAILS mxcd;
163+
mxcd.cbStruct = sizeof(mxcd);
164+
mxcd.cMultipleItems = 0;
165+
166+
/* If sound is muted and we want to increase volume, we are unmuting it. */
167+
if (direction == APPCOMMAND_VOLUME_UP)
168+
{
169+
MIXERCONTROLDETAILS_BOOLEAN mxcdMute;
170+
171+
mxcd.cChannels = 1;
172+
mxcd.dwControlID = m_dwMasterMuteControlID;
173+
mxcd.cbDetails = sizeof(mxcdMute);
174+
mxcd.paDetails = &mxcdMute;
175+
mxcdMute.fValue = false;
176+
177+
mixerSetControlDetails((HMIXEROBJ)m_hMixer, &mxcd, MIXER_OBJECTF_HMIXER | MIXER_SETCONTROLDETAILSF_VALUE);
178+
}
179+
180+
PMIXERCONTROLDETAILS_UNSIGNED mxcdVolume =
181+
(PMIXERCONTROLDETAILS_UNSIGNED)HeapAlloc(hProcessHeap,
182+
0,
183+
m_dwMasterChannels * sizeof(*mxcdVolume));
184+
185+
if (mxcdVolume == NULL)
186+
{
187+
ERR("CMultimediaBackend: HeapAlloc failed, requested %lu bytes\n", m_dwMasterChannels * sizeof(*mxcdVolume));
188+
return FALSE;
189+
}
190+
191+
mxcd.cChannels = m_dwMasterChannels;
192+
mxcd.dwControlID = m_dwMasterVolumeID;
193+
mxcd.cbDetails = sizeof(*mxcdVolume);
194+
mxcd.paDetails = mxcdVolume;
195+
196+
mixerGetControlDetailsW((HMIXEROBJ)m_hMixer, &mxcd, MIXER_OBJECTF_HMIXER | MIXER_GETCONTROLDETAILSF_VALUE);
197+
198+
/* TODO: implement restoring balance between channels when all channels were put to 0 */
199+
for (DWORD i = 0; i < m_dwMasterChannels; ++i)
200+
{
201+
if (direction == APPCOMMAND_VOLUME_DOWN)
202+
{
203+
/* Protecting from underflow when decreasing volume */
204+
if (mxcdVolume[i].dwValue < m_dwVolumeStep)
205+
mxcdVolume[i].dwValue = 0;
206+
else
207+
mxcdVolume[i].dwValue -= m_dwVolumeStep;
208+
}
209+
else
210+
{
211+
/* Protecting from overflow when increasing volume */
212+
if (m_dwMasterRanges[1] - mxcdVolume[i].dwValue < m_dwVolumeStep)
213+
mxcdVolume[i].dwValue = m_dwMasterRanges[1];
214+
else
215+
mxcdVolume[i].dwValue += m_dwVolumeStep;
216+
}
217+
}
218+
219+
mixerSetControlDetails((HMIXEROBJ)m_hMixer, &mxcd, MIXER_OBJECTF_HMIXER | MIXER_SETCONTROLDETAILSF_VALUE);
220+
HeapFree(hProcessHeap, 0, mxcdVolume);
221+
return TRUE;
222+
}

base/shell/explorer/mediabtns.h

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* PROJECT: ReactOS Explorer
3+
* LICENSE: LGPL-2.1-or-later (https://spdx.org/licenses/LGPL-2.1-or-later)
4+
* PURPOSE: Backend class for media buttons (header)
5+
* COPYRIGHT: Copyright 2026 Vitaly Orekhov <vkvo2000@vivaldi.net>
6+
*/
7+
8+
typedef MMRESULT (WINAPI* pmxControlDetails)(HMIXEROBJ hmxobj, LPMIXERCONTROLDETAILS pmxcd, DWORD fdwDetails);
9+
typedef MMRESULT (WINAPI* pmxLine)(HMIXEROBJ hmxobj, LPMIXERLINEW pmxcd, DWORD fdwDetails);
10+
typedef MMRESULT (WINAPI* pmxLineControls)(HMIXEROBJ hmxobj, LPMIXERLINECONTROLSW pmxcd, DWORD fdwDetails);
11+
typedef MMRESULT (WINAPI* pmxOpen)(LPHMIXER phmx, UINT uMxId, DWORD_PTR dwCallback, DWORD_PTR dwInstance, DWORD fdwOpen);
12+
typedef MMRESULT (WINAPI* pmxClose)(HMIXER hmx);
13+
typedef MMRESULT (WINAPI* pwoGetErrText)(MMRESULT mmrError, LPWSTR pszText, UINT cchText);
14+
#define WINMM_PROC(func, type) ((func) = (type)GetProcAddress(m_hWinMM, #func))
15+
16+
/* Media volume buttons must be synchronized with sndvol32 meters */
17+
#define VOLUME_STEPS_COUNT 25
18+
19+
class CMultimediaBackend
20+
{
21+
public:
22+
CMultimediaBackend();
23+
~CMultimediaBackend();
24+
CMultimediaBackend(CMultimediaBackend&) = delete;
25+
CMultimediaBackend(CMultimediaBackend&&) = delete;
26+
CMultimediaBackend& operator=(CMultimediaBackend&) = delete;
27+
CMultimediaBackend& operator=(CMultimediaBackend&&) = delete;
28+
29+
void GetMasterMixer();
30+
bool Mute();
31+
bool AdjustVolume(_In_ UINT direction, _In_ HANDLE hProcessHeap);
32+
33+
private:
34+
pmxControlDetails mixerSetControlDetails;
35+
pmxControlDetails mixerGetControlDetailsW;
36+
pmxLine mixerGetLineInfoW;
37+
pmxLineControls mixerGetLineControlsW;
38+
pmxOpen mixerOpen;
39+
pmxClose mixerClose;
40+
pwoGetErrText waveOutGetErrorTextW;
41+
42+
HMODULE m_hWinMM;
43+
HMIXER m_hMixer = NULL;
44+
DWORD m_dwMasterMuteControlID;
45+
DWORD m_dwMasterVolumeID;
46+
DWORD m_dwMasterChannels;
47+
DWORD m_dwMasterRanges[2];
48+
DWORD m_dwVolumeStep;
49+
bool m_isWinMMAvailable;
50+
};

base/shell/explorer/precomp.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include <atlstr.h>
3232
#include <atlcoll.h>
3333
#include <atlsimpcoll.h>
34+
#include <mmddk.h>
3435
#include <shellapi.h>
3536
#include <shlobj.h>
3637
#include <shlwapi.h>

base/shell/explorer/taskswnd.cpp

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
#include "precomp.h"
10+
#include "mediabtns.h"
1011
#include <commoncontrols.h>
1112
#include <regstr.h>
1213
#include <shlwapi_undoc.h>
@@ -1530,6 +1531,10 @@ class CTaskSwitchWnd :
15301531
#if DUMP_TASKS != 0
15311532
SetTimer(hwnd, 1, 5000, NULL);
15321533
#endif
1534+
1535+
/* WinMM is needed to change system volume via media buttons */
1536+
Mixer.GetMasterMixer();
1537+
15331538
return TRUE;
15341539
}
15351540

@@ -1598,12 +1603,12 @@ class CTaskSwitchWnd :
15981603
return TRUE;
15991604
switch (uAppCmd)
16001605
{
1606+
// When MMDevAPI arrives, try IMMDeviceEnumerator::GetDefaultAudioEndpoint first and then fall back to WinMM.
16011607
case APPCOMMAND_VOLUME_MUTE:
1608+
return Mixer.Mute();
16021609
case APPCOMMAND_VOLUME_DOWN:
16031610
case APPCOMMAND_VOLUME_UP:
1604-
// TODO: Try IMMDeviceEnumerator::GetDefaultAudioEndpoint first and then fall back to mixer.
1605-
FIXME("Call the mixer API to change the global volume\n");
1606-
return TRUE;
1611+
return Mixer.AdjustVolume(uAppCmd, hProcessHeap);
16071612
case APPCOMMAND_BROWSER_SEARCH:
16081613
return SHFindFiles(NULL, NULL);
16091614
}
@@ -2279,6 +2284,9 @@ class CTaskSwitchWnd :
22792284
BEGIN_COM_MAP(CTaskSwitchWnd)
22802285
COM_INTERFACE_ENTRY_IID(IID_IOleWindow, IOleWindow)
22812286
END_COM_MAP()
2287+
2288+
private:
2289+
CMultimediaBackend Mixer;
22822290
};
22832291

22842292
HRESULT CTaskSwitchWnd_CreateInstance(IN HWND hWndParent, IN OUT ITrayWindow *Tray, REFIID riid, void **ppv)

0 commit comments

Comments
 (0)