Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow the volume of NVDA sounds to be set within NVDA. #15038

Merged
merged 4 commits into from Jun 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion nvdaHelper/local/nvdaHelperLocal.def
Expand Up @@ -75,6 +75,6 @@ EXPORTS
wasPlay_sync
wasPlay_pause
wasPlay_resume
wasPlay_setSessionVolume
wasPlay_setChannelVolume
wasPlay_startup
wasPlay_getDevices
53 changes: 19 additions & 34 deletions nvdaHelper/local/wasapi.cpp
Expand Up @@ -38,8 +38,7 @@ const IID IID_IAudioClient = __uuidof(IAudioClient);
const IID IID_IAudioRenderClient = __uuidof(IAudioRenderClient);
const IID IID_IAudioClock = __uuidof(IAudioClock);
const IID IID_IMMNotificationClient = __uuidof(IMMNotificationClient);
const IID IID_IAudioSessionControl = __uuidof(IAudioSessionControl);
const IID IID_ISimpleAudioVolume = __uuidof(ISimpleAudioVolume);
const IID IID_IAudioStreamVolume = __uuidof(IAudioStreamVolume);

/**
* C++ RAII class to manage the lifecycle of a standard Windows HANDLE closed
Expand Down Expand Up @@ -152,12 +151,9 @@ class WasapiPlayer {
/**
* Constructor.
* Specify an empty (not null) deviceId to use the default device.
* Pass GUID_NULL for sessionGuid to use the default audio session.
* Specify an empty (not null) sessionName if you do not wish to set the
* session display name.
*/
WasapiPlayer(wchar_t* deviceId, WAVEFORMATEX format,
ChunkCompletedCallback callback, GUID sessionGuid, wchar_t* sessionName);
ChunkCompletedCallback callback);

/**
* Open the audio device.
Expand All @@ -177,7 +173,7 @@ class WasapiPlayer {
HRESULT sync();
HRESULT pause();
HRESULT resume();
HRESULT setSessionVolume(float level);
HRESULT setChannelVolume(unsigned int channel, float level);

private:
void maybeFireCallback();
Expand Down Expand Up @@ -211,8 +207,6 @@ class WasapiPlayer {
// The maximum number of frames that will fit in the buffer.
UINT32 bufferFrames;
std::wstring deviceId;
GUID sessionGuid;
std::wstring sessionName;
WAVEFORMATEX format;
ChunkCompletedCallback callback;
PlayState playState = PlayState::stopped;
Expand All @@ -228,9 +222,8 @@ class WasapiPlayer {
};

WasapiPlayer::WasapiPlayer(wchar_t* deviceId, WAVEFORMATEX format,
ChunkCompletedCallback callback, GUID sessionGuid, wchar_t* sessionName)
: deviceId(deviceId), format(format), callback(callback),
sessionGuid(sessionGuid), sessionName(sessionName) {
ChunkCompletedCallback callback)
: deviceId(deviceId), format(format), callback(callback) {
wakeEvent = CreateEvent(nullptr, false, false, nullptr);
}

Expand Down Expand Up @@ -260,21 +253,10 @@ HRESULT WasapiPlayer::open(bool force) {
}
hr = client->Initialize(AUDCLNT_SHAREMODE_SHARED,
AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY,
BUFFER_SIZE, 0, &format, &sessionGuid);
BUFFER_SIZE, 0, &format, nullptr);
if (FAILED(hr)) {
return hr;
}
if (!sessionName.empty()) {
CComPtr<IAudioSessionControl> control;
hr = client->GetService(IID_IAudioSessionControl, (void**)&control);
if (FAILED(hr)) {
return hr;
}
hr = control->SetDisplayName(sessionName.c_str(), nullptr);
if (FAILED(hr)) {
return hr;
}
}
hr = client->GetBufferSize(&bufferFrames);
if (FAILED(hr)) {
return hr;
Expand Down Expand Up @@ -510,9 +492,9 @@ HRESULT WasapiPlayer::resume() {
return S_OK;
}

HRESULT WasapiPlayer::setSessionVolume(float level) {
CComPtr<ISimpleAudioVolume> volume;
HRESULT hr = client->GetService(IID_ISimpleAudioVolume, (void**)&volume);
HRESULT WasapiPlayer::setChannelVolume(unsigned int channel, float level) {
CComPtr<IAudioStreamVolume> volume;
HRESULT hr = client->GetService(IID_IAudioStreamVolume, (void**)&volume);
if (hr == AUDCLNT_E_DEVICE_INVALIDATED) {
// If we're using a specific device, it's just been invalidated. Fall back
// to the default device.
Expand All @@ -521,12 +503,12 @@ HRESULT WasapiPlayer::setSessionVolume(float level) {
if (FAILED(hr)) {
return hr;
}
hr = client->GetService(IID_ISimpleAudioVolume, (void**)&volume);
hr = client->GetService(IID_IAudioStreamVolume, (void**)&volume);
}
if (FAILED(hr)) {
return hr;
}
return volume->SetMasterVolume(level, nullptr);
return volume->SetChannelVolume(channel, level);
}

/*
Expand All @@ -535,10 +517,9 @@ HRESULT WasapiPlayer::setSessionVolume(float level) {
*/

WasapiPlayer* wasPlay_create(wchar_t* deviceId, WAVEFORMATEX format,
WasapiPlayer::ChunkCompletedCallback callback, GUID sessionGuid,
wchar_t* sessionName
WasapiPlayer::ChunkCompletedCallback callback
) {
return new WasapiPlayer(deviceId, format, callback, sessionGuid, sessionName);
return new WasapiPlayer(deviceId, format, callback);
}

void wasPlay_destroy(WasapiPlayer* player) {
Expand Down Expand Up @@ -571,8 +552,12 @@ HRESULT wasPlay_resume(WasapiPlayer* player) {
return player->resume();
}

HRESULT wasPlay_setSessionVolume(WasapiPlayer* player, float level) {
return player->setSessionVolume(level);
HRESULT wasPlay_setChannelVolume(
WasapiPlayer* player,
unsigned int channel,
float level
) {
return player->setChannelVolume(channel, level);
}

/**
Expand Down
1 change: 1 addition & 0 deletions source/config/configSpec.py
Expand Up @@ -59,6 +59,7 @@
audioDuckingMode = integer(default=0)
wasapi = boolean(default=true)
soundVolumeFollowsVoice = boolean(default=false)
soundVolume = integer(default=100, min=0, max=100)

# Braille settings
[braille]
Expand Down
33 changes: 28 additions & 5 deletions source/gui/settingsDialogs.py
Expand Up @@ -3065,7 +3065,7 @@ def __init__(self, parent):
wx.CheckBox(audioBox, label=label)
)
self.bindHelpEvent("WASAPI", self.wasapiCheckBox)
self.wasapiCheckBox.Bind(wx.EVT_CHECKBOX, self.onWasapiChange)
self.wasapiCheckBox.Bind(wx.EVT_CHECKBOX, self.onAudioCheckBoxChange)
self.wasapiCheckBox.SetValue(
config.conf["audio"]["wasapi"]
)
Expand All @@ -3078,13 +3078,28 @@ def __init__(self, parent):
wx.CheckBox(audioBox, label=label)
)
self.bindHelpEvent("SoundVolumeFollowsVoice", self.soundVolFollowCheckBox)
self.soundVolFollowCheckBox.Bind(wx.EVT_CHECKBOX, self.onAudioCheckBoxChange)
self.soundVolFollowCheckBox.SetValue(
config.conf["audio"]["soundVolumeFollowsVoice"]
)
self.soundVolFollowCheckBox.defaultValue = self._getDefaultValue(
["audio", "soundVolumeFollowsVoice"])
if not self.wasapiCheckBox.GetValue():
self.soundVolFollowCheckBox.Disable()
# Translators: This is the label for a slider control in the
# Advanced settings panel.
label = _("Volume of NVDA sounds (requires WASAPI)")
self.soundVolSlider: nvdaControls.EnhancedInputSlider = audioGroup.addLabeledControl(
label,
nvdaControls.EnhancedInputSlider,
minValue=0,
maxValue=100
)
self.bindHelpEvent("SoundVolume", self.soundVolSlider)
self.soundVolSlider.SetValue(
config.conf["audio"]["soundVolume"]
)
self.soundVolSlider.defaultValue = self._getDefaultValue(
["audio", "soundVolume"])
self.onAudioCheckBoxChange()

# Translators: This is the label for a group of advanced options in the
# Advanced settings panel
Expand Down Expand Up @@ -3147,8 +3162,13 @@ def onOpenScratchpadDir(self,evt):
path=config.getScratchpadDir(ensureExists=True)
os.startfile(path)

def onWasapiChange(self, evt: wx.CommandEvent):
self.soundVolFollowCheckBox.Enable(evt.IsChecked())
def onAudioCheckBoxChange(self, evt: Optional[wx.CommandEvent] = None):
wasapi = self.wasapiCheckBox.IsChecked()
self.soundVolFollowCheckBox.Enable(wasapi)
self.soundVolSlider.Enable(
wasapi
and not self.soundVolFollowCheckBox.IsChecked()
)

def _getDefaultValue(self, configPath):
return config.conf.getConfigValidation(configPath).default
Expand Down Expand Up @@ -3178,6 +3198,7 @@ def haveConfigDefaultsBeenRestored(self):
and self.reportTransparentColorCheckBox.GetValue() == self.reportTransparentColorCheckBox.defaultValue
and self.wasapiCheckBox.GetValue() == self.wasapiCheckBox.defaultValue
and self.soundVolFollowCheckBox.GetValue() == self.soundVolFollowCheckBox.defaultValue
and self.soundVolSlider.GetValue() == self.soundVolSlider.defaultValue
and set(self.logCategoriesList.CheckedItems) == set(self.logCategoriesList.defaultCheckedItems)
and self.playErrorSoundCombo.GetSelection() == self.playErrorSoundCombo.defaultValue
and True # reduce noise in diff when the list is extended.
Expand Down Expand Up @@ -3205,6 +3226,7 @@ def restoreToDefaults(self):
self.reportTransparentColorCheckBox.SetValue(self.reportTransparentColorCheckBox.defaultValue)
self.wasapiCheckBox.SetValue(self.wasapiCheckBox.defaultValue)
self.soundVolFollowCheckBox.SetValue(self.soundVolFollowCheckBox.defaultValue)
self.soundVolSlider.SetValue(self.soundVolSlider.defaultValue)
self.logCategoriesList.CheckedItems = self.logCategoriesList.defaultCheckedItems
self.playErrorSoundCombo.SetSelection(self.playErrorSoundCombo.defaultValue)
self._defaultsRestored = True
Expand Down Expand Up @@ -3237,6 +3259,7 @@ def onSave(self):
)
config.conf["audio"]["wasapi"] = self.wasapiCheckBox.IsChecked()
config.conf["audio"]["soundVolumeFollowsVoice"] = self.soundVolFollowCheckBox.IsChecked()
config.conf["audio"]["soundVolume"] = self.soundVolSlider.GetValue()
config.conf["annotations"]["reportDetails"] = self.annotationsDetailsCheckBox.IsChecked()
config.conf["annotations"]["reportAriaDescription"] = self.ariaDescCheckBox.IsChecked()
config.conf["braille"]["enableHidBrailleSupport"] = self.supportHidBrailleCombo.GetSelection()
Expand Down