diff --git a/.github/workflows/cmake-windows.yml b/.github/workflows/cmake-windows.yml index 4e9372ae..d052e0fe 100644 --- a/.github/workflows/cmake-windows.yml +++ b/.github/workflows/cmake-windows.yml @@ -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 diff --git a/ci/Invoke-PgoProfileCollection.ps1 b/ci/Invoke-PgoProfileCollection.ps1 new file mode 100644 index 00000000..7b2b2d4d --- /dev/null +++ b/ci/Invoke-PgoProfileCollection.ps1 @@ -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 diff --git a/ci/Wait-AudioDevices.ps1 b/ci/Wait-AudioDevices.ps1 new file mode 100644 index 00000000..1734152e --- /dev/null +++ b/ci/Wait-AudioDevices.ps1 @@ -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" diff --git a/test/GeneratePGOProfiles.ps1 b/test/GeneratePGOProfiles.ps1 index 9b415abf..1454437b 100644 --- a/test/GeneratePGOProfiles.ps1 +++ b/test/GeneratePGOProfiles.ps1 @@ -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"