mirror of
https://github.com/reactos/reactos.git
synced 2026-09-08 18:30:51 +08:00
This provides a more convenient way to adjust system volume: through media buttons! Handles `APPCOMMAND_VOLUME_MUTE`, `APPCOMMAND_VOLUME_UP`, and `APPCOMMAND_VOLUME_DOWN` notifications. CORE-12323
353 lines
12 KiB
C++
353 lines
12 KiB
C++
/*
|
|
* PROJECT: ReactOS Explorer
|
|
* LICENSE: LGPL-2.1-or-later (https://spdx.org/licenses/LGPL-2.1-or-later)
|
|
* PURPOSE: Backend class for media buttons
|
|
* COPYRIGHT: Copyright 2026 Vitaly Orekhov <vkvo2000@vivaldi.net>
|
|
*/
|
|
|
|
#include "precomp.h"
|
|
#include <math.h>
|
|
#include "mediabtns.h"
|
|
|
|
CMultimediaBackend::CMultimediaBackend()
|
|
{
|
|
HMODULE hWinMM = LoadLibraryW(L"winmm.dll");
|
|
bool bFailedGetProcAddress = false;
|
|
|
|
if (!hWinMM)
|
|
return;
|
|
|
|
bFailedGetProcAddress |= !WINMM_PROC(mixerOpen, pmxOpen);
|
|
bFailedGetProcAddress |= !WINMM_PROC(mixerClose, pmxClose);
|
|
bFailedGetProcAddress |= !WINMM_PROC(mixerSetControlDetails, pmxControlDetails);
|
|
bFailedGetProcAddress |= !WINMM_PROC(mixerGetControlDetailsW, pmxControlDetails);
|
|
bFailedGetProcAddress |= !WINMM_PROC(mixerGetID, pmxGetID);
|
|
bFailedGetProcAddress |= !WINMM_PROC(mixerGetLineInfoW, pmxLine);
|
|
bFailedGetProcAddress |= !WINMM_PROC(mixerGetLineControlsW, pmxLineControls);
|
|
bFailedGetProcAddress |= !WINMM_PROC(waveOutGetErrorTextW, pwoGetErrText);
|
|
bFailedGetProcAddress |= !WINMM_PROC(waveOutGetNumDevs, pwoGetNumDevs);
|
|
bFailedGetProcAddress |= !WINMM_PROC(waveOutMessage, pwoMessage);
|
|
|
|
if (bFailedGetProcAddress)
|
|
{
|
|
ERR("CMultimediaBackend::ctor: One or more entry points to WinMM functions not found\n");
|
|
FreeLibrary(hWinMM);
|
|
return;
|
|
}
|
|
|
|
m_hWinMM = hWinMM;
|
|
};
|
|
|
|
CMultimediaBackend::~CMultimediaBackend()
|
|
{
|
|
if (m_hMixer)
|
|
mixerClose(m_hMixer);
|
|
|
|
HeapFree(hProcessHeap, 0, m_pdwChannelSteps);
|
|
|
|
FreeLibrary(m_hWinMM);
|
|
}
|
|
|
|
void
|
|
CMultimediaBackend::Initialize()
|
|
{
|
|
if (m_hMixer)
|
|
mixerClose(m_hMixer);
|
|
m_hMixer = NULL;
|
|
|
|
if (m_pdwChannelSteps != NULL)
|
|
HeapFree(hProcessHeap, 0, m_pdwChannelSteps);
|
|
m_pdwChannelSteps = NULL;
|
|
|
|
if (waveOutGetNumDevs() == 0)
|
|
{
|
|
ERR("CMultimediaBackend::Initialize: No waveform-audio output devices detected\n");
|
|
return;
|
|
}
|
|
|
|
DWORD dwWaveOutDeviceID;
|
|
DWORD dw2 = 0;
|
|
|
|
MMRESULT mmres = waveOutMessage((HWAVEOUT)UlongToHandle(WAVE_MAPPER), DRVM_MAPPER_PREFERRED_GET,
|
|
(DWORD_PTR)&dwWaveOutDeviceID, (DWORD_PTR)&dw2);
|
|
if (mmres != MMSYSERR_NOERROR)
|
|
LogError(__func__, "waveOutMessage failed", mmres);
|
|
|
|
if (dwWaveOutDeviceID == (DWORD)-1)
|
|
{
|
|
TRACE("CMultimediaBackend::Initialize: No default output device was assigned, falling back to the first device\n");
|
|
dwWaveOutDeviceID = 0;
|
|
}
|
|
|
|
UINT mxID;
|
|
mmres = mixerGetID((HMIXEROBJ)UlongToHandle(dwWaveOutDeviceID), &mxID, MIXER_OBJECTF_WAVEOUT);
|
|
if (mmres != MMSYSERR_NOERROR)
|
|
{
|
|
LogError(__func__, "mixerGetID failed", mmres);
|
|
|
|
if (mmres == MMSYSERR_NODRIVER)
|
|
{
|
|
ERR(" Not providing fallback to the first mixer device.\n");
|
|
return;
|
|
}
|
|
|
|
mxID = 0;
|
|
}
|
|
|
|
mmres = mixerOpen(&m_hMixer, mxID, NULL, 0, MIXER_OBJECTF_MIXER);
|
|
if (mmres != MMSYSERR_NOERROR)
|
|
{
|
|
m_hMixer = NULL;
|
|
LogError(__func__, "mixerOpen failed", mmres);
|
|
return;
|
|
}
|
|
|
|
MIXERLINEW mxln;
|
|
mxln.cbStruct = sizeof(mxln);
|
|
mxln.dwComponentType = MIXERLINE_COMPONENTTYPE_DST_SPEAKERS;
|
|
|
|
mmres = mixerGetLineInfoW((HMIXEROBJ)m_hMixer, &mxln, MIXER_OBJECTF_HMIXER | MIXER_GETLINEINFOF_COMPONENTTYPE);
|
|
if (mmres != MMSYSERR_NOERROR)
|
|
{
|
|
LogError(__func__, "mixerGetLineInfoW failed", mmres);
|
|
return;
|
|
}
|
|
|
|
m_dwMasterChannels = mxln.cChannels;
|
|
|
|
MIXERLINECONTROLS mxlctrl;
|
|
MIXERCONTROLW mxc;
|
|
mxlctrl.cbStruct = sizeof(mxlctrl);
|
|
mxlctrl.dwLineID = mxln.dwLineID;
|
|
mxlctrl.dwControlID = MIXERCONTROL_CONTROLTYPE_MUTE;
|
|
mxlctrl.cControls = 1;
|
|
mxlctrl.cbmxctrl = sizeof(mxc);
|
|
mxlctrl.pamxctrl = &mxc;
|
|
|
|
mmres = mixerGetLineControlsW((HMIXEROBJ)m_hMixer, &mxlctrl, MIXER_OBJECTF_HMIXER | MIXER_GETLINECONTROLSF_ONEBYTYPE);
|
|
if (mmres != MMSYSERR_NOERROR)
|
|
{
|
|
LogError(__func__, "mixerGetLineControlsW failed retrieving Master Mute control", mmres);
|
|
return;
|
|
}
|
|
|
|
m_dwMasterMuteControlID = mxc.dwControlID;
|
|
|
|
mxlctrl.dwControlID = MIXERCONTROL_CONTROLTYPE_VOLUME;
|
|
|
|
mmres = mixerGetLineControlsW((HMIXEROBJ)m_hMixer, &mxlctrl, MIXER_OBJECTF_HMIXER | MIXER_GETLINECONTROLSF_ONEBYTYPE);
|
|
if (mmres != MMSYSERR_NOERROR)
|
|
{
|
|
LogError(__func__, "mixerGetLineControlsW failed retrieving Master Volume control", mmres);
|
|
return;
|
|
}
|
|
|
|
m_dwMasterVolumeID = mxc.dwControlID;
|
|
m_dwMasterRanges.dwMinimum = mxc.Bounds.dwMinimum;
|
|
m_dwMasterRanges.dwMaximum = mxc.Bounds.dwMaximum;
|
|
|
|
m_pdwChannelSteps = (PDWORD)HeapAlloc(hProcessHeap, 0, m_dwMasterChannels * sizeof(*m_pdwChannelSteps));
|
|
|
|
TRACE("CMultimediaBackend::Initialize: Master controls configured successfully:\n"
|
|
" Master Mute dwControlID: %lu; Master Volume dwControlID %lu\n"
|
|
" Master Volume channels: %lu; Master Volume value range: %lu-%lu\n",
|
|
m_dwMasterMuteControlID, m_dwMasterVolumeID,
|
|
m_dwMasterChannels, m_dwMasterRanges.dwMinimum, m_dwMasterRanges.dwMaximum);
|
|
}
|
|
|
|
bool
|
|
CMultimediaBackend::Mute()
|
|
{
|
|
if (!m_hWinMM)
|
|
{
|
|
WARN("CMultimediaBackend::Mute: WinMM functions unavailable; cannot (un)mute sound. Check the prior class constructor calls\n");
|
|
return false;
|
|
}
|
|
|
|
MIXERCONTROLDETAILS mxcd;
|
|
MIXERCONTROLDETAILS_BOOLEAN mxcdMute;
|
|
mxcd.cbStruct = sizeof(mxcd);
|
|
mxcd.dwControlID = m_dwMasterMuteControlID;
|
|
mxcd.cChannels = 1;
|
|
mxcd.cMultipleItems = 0;
|
|
mxcd.cbDetails = sizeof(mxcdMute);
|
|
mxcd.paDetails = &mxcdMute;
|
|
|
|
mixerGetControlDetailsW((HMIXEROBJ)m_hMixer, &mxcd, MIXER_OBJECTF_HMIXER | MIXER_GETCONTROLDETAILSF_VALUE);
|
|
mxcdMute.fValue = !mxcdMute.fValue;
|
|
mixerSetControlDetails((HMIXEROBJ)m_hMixer, &mxcd, MIXER_OBJECTF_HMIXER | MIXER_SETCONTROLDETAILSF_VALUE);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
CMultimediaBackend::AdjustVolume(_In_ UINT direction, _In_ HANDLE hProcessHeap)
|
|
{
|
|
if (!m_hWinMM)
|
|
{
|
|
WARN("CMultimediaBackend: WinMM functions unavailable; cannot %s volume. Check the prior class constructor calls\n",
|
|
direction == APPCOMMAND_VOLUME_DOWN ? "decrease" : "increase");
|
|
return FALSE;
|
|
}
|
|
|
|
MMRESULT mmres;
|
|
|
|
MIXERCONTROLDETAILS mxcd;
|
|
mxcd.cbStruct = sizeof(mxcd);
|
|
mxcd.cMultipleItems = 0;
|
|
|
|
/* If sound is muted and we want to increase volume, we are unmuting it. */
|
|
if (direction == APPCOMMAND_VOLUME_UP)
|
|
{
|
|
MIXERCONTROLDETAILS_BOOLEAN mxcdMute;
|
|
mxcd.cChannels = 1;
|
|
mxcd.dwControlID = m_dwMasterMuteControlID;
|
|
mxcd.cbDetails = sizeof(mxcdMute);
|
|
mxcd.paDetails = &mxcdMute;
|
|
mxcdMute.fValue = false;
|
|
|
|
mmres = mixerSetControlDetails((HMIXEROBJ)m_hMixer, &mxcd, MIXER_OBJECTF_HMIXER | MIXER_SETCONTROLDETAILSF_VALUE);
|
|
if (mmres != MMSYSERR_NOERROR)
|
|
LogError(__func__, "mixerSetControlDetails failed unmuting Master Volume", mmres);
|
|
}
|
|
|
|
if (GetCurrentVolume(&mxcd) != MMSYSERR_NOERROR)
|
|
return FALSE;
|
|
|
|
PMIXERCONTROLDETAILS_UNSIGNED mxcdVolume = (PMIXERCONTROLDETAILS_UNSIGNED)mxcd.paDetails;
|
|
|
|
bool bEitherCeiling = [&]()
|
|
{
|
|
DWORD dwValue = m_dwMasterRanges.dwMinimum;
|
|
|
|
for (DWORD i = 0; i < m_dwMasterChannels; ++i)
|
|
{
|
|
if (mxcdVolume[i].dwValue > dwValue)
|
|
dwValue = mxcdVolume[i].dwValue;
|
|
}
|
|
|
|
return direction == APPCOMMAND_VOLUME_DOWN
|
|
? dwValue == m_dwMasterRanges.dwMinimum
|
|
: dwValue == m_dwMasterRanges.dwMaximum;
|
|
}();
|
|
|
|
if (bEitherCeiling)
|
|
{
|
|
TRACE("CMultimediaBackend::AdjustVolume: Volume boundary hit, no action\n");
|
|
goto cleanup;
|
|
}
|
|
|
|
CalculateSteps(mxcdVolume);
|
|
|
|
for (DWORD i = 0; i < m_dwMasterChannels; ++i)
|
|
{
|
|
if (direction == APPCOMMAND_VOLUME_DOWN)
|
|
{
|
|
/* Protecting from underflow when decreasing volume */
|
|
if (mxcdVolume[i].dwValue < m_pdwChannelSteps[i])
|
|
mxcdVolume[i].dwValue = m_dwMasterRanges.dwMinimum;
|
|
else
|
|
mxcdVolume[i].dwValue -= m_pdwChannelSteps[i];
|
|
}
|
|
else if (direction == APPCOMMAND_VOLUME_UP)
|
|
{
|
|
/* Protecting from overflow when increasing volume */
|
|
if (m_dwMasterRanges.dwMaximum - mxcdVolume[i].dwValue < m_pdwChannelSteps[i])
|
|
mxcdVolume[i].dwValue = m_dwMasterRanges.dwMaximum;
|
|
else
|
|
mxcdVolume[i].dwValue += m_pdwChannelSteps[i];
|
|
}
|
|
}
|
|
|
|
mmres = mixerSetControlDetails((HMIXEROBJ)m_hMixer, &mxcd, MIXER_OBJECTF_HMIXER | MIXER_SETCONTROLDETAILSF_VALUE);
|
|
if (mmres == MMSYSERR_NOERROR)
|
|
{
|
|
TRACE("CMultimediaBackend::AdjustVolume: Volume values were updated:\n");
|
|
for (DWORD i = 0; i < m_dwMasterChannels; ++i)
|
|
TRACE(" Channel %lu volume: %lu\n", i, mxcdVolume[i].dwValue);
|
|
}
|
|
else
|
|
{
|
|
LogError(__func__, "mixerSetControlDetails failed updating Master Volume control details", mmres);
|
|
}
|
|
|
|
cleanup:
|
|
HeapFree(hProcessHeap, 0, mxcdVolume);
|
|
return TRUE;
|
|
}
|
|
|
|
void
|
|
CMultimediaBackend::CalculateSteps(_Inout_ PMIXERCONTROLDETAILS_UNSIGNED pmxcdVolume)
|
|
{
|
|
DWORD dwHighestVolume = m_dwMasterRanges.dwMinimum;
|
|
DWORD dwLoudestChIdx = 0;
|
|
|
|
/* Find the loudest channel of all available */
|
|
for (DWORD i = 0; i < m_dwMasterChannels; ++i)
|
|
{
|
|
if (pmxcdVolume[i].dwValue < dwHighestVolume)
|
|
continue;
|
|
|
|
dwLoudestChIdx = i;
|
|
dwHighestVolume = pmxcdVolume[dwLoudestChIdx].dwValue;
|
|
}
|
|
|
|
/* Calculate steps for uneven channels */
|
|
for (DWORD i = 0; i < m_dwMasterChannels; ++i)
|
|
{
|
|
/* As we rely on MIXERLINEW.cChannels, there may be a case of multiple channels having equal high volume. */
|
|
if (dwHighestVolume == m_dwMasterRanges.dwMinimum)
|
|
{
|
|
TRACE("CMultimediaBackend::CalculateSteps: All channels were at minimum volume, steps will be unchanged\n");
|
|
break;
|
|
}
|
|
else if (i == dwLoudestChIdx || (pmxcdVolume[i].dwValue == dwHighestVolume))
|
|
{
|
|
m_pdwChannelSteps[i] = (m_dwMasterRanges.dwMaximum - m_dwMasterRanges.dwMinimum) / VOLUME_STEPS_COUNT;
|
|
TRACE("CMultimediaBackend::CalculateSteps: Channel %lu is the loudest, volume step will be %lu\n", i, m_pdwChannelSteps[i]);
|
|
}
|
|
else
|
|
{
|
|
if (dwHighestVolume > 0)
|
|
{
|
|
float dwStepRatio = (float)pmxcdVolume[i].dwValue / dwHighestVolume;
|
|
m_pdwChannelSteps[i] = ceil(dwStepRatio * m_pdwChannelSteps[dwLoudestChIdx]);
|
|
}
|
|
else
|
|
{
|
|
m_pdwChannelSteps[i] = 0;
|
|
}
|
|
TRACE("CMultimediaBackend::CalculateSteps: Channel %lu volume step will be %lu\n", i, m_pdwChannelSteps[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
CMultimediaBackend::LogError(_In_ const char* pszFunctionName, _In_ const char* pszMessage, _In_ MMRESULT mmres)
|
|
{
|
|
static WCHAR pszWinMMErrText[MAXERRORLENGTH] = {0};
|
|
waveOutGetErrorTextW(mmres, pszWinMMErrText, _countof(pszWinMMErrText));
|
|
ERR("CMultimediaBackend::%s: %: %lu (%S)\n", pszFunctionName, pszMessage, mmres, pszWinMMErrText);
|
|
}
|
|
|
|
MMRESULT
|
|
CMultimediaBackend::GetCurrentVolume(_Inout_ PMIXERCONTROLDETAILS pmxcd)
|
|
{
|
|
PMIXERCONTROLDETAILS_UNSIGNED mxcdVolume =
|
|
(PMIXERCONTROLDETAILS_UNSIGNED)HeapAlloc(hProcessHeap, 0,
|
|
m_dwMasterChannels * sizeof(*mxcdVolume));
|
|
|
|
if (mxcdVolume == NULL)
|
|
{
|
|
ERR("CMultimediaBackend::GetCurrentVolume: HeapAlloc failed, requested %lu bytes\n", m_dwMasterChannels * sizeof(*mxcdVolume));
|
|
return MMSYSERR_NOMEM;
|
|
}
|
|
|
|
pmxcd->cChannels = m_dwMasterChannels;
|
|
pmxcd->dwControlID = m_dwMasterVolumeID;
|
|
pmxcd->cbDetails = sizeof(*mxcdVolume);
|
|
pmxcd->paDetails = mxcdVolume;
|
|
|
|
return mixerGetControlDetailsW((HMIXEROBJ)m_hMixer, pmxcd, MIXER_OBJECTF_HMIXER | MIXER_GETCONTROLDETAILSF_VALUE);
|
|
}
|