CI (Windows): make collect-pgo-profile wait for real audio endpoints (#1483)

"Generate PGO data" fails intermittently when a VB-Cable / VAC endpoint
isn't enumerable at the moment FreeDV starts a UT: FreeDV pops a fatal
"device cannot be found" message box and aborts.

The old "Start Windows Audio Service" step only polled WMI
Win32_SoundDevice (driver nodes, not the MMDevice endpoints FreeDV
uses), had an operator-precedence bug in its until-condition
((A -and B) -or C), ran once minutes before the test, and never checked
the specific endpoints the job needs.

* ci/Wait-AudioDevices.ps1: restarts AudioEndpointBuilder/audiosrv and
  polls "Get-AudioDevice -List" (the active-endpoint surface FreeDV
  actually enumerates) until every required playback/recording endpoint
  is present, nudging the audio stack again halfway through and dumping
  full diagnostics before failing on timeout.
* ci/Invoke-PgoProfileCollection.ps1: runs the endpoint wait +
  GeneratePGOProfiles.ps1 and retries the whole thing up to 3x, since an
  endpoint can also drop mid-run right after SoX releases the capture
  device. Partial .profraw files are cleared between attempts.
* collect-pgo-profile now calls these; the Generate PGO data timeout
  goes 10 -> 25 min to cover the retries.
* GeneratePGOProfiles.ps1: best-effort wait for the endpoints before
  each of the TX and RX passes.

The identical "Start Windows Audio Service" step in the test job is left
as-is here; this change is scoped to collect-pgo-profile.


Claude-Session: https://claude.ai/code/session_01AYJXLpaEyPTsegX93QzE1a

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
ms-fix-rade-reporting-awgn-flake^2
Mooneer Salem 2026-09-07 23:57:12 -07:00 committed by GitHub
parent f3159ca346
commit 1665b53ef7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 245 additions and 16 deletions

View File

@ -168,29 +168,26 @@ jobs:
New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\" -Name "AppPrivacy" -Force
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\AppPrivacy" -Name LetAppsAccessMicrophone -Value 0
- name: Start Windows Audio Service
- name: Wait for virtual audio endpoints
shell: pwsh
run: |
Restart-Service -Name audiosrv -Force
$deadline = (Get-Date).AddSeconds(60)
do {
Start-Sleep -Seconds 2
$deviceNames = (Get-CimInstance win32_sounddevice).Name
} until (
($deviceNames -match "VB-Audio Virtual Cable") -and
($deviceNames -match "Virtual Audio Cable") -or
((Get-Date) -gt $deadline)
)
# Restart the audio stack and poll until every endpoint FreeDV needs is
# actually enumerable, instead of starting a test and letting it abort
# with a fatal "device cannot be found" message box when a virtual
# cable endpoint hasn't come up (or has briefly dropped) yet.
& "${{github.workspace}}\ci\Wait-AudioDevices.ps1" `
-Playback "${{ env.COMPUTER_TO_RADIO_DEVICE }}", "${{ env.COMPUTER_TO_SPEAKER_DEVICE }}" `
-Recording "${{ env.RADIO_TO_COMPUTER_DEVICE }}", "${{ env.MICROPHONE_TO_COMPUTER_DEVICE }}" `
-TimeoutSeconds 180
- name: List audio devices
shell: pwsh
run: |
Get-CimInstance win32_sounddevice
Get-AudioDevice -List | Format-Table -AutoSize Index, Default, Type, Name
- name: Set Windows default audio devices
shell: pwsh
run: |
Invoke-WebRequest https://github.com/frgnca/AudioDeviceCmdlets/releases/download/v3.1/AudioDeviceCmdlets.dll -OutFile AudioDeviceCmdlets.dll
Install-Module -Name AudioDeviceCmdlets -Force -Confirm:$false
Get-AudioDevice -List | where { $_.Name -eq "${{ env.MICROPHONE_TO_COMPUTER_DEVICE }}" } | Set-AudioDevice
Get-AudioDevice -List | where { $_.Name -eq "${{ env.COMPUTER_TO_SPEAKER_DEVICE }}" } | Set-AudioDevice
@ -209,9 +206,13 @@ jobs:
- name: Generate PGO data
shell: pwsh
working-directory: ${{github.workspace}}\FreeDV-Install-Location\bin
timeout-minutes: 25
run: |
.\GeneratePGOProfiles.ps1 -RadioToComputerDevice "${{env.RADIO_TO_COMPUTER_DEVICE}}" -ComputerToRadioDevice "${{env.COMPUTER_TO_RADIO_DEVICE}}" -MicrophoneToComputerDevice "${{env.MICROPHONE_TO_COMPUTER_DEVICE}}" -ComputerToSpeakerDevice "${{env.COMPUTER_TO_SPEAKER_DEVICE}}"
timeout-minutes: 10
& "${{github.workspace}}\ci\Invoke-PgoProfileCollection.ps1" `
-RadioToComputerDevice "${{env.RADIO_TO_COMPUTER_DEVICE}}" `
-ComputerToRadioDevice "${{env.COMPUTER_TO_RADIO_DEVICE}}" `
-MicrophoneToComputerDevice "${{env.MICROPHONE_TO_COMPUTER_DEVICE}}" `
-ComputerToSpeakerDevice "${{env.COMPUTER_TO_SPEAKER_DEVICE}}"
- name: Stash profile data
uses: actions/upload-artifact@v7

View File

@ -0,0 +1,73 @@
<#
.SYNOPSIS
Runs GeneratePGOProfiles.ps1 with an audio-endpoint pre-check, retrying a few
times.
.DESCRIPTION
On CI the virtual-cable audio endpoints (VB-Cable / VAC) intermittently fail to
be enumerable when FreeDV starts a UT -- sometimes not yet up when the job
begins, sometimes briefly dropping out mid-run right after SoX releases the
capture device. Either way FreeDV aborts with a fatal "device cannot be found"
message box.
This wrapper waits for the required endpoints (ci/Wait-AudioDevices.ps1, which
also restarts the audio stack), runs GeneratePGOProfiles.ps1, and retries the
whole thing a few times before giving up. Must be run from the folder
containing freedv.exe / GeneratePGOProfiles.ps1.
.PARAMETER Attempts
How many times to try the full collection. Default 3.
#>
param (
[Parameter(Mandatory = $true)] [string] $RadioToComputerDevice,
[Parameter(Mandatory = $true)] [string] $ComputerToRadioDevice,
[Parameter(Mandatory = $true)] [string] $MicrophoneToComputerDevice,
[Parameter(Mandatory = $true)] [string] $ComputerToSpeakerDevice,
[int] $Attempts = 3
)
$ErrorActionPreference = 'Stop'
$waitScript = Join-Path $PSScriptRoot 'Wait-AudioDevices.ps1'
for ($attempt = 1; $attempt -le $Attempts; $attempt++) {
Write-Host "=== GeneratePGOProfiles attempt $attempt/$Attempts ==="
# Playback (render) endpoints FreeDV emits to; recording (capture) endpoints
# FreeDV / SoX read from. "Line 1 (Virtual Audio Cable)" is used for both.
try {
& $waitScript `
-Playback $ComputerToRadioDevice, $ComputerToSpeakerDevice `
-Recording $RadioToComputerDevice, $MicrophoneToComputerDevice `
-TimeoutSeconds 120
}
catch {
Write-Host "::warning::Audio endpoint wait failed on attempt ${attempt}: $_"
}
$rc = 1
try {
& .\GeneratePGOProfiles.ps1 `
-RadioToComputerDevice $RadioToComputerDevice `
-ComputerToRadioDevice $ComputerToRadioDevice `
-MicrophoneToComputerDevice $MicrophoneToComputerDevice `
-ComputerToSpeakerDevice $ComputerToSpeakerDevice
$rc = $LASTEXITCODE
}
catch {
Write-Host "::warning::GeneratePGOProfiles threw on attempt ${attempt}: $_"
$rc = 1
}
if ($rc -eq 0) {
Write-Host "PGO data generated on attempt $attempt."
exit 0
}
Write-Host "::warning::GeneratePGOProfiles failed on attempt $attempt (exit $rc)."
# Drop any partial .profraw so the retry (and the upload) start clean.
Remove-Item -ErrorAction SilentlyContinue *.profraw
}
Write-Host "::error::GeneratePGOProfiles failed after $Attempts attempts."
exit 1

View File

@ -0,0 +1,119 @@
<#
.SYNOPSIS
Blocks until the named Windows audio endpoints are enumerable, or fails with
diagnostics after a timeout.
.DESCRIPTION
FreeDV enumerates audio devices through the Core Audio MMDevice API and aborts
with a fatal "device cannot be found" message box if a configured device is
missing when a test starts. On CI the virtual cables (VB-Cable / VAC) sometimes
take a while to expose their render / capture endpoints after the audio service
is (re)started, or briefly drop out of enumeration after another process
releases them -- which makes the PGO profile collection job fail intermittently.
This script polls "Get-AudioDevice -List" (from AudioDeviceCmdlets, the same
active-endpoint enumeration surface FreeDV uses) until every required endpoint
is present. It restarts the Windows audio stack once up front and once more
partway through as a nudge. On timeout it prints a full device dump and throws.
.PARAMETER Playback
Names of required playback (render) endpoints.
.PARAMETER Recording
Names of required recording (capture) endpoints.
.PARAMETER TimeoutSeconds
Total time to wait for all endpoints. Default 180.
.PARAMETER NoRestart
Do not restart the audio services (just poll).
.EXAMPLE
PS> ./ci/Wait-AudioDevices.ps1 `
-Playback "Speakers (VB-Audio Virtual Cable)", "Line 1 (Virtual Audio Cable)" `
-Recording "CABLE Output (VB-Audio Virtual Cable)", "Line 1 (Virtual Audio Cable)"
#>
param (
[string[]] $Playback = @(),
[string[]] $Recording = @(),
[int] $TimeoutSeconds = 180,
[switch] $NoRestart
)
$ErrorActionPreference = 'Stop'
if (-not (Get-Module -ListAvailable -Name AudioDeviceCmdlets)) {
Install-Module -Name AudioDeviceCmdlets -Force -Confirm:$false -Scope CurrentUser
}
Import-Module AudioDeviceCmdlets
$Playback = @($Playback | Where-Object { $_ } | Select-Object -Unique)
$Recording = @($Recording | Where-Object { $_ } | Select-Object -Unique)
function Get-MissingEndpoints {
$devices = Get-AudioDevice -List
$havePlay = @($devices | Where-Object { $_.Type -eq 'Playback' } | ForEach-Object { $_.Name })
$haveRec = @($devices | Where-Object { $_.Type -eq 'Recording' } | ForEach-Object { $_.Name })
$missing = @()
foreach ($name in $Playback) { if ($havePlay -notcontains $name) { $missing += "playback : $name" } }
foreach ($name in $Recording) { if ($haveRec -notcontains $name) { $missing += "recording: $name" } }
return $missing
}
function Restart-AudioStack {
if ($NoRestart) { return }
Write-Host "Restarting Windows audio services (AudioEndpointBuilder, audiosrv)..."
# AudioEndpointBuilder owns endpoint enumeration; audiosrv depends on it and
# is restarted with it. -Force also restarts dependent services.
foreach ($svc in 'AudioEndpointBuilder', 'audiosrv') {
try { Restart-Service -Name $svc -Force -ErrorAction Stop }
catch { Write-Host " ($svc could not be restarted: $_)" }
}
Start-Sleep -Seconds 3
}
function Show-DeviceDump {
Write-Host "--- Get-AudioDevice -List ---"
Get-AudioDevice -List | Format-Table -AutoSize Index, Default, Type, Name | Out-String | Write-Host
Write-Host "--- Win32_SoundDevice ---"
Get-CimInstance Win32_SoundDevice | Format-Table -AutoSize Name, Status, StatusInfo | Out-String | Write-Host
Write-Host "--- AudioEndpoint PnP devices ---"
Get-PnpDevice -Class AudioEndpoint -ErrorAction SilentlyContinue |
Format-Table -AutoSize Status, FriendlyName | Out-String | Write-Host
}
Write-Host "Waiting up to $TimeoutSeconds s for audio endpoints:"
$Playback | ForEach-Object { Write-Host " [playback ] $_" }
$Recording | ForEach-Object { Write-Host " [recording] $_" }
Restart-AudioStack
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
$nudged = $false
$missing = Get-MissingEndpoints
while ($missing.Count -gt 0 -and (Get-Date) -lt $deadline) {
$remaining = ($deadline - (Get-Date)).TotalSeconds
if (-not $nudged -and $remaining -lt ($TimeoutSeconds / 2)) {
Write-Host "Still missing after half the timeout; nudging the audio stack:"
$missing | ForEach-Object { Write-Host " $_" }
Restart-AudioStack
$nudged = $true
}
Start-Sleep -Seconds 3
$missing = Get-MissingEndpoints
}
if ($missing.Count -eq 0) {
Write-Host "All required audio endpoints are present."
Get-AudioDevice -List | Format-Table -AutoSize Index, Default, Type, Name | Out-String | Write-Host
exit 0
}
Write-Host "::error::Timed out waiting for audio endpoints; still missing:"
$missing | ForEach-Object { Write-Host " $_" }
Show-DeviceDump
throw "Required audio endpoints not available after $TimeoutSeconds s"

