diff --git a/.github/workflows/cmake-windows.yml b/.github/workflows/cmake-windows.yml index 0bb98f4e..7804793c 100644 --- a/.github/workflows/cmake-windows.yml +++ b/.github/workflows/cmake-windows.yml @@ -111,11 +111,14 @@ jobs: 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 - Import-Certificate -FilePath ${{github.workspace}}\ci\vbcable.cer -CertStoreLocation Cert:\LocalMachine\root - Import-Certificate -FilePath ${{github.workspace}}\ci\vbcable.cer -CertStoreLocation Cert:\LocalMachine\TrustedPublisher - Get-ChildItem -Path $Env:WindowsSdkDir -Recurse -Filter "devgen.exe" | Select-Object FullName - & "${{ env.WindowsSdkDir }}\Tools\${{ env.WindowsSDKVersion }}\${{ env.Platform }}\devgen.exe" /add /bus ROOT /hardwareid VBAudioVACWDM - pnputil /add-driver VBCable_Driver_Pack45\vbMmeCable64_win10.inf /install + $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 VAC ("analog" sound device) run: | @@ -128,10 +131,7 @@ jobs: $cert = (Get-AuthenticodeSignature $driverFile).SignerCertificate; [System.IO.File]::WriteAllBytes($outputFile, $cert.Export($exportType)); - Import-Certificate -FilePath ${{github.workspace}}\vac.cer -CertStoreLocation Cert:\LocalMachine\root - Import-Certificate -FilePath ${{github.workspace}}\vac.cer -CertStoreLocation Cert:\LocalMachine\TrustedPublisher - & "${{ env.WindowsSdkDir }}\Tools\${{ env.WindowsSDKVersion }}\${{ env.Platform }}\devgen.exe" /add /bus ROOT /hardwareid VirtualAudioCable_83ed7f0e-2028-4956-b0b4-39c76fdaef1d - pnputil /add-driver "vac470lite\vrtaucbl.inf" /install + .\ci\Install-Driver.ps1 $outputFile "vac470lite/vrtaucbl.inf" VirtualAudioCable_83ed7f0e-2028-4956-b0b4-39c76fdaef1d - name: Screenshot if failed if: failure() diff --git a/USER_MANUAL.md b/USER_MANUAL.md index d2efec95..ca4cf3a1 100644 --- a/USER_MANUAL.md +++ b/USER_MANUAL.md @@ -952,7 +952,7 @@ LDPC | Low Density Parity Check Codes - a family of powerful FEC codes * Update Hamlib to 4.7.0. (PR #1226) * Update wxWidgets to 3.3.2. (PR #1244) * Flex: Update Docker container version to match AppImage version. (PR #1256) - * Reenable Windows on ARM builds previously disabled in 2.0.0. (PR #1297) + * Reenable Windows on ARM builds previously disabled in 2.0.0. (PR #1297, #1300) 4. Documentation: * Update README instructions to reflect current Windows build steps. (PR #1232) * Add OmniRig troubleshooting to the user manual. (PR #1264) diff --git a/ci/Install-Driver.ps1 b/ci/Install-Driver.ps1 new file mode 100644 index 00000000..87f1923e --- /dev/null +++ b/ci/Install-Driver.ps1 @@ -0,0 +1,26 @@ +# Make sure we are running as Admin +If (-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")){ + $arguments = "& '" + $myinvocation.mycommand.definition + "'" + Start-Process powershell -Verb runAs -ArgumentList $arguments + Break + } + +$certName = $args[0] +$infName = $args[1] +$hwId = $args[2] + +Import-Certificate -FilePath $certName -CertStoreLocation Cert:\LocalMachine\root + +# Some runners in the GH environment will emit "Access Denied" for Import-Certificate +# if the proper TrustedPublisher Registry key doesn't exist first. Create prior to +# attempting import. +# See https://learn.microsoft.com/en-us/answers/questions/1679945/(import-certificate)-unauthorizedaccessexception-e +$Key = "HKLM:\Software\Microsoft\SystemCertificates\TrustedPublisher" +If (-Not (Test-Path $Key)) { + New-Item -Path $Key -ItemType RegistryKey -Force +} +` +Import-Certificate -FilePath $certName -CertStoreLocation Cert:\LocalMachine\TrustedPublisher + +& "$Env:WindowsSdkDir\Tools\$Env:WindowsSDKVersion\$Env:Platform\devgen.exe" /add /bus ROOT /hardwareid $hwId +pnputil /add-driver $infName /install diff --git a/src/audio/WASAPIAudioDevice.cpp b/src/audio/WASAPIAudioDevice.cpp index 501679b6..70253192 100644 --- a/src/audio/WASAPIAudioDevice.cpp +++ b/src/audio/WASAPIAudioDevice.cpp @@ -27,16 +27,17 @@ #include #include #include +#include #include "../util/logging/ulog.h" -#define BLOCK_TIME_NS (10000000) +#define BLOCK_TIME_NS (20000000) // Nanoseconds per REFERENCE_TIME unit #define NS_PER_REFTIME (100) thread_local HANDLE WASAPIAudioDevice::HelperTask_ = nullptr; -WASAPIAudioDevice::WASAPIAudioDevice(ComPtr client, ComPtr device, IAudioEngine::AudioDirection direction, int sampleRate, int numChannels) +WASAPIAudioDevice::WASAPIAudioDevice(ComPtr client, ComPtr device, IAudioEngine::AudioDirection direction, int sampleRate, int numChannels) : Win32COMObject("WASAPIDev") , client_(client) , device_(device) @@ -52,6 +53,7 @@ WASAPIAudioDevice::WASAPIAudioDevice(ComPtr client, ComPtrSetClientProperties(&prop); + if (FAILED(hr)) + { + // Try disabling offload as not all devices support it. + prop.bIsOffload = FALSE; + hr = client_->SetClientProperties(&prop); + if (FAILED(hr)) + { + // Non-critical error, can continue without setting properties. + std::stringstream ss; + ss << "Could not set AudioClient properties (hr = " << hr << ")"; + log_warn(ss.str().c_str()); + } + } + // Populate stream format based on requested sample // rate/number of channels. // NOTE: this should already have been determined valid // by the audio engine! - HRESULT hr = client_->GetMixFormat(&streamFormatPtr); + hr = client_->GetMixFormat(&streamFormatPtr); if (SUCCEEDED(hr)) { freeStreamFormat = true; @@ -187,16 +210,65 @@ void WASAPIAudioDevice::start() if (!initialized_) { + //REFERENCE_TIME desiredRefTime = BLOCK_TIME_NS / NS_PER_REFTIME; // REFERENCE_TIME is in 100ns units + UINT32 defaultPeriodInFrames = 0; + UINT32 fundamentalPeriodInFrames = 0; + UINT32 minPeriodInFrames = 0; + UINT32 maxPeriodInFrames = 0; + hr = client_->GetSharedModeEnginePeriod( + streamFormatPtr, + &defaultPeriodInFrames, + &fundamentalPeriodInFrames, + &minPeriodInFrames, + &maxPeriodInFrames); + if (FAILED(hr)) + { + std::stringstream ss; + ss << "Could not get audio engine period (hr = " << hr << ")"; + log_error(ss.str().c_str()); + if (onAudioErrorFunction) + { + onAudioErrorFunction(*this, ss.str(), onAudioErrorState); + } + prom->set_value(); + return; + } + + log_info( + "Default period: %d, fundamental period: %d, minPeriod: %d, maxPeriod: %d", + defaultPeriodInFrames, + fundamentalPeriodInFrames, + minPeriodInFrames, + maxPeriodInFrames); + // Initialize the audio client with the above format - hr = client_->Initialize( - AUDCLNT_SHAREMODE_SHARED, - AUDCLNT_STREAMFLAGS_EVENTCALLBACK | - AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | - AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY, - BLOCK_TIME_NS / NS_PER_REFTIME, // REFERENCE_TIME is in 100ns units - 0, + hr = client_->InitializeSharedAudioStream( + AUDCLNT_STREAMFLAGS_EVENTCALLBACK, + defaultPeriodInFrames, streamFormatPtr, nullptr); + if (AUDCLNT_E_ENGINE_PERIODICITY_LOCKED == hr) + { + // Try again with actual period + WAVEFORMATEX* tempFormat = nullptr; + hr = client_->GetCurrentSharedModeEnginePeriod( + &tempFormat, + &defaultPeriodInFrames); + (void)tempFormat; // ignore warnings + if (FAILED(hr)) + { + std::stringstream ss; + ss << "Could not get current engine period (hr = " << hr << ")"; + log_warn(ss.str().c_str()); + } + + hr = client_->InitializeSharedAudioStream( + AUDCLNT_STREAMFLAGS_EVENTCALLBACK, + defaultPeriodInFrames, + streamFormatPtr, + nullptr); + } + if (freeStreamFormat) { CoTaskMemFree(streamFormatPtr); @@ -402,7 +474,7 @@ void WASAPIAudioDevice::start() // Capture references for use by this thread. ComPtr renderClientRef = renderClient_; ComPtr captureClientRef = captureClient_; - ComPtr clientRef = client_; + ComPtr clientRef = client_; HRESULT res = CoInitializeEx(nullptr, COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE); if (FAILED(res)) @@ -525,7 +597,7 @@ void WASAPIAudioDevice::setHelperRealTime() void WASAPIAudioDevice::startRealTimeWork() { - // empty + startTime_ = std::chrono::steady_clock::now(); } void WASAPIAudioDevice::stopRealTimeWork(bool fastMode) @@ -537,9 +609,21 @@ void WASAPIAudioDevice::stopRealTimeWork(bool fastMode) return; } + int64_t msec = ((1000 * bufferFrameCount_) / sampleRate_) >> (fastMode ? 1 : 0); + msec -= extraTimeMs_; + if (msec <= 0) + { + extraTimeMs_ = 0; + return; + } + // Wait a maximum of (bufferSize / sampleRate) seconds for the semaphore to return DWORD result = WaitForSingleObject(semaphore_, ((1000 * bufferFrameCount_) / sampleRate_) >> (fastMode ? 1 : 0)); + auto endTime = std::chrono::steady_clock::now(); + auto duration = std::chrono::ceil(endTime - startTime_).count() - msec; + extraTimeMs_ = std::max((int64_t)0, (int64_t)duration); // cap extra time to >= 0. + if (result != WAIT_TIMEOUT && result != WAIT_OBJECT_0) { // Fallback to a simple sleep. diff --git a/src/audio/WASAPIAudioDevice.h b/src/audio/WASAPIAudioDevice.h index 200c494a..4d1be405 100644 --- a/src/audio/WASAPIAudioDevice.h +++ b/src/audio/WASAPIAudioDevice.h @@ -69,10 +69,10 @@ public: protected: friend class WASAPIAudioEngine; - WASAPIAudioDevice(ComPtr client, ComPtr device, IAudioEngine::AudioDirection direction, int sampleRate, int numChannels); + WASAPIAudioDevice(ComPtr client, ComPtr device, IAudioEngine::AudioDirection direction, int sampleRate, int numChannels); private: - ComPtr client_; + ComPtr client_; ComPtr device_; ComPtr renderClient_; ComPtr captureClient_; @@ -93,6 +93,10 @@ private: bool isFloatingPoint_; short* tmpBuf_; + // For handling additional wakeup time after semaphore timeout + int extraTimeMs_; + std::chrono::time_point startTime_; + void renderAudio_(ComPtr renderClient); void captureAudio_(ComPtr captureClient); void copyFromWindowsBuffer_(void* buf, int numFrames); diff --git a/src/audio/WASAPIAudioEngine.cpp b/src/audio/WASAPIAudioEngine.cpp index 1ac8923b..ba296ac4 100644 --- a/src/audio/WASAPIAudioEngine.cpp +++ b/src/audio/WASAPIAudioEngine.cpp @@ -283,7 +283,7 @@ std::shared_ptr WASAPIAudioEngine::getAudioDevice(wxString deviceN log_info("Creating WASAPIAudioDevice for device %s (ID %d, direction = %d, sample rate = %d, number of channels = %d)", (const char*)deviceName.ToUTF8(), (int)dev.deviceId, (int)direction, sampleRate, numChannels); ComPtr device = nullptr; - ComPtr client = nullptr; + ComPtr client = nullptr; HRESULT hr = coll->Item(dev.deviceId, device.GetAddressOf()); if (FAILED(hr)) @@ -299,7 +299,7 @@ std::shared_ptr WASAPIAudioEngine::getAudioDevice(wxString deviceN } hr = device->Activate( - IID_IAudioClient, CLSCTX_ALL, + IID_IAudioClient3, CLSCTX_ALL, nullptr, (void**)client.GetAddressOf()); if (FAILED(hr)) { @@ -461,8 +461,8 @@ AudioDeviceSpecification WASAPIAudioEngine::getDeviceSpecification_(ComPtr audioClient = nullptr; - hr = device->Activate(IID_IAudioClient, CLSCTX_ALL, nullptr, (void**)audioClient.GetAddressOf()); + ComPtr audioClient = nullptr; + hr = device->Activate(IID_IAudioClient3, CLSCTX_ALL, nullptr, (void**)audioClient.GetAddressOf()); if (FAILED(hr)) { std::stringstream ss;