Created
October 3, 2025 14:47
-
-
Save DJStompZone/9258d0b6d00ac7e864e4fa9d487e9df8 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <# | |
| .SYNOPSIS | |
| Purges audio devices and driver packages for a clean OEM reinstall. | |
| .DESCRIPTION | |
| This script removes present audio-related PnP devices (classes MEDIA, AudioEndpoint, and SoftwareComponent entries that look audio-related), | |
| then deletes their third-party driver packages using pnputil. It uses Write-Progress throughout, supports WhatIf, can optionally create a System Restore Point, | |
| and offers a switch to include Microsoft-signed packages if you truly want scorched earth. | |
| By default it: | |
| • Removes devices in classes: MEDIA, AudioEndpoint, and audio-related SoftwareComponent | |
| • Deletes third-party driver packages with Class MEDIA or AudioEndpoint (i.e., non-Microsoft providers) | |
| • Leaves Microsoft providers intact unless -IncludeMicrosoft is supplied | |
| • Prompts for reboot at the end | |
| .PARAMETER IncludeMicrosoft | |
| Include Microsoft-provided driver packages in deletion. Use only if you want to remove generic MS audio stacks too. They will come back after reboot/scan. | |
| .PARAMETER CreateRestorePoint | |
| Attempt to create a system restore point named "AudioPurge (timestamp)" before changes. Requires System Restore enabled on the OS drive. | |
| .PARAMETER LogPath | |
| Path to write a transcript log (e.g., C:\Users\Public\AudioPurge.log). Defaults to %TEMP%\AudioPurge_<timestamp>.log | |
| .PARAMETER ForceReboot | |
| Automatically reboot without prompting when finished. | |
| .PARAMETER WhatIf | |
| Show what would happen without making changes. | |
| .EXAMPLE | |
| .\Purge-AudioDrivers.ps1 | |
| Runs with defaults: removes devices, deletes third-party audio driver packages, prompts to reboot. | |
| .EXAMPLE | |
| .\Purge-AudioDrivers.ps1 -IncludeMicrosoft -CreateRestorePoint -ForceReboot | |
| Full wipe including Microsoft packages, makes a restore point first, then reboots automatically. | |
| .NOTES | |
| Author: DJ Stomp <85457381+DJStompZone@users.noreply.github.com> | |
| License: MIT | |
| Repo: https://github.com/djstompzone/placeholder | |
| Requires: Windows 10/11 with pnputil, PowerShell 5.1+; Admin rights. | |
| #> | |
| [CmdletBinding(SupportsShouldProcess=$true)] | |
| param( | |
| [switch]$IncludeMicrosoft, | |
| [switch]$CreateRestorePoint, | |
| [string]$LogPath = $(Join-Path $env:TEMP ("AudioPurge_{0:yyyyMMdd_HHmmss}.log" -f (Get-Date))), | |
| [switch]$ForceReboot | |
| ) | |
| function Assert-Elevation { | |
| $id = [Security.Principal.WindowsIdentity]::GetCurrent() | |
| $p = New-Object Security.Principal.WindowsPrincipal($id) | |
| if (-not $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { | |
| Write-Host "[*] Elevation required. Relaunching as Administrator..." -ForegroundColor Yellow | |
| $psi = @{ | |
| FilePath = "powershell.exe" | |
| ArgumentList = @("-NoProfile","-ExecutionPolicy","Bypass","-File",("`"{0}`"" -f $PSCommandPath)) | |
| Verb = "RunAs" | |
| } | |
| if ($PSBoundParameters.ContainsKey('IncludeMicrosoft')) { $psi.ArgumentList += "-IncludeMicrosoft" } | |
| if ($PSBoundParameters.ContainsKey('CreateRestorePoint')) { $psi.ArgumentList += "-CreateRestorePoint" } | |
| if ($PSBoundParameters.ContainsKey('LogPath')) { $psi.ArgumentList += @("-LogPath","`"$LogPath`"") } | |
| if ($PSBoundParameters.ContainsKey('ForceReboot')) { $psi.ArgumentList += "-ForceReboot" } | |
| if ($WhatIfPreference) { $psi.ArgumentList += "-WhatIf" } | |
| Start-Process @psi | |
| exit | |
| } | |
| } | |
| function Start-TranscriptSafe { | |
| param([string]$Path) | |
| try { | |
| Start-Transcript -Path $Path -Force -ErrorAction Stop | Out-Null | |
| Write-Host ("[+] Logging to: {0}" -f $Path) | |
| } catch { | |
| Write-Warning ("[!] Could not start transcript: {0}" -f $_.Exception.Message) | |
| } | |
| } | |
| function Stop-TranscriptSafe { | |
| try { Stop-Transcript | Out-Null } catch {} | |
| } | |
| function New-RestorePointIfRequested { | |
| param([switch]$Enable) | |
| if (-not $Enable) { return } | |
| Write-Host "[*] Creating System Restore Point (this can take a minute)..." -ForegroundColor Yellow | |
| try { | |
| # Checkpoint-Computer works only if System Restore is enabled | |
| Checkpoint-Computer -Description ("AudioPurge {0:yyyy-MM-dd HH:mm:ss}" -f (Get-Date)) -RestorePointType "MODIFY_SETTINGS" -ErrorAction Stop | |
| Write-Host "[+] Restore point created." | |
| } catch { | |
| Write-Warning ("[!] Failed to create restore point: {0}" -f $_.Exception.Message) | |
| } | |
| } | |
| function Invoke-PnpUtil { | |
| param([string[]]$Arguments, [int]$ExpectedExit = 0) | |
| $psi = New-Object System.Diagnostics.ProcessStartInfo | |
| $psi.FileName = "$env:SystemRoot\System32\pnputil.exe" | |
| $psi.Arguments = ($Arguments -join " ") | |
| $psi.RedirectStandardOutput = $true | |
| $psi.RedirectStandardError = $true | |
| $psi.UseShellExecute = $false | |
| $psi.CreateNoWindow = $true | |
| $p = New-Object System.Diagnostics.Process | |
| $p.StartInfo = $psi | |
| [void]$p.Start() | |
| $stdout = $p.StandardOutput.ReadToEnd() | |
| $stderr = $p.StandardError.ReadToEnd() | |
| $p.WaitForExit() | |
| [pscustomobject]@{ | |
| ExitCode = $p.ExitCode | |
| StdOut = $stdout | |
| StdErr = $stderr | |
| Ok = ($p.ExitCode -eq $ExpectedExit) | |
| CmdLine = "pnputil $($psi.Arguments)" | |
| } | |
| } | |
| function Get-AudioDevices { | |
| # MEDIA and AudioEndpoint are definitive. SoftwareComponent holds APO/FX/etc. We filter only those that smell like audio. | |
| $classes = @("MEDIA","AudioEndpoint","SoftwareComponent") | |
| $devices = @() | |
| foreach ($cls in $classes) { | |
| try { | |
| $chunk = Get-PnpDevice -Class $cls -ErrorAction SilentlyContinue | |
| if ($null -ne $chunk) { $devices += $chunk } | |
| } catch {} | |
| } | |
| # Filter SoftwareComponent to audio-related ones (friendly name or instance id hints) | |
| $devices | Where-Object { | |
| if ($_.Class -ne 'SoftwareComponent') { return $true } | |
| ($_.FriendlyName -match '(?i)audio|sound|realtek|nahimic|dts|dolby|ap( o)?|maxx') -or ($_.InstanceId -match '(?i)audio|apoaudio|realtek|nahimic|dts|dolby') | |
| } | |
| } | |
| function Remove-Devices { | |
| param([array]$Devices) | |
| if (-not $Devices -or $Devices.Count -eq 0) { return @() } | |
| $total = $Devices.Count | |
| $i = 0 | |
| $removed = @() | |
| foreach ($dev in $Devices) { | |
| $i++ | |
| $pct = [int](($i / $total) * 100) | |
| $title = "Removing audio devices ($i of $total)" | |
| $status = "{0} — {1}" -f $dev.Class, ($dev.FriendlyName ?? $dev.InstanceId) | |
| Write-Progress -Activity $title -Status $status -PercentComplete $pct | |
| if ($PSCmdlet.ShouldProcess($dev.InstanceId, "Remove device")) { | |
| $res = Invoke-PnpUtil -Arguments @("/remove-device","`"$($dev.InstanceId)`"") | |
| if (-not $res.Ok) { | |
| Write-Warning ("[!] Failed to remove device: {0}`n {1}" -f $dev.InstanceId, ($res.StdErr.Trim() + " " + $res.StdOut.Trim())) | |
| } else { | |
| $removed += $dev | |
| Write-Verbose ("Removed device: {0}" -f $dev.InstanceId) | |
| } | |
| } | |
| } | |
| Write-Progress -Activity "Removing audio devices" -Completed | |
| return $removed | |
| } | |
| function Parse-PnpDriverBlocks { | |
| param([string]$Text) | |
| # pnputil /enum-drivers output comes in blank-line separated blocks | |
| $blocks = ($Text -split "(\r?\n){2,}") | Where-Object { $_ -match 'Published Name\s*:' } | |
| foreach ($b in $blocks) { | |
| $obj = [pscustomobject]@{ | |
| PublishedName = (($b -split "`n" | Where-Object { $_ -match 'Published Name' } | Select-Object -First 1) -replace '.*:\s*','').Trim() | |
| Provider = (($b -split "`n" | Where-Object { $_ -match 'Driver Package Provider' } | Select-Object -First 1) -replace '.*:\s*','').Trim() | |
| Class = (($b -split "`n" | Where-Object { $_ -match '^Class\s*:' } | Select-Object -First 1) -replace '.*:\s*','').Trim() | |
| ClassGuid = (($b -split "`n" | Where-Object { $_ -match 'Class GUID' } | Select-Object -First 1) -replace '.*:\s*','').Trim() | |
| DateVersion = (($b -split "`n" | Where-Object { $_ -match 'Driver Date and Version' } | Select-Object -First 1) -replace '.*:\s*','').Trim() | |
| Signer = (($b -split "`n" | Where-Object { $_ -match 'Signer Name' } | Select-Object -First 1) -replace '.*:\s*','').Trim() | |
| Raw = $b | |
| } | |
| if ($obj.PublishedName) { $obj } | |
| } | |
| } | |
| function Get-AudioDriverPackages { | |
| param([switch]$IncludeMicrosoft) | |
| $targets = @() | |
| foreach ($cls in @("MEDIA","AudioEndpoint")) { | |
| $enum = Invoke-PnpUtil -Arguments @("/enum-drivers","/class",$cls) | |
| if ($enum.StdOut) { $targets += Parse-PnpDriverBlocks -Text $enum.StdOut } | |
| } | |
| # Dedup | |
| $targets = $targets | Sort-Object PublishedName -Unique | |
| if (-not $IncludeMicrosoft) { | |
| $targets = $targets | Where-Object { $_.Provider -notmatch '^(?i)Microsoft' } | |
| } | |
| $targets | |
| } | |
| function Delete-DriverPackages { | |
| param([array]$Packages) | |
| if (-not $Packages -or $Packages.Count -eq 0) { return @() } | |
| $total = $Packages.Count | |
| $i = 0 | |
| $deleted = @() | |
| foreach ($pkg in $Packages) { | |
| $i++ | |
| $pct = [int](($i / $total) * 100) | |
| $title = "Deleting driver packages ($i of $total)" | |
| $status = "{0} — {1} [{2}]" -f $pkg.PublishedName, $pkg.Provider, $pkg.Class | |
| Write-Progress -Activity $title -Status $status -PercentComplete $pct | |
| if ($PSCmdlet.ShouldProcess($pkg.PublishedName, "pnputil /delete-driver /uninstall /force")) { | |
| $res = Invoke-PnpUtil -Arguments @("/delete-driver",$pkg.PublishedName,"/uninstall","/force") | |
| if (-not $res.Ok) { | |
| # If in use, device removal may lag; attempt a non-uninstall delete as a fallback | |
| $fallback = $false | |
| if ($res.StdErr -match '(in use|currently in use|failed to delete)') { | |
| $fallback = $true | |
| $res2 = Invoke-PnpUtil -Arguments @("/delete-driver",$pkg.PublishedName,"/force") | |
| if ($res2.Ok) { $deleted += $pkg } | |
| else { | |
| Write-Warning ("[!] Failed to delete package {0}: {1}" -f $pkg.PublishedName, ($res2.StdErr.Trim() + " " + $res2.StdOut.Trim())) | |
| } | |
| } | |
| if (-not $fallback) { | |
| Write-Warning ("[!] Failed to delete package {0}: {1}" -f $pkg.PublishedName, ($res.StdErr.Trim() + " " + $res.StdOut.Trim())) | |
| } | |
| } else { | |
| $deleted += $pkg | |
| Write-Verbose ("Deleted package: {0}" -f $pkg.PublishedName) | |
| } | |
| } | |
| } | |
| Write-Progress -Activity "Deleting driver packages" -Completed | |
| return $deleted | |
| } | |
| try { | |
| Assert-Elevation | |
| Start-TranscriptSafe -Path $LogPath | |
| if ($PSCmdlet.ShouldProcess("System","Audio driver purge starting")) { | |
| New-RestorePointIfRequested -Enable:$CreateRestorePoint | |
| Write-Host "`n[*] Enumerating audio devices..." -ForegroundColor Cyan | |
| $devices = Get-AudioDevices | |
| if ($devices.Count -gt 0) { | |
| Write-Host ("[+] Found {0} audio-related devices to remove." -f $devices.Count) | |
| $removed = Remove-Devices -Devices $devices | |
| Write-Host ("[+] Removed {0} devices (requested)." -f $removed.Count) | |
| } else { | |
| Write-Host "[=] No removable audio devices found." | |
| } | |
| Write-Host "`n[*] Enumerating driver packages..." -ForegroundColor Cyan | |
| $pkgs = Get-AudioDriverPackages -IncludeMicrosoft:$IncludeMicrosoft | |
| if ($pkgs.Count -gt 0) { | |
| Write-Host ("[+] Found {0} driver packages to delete{1}." -f $pkgs.Count, $(if (-not $IncludeMicrosoft) { " (third-party only)" } else { "" })) | |
| $deleted = Delete-DriverPackages -Packages $pkgs | |
| Write-Host ("[+] Deleted {0} packages (requested)." -f $deleted.Count) | |
| } else { | |
| Write-Host "[=] No matching driver packages found." | |
| } | |
| Write-Host "`n[*] Finalizing..." -ForegroundColor Cyan | |
| Write-Host " • Recommend reboot now. After reboot: install OEM audio package, or run 'pnputil /scan-devices' to rediscover devices." | |
| if ($ForceReboot -and -not $WhatIfPreference) { | |
| Write-Host "[*] Rebooting in 5 seconds..." | |
| Start-Sleep -Seconds 5 | |
| Restart-Computer -Force | |
| } else { | |
| $ans = Read-Host "Reboot now? (Y/N)" | |
| if ($ans -match '^(?i)Y') { Restart-Computer -Force } | |
| } | |
| } | |
| } | |
| finally { | |
| Stop-TranscriptSafe | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment