diff --git a/.github/workflows/cmake-windows.yml b/.github/workflows/cmake-windows.yml index d052e0fe..d7bd6256 100644 --- a/.github/workflows/cmake-windows.yml +++ b/.github/workflows/cmake-windows.yml @@ -81,6 +81,147 @@ jobs: path: ${{github.workspace}}/build_windows/instrumented-${{ steps.installer-filename.outputs.INSTALLER_FILENAME }} archive: false + # Sanitizer coverage for the Windows builds. llvm-mingw supports UBSan on + # every architecture it targets and ASan on x86 only (no aarch64 ASan + # runtime is shipped) -- see https://github.com/mstorsjo/llvm-mingw#readme. + # Not PGO-optimized: these exist purely to catch memory-safety/UB bugs + # that a plain Release build's counters can't see, not to represent + # production performance. + build-sanitizer: + strategy: + fail-fast: false + matrix: + include: + - sanitizer_name: UBSAN + sanitizer_flag: -DENABLE_UBSAN=1 + arch: x86_64 + - sanitizer_name: UBSAN + sanitizer_flag: -DENABLE_UBSAN=1 + arch: aarch64 + - sanitizer_name: ASAN + sanitizer_flag: -DENABLE_ASAN=1 + arch: x86_64 + + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v6 + + - name: ccache + uses: hendrikmuhs/ccache-action@v1.2.23 + with: + key: ${{ github.job }}-${{ matrix.sanitizer_name }}-${{ matrix.arch }} + max-size: "5G" + verbose: 2 + + - name: Install required packages + shell: bash + working-directory: ${{github.workspace}} + run: | + sudo apt-get update + sudo apt-get install nsis autoconf automake libtool libltdl-dev pkg-config + + - name: Download MinGW LLVM + shell: bash + working-directory: ${{github.workspace}} + run: | + wget https://github.com/mstorsjo/llvm-mingw/releases/download/20251216/llvm-mingw-20251216-ucrt-ubuntu-22.04-x86_64.tar.xz + tar xvf llvm-mingw-20251216-ucrt-ubuntu-22.04-x86_64.tar.xz + + - name: Configure freedv-gui (unsigned, ${{ matrix.sanitizer_name }}) + shell: bash + working-directory: ${{github.workspace}} + run: | + export PATH=${{github.workspace}}/llvm-mingw-20251216-ucrt-ubuntu-22.04-x86_64/bin:$PATH + mkdir build_windows + cd build_windows + cmake ${{ matrix.sanitizer_flag }} -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_TOOLCHAIN_FILE=${{github.workspace}}/cross-compile/freedv-mingw-llvm-${{ matrix.arch }}.cmake .. + + - name: Build freedv-gui (unsigned, ${{ matrix.sanitizer_name }}) + shell: bash + working-directory: ${{github.workspace}}/build_windows + run: | + export PATH=${{github.workspace}}/llvm-mingw-20251216-ucrt-ubuntu-22.04-x86_64/bin:$PATH + make -j$(nproc) package + + - name: Calculate installer filename + shell: bash + id: installer-filename + working-directory: ${{github.workspace}}/build_windows + run: | + export FREEDV_INSTALLER_FILE=`ls FreeDV*.exe` + echo "INSTALLER_FILENAME=${FREEDV_INSTALLER_FILE}" >> "$GITHUB_OUTPUT" + mv ${FREEDV_INSTALLER_FILE} sanitizer-${{ matrix.sanitizer_name }}-${FREEDV_INSTALLER_FILE} + + - name: Stash for test-sanitizer job + uses: actions/upload-artifact@v7 + with: + name: sanitizer-${{ matrix.sanitizer_name }}-${{ steps.installer-filename.outputs.INSTALLER_FILENAME }} + path: ${{github.workspace}}/build_windows/sanitizer-${{ matrix.sanitizer_name }}-${{ steps.installer-filename.outputs.INSTALLER_FILENAME }} + archive: false + + # rade_tx_wav / rade_rx_wav are excluded from the Windows build in the rade_c + # fork (if(NOT WIN32) in src/CMakeLists.txt), so the RADE loss threshold can't be + # derived on the Windows runners. Compute the software-only baseline here on Linux + # and hand the resulting threshold to the Windows RADE loss test. See + # https://github.com/drowe67/radae/blob/dr-tx-bpf/doc/verification/verification_procedure.md + rade-loss-baseline: + runs-on: ubuntu-22.04 + outputs: + baseline: ${{ steps.baseline.outputs.baseline }} + threshold: ${{ steps.baseline.outputs.threshold }} + rade_c_commit: ${{ steps.baseline.outputs.rade_c_commit }} + radae_commit: ${{ steps.baseline.outputs.radae_commit }} + + steps: + - uses: actions/checkout@v6 + + - name: Install required packages + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y cmake autoconf automake libtool libltdl-dev pkg-config sox + + - name: Build rade_c wav tools + shell: bash + working-directory: ${{github.workspace}} + run: | + set -eux + git clone -b dr-tx-bpf --depth 1 https://github.com/freedv/rade_c rade_c + cmake -S rade_c -B rade_c/build -DCMAKE_BUILD_TYPE=Release \ + -DOPUS_URL=https://github.com/xiph/opus/archive/940d4e5af64351ca8ba8390df3f555484c567fbb.zip + cmake --build rade_c/build -j$(nproc) + + - name: Compute RADE loss threshold + id: baseline + shell: bash + working-directory: ${{github.workspace}} + run: | + set -eux + git clone -b main --depth 1 https://github.com/drowe67/radae.git rade_src + python3 -m venv rade-venv + . ./rade-venv/bin/activate + pip3 install torch --index-url https://download.pytorch.org/whl/cpu + pip3 install matplotlib + + TOOLS="$PWD/rade_c/build/src" + # rade_tx_wav requires 16 kHz mono 16-bit PCM (all.wav already is). + sox rade_src/wav/all.wav -r 16000 -c 1 -b 16 -e signed-integer baseline_in.wav + LD_LIBRARY_PATH="$TOOLS" "$TOOLS/rade_tx_wav" -f baseline_txfeatures.f32 baseline_in.wav baseline_tx.wav + LD_LIBRARY_PATH="$TOOLS" "$TOOLS/rade_rx_wav" -f baseline_rxfeatures.f32 baseline_tx.wav baseline_decoded.wav + + BASELINE_OUTPUT=$(PYTHONPATH="$PWD/rade_src" python3 rade_src/loss.py \ + baseline_txfeatures.f32 baseline_rxfeatures.f32 --clip_start 100 --clip_end 300) + echo "$BASELINE_OUTPUT" + BASELINE_LOSS=$(printf '%s\n' "$BASELINE_OUTPUT" | sed -n 's/.*loss: *\([0-9][0-9.]*\).*/\1/p' | head -1) + # Verification procedure: allow the round trip to be within +10% of baseline. + THRESHOLD=$(awk -v b="$BASELINE_LOSS" 'BEGIN { if (b + 0 <= 0) exit 1; printf "%.4f\n", b * 1.10 }') + echo "Software-only baseline loss: $BASELINE_LOSS -> threshold (x1.10): $THRESHOLD" + echo "baseline=$BASELINE_LOSS" >> "$GITHUB_OUTPUT" + echo "threshold=$THRESHOLD" >> "$GITHUB_OUTPUT" + echo "rade_c_commit=$(git -C rade_c rev-parse HEAD)" >> "$GITHUB_OUTPUT" + echo "radae_commit=$(git -C rade_src rev-parse HEAD)" >> "$GITHUB_OUTPUT" + collect-pgo-profile: strategy: fail-fast: false @@ -107,7 +248,7 @@ jobs: - uses: actions/download-artifact@v8 with: merge-multiple: true - pattern: instrumented-FreeDV-2*-${{ matrix.arch }}.exe + pattern: instrumented-FreeDV-*-${{ matrix.arch }}.exe path: ${{github.workspace}} - uses: TheMrMilchmann/setup-msvc-dev@v4 @@ -380,7 +521,7 @@ jobs: arch: x86_64 runs-on: ${{ matrix.runner }} - needs: build-pgo-use-profile + needs: [build-pgo-use-profile, rade-loss-baseline] env: RADIO_TO_COMPUTER_DEVICE: "CABLE Output (VB-Audio Virtual Cable)" COMPUTER_TO_RADIO_DEVICE: "Speakers (VB-Audio Virtual Cable)" @@ -392,7 +533,7 @@ jobs: - uses: actions/download-artifact@v8 with: merge-multiple: true - pattern: FreeDV-2*-${{ matrix.arch }}.exe + pattern: FreeDV-*-${{ matrix.arch }}.exe path: ${{github.workspace}} - uses: TheMrMilchmann/setup-msvc-dev@v4 @@ -438,12 +579,20 @@ jobs: Copy-Item -Path ${{github.workspace}}/test/freedv-ctest-fullduplex.conf.tmpl -Destination ${{github.workspace}}\FreeDV-Install-Location\bin Copy-Item -Path ${{github.workspace}}/test/TestFreeDVReporting.ps1 -Destination ${{github.workspace}}\FreeDV-Install-Location\bin Copy-Item -Path ${{github.workspace}}/test/freedv-ctest-reporting.conf.tmpl -Destination ${{github.workspace}}\FreeDV-Install-Location\bin + Copy-Item -Path ${{github.workspace}}/test/TestFreeDVRadeLoss.ps1 -Destination ${{github.workspace}}\FreeDV-Install-Location\bin + Copy-Item -Path ${{github.workspace}}/test/freedv-ctest-loss.conf.tmpl -Destination ${{github.workspace}}\FreeDV-Install-Location\bin Copy-Item -Path ${{github.workspace}}/test/hamlibserver.py -Destination ${{github.workspace}}\FreeDV-Install-Location\bin - + - name: Install SoX shell: pwsh run: | choco install sox.portable + + - name: Install Python dependencies + shell: pwsh + run: | + pip install torch --index-url https://download.pytorch.org/whl/cpu + pip install matplotlib - name: Grant FreeDV access to the microphone run: | @@ -456,29 +605,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 @@ -494,6 +640,22 @@ jobs: powercfg /setacvalueindex SCHEME_CURRENT 54533251-82be-4824-96c1-47b60b740d00 893dee8e-2bef-41e0-89c6-b55d0929964c 100 powercfg /setactive SCHEME_CURRENT + - name: Configure crash dump collection for FreeDV + shell: pwsh + run: | + # Windows Error Reporting will write a dump here any time freedv.exe + # crashes (unhandled exception/access violation), even if the test + # script itself doesn't notice anything went wrong (e.g. a crash + # mid-run that a subsequent step just sees as a hang or empty output). + $dumpFolder = "${{github.workspace}}\CrashDumps" + New-Item -Path $dumpFolder -ItemType Directory -Force | Out-Null + + $werKey = "HKLM:\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\freedv.exe" + New-Item -Path $werKey -Force | Out-Null + Set-ItemProperty -Path $werKey -Name DumpFolder -Value $dumpFolder -Type ExpandString + Set-ItemProperty -Path $werKey -Name DumpCount -Value 10 -Type DWord + Set-ItemProperty -Path $werKey -Name DumpType -Value 2 -Type DWord + - name: Test RADE shell: pwsh working-directory: ${{github.workspace}}\FreeDV-Install-Location\bin @@ -502,6 +664,14 @@ jobs: .\TestFreeDVFullDuplex.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}}" -ModeToTest RADEV1 -NumberOfRuns 1 timeout-minutes: 10 + - name: Test RADE Loss + shell: pwsh + working-directory: ${{github.workspace}}\FreeDV-Install-Location\bin + if: ${{ !cancelled() }} + run: | + .\TestFreeDVRadeLoss.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}}" -LossThreshold ${{ needs.rade-loss-baseline.outputs.threshold }} *>&1 | Tee-Object -FilePath rade_loss.log + timeout-minutes: 10 + - name: Test RADE Reporting shell: pwsh working-directory: ${{github.workspace}}\FreeDV-Install-Location\bin @@ -510,30 +680,6 @@ jobs: .\TestFreeDVReporting.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 - - name: Test 700D - shell: pwsh - working-directory: ${{github.workspace}}\FreeDV-Install-Location\bin - if: ${{ !cancelled() }} - run: | - .\TestFreeDVFullDuplex.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}}" -ModeToTest 700D -NumberOfRuns 1 - timeout-minutes: 10 - - - name: Test 700E - shell: pwsh - working-directory: ${{github.workspace}}\FreeDV-Install-Location\bin - if: ${{ !cancelled() }} - run: | - .\TestFreeDVFullDuplex.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}}" -ModeToTest 700E -NumberOfRuns 1 - timeout-minutes: 10 - - - name: Test 1600 - shell: pwsh - working-directory: ${{github.workspace}}\FreeDV-Install-Location\bin - if: ${{ !cancelled() }} - run: | - .\TestFreeDVFullDuplex.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}}" -ModeToTest 1600 -NumberOfRuns 1 - timeout-minutes: 10 - - name: Screenshot if failed if: failure() shell: pwsh @@ -549,3 +695,256 @@ jobs: path: ${{github.workspace}}/screenshot-${{ matrix.arch }}.png archive: false + - name: Upload crash dumps + uses: actions/upload-artifact@v7 + if: always() + with: + name: crashdumps-${{ matrix.arch }} + path: ${{github.workspace}}\CrashDumps\*.dmp + if-no-files-found: ignore + + # Runs the same test suite as `test`, but against the sanitizer-instrumented + # builds from `build-sanitizer`. These builds are slower and less reliable + # for real-time audio than a plain Release build (the same reason macOS/Linux + # CI treat sanitizer runs as observational rather than pass/fail-gating -- + # see SANITIZERS_ENABLED in CMakeLists.txt), so the test-execution steps are + # continue-on-error: what we actually care about here is whether a sanitizer + # catches a real memory-safety/UB bug (an abort with a diagnostic in the + # log), not whether a loss threshold was met under instrumentation overhead. + test-sanitizer: + strategy: + fail-fast: false + matrix: + include: + - sanitizer_name: UBSAN + arch: x86_64 + runner: windows-2022 + - sanitizer_name: UBSAN + arch: aarch64 + runner: windows-11-arm + - sanitizer_name: ASAN + arch: x86_64 + runner: windows-2022 + + runs-on: ${{ matrix.runner }} + needs: [build-sanitizer, rade-loss-baseline] + env: + RADIO_TO_COMPUTER_DEVICE: "CABLE Output (VB-Audio Virtual Cable)" + COMPUTER_TO_RADIO_DEVICE: "Speakers (VB-Audio Virtual Cable)" + MICROPHONE_TO_COMPUTER_DEVICE: "Line 1 (Virtual Audio Cable)" + COMPUTER_TO_SPEAKER_DEVICE: "Line 1 (Virtual Audio Cable)" + # LSan has no Windows runtime, so it's intentionally not configured here. + # Options are colon-separated key=value pairs, so an absolute Windows + # path (drive-letter colon, e.g. D:\...) breaks the parser -- it reads + # "suppressions=D" as one pair and "\...\asan_suppressions.txt" (no + # '=') as the next, and aborts with "expected '=' in ASAN_OPTIONS" + # before FreeDV even starts. Use bare relative filenames instead, + # resolved against each test step's working directory (see the + # "Copy test scripts to install folder" step below). + ASAN_OPTIONS: suppressions=asan_suppressions.txt + UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1:suppressions=ubsan_suppressions.txt + + steps: + - uses: actions/checkout@v6 + - uses: actions/download-artifact@v8 + with: + merge-multiple: true + pattern: sanitizer-${{ matrix.sanitizer_name }}-FreeDV-*-${{ matrix.arch }}.exe + path: ${{github.workspace}} + + - uses: TheMrMilchmann/setup-msvc-dev@v4 + with: + arch: x64 + + - name: Install VAC ("analog" sound device) + run: | + Invoke-WebRequest https://software.muzychenko.net/freeware/vac470lite.zip -OutFile vac470lite.zip + Expand-Archive -Path vac470lite.zip -DestinationPath vac470lite + $driverFile = 'vac470lite\${{ env.Platform }}\vrtaucbl.sys'; + $outputFile = '${{github.workspace}}\vac.cer'; + $exportType = [System.Security.Cryptography.X509Certificates.X509ContentType]::Cert; + + $cert = (Get-AuthenticodeSignature $driverFile).SignerCertificate; + [System.IO.File]::WriteAllBytes($outputFile, $cert.Export($exportType)); + + .\ci\Install-Driver.ps1 $outputFile "vac470lite/vrtaucbl.inf" VirtualAudioCable_83ed7f0e-2028-4956-b0b4-39c76fdaef1d + + - name: Install VB-Cable ("Radio" sound device) + run: | + Invoke-WebRequest https://download.vb-audio.com/Download_CABLE/VBCABLE_Driver_Pack45.zip -OutFile VBCABLE_Driver_Pack45.zip + Expand-Archive -Path VBCABLE_Driver_Pack45.zip -DestinationPath VBCABLE_Driver_Pack45 + $driverFile = 'VBCable_Driver_Pack45\vbaudio_cable64_win10.sys'; + $outputFile = '${{github.workspace}}\vbcable.cer'; + $exportType = [System.Security.Cryptography.X509Certificates.X509ContentType]::Cert; + + $cert = (Get-AuthenticodeSignature $driverFile).SignerCertificate; + [System.IO.File]::WriteAllBytes($outputFile, $cert.Export($exportType)); + + .\ci\Install-Driver.ps1 $outputFile VBCable_Driver_Pack45\vbMmeCable64_win10.inf VBAudioVACWDM + + - name: Install FreeDV on hard drive + shell: pwsh + run: | + Get-ChildItem *.exe | Rename-Item -NewName FreeDV.exe + .\FreeDV.exe /S /D=${{github.workspace}}\FreeDV-Install-Location | Out-Null + + - name: Copy test scripts to install folder + shell: pwsh + run: | + Copy-Item -Path ${{github.workspace}}/test/TestFreeDVFullDuplex.ps1 -Destination ${{github.workspace}}\FreeDV-Install-Location\bin + Copy-Item -Path ${{github.workspace}}/test/freedv-ctest-fullduplex.conf.tmpl -Destination ${{github.workspace}}\FreeDV-Install-Location\bin + Copy-Item -Path ${{github.workspace}}/test/TestFreeDVReporting.ps1 -Destination ${{github.workspace}}\FreeDV-Install-Location\bin + Copy-Item -Path ${{github.workspace}}/test/freedv-ctest-reporting.conf.tmpl -Destination ${{github.workspace}}\FreeDV-Install-Location\bin + Copy-Item -Path ${{github.workspace}}/test/hamlibserver.py -Destination ${{github.workspace}}\FreeDV-Install-Location\bin + Copy-Item -Path ${{github.workspace}}/test/TestFreeDVRadeLoss.ps1 -Destination ${{github.workspace}}\FreeDV-Install-Location\bin + Copy-Item -Path ${{github.workspace}}/test/freedv-ctest-loss.conf.tmpl -Destination ${{github.workspace}}\FreeDV-Install-Location\bin + # Sanitizer options need bare relative filenames (see ASAN_OPTIONS/ + # UBSAN_OPTIONS above), so the suppression files need to live next + # to freedv.exe rather than being referenced by absolute path. + Copy-Item -Path ${{github.workspace}}/test/asan_suppressions.txt -Destination ${{github.workspace}}\FreeDV-Install-Location\bin + Copy-Item -Path ${{github.workspace}}/test/ubsan_suppressions.txt -Destination ${{github.workspace}}\FreeDV-Install-Location\bin + + - name: Install SoX + shell: pwsh + run: | + choco install sox.portable + + - name: Install Python dependencies + shell: pwsh + run: | + pip install torch --index-url https://download.pytorch.org/whl/cpu + pip install matplotlib + + - name: Grant FreeDV access to the microphone + run: | + New-Item -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore" -Name "microphone" -Force + Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\microphone" -Name Value -Value Allow + New-Item -Path "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore" -Name "microphone" -Force + Set-ItemProperty -Path "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\microphone" -Name Value -Value Allow + New-Item -Path "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\microphone\" -Name "NonPackaged" -Force + Set-ItemProperty -Path "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\microphone\NonPackaged" -Name Value -Value Allow + 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: Wait for virtual audio endpoints + shell: pwsh + run: | + & "${{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-AudioDevice -List | Format-Table -AutoSize Index, Default, Type, Name + + - name: Set Windows default audio devices + shell: pwsh + run: | + 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 + + - name: Set High Performance power plan to prevent CPU frequency scaling + if: ${{ matrix.runner == 'windows-11-arm' }} + shell: pwsh + run: | + powercfg /setactive 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c + powercfg /setacvalueindex SCHEME_CURRENT 54533251-82be-4824-96c1-47b60b740d00 893dee8e-2bef-41e0-89c6-b55d0929964c 100 + powercfg /setactive SCHEME_CURRENT + + - name: Configure crash dump collection for FreeDV + shell: pwsh + run: | + $dumpFolder = "${{github.workspace}}\CrashDumps" + New-Item -Path $dumpFolder -ItemType Directory -Force | Out-Null + + $werKey = "HKLM:\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\freedv.exe" + New-Item -Path $werKey -Force | Out-Null + Set-ItemProperty -Path $werKey -Name DumpFolder -Value $dumpFolder -Type ExpandString + Set-ItemProperty -Path $werKey -Name DumpCount -Value 10 -Type DWord + Set-ItemProperty -Path $werKey -Name DumpType -Value 2 -Type DWord + + - name: Test RADE + id: test-rade + shell: pwsh + working-directory: ${{github.workspace}}\FreeDV-Install-Location\bin + if: ${{ !cancelled() }} + continue-on-error: true + run: | + .\TestFreeDVFullDuplex.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}}" -ModeToTest RADEV1 -NumberOfRuns 1 *>&1 | Tee-Object -FilePath fullduplex.log + timeout-minutes: 15 + + - name: Test RADE Loss + id: test-rade-loss + shell: pwsh + working-directory: ${{github.workspace}}\FreeDV-Install-Location\bin + if: ${{ !cancelled() }} + continue-on-error: true + run: | + .\TestFreeDVRadeLoss.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}}" -LossThreshold ${{ needs.rade-loss-baseline.outputs.threshold }} *>&1 | Tee-Object -FilePath rade_loss.log + timeout-minutes: 15 + + - name: Test RADE Reporting + id: test-rade-reporting + shell: pwsh + working-directory: ${{github.workspace}}\FreeDV-Install-Location\bin + if: ${{ !cancelled() }} + continue-on-error: true + run: | + .\TestFreeDVReporting.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}}" *>&1 | Tee-Object -FilePath reporting.log + timeout-minutes: 15 + + # The three test steps above are continue-on-error so an ordinary loss- + # threshold miss (expected under instrumentation overhead, same leniency + # ctest gives sanitizer builds on macOS/Linux via SANITIZERS_ENABLED) + # doesn't stop the other tests from running or fail the job by itself. + # But that also means a *real* sanitizer abort would otherwise pass + # silently, since continue-on-error suppresses the job's conclusion, not + # just the step's outcome -- so explicitly scan every test's captured + # output for a sanitizer report and fail for real if one is found. + - name: Check for sanitizer errors + id: check-sanitizer-errors + if: ${{ !cancelled() }} + shell: pwsh + working-directory: ${{github.workspace}}\FreeDV-Install-Location\bin + run: | + $pattern = 'AddressSanitizer|UndefinedBehaviorSanitizer|LeakSanitizer' + $hits = Get-ChildItem -Path . -Filter *.log | ForEach-Object { Select-String -Path $_.FullName -Pattern $pattern } + if ($hits) { + Write-Host "::error::Sanitizer violation(s) found in test output:" + $hits | ForEach-Object { Write-Host "$($_.Filename):$($_.LineNumber): $($_.Line)" } + exit 1 + } + Write-Host "No sanitizer violations found in test output." + + - name: Screenshot if failed + if: ${{ steps.test-rade.outcome == 'failure' || steps.test-rade-loss.outcome == 'failure' || steps.test-rade-reporting.outcome == 'failure' || steps.check-sanitizer-errors.outcome == 'failure' }} + shell: pwsh + run: | + Invoke-WebRequest https://raw.githubusercontent.com/npocmaka/batch.scripts/refs/heads/master/hybrids/.net/c/screenCapture.bat -OutFile screenCapture.bat + & .\screenCapture.bat screenshot-${{ matrix.sanitizer_name }}-${{ matrix.arch }}.png + + - name: Upload screenshot + uses: actions/upload-artifact@v7 + if: ${{ steps.test-rade.outcome == 'failure' || steps.test-rade-loss.outcome == 'failure' || steps.test-rade-reporting.outcome == 'failure' || steps.check-sanitizer-errors.outcome == 'failure' }} + with: + name: screenshot-${{ matrix.sanitizer_name }}-${{ matrix.arch }}.png + path: ${{github.workspace}}/screenshot-${{ matrix.sanitizer_name }}-${{ matrix.arch }}.png + archive: false + + - name: Upload crash dumps + uses: actions/upload-artifact@v7 + if: always() + with: + name: crashdumps-${{ matrix.sanitizer_name }}-${{ matrix.arch }} + path: ${{github.workspace}}\CrashDumps\*.dmp + if-no-files-found: ignore + + - name: Upload test logs + uses: actions/upload-artifact@v7 + if: always() + with: + name: sanitizer-logs-${{ matrix.sanitizer_name }}-${{ matrix.arch }} + path: ${{github.workspace}}\FreeDV-Install-Location\bin\*.log + if-no-files-found: ignore diff --git a/test/TestFreeDVRadeLoss.ps1 b/test/TestFreeDVRadeLoss.ps1 new file mode 100644 index 00000000..e020168d --- /dev/null +++ b/test/TestFreeDVRadeLoss.ps1 @@ -0,0 +1,398 @@ +<# + .SYNOPSIS + Executes RADE loss test of FreeDV. + + .DESCRIPTION + This script pushes a known test file through FreeDV's TX pipeline while recording the resulting audio from + the real Windows sound card path, then plays that recording back into FreeDV's RX pipeline. The RADE features + captured on the TX and RX sides are compared using the loss.py tool from the radae repository, which prints + PASS or FAIL depending on whether the feature loss introduced by the round trip through real audio hardware + is below the given threshold. This is the PowerShell equivalent of test/test_rade_loss.sh. + + When -LossThreshold is not supplied, the threshold is derived from a software-only baseline as described in + the RADE integration verification procedure + (https://github.com/drowe67/radae/blob/dr-tx-bpf/doc/verification/verification_procedure.md): all.wav is run + through rade_tx_wav + rade_rx_wav and loss.py, and the hardware round trip must stay within +10% of that + baseline loss. rade_tx_wav / rade_rx_wav are not built for Windows in the rade_c fork, so Windows CI computes + the baseline on a Linux runner and passes the value in via -LossThreshold; -RadeCToolsDir lets a local run + point at a rade_c build that does have the tools. + + .INPUTS + None. You can't pipe objects to this script. + + .OUTPUTS + The script outputs FreeDV and loss.py logging as well as a final Passed/Failures summary to the console. + + .EXAMPLE + PS> .\TestFreeDVRadeLoss.ps1 -RadioToComputerDevice "CABLE Output (VB-Audio Virtual Cable)" -ComputerToRadioDevice "Speakers (VB-Audio Virtual Cable)" -ComputerToSpeakerDevice "Line 1 (Virtual Audio Cable)" -MicrophoneToComputerDevice "Line 1 (Virtual Audio Cable)" + +#> + +param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + # The sound device to receive RX audio from. Also used to record FreeDV's TX output, as it is the + # recording half of the same virtual cable that ComputerToRadioDevice plays into. + $RadioToComputerDevice, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + # The sound device to emit decoded audio to. + $ComputerToSpeakerDevice, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + # The sound device to receive analog audio from. + $MicrophoneToComputerDevice, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + # The sound device to emit TX audio to. Also used to play the recorded TX audio back into + # FreeDV's RX input, as it is the playback half of the same virtual cable that RadioToComputerDevice records from. + $ComputerToRadioDevice, + + [double] + # The maximum acceptable RADE feature loss fraction before the test is considered failed. + # When left at 0 (the default) it is computed from a software-only baseline (see .DESCRIPTION): + # baseline loss from rade_tx_wav/rade_rx_wav on all.wav, times 1.10. + $LossThreshold = 0, + + [string] + # Directory containing rade_tx_wav.exe / rade_rx_wav.exe (from a rade_c build), used to compute + # the baseline when -LossThreshold is not supplied. Auto-detected when possible. + $RadeCToolsDir = "", + + [string] + # Path or filename of the Python interpreter used to run loss.py. + $PythonBinary = "python.exe") + +# Fallback threshold used only when -LossThreshold is not supplied and the software-only +# baseline cannot be computed (e.g. rade_tx_wav / rade_rx_wav unavailable). +$FallbackLossThreshold = 0.0891 +$LossTolerance = 1.10 + +<# + .Description + Runs all.wav through the rade_c software-only path (rade_tx_wav -> rade_rx_wav -> loss.py) and returns + the baseline feature loss multiplied by $LossTolerance, i.e. the maximum loss the hardware round trip is + allowed to introduce. Returns $null if the tools, test corpus or loss figure can't be found, in which + case the caller should fall back to $FallbackLossThreshold. Mirrors compute_loss_threshold() in + test/test_rade_loss.sh. +#> +function Get-RadeLossThreshold { + param ( + $current_loc, + $RadeCToolsDir, + $PythonBinary, + $Tolerance + ) + + # Locate rade_tx_wav.exe / rade_rx_wav.exe from a rade_c build. + $candidates = New-Object System.Collections.Generic.List[string] + if ($RadeCToolsDir) { $candidates.Add($RadeCToolsDir) } + $candidates.Add("$current_loc") + $candidates.Add((Join-Path $current_loc "_deps\freedv_backend-build\rade_build\src")) + $found = Get-ChildItem -Path $current_loc -Recurse -Filter "rade_tx_wav.exe" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($found) { $candidates.Add($found.DirectoryName) } + + $toolsDir = $null + foreach ($c in $candidates) { + if ($c -and (Test-Path (Join-Path $c "rade_tx_wav.exe")) -and (Test-Path (Join-Path $c "rade_rx_wav.exe"))) { + $toolsDir = $c + break + } + } + if (-not $toolsDir) { + Write-Host "Could not find rade_tx_wav.exe / rade_rx_wav.exe; pass -RadeCToolsDir or -LossThreshold." + return $null + } + + $allWav = Join-Path $current_loc "rade_src\wav\all.wav" + if (-not (Test-Path $allWav)) { + Write-Host "Could not find $allWav for baseline computation." + return $null + } + + $baseIn = Join-Path $current_loc "baseline_in.wav" + $baseTxWav = Join-Path $current_loc "baseline_tx.wav" + $baseDecoded = Join-Path $current_loc "baseline_decoded.wav" + $baseTxF = Join-Path $current_loc "baseline_txfeatures.f32" + $baseRxF = Join-Path $current_loc "baseline_rxfeatures.f32" + + # rade_tx_wav requires 16 kHz mono 16-bit PCM (all.wav already is); normalise defensively. + # Output of the native tools is captured (not left on the pipeline) so it can't corrupt the return value. + $toolOut = & sox.exe $allWav -r 16000 -c 1 -b 16 -e signed-integer $baseIn 2>&1 + if ($LASTEXITCODE -ne 0) { Write-Host "$toolOut"; return $null } + + # Prepend the tools directory to PATH so librade.dll resolves next to the executables. + $oldPath = $env:PATH + $env:PATH = "$toolsDir;$env:PATH" + try { + # --v2 to match the mode FreeDV runs: src/freedv_interface.cpp opens RADE + # with RADE_MODE_V2 unconditionally on this branch, whatever the -utmode label. + $toolOut = & (Join-Path $toolsDir "rade_tx_wav.exe") --v2 -f $baseTxF $baseIn $baseTxWav 2>&1 + if ($LASTEXITCODE -ne 0) { Write-Host "$toolOut"; return $null } + $toolOut = & (Join-Path $toolsDir "rade_rx_wav.exe") --v2 -f $baseRxF $baseTxWav $baseDecoded 2>&1 + if ($LASTEXITCODE -ne 0) { Write-Host "$toolOut"; return $null } + } + finally { + $env:PATH = $oldPath + } + + $baselineOutput = & $PythonBinary (Join-Path $current_loc "rade_src\loss.py") $baseTxF $baseRxF --clip_start 100 --clip_end 300 2>&1 + Write-Host "software-only baseline: $baselineOutput" + + $m = [regex]::Match(($baselineOutput -join "`n"), 'loss:\s*([0-9]+\.?[0-9]*)') + if (-not $m.Success) { return $null } + + $baselineLoss = [double]$m.Groups[1].Value + if ($baselineLoss -le 0) { return $null } + + return [math]::Round($baselineLoss * $Tolerance, 4) +} + +<# + .Description + Starts FreeDV in RX mode listening on the real sound card, plays the given wav file back into it, then + compares the resulting RX RADE features against the reference txfeatures.f32 using loss.py. Returns + $true/$false depending on whether the loss is within the given threshold. Factored out of Test-RadeLoss + so it can be retried against a phase-shifted copy of the recording (see the RADEV2 sync-timing workaround + below). +#> +function Invoke-RadeLossAttempt { + param ( + $current_loc, + $psi, + $ComputerToRadioDevice, + $PlaybackFile, + $PythonBinary, + $LossThreshold + ) + + $process = New-Object System.Diagnostics.Process + $process.StartInfo = $psi + [void]$process.Start() + $process.PriorityClass = [System.Diagnostics.ProcessPriorityClass]::AboveNormal + + Start-Sleep -Milliseconds 4995 + + # Play the recorded TX audio back into FreeDV's RX input + $playPsi = New-Object System.Diagnostics.ProcessStartInfo + $playPsi.CreateNoWindow = $true + $playPsi.UseShellExecute = $false + $playPsi.RedirectStandardError = $false + $playPsi.RedirectStandardOutput = $false + $playPsi.FileName = "sox.exe" + $playPsi.WorkingDirectory = $current_loc + $quoted_play_device = "`"" + $ComputerToRadioDevice + "`"" + $playPsi.Arguments = @("--buffer 128000 -t wav `"$PlaybackFile`" -t waveaudio $quoted_play_device") + + $playProcess = New-Object System.Diagnostics.Process + $playProcess.StartInfo = $playPsi + [void]$playProcess.Start() + + # Read output from RX run + $err_output = $process.StandardError.ReadToEnd(); + $output = $process.StandardOutput.ReadToEnd(); + $process.WaitForExit() + + Write-Host "$err_output" + + $rxExitCode = $process.ExitCode + + try { + if (-not $playProcess.HasExited) { + $playProcess.Kill() + } + } catch { + # Ignore failure as the process may have already exited + } + $playProcess.WaitForExit() + + if ($rxExitCode -ne 0) { + return $false + } + + # Compare TX/RX RADE features using loss.py from the radae repo + $lossPsi = New-Object System.Diagnostics.ProcessStartInfo + $lossPsi.CreateNoWindow = $true + $lossPsi.UseShellExecute = $false + $lossPsi.RedirectStandardError = $true + $lossPsi.RedirectStandardOutput = $true + $lossPsi.FileName = $PythonBinary + $lossPsi.WorkingDirectory = $current_loc + $lossPsi.Arguments = @("`"$current_loc\rade_src\loss.py`" `"$current_loc\txfeatures.f32`" `"$current_loc\rxfeatures.f32`" --loss_test $LossThreshold --clip_start 100 --clip_end 300") + + $lossProcess = New-Object System.Diagnostics.Process + $lossProcess.StartInfo = $lossPsi + [void]$lossProcess.Start() + + $loss_output = $lossProcess.StandardOutput.ReadToEnd(); + $loss_err = $lossProcess.StandardError.ReadToEnd(); + $lossProcess.WaitForExit() + + Write-Host "$loss_output" + Write-Host "$loss_err" + + $lossPasses = ($loss_output -split "`r?`n") | Where { $_.Contains("PASS") } + return (($lossProcess.ExitCode -eq 0) -and ($lossPasses.Count -ge 1)) +} + +<# + .Description + Performs the actual test by cloning the radae test corpus, transmitting a known test file through FreeDV, + recording the resulting audio, playing it back through FreeDV's RX pipeline, and comparing the TX/RX RADE + features using loss.py. +#> +function Test-RadeLoss { + param ( + $RadioToComputerDevice, + $ComputerToSpeakerDevice, + $MicrophoneToComputerDevice, + $ComputerToRadioDevice, + $LossThreshold, + $RadeCToolsDir, + $PythonBinary + ) + + $current_loc = Get-Location + + # Clone radae repo (contains the test audio and the loss.py comparison tool) if not already present. + if (-not (Test-Path "$current_loc\rade_src")) { + & git.exe clone -b main https://github.com/drowe67/radae.git "$current_loc\rade_src" + } + + # Resample test file to 48 kHz. Needed for CI environment to reduce CPU usage. + & sox.exe "$current_loc\rade_src\wav\all.wav" -r 48000 "$current_loc\tx_in.wav" + + # Resolve the loss threshold: use -LossThreshold when supplied, otherwise derive it from the + # software-only baseline (see .DESCRIPTION), falling back to a fixed value if that can't be done. + if ($LossThreshold -le 0) { + $computed = Get-RadeLossThreshold -current_loc $current_loc -RadeCToolsDir $RadeCToolsDir -PythonBinary $PythonBinary -Tolerance $LossTolerance + if ($null -ne $computed) { + $LossThreshold = $computed + Write-Host "RADE loss threshold: $LossThreshold (software-only baseline x $LossTolerance)" + } else { + $LossThreshold = $FallbackLossThreshold + Write-Host "WARNING: could not compute RADE loss baseline; using fallback threshold $LossThreshold" + } + } else { + Write-Host "RADE loss threshold: $LossThreshold (supplied via -LossThreshold)" + } + + # Generate new conf + $conf_tmpl = Get-Content "$current_loc\freedv-ctest-loss.conf.tmpl" + $conf_tmpl = $conf_tmpl.Replace("@FREEDV_RADIO_TO_COMPUTER_DEVICE@", $RadioToComputerDevice) + $conf_tmpl = $conf_tmpl.Replace("@FREEDV_COMPUTER_TO_RADIO_DEVICE@", $ComputerToRadioDevice) + $conf_tmpl = $conf_tmpl.Replace("@FREEDV_MICROPHONE_TO_COMPUTER_DEVICE@", $MicrophoneToComputerDevice) + $conf_tmpl = $conf_tmpl.Replace("@FREEDV_COMPUTER_TO_SPEAKER_DEVICE@", $ComputerToSpeakerDevice) + $tmp_file = New-TemporaryFile + $conf_tmpl | Set-Content -Path $tmp_file.FullName + $quoted_tmp_filename = "`"" + $tmp_file.FullName + "`"" + + # Start recording FreeDV's TX output + $recordPsi = New-Object System.Diagnostics.ProcessStartInfo + $recordPsi.CreateNoWindow = $true + $recordPsi.UseShellExecute = $false + $recordPsi.RedirectStandardError = $false + $recordPsi.RedirectStandardOutput = $false + $recordPsi.FileName = "sox.exe" + $recordPsi.WorkingDirectory = $current_loc + $quoted_record_device = "`"" + $RadioToComputerDevice + "`"" + $recordPsi.Arguments = @("--buffer 128000 -t waveaudio $quoted_record_device -c 1 -t wav -r 48000 -b 16 -e signed-integer `"$current_loc\test.wav`"") + + $recordProcess = New-Object System.Diagnostics.Process + $recordProcess.StartInfo = $recordPsi + [void]$recordProcess.Start() + + # Start freedv.exe in TX mode + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.CreateNoWindow = $true + $psi.UseShellExecute = $false + $psi.RedirectStandardError = $true + $psi.RedirectStandardOutput = $true + $psi.FileName = "$current_loc\freedv.exe" + $psi.WorkingDirectory = $current_loc + $psi.Arguments = @("/f $quoted_tmp_filename /ut tx /utmode RADEV1 /txfile `"$current_loc\tx_in.wav`" /txfeaturefile `"$current_loc\txfeatures.f32`"") + + $process = New-Object System.Diagnostics.Process + $process.StartInfo = $psi + [void]$process.Start() + $process.PriorityClass = [System.Diagnostics.ProcessPriorityClass]::AboveNormal + + # Read output from TX run + $err_output = $process.StandardError.ReadToEnd(); + $output = $process.StandardOutput.ReadToEnd(); + $process.WaitForExit() + + Write-Host "$err_output" + + $txExitCode = $process.ExitCode + + # Stop recording + try { + $recordProcess.Kill() + } catch { + # Ignore failure as the process may have already exited + } + $recordProcess.WaitForExit() + + if ($txExitCode -ne 0) { + return $false + } + + # Workaround/performance improvement: strip silence at beginning and end of recording + # As well as reducing the amount of audio that needs to be played back, it also helps + # ensure we don't accidentally run into a potential RADEV2 bug (https://github.com/freedv/rade_c/issues/8) + # Note: commands adapted from https://digitalcardboard.com/blog/2009/08/25/the-sox-of-silence/ + $recordPsi.Arguments = @("test.wav test_stripped.wav silence 1 0.1 1% reverse") + $stripProcess = New-Object System.Diagnostics.Process + $stripProcess.StartInfo = $recordPsi + [void]$stripProcess.Start() + $stripProcess.WaitForExit() + + $recordPsi.Arguments = @("test_stripped.wav test.wav silence 1 0.1 1% reverse") + $stripProcess = New-Object System.Diagnostics.Process + $stripProcess.StartInfo = $recordPsi + [void]$stripProcess.Start() + $stripProcess.WaitForExit() + + # Restart FreeDV in RX mode, reading live from the sound card so that any dropouts introduced by the + # real audio path get captured in the RX feature file (mirrors test/test_rade_loss.sh). + $psi.Arguments = @("/f $quoted_tmp_filename /ut rx /utmode RADEV1 /txtime 70 /rxfeaturefile `"$current_loc\rxfeatures.f32`"") + + $passed = Invoke-RadeLossAttempt -current_loc $current_loc -psi $psi -ComputerToRadioDevice $ComputerToRadioDevice -PlaybackFile "$current_loc\test.wav" -PythonBinary $PythonBinary -LossThreshold $LossThreshold + + return $passed +} + +$passes = 0 +$fails = 0 + +$result = Test-RadeLoss ` + -RadioToComputerDevice $RadioToComputerDevice ` + -ComputerToSpeakerDevice $ComputerToSpeakerDevice ` + -MicrophoneToComputerDevice $MicrophoneToComputerDevice ` + -ComputerToRadioDevice $ComputerToRadioDevice ` + -LossThreshold $LossThreshold ` + -RadeCToolsDir $RadeCToolsDir ` + -PythonBinary $PythonBinary +if ($result -eq $true) +{ + $passes++ +} +else +{ + $fails++ +} + +Write-Host "Mode: RADEV1, Passed: $passes, Failures: $fails" + +if ($fails -gt 0) { + throw "Test failed" + exit 1 +} diff --git a/test/ubsan_suppressions.txt b/test/ubsan_suppressions.txt index b1459206..32e91d72 100644 --- a/test/ubsan_suppressions.txt +++ b/test/ubsan_suppressions.txt @@ -3,3 +3,5 @@ vptr:wx/window.h vptr:src/osx/core/colour.cpp vptr:src/osx/window_osx.cpp vptr:src/common/containr.cpp +null:src/msw/tooltip.cpp +function:crypto/stack/stack.c