<# .SYNOPSIS Reports, enables, or disables Audio Enhancements (SysFx) for an endpoint. .DESCRIPTION Finds Windows audio endpoints by FriendlyName, selects one endpoint, and either reports current Audio Enhancements state, enables enhancements, or disables enhancements. When enabling or disabling, the script can optionally back up FxProperties and restart the Windows Audio service to apply the change. .PARAMETER NameLike FriendlyName substring used to match audio endpoints. Example: Jabra finds any endpoint whose FriendlyName contains "Jabra". .PARAMETER EndpointIndex Zero-based index of the endpoint to process from the filtered match list. Default: 0. Use this when multiple endpoints match NameLike. .PARAMETER EndpointType Endpoint category to search. Allowed values: Capture, Render. Default: Capture. .PARAMETER BackupDirectory Directory used to store the FxProperties .reg backup file. Default: current working directory. If the directory does not exist, the script creates it. .PARAMETER SkipBackup Skips exporting the FxProperties registry key backup. By default, backup export runs before changes are applied. .PARAMETER NoServiceRestart Skips restarting the Windows Audio service after registry update. By default, the script restarts audiosrv to apply endpoint changes immediately. .PARAMETER PassThru Returns the full result object to the pipeline. Without PassThru, a concise projection is returned for interactive use. .PARAMETER Enable Sets audio enhancements to enabled for the selected endpoint. Equivalent registry value: Disable_SysFx = 0. .PARAMETER Disable Sets audio enhancements to disabled for the selected endpoint. Equivalent registry value: Disable_SysFx = 1. If neither Enable nor Disable is provided, the script reports current state only. .PARAMETER ExportRegFile Generates a .reg file with the requested registry changes without applying them. The file is saved to BackupDirectory with a timestamped name (FxProperties--.reg). Backup export still runs unless SkipBackup is also specified. When this parameter is used, service restart is automatically skipped. Useful for creating deployment scripts or sharing registry changes. .EXAMPLE .\Manage-Audio-Enhancements.ps1 -NameLike "Jabra" -Verbose Reports current enhancements state for the first matching endpoint and writes verbose diagnostics. .EXAMPLE .\Manage-Audio-Enhancements.ps1 -NameLike "Jabra" -EndpointIndex 1 -Disable -SkipBackup -NoServiceRestart -WhatIf Shows what would happen when disabling enhancements for the second matching endpoint without making changes. .EXAMPLE .\Manage-Audio-Enhancements.ps1 -NameLike "USB" -EndpointType Render -BackupDirectory "C:\Backups\Audio" -PassThru Reports current state for a matching render endpoint and returns the full result object. .EXAMPLE .\Manage-Audio-Enhancements.ps1 -NameLike "Jabra" -Disable -Verbose Disables audio enhancements for the first matching endpoint. .EXAMPLE .\Manage-Audio-Enhancements.ps1 -NameLike "Jabra" -Enable -NoServiceRestart Enables audio enhancements for the first matching endpoint without restarting audiosrv. .EXAMPLE .\Manage-Audio-Enhancements.ps1 -NameLike "C920" -Disable -ExportRegFile Generates a .reg file to disable audio enhancements for C920 without applying it. .NOTES Requires Administrator privileges to write under HKLM and restart services. #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$NameLike, [Parameter()] [ValidateRange(0, 999)] [int]$EndpointIndex = 0, [Parameter()] [ValidateSet('Capture', 'Render')] [string]$EndpointType = 'Capture', [Parameter()] [ValidateNotNullOrEmpty()] [string]$BackupDirectory = (Get-Location).Path, [Parameter()] [switch]$SkipBackup, [Parameter()] [switch]$NoServiceRestart, [Parameter()] [switch]$PassThru, [switch]$Enable, [switch]$Disable, [Parameter()] [switch]$ExportRegFile ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' #region Constants $script:DisableSysFxValueName = '{1da5d803-d492-4edd-8c23-e0c0ffee7f0e},5' $script:MmDevicesCaptureBase = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\MMDevices\Audio\Capture' $script:MmDevicesRenderBase = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\MMDevices\Audio\Render' $script:AudioServiceName = 'audiosrv' #endregion #region Classes <# .SYNOPSIS Encapsulates endpoint discovery and SysFx disable operations. #> class AudioEnhancementManager { [string]$DisableSysFxValueName [string]$MmDevicesBase [string]$AudioServiceName <# .SYNOPSIS Creates a manager for the selected endpoint type. #> AudioEnhancementManager( [string]$DisableSysFxValueName, [string]$MmDevicesBase, [string]$AudioServiceName ) { if ([string]::IsNullOrWhiteSpace($DisableSysFxValueName)) { throw [System.ArgumentException]::new('Process: Initialize manager. Error: Disable SysFx value name is empty. Cause: Required constructor input was missing. Solution: Provide a valid property key name.') } if ([string]::IsNullOrWhiteSpace($MmDevicesBase)) { throw [System.ArgumentException]::new('Process: Initialize manager. Error: MMDevices base path is empty. Cause: Required constructor input was missing. Solution: Provide a valid registry base path.') } if ([string]::IsNullOrWhiteSpace($AudioServiceName)) { throw [System.ArgumentException]::new('Process: Initialize manager. Error: Audio service name is empty. Cause: Required constructor input was missing. Solution: Provide a valid service name.') } $this.DisableSysFxValueName = $DisableSysFxValueName $this.MmDevicesBase = $MmDevicesBase $this.AudioServiceName = $AudioServiceName } <# .SYNOPSIS Retrieves PnP endpoints matching endpoint type and name pattern. #> [object[]] GetMatchingEndpoints([string]$NameLike, [string]$EndpointType) { if ([string]::IsNullOrWhiteSpace($NameLike)) { throw [System.ArgumentException]::new('Process: Discover endpoints. Error: NameLike is empty. Cause: Search criteria was not provided. Solution: Pass -NameLike with a non-empty value.') } if ([string]::IsNullOrWhiteSpace($EndpointType)) { throw [System.ArgumentException]::new('Process: Discover endpoints. Error: EndpointType is empty. Cause: Endpoint type criteria was not provided. Solution: Pass Capture or Render.') } $instancePrefix = '' if ($EndpointType -eq 'Capture') { $instancePrefix = 'SWD\MMDEVAPI\{0.0.1.00000000}.' } else { $instancePrefix = 'SWD\MMDEVAPI\{0.0.0.00000000}.' } if (-not (Get-Command -Name Get-PnpDevice -ErrorAction SilentlyContinue)) { throw [System.InvalidOperationException]::new('Process: Discover endpoints. Error: Get-PnpDevice command is unavailable. Cause: PnpDevice cmdlets are not present in this PowerShell session. Solution: Run on a supported Windows host with PnpDevice cmdlets available.') } $endpointMatches = Get-PnpDevice -Class AudioEndpoint | Where-Object { $_.FriendlyName -like "*$NameLike*" -and $_.InstanceId -like "$instancePrefix*" } | Sort-Object -Property FriendlyName, InstanceId if (-not $endpointMatches) { throw [System.InvalidOperationException]::new("Process: Discover endpoints. Error: No matching endpoints found. Cause: No $EndpointType endpoint friendly name contains '$NameLike'. Solution: Verify the endpoint name and type with Get-PnpDevice -Class AudioEndpoint.") } return @($endpointMatches) } <# .SYNOPSIS Extracts endpoint GUID from an MMDEVAPI instance ID. #> [string] GetEndpointGuid([string]$InstanceId) { if ([string]::IsNullOrWhiteSpace($InstanceId)) { throw [System.ArgumentException]::new('Process: Parse endpoint ID. Error: InstanceId is empty. Cause: Endpoint instance identifier was missing. Solution: Use a valid endpoint object from Get-PnpDevice.') } $guidMatch = [regex]::Match($InstanceId, '\}\.\{(?[0-9A-Fa-f-]+)\}$') if (-not $guidMatch.Success) { throw [System.FormatException]::new("Process: Parse endpoint ID. Error: Unexpected InstanceId format. Cause: '$InstanceId' did not match expected MMDEVAPI format. Solution: Confirm endpoint source and format before processing.") } $endpointGuid = "{$($guidMatch.Groups['guid'].Value)}" return $endpointGuid } <# .SYNOPSIS Builds and validates FxProperties path for an endpoint. #> [string] GetFxPropertiesPath([string]$EndpointGuid) { if ([string]::IsNullOrWhiteSpace($EndpointGuid)) { throw [System.ArgumentException]::new('Process: Build FxProperties path. Error: EndpointGuid is empty. Cause: Parsed GUID value was missing. Solution: Ensure GUID extraction succeeded.') } $fxPath = Join-Path -Path $this.MmDevicesBase -ChildPath "$EndpointGuid\FxProperties" if (-not (Test-Path -LiteralPath $fxPath)) { throw [System.IO.DirectoryNotFoundException]::new("Process: Locate FxProperties key. Error: Registry path not found. Cause: '$fxPath' does not exist for this endpoint. Solution: Confirm endpoint GUID and registry permissions.") } return $fxPath } <# .SYNOPSIS Exports endpoint FxProperties to a .reg backup file. #> [string] ExportFxBackup([string]$EndpointGuid, [string]$BackupDirectory) { if ([string]::IsNullOrWhiteSpace($EndpointGuid)) { throw [System.ArgumentException]::new('Process: Export backup. Error: EndpointGuid is empty. Cause: Parsed GUID value was missing. Solution: Ensure GUID extraction succeeded.') } if ([string]::IsNullOrWhiteSpace($BackupDirectory)) { throw [System.ArgumentException]::new('Process: Export backup. Error: BackupDirectory is empty. Cause: Backup target was not provided. Solution: Pass -BackupDirectory with a valid path.') } if (-not (Test-Path -LiteralPath $BackupDirectory)) { $null = New-Item -Path $BackupDirectory -ItemType Directory -Force -ErrorAction Stop } $backupFileName = "FxProperties-$($EndpointGuid.Trim('{}')).reg" $backupFilePath = Join-Path -Path $BackupDirectory -ChildPath $backupFileName $exportPath = "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\MMDevices\Audio\$($this.GetAudioSubPath())\$EndpointGuid\FxProperties" $exportCommand = "reg.exe export `"$exportPath`" `"$backupFilePath`" /y" $null = & cmd.exe /c $exportCommand if ($LASTEXITCODE -ne 0) { Write-Verbose 'Initial backup export failed in current session; requesting UAC elevation.' [AudioEnhancementRuntime]::InvokeElevatedPowerShellCommand($exportCommand, 'Export FxProperties backup') } if (-not (Test-Path -LiteralPath $backupFilePath)) { throw [System.IO.IOException]::new("Process: Export backup. Error: Backup file was not created. Cause: Registry export command completed without creating '$backupFilePath'. Solution: Verify registry access and backup directory permissions.") } return $backupFilePath } <# .SYNOPSIS Sets Disable_SysFx to the requested value, optionally exporting to a .reg file instead of applying. #> [string] SetDisableSysFxValue([string]$FxPath, [int]$DisableSysFxValue, [string]$ExportDirectory = '', [string]$Action = '') { if ([string]::IsNullOrWhiteSpace($FxPath)) { throw [System.ArgumentException]::new('Process: Disable SysFx. Error: FxPath is empty. Cause: Target registry path was missing. Solution: Build a valid FxProperties path before writing values.') } if (($DisableSysFxValue -ne 0) -and ($DisableSysFxValue -ne 1)) { throw [System.ArgumentOutOfRangeException]::new('DisableSysFxValue', $DisableSysFxValue, 'Process: Set SysFx value. Error: DisableSysFxValue is invalid. Cause: Only values 0 (enabled) and 1 (disabled) are supported. Solution: Pass 0 or 1.') } # If ExportDirectory is provided, export to .reg file instead of applying if (-not [string]::IsNullOrWhiteSpace($ExportDirectory)) { $valueName = $this.DisableSysFxValueName $regFilePath = $this.ExportRegFile($FxPath, $valueName, $DisableSysFxValue, $ExportDirectory, $Action) Write-Verbose "Registry file exported to: $regFilePath" return $regFilePath } $valueName = $this.DisableSysFxValueName try { $null = New-ItemProperty -Path $FxPath -Name $valueName -PropertyType DWord -Value $DisableSysFxValue -Force -ErrorAction Stop } catch { Write-Verbose "Registry write denied (exception: $($_.Exception.GetType().Name)); generating .reg file for user approval via regedit." $this.ImportViaRegFile($FxPath, $valueName, $DisableSysFxValue) } Start-Sleep -Milliseconds 300 Write-Verbose 'Verifying registry value was set.' $verifyValue = (Get-ItemProperty -Path $FxPath -Name $valueName -ErrorAction SilentlyContinue).$valueName if ($verifyValue -ne $DisableSysFxValue) { throw [System.InvalidOperationException]::new("Process: Set SysFx value. Error: Registry write verification failed. Cause: Value at '$FxPath' is '$verifyValue' instead of '$DisableSysFxValue'. Solution: Re-run with elevation and confirm endpoint registry permissions.") } Write-Verbose 'Registry write verified successfully.' return '' } <# .SYNOPSIS Generates a .reg file with registry changes and saves it to the specified directory. #> [string] ExportRegFile([string]$FxPath, [string]$ValueName, [int]$DisableSysFxValue, [string]$ExportDirectory, [string]$Action) { if ([string]::IsNullOrWhiteSpace($FxPath)) { throw [System.ArgumentException]::new('Process: Export registry file. Error: FxPath is empty. Cause: Registry path was not provided. Solution: Pass a valid FxProperties registry path.') } if ([string]::IsNullOrWhiteSpace($ValueName)) { throw [System.ArgumentException]::new('Process: Export registry file. Error: ValueName is empty. Cause: Registry value name was not provided. Solution: Pass a valid property key name.') } if ([string]::IsNullOrWhiteSpace($ExportDirectory)) { throw [System.ArgumentException]::new('Process: Export registry file. Error: ExportDirectory is empty. Cause: Export target directory was not provided. Solution: Pass a valid directory path.') } if (-not (Test-Path -LiteralPath $ExportDirectory)) { $null = New-Item -Path $ExportDirectory -ItemType Directory -Force -ErrorAction Stop } $regPath = $FxPath -replace '^HKLM:\\', 'HKEY_LOCAL_MACHINE\' $hexValue = $DisableSysFxValue.ToString("X8") $regContent = @" Windows Registry Editor Version 5.00 [$regPath] "$ValueName"=dword:$hexValue "@ $timestamp = Get-Date -Format 'yyyyMMdd_HHmmss' $regFileName = "FxProperties-$Action-$timestamp.reg" $regFilePath = Join-Path -Path $ExportDirectory -ChildPath $regFileName Set-Content -Path $regFilePath -Value $regContent -Encoding ASCII -Force return $regFilePath } <# .SYNOPSIS Creates a temporary .reg file and imports it via regedit.exe with user approval. #> [void] ImportViaRegFile([string]$FxPath, [string]$ValueName, [int]$DisableSysFxValue) { if ([string]::IsNullOrWhiteSpace($FxPath)) { throw [System.ArgumentException]::new('Process: Import registry. Error: FxPath is empty. Cause: Registry path was not provided. Solution: Pass a valid FxProperties registry path.') } if ([string]::IsNullOrWhiteSpace($ValueName)) { throw [System.ArgumentException]::new('Process: Import registry. Error: ValueName is empty. Cause: Registry value name was not provided. Solution: Pass a valid property key name.') } $regPath = $FxPath -replace '^HKLM:\\', 'HKEY_LOCAL_MACHINE\' $hexValue = $DisableSysFxValue.ToString("X8") $regContent = @" Windows Registry Editor Version 5.00 [$regPath] "$ValueName"=dword:$hexValue "@ $regFilePath = Join-Path ([System.IO.Path]::GetTempPath()) "audio_fx_$([guid]::NewGuid()).reg" Set-Content -Path $regFilePath -Value $regContent -Encoding ASCII -Force try { Write-Information 'Opening Registry Editor for user approval. Please click Yes in the UAC/import confirmation dialog.' $proc = Start-Process -FilePath regedit.exe -ArgumentList $regFilePath -PassThru -Wait if ($proc.ExitCode -ne 0) { throw [System.InvalidOperationException]::new("Process: Import registry via regedit. Error: Registry import cancelled or failed. Cause: regedit.exe exited with code $($proc.ExitCode). Solution: Approve the confirmation dialog to import the registry changes.") } } finally { Start-Sleep -Milliseconds 500 Remove-Item -Path $regFilePath -Force -ErrorAction SilentlyContinue } } <# .SYNOPSIS Reads the current Disable_SysFx value for verification. #> [int] GetDisableSysFxValue([string]$FxPath) { if ([string]::IsNullOrWhiteSpace($FxPath)) { throw [System.ArgumentException]::new('Process: Verify SysFx value. Error: FxPath is empty. Cause: Target registry path was missing. Solution: Build a valid FxProperties path before reading values.') } $item = Get-ItemProperty -Path $FxPath -ErrorAction Stop if ($null -eq $item.PSObject.Properties[$this.DisableSysFxValueName]) { return 0 } $value = [int]$item.($this.DisableSysFxValueName) return $value } <# .SYNOPSIS Restarts the Windows Audio service; skips restart if not elevated. #> [void] RestartAudioService() { Write-Verbose "Restarting Windows Audio service..." # Check if elevated $isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator") $serviceName = $this.AudioServiceName if (-not $isAdmin) { Write-Information 'Service restart skipped: not running elevated. Audio Enhancements registry value was set; restart the service manually or reboot to apply changes.' Write-Verbose 'Service restart requires administrator privileges.' } else { Restart-Service -Name $serviceName -Force Write-Verbose "Service restarted successfully." } } <# .SYNOPSIS Derives Capture/Render subpath from manager base path. #> [string] GetAudioSubPath() { $subPath = '' if ($this.MmDevicesBase -like '*\Capture') { $subPath = 'Capture' } else { $subPath = 'Render' } return $subPath } } <# .SYNOPSIS Provides shared runtime operations for elevation checks and endpoint path resolution. #> class AudioEnhancementRuntime { <# .SYNOPSIS Returns true when current process token is elevated to Administrator. #> static [bool] IsAdministrator() { $currentIdentity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = [Security.Principal.WindowsPrincipal]::new($currentIdentity) $isAdmin = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) return $isAdmin } <# .SYNOPSIS Runs a PowerShell command in an elevated session and validates exit status. #> static [void] InvokeElevatedPowerShellCommand([string]$Command, [string]$ErrorContext) { if ([string]::IsNullOrWhiteSpace($Command)) { throw [System.ArgumentException]::new('Process: Run elevated command. Error: Command is empty. Cause: Elevated execution text was not provided. Solution: Provide a non-empty command string.') } if ([string]::IsNullOrWhiteSpace($ErrorContext)) { throw [System.ArgumentException]::new('Process: Run elevated command. Error: ErrorContext is empty. Cause: Operation context was not provided. Solution: Pass a non-empty operation name for diagnostics.') } $encodedCommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($Command)) $process = Start-Process -FilePath pwsh.exe ` -ArgumentList '-NoProfile', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', $encodedCommand ` -Verb RunAs -PassThru -Wait if ($process.ExitCode -ne 0) { throw [System.InvalidOperationException]::new("Process: $ErrorContext. Error: Elevated command failed. Cause: UAC-elevated PowerShell exited with code $($process.ExitCode). Solution: Confirm admin approval and rerun the operation.") } } <# .SYNOPSIS Resolves MMDevices registry base path for the selected endpoint type. #> static [string] GetMmDevicesBasePath([string]$EndpointType) { if ([string]::IsNullOrWhiteSpace($EndpointType)) { throw [System.ArgumentException]::new('Process: Resolve MMDevices path. Error: EndpointType is empty. Cause: Endpoint type criteria was not provided. Solution: Pass Capture or Render.') } if (($EndpointType -ne 'Capture') -and ($EndpointType -ne 'Render')) { throw [System.ArgumentOutOfRangeException]::new('EndpointType', $EndpointType, 'Process: Resolve MMDevices path. Error: EndpointType is invalid. Cause: Only Capture and Render are supported. Solution: Pass Capture or Render.') } $path = '' if ($EndpointType -eq 'Capture') { $path = $script:MmDevicesCaptureBase } else { $path = $script:MmDevicesRenderBase } return $path } } <# .SYNOPSIS Represents immutable input options for an audio enhancement operation. #> class AudioEnhancementRequest { [string]$NameLike [int]$EndpointIndex [string]$EndpointType [string]$BackupDirectory [bool]$SkipBackup [bool]$NoServiceRestart [bool]$PassThru [bool]$Enable [bool]$Disable [bool]$ExportRegFile <# .SYNOPSIS Creates and validates a request instance. #> AudioEnhancementRequest( [string]$NameLike, [int]$EndpointIndex, [string]$EndpointType, [string]$BackupDirectory, [bool]$SkipBackup, [bool]$NoServiceRestart, [bool]$PassThru, [bool]$Enable, [bool]$Disable, [bool]$ExportRegFile ) { if ([string]::IsNullOrWhiteSpace($NameLike)) { throw [System.ArgumentException]::new('Process: Build request. Error: NameLike is empty. Cause: Endpoint search criteria was not provided. Solution: Pass -NameLike with a non-empty value.') } if (($EndpointType -ne 'Capture') -and ($EndpointType -ne 'Render')) { throw [System.ArgumentOutOfRangeException]::new('EndpointType', $EndpointType, 'Process: Build request. Error: EndpointType is invalid. Cause: Only Capture and Render are supported. Solution: Pass Capture or Render.') } if ($EndpointIndex -lt 0) { throw [System.ArgumentOutOfRangeException]::new('EndpointIndex', $EndpointIndex, 'Process: Build request. Error: EndpointIndex is invalid. Cause: Negative endpoint index is not supported. Solution: Pass a value greater than or equal to 0.') } if ([string]::IsNullOrWhiteSpace($BackupDirectory)) { throw [System.ArgumentException]::new('Process: Build request. Error: BackupDirectory is empty. Cause: Backup directory input was missing. Solution: Pass -BackupDirectory with a valid path.') } if ($Enable -and $Disable) { throw [System.ArgumentException]::new('Process: Resolve requested action. Error: Conflicting action switches were provided. Cause: Both -Enable and -Disable were passed. Solution: Specify only one action switch, or omit both to report current state.') } $this.NameLike = $NameLike $this.EndpointIndex = $EndpointIndex $this.EndpointType = $EndpointType $this.BackupDirectory = $BackupDirectory $this.SkipBackup = $SkipBackup $this.NoServiceRestart = $NoServiceRestart $this.PassThru = $PassThru $this.Enable = $Enable $this.Disable = $Disable $this.ExportRegFile = $ExportRegFile } } <# .SYNOPSIS Coordinates endpoint discovery, optional modification, and result construction. #> class AudioEnhancementWorkflow { [AudioEnhancementRequest]$Request [AudioEnhancementManager]$Manager <# .SYNOPSIS Creates a workflow for a validated request. #> AudioEnhancementWorkflow([AudioEnhancementRequest]$Request) { if ($null -eq $Request) { throw [System.ArgumentNullException]::new('Request', 'Process: Initialize workflow. Error: Request is null. Cause: Operation input object was not provided. Solution: Construct and pass a valid AudioEnhancementRequest instance.') } $this.Request = $Request $this.Manager = [AudioEnhancementManager]::new( $script:DisableSysFxValueName, ([AudioEnhancementRuntime]::GetMmDevicesBasePath($Request.EndpointType)), $script:AudioServiceName ) } <# .SYNOPSIS Executes the requested operation and returns full result data. #> [pscustomobject] Execute([System.Management.Automation.PSCmdlet]$Cmdlet, [bool]$WhatIfEnabled) { if ($null -eq $Cmdlet) { throw [System.ArgumentNullException]::new('Cmdlet', 'Process: Execute workflow. Error: Cmdlet context is null. Cause: SupportsShouldProcess context was not provided. Solution: Call Execute from an advanced function and pass $PSCmdlet.') } $requestedActionInfo = $this.GetRequestedActionInfo() $requestedAction = [string]$requestedActionInfo.RequestedAction $targetDisableSysFxValue = [int]$requestedActionInfo.TargetDisableSysFxValue $this.WriteElevationInformation($requestedAction, $WhatIfEnabled) Write-Verbose "Discovering $($this.Request.EndpointType) endpoints matching '$($this.Request.NameLike)'." $matchingEndpoints = $this.Manager.GetMatchingEndpoints($this.Request.NameLike, $this.Request.EndpointType) $selectedEndpoint = $this.SelectEndpoint($matchingEndpoints) $endpointGuid = $this.Manager.GetEndpointGuid($selectedEndpoint.InstanceId) $fxPath = $this.Manager.GetFxPropertiesPath($endpointGuid) $backupPath = $this.ApplyRequestedAction($Cmdlet, $requestedAction, $targetDisableSysFxValue, $endpointGuid, $fxPath) $currentValue = $this.Manager.GetDisableSysFxValue($fxPath) $audioEnhancementsState = $this.GetAudioEnhancementsState($currentValue) $serviceRestarted = $this.GetServiceRestartedState($requestedAction, $WhatIfEnabled) # Determine result path fields: when ExportRegFile is active, backupPath holds the .reg file path $backupPathField = if ($this.Request.ExportRegFile) { $null } else { $backupPath } $regFileExportField = if ($this.Request.ExportRegFile) { $backupPath } else { $null } $result = [pscustomobject]@{ RequestedAction = $requestedAction AudioEnhancementsState = $audioEnhancementsState EndpointType = $this.Request.EndpointType FriendlyName = $selectedEndpoint.FriendlyName InstanceId = $selectedEndpoint.InstanceId EndpointGuid = $endpointGuid FxPropertiesPath = $fxPath DisableSysFxKey = $script:DisableSysFxValueName DisableSysFxValue = $currentValue BackupPath = $backupPathField RegFileExportPath = $regFileExportField ServiceRestarted = $serviceRestarted } Write-Information ( "Action={0}; AudioEnhancementsState={1}; DisableSysFxValue={2} for '{3}' ({4})." -f $requestedAction, $audioEnhancementsState, $currentValue, $selectedEndpoint.FriendlyName, $endpointGuid ) return $result } <# .SYNOPSIS Executes workflow and returns either full or projected output. #> [object] ExecuteAndGetOutput([System.Management.Automation.PSCmdlet]$Cmdlet, [bool]$WhatIfEnabled) { return $this.GetOutput($this.Execute($Cmdlet, $WhatIfEnabled)) } <# .SYNOPSIS Projects full result to concise display output. #> [object] GetOutput([pscustomobject]$Result) { if ($null -eq $Result) { throw [System.ArgumentNullException]::new('Result', 'Process: Project output. Error: Result is null. Cause: Workflow output object was not provided. Solution: Pass the result object returned by Execute.') } if ($this.Request.PassThru) { return $Result } return ($Result | Select-Object RequestedAction, FriendlyName, EndpointType, EndpointGuid, AudioEnhancementsState, DisableSysFxValue, BackupPath, RegFileExportPath, ServiceRestarted) } <# .SYNOPSIS Resolves requested action and registry target value. #> [pscustomobject] GetRequestedActionInfo() { $requestedAction = 'Report' $targetDisableSysFxValue = -1 if ($this.Request.Enable) { $requestedAction = 'Enable' $targetDisableSysFxValue = 0 } elseif ($this.Request.Disable) { $requestedAction = 'Disable' $targetDisableSysFxValue = 1 } return [pscustomobject]@{ RequestedAction = $requestedAction TargetDisableSysFxValue = $targetDisableSysFxValue } } <# .SYNOPSIS Writes user-facing elevation context for non-admin modification operations. #> [void] WriteElevationInformation([string]$RequestedAction, [bool]$WhatIfEnabled) { $isAdministrator = [AudioEnhancementRuntime]::IsAdministrator() if ((-not $isAdministrator) -and ($RequestedAction -ne 'Report') -and (-not $WhatIfEnabled)) { Write-Information 'Running non-elevated; elevation will be requested when registry and service operations are performed.' } if ((-not $isAdministrator) -and ($RequestedAction -ne 'Report') -and $WhatIfEnabled) { Write-Information 'Running non-elevated because -WhatIf is enabled; write and restart operations will be simulated only.' } } <# .SYNOPSIS Selects a single endpoint and reports available matches when multiple are found. #> [object] SelectEndpoint([object[]]$MatchingEndpoints) { if ($null -eq $MatchingEndpoints -or $MatchingEndpoints.Count -eq 0) { throw [System.InvalidOperationException]::new('Process: Select endpoint. Error: Matching endpoint list is empty. Cause: Endpoint discovery returned no values. Solution: Verify endpoint filters and retry discovery.') } if ($this.Request.EndpointIndex -ge $MatchingEndpoints.Count) { throw [System.ArgumentOutOfRangeException]::new('EndpointIndex', $this.Request.EndpointIndex, "Process: Select endpoint. Error: EndpointIndex is out of range. Cause: Only $($MatchingEndpoints.Count) matching endpoint(s) were found. Solution: Use an index between 0 and $($MatchingEndpoints.Count - 1).") } if ($MatchingEndpoints.Count -gt 1) { Write-Information 'Multiple matches found:' $MatchingEndpoints | Select-Object -Property FriendlyName, InstanceId | ForEach-Object { Write-Information ('- {0} | {1}' -f $_.FriendlyName, $_.InstanceId) } Write-Information "Selecting index $($this.Request.EndpointIndex)." } return $MatchingEndpoints[$this.Request.EndpointIndex] } <# .SYNOPSIS Applies write operations for enable/disable requests and returns backup path when created. #> [string] ApplyRequestedAction( [System.Management.Automation.PSCmdlet]$Cmdlet, [string]$RequestedAction, [int]$TargetDisableSysFxValue, [string]$EndpointGuid, [string]$FxPath ) { $resultPath = $null if ($RequestedAction -eq 'Report') { return $resultPath } if (-not $this.Request.SkipBackup) { if ($Cmdlet.ShouldProcess($FxPath, 'Export FxProperties backup')) { Write-Verbose "Exporting FxProperties backup to '$($this.Request.BackupDirectory)'." $resultPath = $this.Manager.ExportFxBackup($EndpointGuid, $this.Request.BackupDirectory) Write-Information "Backup created at '$resultPath'." } } if ($this.Request.ExportRegFile) { if ($Cmdlet.ShouldProcess($FxPath, "Export .reg file for $($script:DisableSysFxValueName)=$TargetDisableSysFxValue")) { Write-Verbose "Exporting .reg file with Disable_SysFx=$TargetDisableSysFxValue to '$($this.Request.BackupDirectory)'." $regFilePath = $this.Manager.SetDisableSysFxValue( $FxPath, $TargetDisableSysFxValue, $this.Request.BackupDirectory, $RequestedAction ) Write-Information "Registry modification file created at '$regFilePath'." $resultPath = $regFilePath } } else { if ($Cmdlet.ShouldProcess($FxPath, "Set $($script:DisableSysFxValueName)=$TargetDisableSysFxValue")) { Write-Verbose "Setting Disable_SysFx value to '$TargetDisableSysFxValue' on '$FxPath'." $this.Manager.SetDisableSysFxValue($FxPath, $TargetDisableSysFxValue, '', '') } if (-not $this.Request.NoServiceRestart) { if ($Cmdlet.ShouldProcess($script:AudioServiceName, 'Restart Windows Audio service')) { Write-Verbose 'Restarting Windows Audio service.' $this.Manager.RestartAudioService() } } } return $resultPath } <# .SYNOPSIS Maps Disable_SysFx integer value to human-readable state. #> [string] GetAudioEnhancementsState([int]$CurrentValue) { if ($CurrentValue -eq 1) { return 'Disabled' } return 'Enabled' } <# .SYNOPSIS Calculates whether service restart was executed for current request. #> [bool] GetServiceRestartedState([string]$RequestedAction, [bool]$WhatIfEnabled) { if (($RequestedAction -ne 'Report') -and (-not $this.Request.NoServiceRestart) -and (-not $WhatIfEnabled)) { return $true } return $false } } #endregion #region Main function Main { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param( [string]$NameLike, [int]$EndpointIndex, [string]$EndpointType, [string]$BackupDirectory, [switch]$SkipBackup, [switch]$NoServiceRestart, [switch]$PassThru, [switch]$Enable, [switch]$Disable, [switch]$ExportRegFile ) ([AudioEnhancementWorkflow]::new( [AudioEnhancementRequest]::new( $NameLike, $EndpointIndex, $EndpointType, $BackupDirectory, [bool]$SkipBackup, [bool]$NoServiceRestart, [bool]$PassThru, [bool]$Enable, [bool]$Disable, [bool]$ExportRegFile ) )).ExecuteAndGetOutput($PSCmdlet, [bool]$WhatIfPreference) } Main -NameLike $NameLike -EndpointIndex $EndpointIndex -EndpointType $EndpointType -BackupDirectory $BackupDirectory -SkipBackup:$SkipBackup -NoServiceRestart:$NoServiceRestart -PassThru:$PassThru -Enable:$Enable -Disable:$Disable -ExportRegFile:$ExportRegFile -Confirm:$false #endregion