View File

@ -51,6 +51,36 @@ param (
$current_loc = Get-Location
# Best-effort: wait for the configured audio endpoints to be enumerable before
# starting FreeDV, so a virtual cable that has briefly dropped out of the
# MMDevice list doesn't make the UT abort with a fatal "device cannot be found"
# message box. The CI workflow does a stricter, failing check up front; this is
# just a short guard against a mid-script drop between the TX and RX passes.
function Wait-ForAudioDevices {
param (
[string[]] $Names,
[int] $TimeoutSeconds = 90
)
if (-not (Get-Module -ListAvailable -Name AudioDeviceCmdlets)) { return }
Import-Module AudioDeviceCmdlets -ErrorAction SilentlyContinue
$wanted = @($Names | Where-Object { $_ } | Select-Object -Unique)
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
while ($true) {
try { $have = @((Get-AudioDevice -List).Name) } catch { $have = @() }
$missing = @($wanted | Where-Object { $have -notcontains $_ })
if ($missing.Count -eq 0) { return }
if ((Get-Date) -ge $deadline) {
Write-Host "WARNING: audio device(s) still missing after ${TimeoutSeconds}s: $($missing -join ', ')"
return
}
Start-Sleep -Seconds 3
}
}
$allDevices = @($RadioToComputerDevice, $ComputerToRadioDevice, $MicrophoneToComputerDevice, $ComputerToSpeakerDevice)
# Clone the RADE test corpus if not already present, then resample the TX test file to 48 kHz
# to reduce CPU usage during the run.
if (-not (Test-Path "$current_loc\rade_src")) {
@ -91,6 +121,8 @@ $soxProcess.StartInfo = $soxPsi
[void]$soxProcess.Start()
# Start FreeDV in test mode to record TX
Wait-ForAudioDevices -Names $allDevices
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.CreateNoWindow = $true
$psi.UseShellExecute = $false
@ -121,6 +153,10 @@ try {
}
$soxProcess.WaitForExit()
# Killing SoX above can briefly disturb the shared audio engine; make sure the
# endpoints are back before the RX pass starts.
Wait-ForAudioDevices -Names $allDevices
$psi.Arguments = @("/f $quoted_conf_filename /ut rx /utmode RADEV1 /rxfile `"$current_loc\test.wav`" /rxfeaturefile `"$current_loc\rxfeatures.f32`"")
$conf_tmpl = Get-Content "$current_loc\freedv-pgo.conf.tmpl"