From d18d1dce22636c9af4a47da2784c59b88916dc15 Mon Sep 17 00:00:00 2001 From: currentsuspect Date: Mon, 23 Mar 2026 19:39:14 +0000 Subject: [PATCH] chore: comprehensive maintenance pass Changes: - Removed backup file (WASAPIExclusiveDriver.cpp.bak) - Removed redundant CMake C++ standard settings - Added CI job timeouts (30m Linux, 45m Win/Mac, 10m format, 15m analysis) - Added Discord webhook gating - Updated CHANGELOG with recent maintenance work --- .github/workflows/ci.yml | 5 + AestraAudio/CMakeLists.txt | 2 - .../src/Win32/WASAPIExclusiveDriver.cpp.bak | 1134 ----------------- AestraCore/CMakeLists.txt | 2 - meta/CHANGELOGS/CHANGELOG_2026Q1.md | 29 + 5 files changed, 34 insertions(+), 1138 deletions(-) delete mode 100644 AestraAudio/src/Win32/WASAPIExclusiveDriver.cpp.bak diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 920360dc..c3542653 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,7 @@ jobs: build-linux: name: Linux (GCC) runs-on: ubuntu-latest + timeout-minutes: 30 steps: - uses: actions/checkout@v4 with: @@ -58,6 +59,7 @@ jobs: build-windows: name: Windows (MSVC) runs-on: windows-latest + timeout-minutes: 45 steps: - uses: actions/checkout@v4 with: @@ -100,6 +102,7 @@ jobs: build-macos: name: macOS (Clang) runs-on: macos-latest + timeout-minutes: 45 continue-on-error: true steps: - uses: actions/checkout@v4 @@ -141,6 +144,7 @@ jobs: format-check: name: Code Formatting runs-on: ubuntu-latest + timeout-minutes: 10 steps: - uses: actions/checkout@v4 @@ -161,6 +165,7 @@ jobs: static-analysis: name: Static Analysis (clang-tidy) runs-on: ubuntu-latest + timeout-minutes: 15 steps: - uses: actions/checkout@v4 diff --git a/AestraAudio/CMakeLists.txt b/AestraAudio/CMakeLists.txt index 9cf181b6..671c085a 100644 --- a/AestraAudio/CMakeLists.txt +++ b/AestraAudio/CMakeLists.txt @@ -2,8 +2,6 @@ cmake_minimum_required(VERSION 3.22) project(AestraAudio VERSION 1.0.0 LANGUAGES CXX) -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) # Prevent Windows min/max macros from conflicting with std::min/std::max if(WIN32) diff --git a/AestraAudio/src/Win32/WASAPIExclusiveDriver.cpp.bak b/AestraAudio/src/Win32/WASAPIExclusiveDriver.cpp.bak deleted file mode 100644 index cc40fad1..00000000 --- a/AestraAudio/src/Win32/WASAPIExclusiveDriver.cpp.bak +++ /dev/null @@ -1,1134 +0,0 @@ -// © 2025 Aestra Studios — All Rights Reserved. Licensed for personal & educational use only. -#include "WASAPIExclusiveDriver.h" - -// Windows-specific includes (only in .cpp file) -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN -#endif -#ifndef NOMINMAX -#define NOMINMAX -#endif -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#pragma comment(lib, "ole32.lib") -#pragma comment(lib, "avrt.lib") - -#include "AestraLog.h" - -namespace Aestra { -namespace Audio { - -namespace { - const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator); - const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator); - const IID IID_IAudioClient = __uuidof(IAudioClient); - const IID IID_IAudioRenderClient = __uuidof(IAudioRenderClient); - const IID IID_IAudioCaptureClient = __uuidof(IAudioCaptureClient); - - // Common exclusive mode sample rates to test - const uint32_t EXCLUSIVE_SAMPLE_RATES[] = { 44100, 48000, 88200, 96000, 176400, 192000 }; - - std::string HResultToString(HRESULT hr) { - std::ostringstream oss; - oss << "0x" << std::hex << hr; - return oss.str(); - } -} - -WASAPIExclusiveDriver::WASAPIExclusiveDriver() { - LARGE_INTEGER freq; - QueryPerformanceFrequency(&freq); - m_perfFreq = freq.QuadPart; -} - -WASAPIExclusiveDriver::~WASAPIExclusiveDriver() { - shutdown(); -} - -DriverCapability WASAPIExclusiveDriver::getCapabilities() const { - return DriverCapability::PLAYBACK | - DriverCapability::RECORDING | - DriverCapability::DUPLEX | - DriverCapability::EXCLUSIVE_MODE | - DriverCapability::EVENT_DRIVEN | - DriverCapability::HOT_PLUG_DETECTION; -} - -bool WASAPIExclusiveDriver::initialize() { - if (m_state != DriverState::UNINITIALIZED) { - return true; - } - - if (!initializeCOM()) { - return false; - } - - m_state = DriverState::INITIALIZED; - m_lastError = DriverError::NONE; - m_errorMessage.clear(); - - std::cout << "[WASAPI Exclusive] Driver initialized successfully" << std::endl; - Aestra::Log::info("[WASAPI Exclusive] Driver initialized successfully"); - return true; -} - -void WASAPIExclusiveDriver::shutdown() { - stopStream(); - closeStream(); - shutdownCOM(); - m_bufferFrameCount = 0; - m_state = DriverState::UNINITIALIZED; -} - -bool WASAPIExclusiveDriver::isAvailable() const { - // Exclusive mode requires Windows Vista or later - // Check if system supports it by attempting to query capabilities - return true; -} - -bool WASAPIExclusiveDriver::initializeCOM() { - HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); - if (FAILED(hr) && hr != RPC_E_CHANGED_MODE) { - setError(DriverError::INITIALIZATION_FAILED, "COM initialization failed: " + HResultToString(hr)); - return false; - } - - hr = CoCreateInstance( - CLSID_MMDeviceEnumerator, - nullptr, - CLSCTX_ALL, - IID_IMMDeviceEnumerator, - (void**)&m_deviceEnumerator - ); - - if (FAILED(hr)) { - setError(DriverError::INITIALIZATION_FAILED, "Failed to create device enumerator: " + HResultToString(hr)); - CoUninitialize(); - return false; - } - - return true; -} - -void WASAPIExclusiveDriver::shutdownCOM() { - if (m_deviceEnumerator) { - reinterpret_cast(m_deviceEnumerator)->Release(); - m_deviceEnumerator = nullptr; - } - CoUninitialize(); -} - -std::vector WASAPIExclusiveDriver::getDevices() const { - std::vector devices; - - if (!m_deviceEnumerator) { - const_cast(this)->setError(DriverError::INITIALIZATION_FAILED, "Device enumerator not initialized"); - return devices; - } - - enumerateDevices(devices); - return devices; -} - -bool WASAPIExclusiveDriver::enumerateDevices(std::vector& devices) const { - IMMDeviceCollection* deviceCollection = nullptr; - - HRESULT hr = reinterpret_cast(m_deviceEnumerator)->EnumAudioEndpoints( - eRender, - DEVICE_STATE_ACTIVE, - &deviceCollection - ); - - if (SUCCEEDED(hr)) { - UINT count = 0; - deviceCollection->GetCount(&count); - - for (UINT i = 0; i < count; ++i) { - IMMDevice* device = nullptr; - if (SUCCEEDED(deviceCollection->Item(i, &device))) { - AudioDeviceInfo info; - info.id = i; - info.maxOutputChannels = 2; - info.maxInputChannels = 0; - info.isDefaultOutput = (i == 0); - info.isDefaultInput = false; - - // Get supported exclusive sample rates - info.supportedSampleRates = getSupportedExclusiveSampleRates(i); - if (!info.supportedSampleRates.empty()) { - info.preferredSampleRate = info.supportedSampleRates[0]; - } else { - info.preferredSampleRate = 48000; - } - - // Get device name - LPWSTR deviceId = nullptr; - if (SUCCEEDED(device->GetId(&deviceId))) { - IPropertyStore* propertyStore = nullptr; - if (SUCCEEDED(device->OpenPropertyStore(STGM_READ, &propertyStore))) { - PROPVARIANT varName; - PropVariantInit(&varName); - if (SUCCEEDED(propertyStore->GetValue(PKEY_Device_FriendlyName, &varName))) { - int len = WideCharToMultiByte(CP_UTF8, 0, varName.pwszVal, -1, nullptr, 0, nullptr, nullptr); - if (len > 0) { - std::string name(len - 1, '\0'); - WideCharToMultiByte(CP_UTF8, 0, varName.pwszVal, -1, &name[0], len, nullptr, nullptr); - info.name = name + " (Exclusive)"; - } - PropVariantClear(&varName); - } - propertyStore->Release(); - } - CoTaskMemFree(deviceId); - } - - devices.push_back(info); - device->Release(); - } - } - deviceCollection->Release(); - } - - return !devices.empty(); -} - -uint32_t WASAPIExclusiveDriver::getDefaultOutputDevice() { - return 0; -} - -uint32_t WASAPIExclusiveDriver::getDefaultInputDevice() { - return 0; -} - -bool WASAPIExclusiveDriver::isExclusiveModeAvailable(uint32_t deviceId) const { - // Try to open the device and test exclusive mode - IMMDevice* testDevice = nullptr; - - if (!m_deviceEnumerator) { - return false; - } - - HRESULT hr = reinterpret_cast(m_deviceEnumerator)->GetDefaultAudioEndpoint( - eRender, - eConsole, - &testDevice - ); - - if (FAILED(hr)) { - return false; - } - - IAudioClient* testClient = nullptr; - hr = testDevice->Activate( - IID_IAudioClient, - CLSCTX_ALL, - nullptr, - (void**)&testClient - ); - - bool available = false; - if (SUCCEEDED(hr)) { - // Test with a common format - WAVEFORMATEX format = {}; - format.wFormatTag = WAVE_FORMAT_IEEE_FLOAT; - format.nChannels = 2; - format.nSamplesPerSec = 48000; - format.wBitsPerSample = 32; - format.nBlockAlign = (format.nChannels * format.wBitsPerSample) / 8; - format.nAvgBytesPerSec = format.nSamplesPerSec * format.nBlockAlign; - format.cbSize = 0; - - WAVEFORMATEX* closestMatch = nullptr; - hr = testClient->IsFormatSupported(AUDCLNT_SHAREMODE_EXCLUSIVE, &format, &closestMatch); - - available = (hr == S_OK); - - if (closestMatch) { - CoTaskMemFree(closestMatch); - } - - testClient->Release(); - } - - testDevice->Release(); - return available; -} - -std::vector WASAPIExclusiveDriver::getSupportedExclusiveSampleRates(uint32_t deviceId) const { - std::vector supportedRates; - - IMMDevice* testDevice = nullptr; - if (!m_deviceEnumerator) { - return supportedRates; - } - - HRESULT hr = reinterpret_cast(m_deviceEnumerator)->GetDefaultAudioEndpoint( - eRender, - eConsole, - &testDevice - ); - - if (FAILED(hr)) { - return supportedRates; - } - - IAudioClient* testClient = nullptr; - hr = testDevice->Activate( - IID_IAudioClient, - CLSCTX_ALL, - nullptr, - (void**)&testClient - ); - - if (SUCCEEDED(hr)) { - // Test each sample rate - for (uint32_t sampleRate : EXCLUSIVE_SAMPLE_RATES) { - WAVEFORMATEX format = {}; - format.wFormatTag = WAVE_FORMAT_IEEE_FLOAT; - format.nChannels = 2; - format.nSamplesPerSec = sampleRate; - format.wBitsPerSample = 32; - format.nBlockAlign = (format.nChannels * format.wBitsPerSample) / 8; - format.nAvgBytesPerSec = format.nSamplesPerSec * format.nBlockAlign; - format.cbSize = 0; - - WAVEFORMATEX* closestMatch = nullptr; - hr = testClient->IsFormatSupported(AUDCLNT_SHAREMODE_EXCLUSIVE, &format, &closestMatch); - - if (hr == S_OK) { - supportedRates.push_back(sampleRate); - } - - if (closestMatch) { - CoTaskMemFree(closestMatch); - } - } - - testClient->Release(); - } - - testDevice->Release(); - return supportedRates; -} - -bool WASAPIExclusiveDriver::openStream(const AudioStreamConfig& config, AudioCallback callback, void* userData) { - if (m_state == DriverState::STREAM_RUNNING) { - stopStream(); - } - if (m_state == DriverState::STREAM_OPEN) { - closeStream(); - } - - m_config = config; - m_userCallback = callback; - m_userData = userData; - - if (!openDevice(config.deviceId)) { - return false; - } - - if (!initializeAudioClient()) { - closeDevice(); - return false; - } - - m_state = DriverState::STREAM_OPEN; - if (m_usingSharedFallback) { - Aestra::Log::info("[WASAPI] Stream opened in shared fallback mode"); - } else { - Aestra::Log::info("[WASAPI Exclusive] Stream opened successfully"); - } - return true; -} - -bool WASAPIExclusiveDriver::openDevice(uint32_t deviceId) { - if (!m_deviceEnumerator) { - setError(DriverError::DEVICE_NOT_FOUND, "Device enumerator not initialized"); - return false; - } - - HRESULT hr = reinterpret_cast(m_deviceEnumerator)->GetDefaultAudioEndpoint( - eRender, - eConsole, - (IMMDevice**)&m_device - ); - - if (FAILED(hr)) { - setError(DriverError::DEVICE_NOT_FOUND, "Failed to get audio device: " + HResultToString(hr)); - return false; - } - - return true; -} - -void WASAPIExclusiveDriver::closeDevice() { - if (m_waveFormat) { - CoTaskMemFree(m_waveFormat); - m_waveFormat = nullptr; - } - - if (m_renderClient) { - reinterpret_cast(m_renderClient)->Release(); - m_renderClient = nullptr; - } - - if (m_captureClient) { - reinterpret_cast(m_captureClient)->Release(); - m_captureClient = nullptr; - } - - if (m_audioClient) { - reinterpret_cast(m_audioClient)->Release(); - m_audioClient = nullptr; - } - - if (m_device) { - reinterpret_cast(m_device)->Release(); - m_device = nullptr; - } - - if (m_audioEvent) { - CloseHandle(m_audioEvent); - m_audioEvent = nullptr; - } - m_usingSharedFallback = false; - m_bufferFrameCount = 0; -} - -bool WASAPIExclusiveDriver::initializeAudioClient() { - m_usingSharedFallback = false; - HRESULT hr = reinterpret_cast(m_device)->Activate( - IID_IAudioClient, - CLSCTX_ALL, - nullptr, - (void**)&m_audioClient - ); - - if (FAILED(hr)) { - setError(DriverError::STREAM_OPEN_FAILED, "Failed to activate audio client: " + HResultToString(hr)); - return false; - } - - // Find best exclusive format - if (!findBestExclusiveFormat(&m_waveFormat)) { - setError(DriverError::EXCLUSIVE_MODE_UNAVAILABLE, "No compatible exclusive format found"); - return false; - } - - WAVEFORMATEX* wf = reinterpret_cast(m_waveFormat); - m_actualSampleRate = wf->nSamplesPerSec; - - // Log format details for diagnostics - // Log format details for diagnostics - std::stringstream ss; - ss << "[WASAPI Exclusive] Requested format: " - << m_actualSampleRate << " Hz, " - << wf->nChannels << " channels, " - << wf->wBitsPerSample << " bits, " - << (wf->wFormatTag == WAVE_FORMAT_IEEE_FLOAT ? "Float32" : "PCM"); - Aestra::Log::info(ss.str()); - - // Pre-flight check: Test if exclusive mode is available - // This helps detect if another application is already using the device - WAVEFORMATEX* testFormat = nullptr; - hr = reinterpret_cast(m_audioClient)->IsFormatSupported( - AUDCLNT_SHAREMODE_EXCLUSIVE, - reinterpret_cast(m_waveFormat), - &testFormat - ); - - if (testFormat) { - CoTaskMemFree(testFormat); - } - - if (hr == AUDCLNT_E_DEVICE_IN_USE) { - setError(DriverError::DEVICE_IN_USE, - "Device is in use by another application in Exclusive mode. " - "Please close other audio applications or switch to Shared mode."); - Aestra::Log::error("[WASAPI Exclusive] Device busy (AUDCLNT_E_DEVICE_IN_USE)"); - return false; - } - - if (hr == AUDCLNT_E_EXCLUSIVE_MODE_NOT_ALLOWED) { - setError(DriverError::EXCLUSIVE_MODE_UNAVAILABLE, - "Exclusive mode is not allowed for this device. " - "Windows may have disabled exclusive access in device properties."); - Aestra::Log::error("[WASAPI Exclusive] Exclusive mode not allowed (AUDCLNT_E_EXCLUSIVE_MODE_NOT_ALLOWED)"); - return false; - } - - // Calculate minimum buffer duration (100ns units) - // For exclusive mode, use smaller buffers for lower latency - REFERENCE_TIME minDuration = 0; - hr = reinterpret_cast(m_audioClient)->GetDevicePeriod(nullptr, &minDuration); - if (FAILED(hr)) { - minDuration = 30000; // Default to 3ms - } - - // Use requested buffer size, but respect minimum - REFERENCE_TIME requestedDuration = (REFERENCE_TIME)( - 10000000.0 * m_config.bufferSize / m_actualSampleRate - ); - - if (requestedDuration < minDuration) { - requestedDuration = minDuration; - std::cout << "[WASAPI Exclusive] Buffer size adjusted to minimum: " - << (minDuration / 10000.0) << "ms" << std::endl; - } - - // Create event for audio thread - m_audioEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); - if (!m_audioEvent) { - setError(DriverError::STREAM_OPEN_FAILED, "Failed to create audio event"); - return false; - } - - // Initialize in exclusive mode - hr = reinterpret_cast(m_audioClient)->Initialize( - AUDCLNT_SHAREMODE_EXCLUSIVE, - AUDCLNT_STREAMFLAGS_EVENTCALLBACK, - requestedDuration, - requestedDuration, // Exclusive mode requires both durations to match - reinterpret_cast(m_waveFormat), - nullptr - ); - - if (hr == AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED) { - // Need to align buffer size - hr = reinterpret_cast(m_audioClient)->GetBufferSize(&m_bufferFrameCount); - if (SUCCEEDED(hr)) { - reinterpret_cast(m_audioClient)->Release(); - m_audioClient = nullptr; - - // Recalculate aligned duration - requestedDuration = (REFERENCE_TIME)( - 10000000.0 * m_bufferFrameCount / m_actualSampleRate + 0.5 - ); - - std::cout << "[WASAPI Exclusive] Realigning buffer: " << m_bufferFrameCount << " frames" << std::endl; - - // Try again with aligned buffer - hr = reinterpret_cast(m_device)->Activate( - IID_IAudioClient, - CLSCTX_ALL, - nullptr, - (void**)&m_audioClient - ); - - if (SUCCEEDED(hr)) { - hr = reinterpret_cast(m_audioClient)->Initialize( - AUDCLNT_SHAREMODE_EXCLUSIVE, - AUDCLNT_STREAMFLAGS_EVENTCALLBACK, - requestedDuration, - requestedDuration, - reinterpret_cast(m_waveFormat), - nullptr - ); - } - } - } - - // Enhanced error handling with specific diagnostics - if (hr == AUDCLNT_E_DEVICE_IN_USE || hr == AUDCLNT_E_EXCLUSIVE_MODE_NOT_ALLOWED) { - Aestra::Log::error("[WASAPI Exclusive] Exclusive unavailable (" + HResultToString(hr) + "), attempting shared fallback"); - // Clean up exclusive client resources before retrying shared - if (m_audioClient) { reinterpret_cast(m_audioClient)->Release(); m_audioClient = nullptr; } - if (m_renderClient) { reinterpret_cast(m_renderClient)->Release(); m_renderClient = nullptr; } - if (m_captureClient) { reinterpret_cast(m_captureClient)->Release(); m_captureClient = nullptr; } - if (m_audioEvent) { CloseHandle(m_audioEvent); m_audioEvent = nullptr; } - if (m_waveFormat) { CoTaskMemFree(m_waveFormat); m_waveFormat = nullptr; } - return initializeSharedFallback(); - } - - if (hr == AUDCLNT_E_UNSUPPORTED_FORMAT) { - setError(DriverError::STREAM_OPEN_FAILED, - "Audio format not supported by hardware in Exclusive mode. " - "Format: " + std::to_string(m_actualSampleRate) + " Hz, " + - std::to_string(reinterpret_cast(m_waveFormat)->nChannels) + " channels, " + - std::to_string(reinterpret_cast(m_waveFormat)->wBitsPerSample) + " bits. " - "HRESULT: " + HResultToString(hr)); - Aestra::Log::error("[WASAPI Exclusive] Initialize failed: AUDCLNT_E_UNSUPPORTED_FORMAT"); - return false; - } - - if (FAILED(hr)) { - setError(DriverError::STREAM_OPEN_FAILED, - "Failed to initialize exclusive mode. HRESULT: " + HResultToString(hr)); - Aestra::Log::error("[WASAPI Exclusive] Initialize failed: " + HResultToString(hr)); - return false; - } - - // Set event handle - hr = reinterpret_cast(m_audioClient)->SetEventHandle(reinterpret_cast(m_audioEvent)); - if (FAILED(hr)) { - setError(DriverError::STREAM_OPEN_FAILED, "Failed to set event handle: " + HResultToString(hr)); - return false; - } - - // Get actual buffer size - hr = reinterpret_cast(m_audioClient)->GetBufferSize(&m_bufferFrameCount); - if (FAILED(hr)) { - setError(DriverError::STREAM_OPEN_FAILED, "Failed to get buffer size: " + HResultToString(hr)); - return false; - } - - // Get render client - hr = reinterpret_cast(m_audioClient)->GetService(IID_IAudioRenderClient, (void**)&m_renderClient); - if (FAILED(hr)) { - setError(DriverError::STREAM_OPEN_FAILED, "Failed to get render client: " + HResultToString(hr)); - return false; - } - - // Calculate accurate latency metrics - AudioLatencyInfo latencyInfo = AudioLatencyInfo::calculate( - m_bufferFrameCount, - m_actualSampleRate, - 3.0 // Conservative RTL estimate: 3x buffer period - ); - - std::stringstream logSS; - logSS << "[WASAPI Exclusive] Initialized - " - << "Sample Rate: " << m_actualSampleRate << " Hz, " - << "Buffer: " << m_bufferFrameCount << " frames\n" - << " Buffer Period: " << std::fixed << std::setprecision(2) << latencyInfo.bufferPeriodMs << "ms (one-way)\n" - << " Estimated RTL: " << latencyInfo.estimatedRTL_Ms << "ms (round-trip, device-dependent)"; - Aestra::Log::info(logSS.str()); - - return true; -} - -bool WASAPIExclusiveDriver::findBestExclusiveFormat(void** format) { - WAVEFORMATEX** wfFormat = reinterpret_cast(format); - // Try requested sample rate first with different bit depths - // Most consumer devices support 16-bit, some support 24-bit, fewer support 32-bit float - - // Try 16-bit PCM first (most compatible) - if (testExclusiveFormatPCM(m_config.sampleRate, m_config.numOutputChannels, 16, reinterpret_cast(wfFormat))) { - Aestra::Log::info("[WASAPI Exclusive] Using 16-bit PCM at " + std::to_string(m_config.sampleRate) + " Hz"); - return true; - } - - // Try 24-bit PCM - if (testExclusiveFormatPCM(m_config.sampleRate, m_config.numOutputChannels, 24, reinterpret_cast(wfFormat))) { - Aestra::Log::info("[WASAPI Exclusive] Using 24-bit PCM at " + std::to_string(m_config.sampleRate) + " Hz"); - return true; - } - - // Try 32-bit float - if (testExclusiveFormat(m_config.sampleRate, m_config.numOutputChannels, reinterpret_cast(wfFormat))) { - Aestra::Log::info("[WASAPI Exclusive] Using 32-bit float at " + std::to_string(m_config.sampleRate) + " Hz"); - return true; - } - - // Try common sample rates with 16-bit PCM (most compatible) - for (uint32_t sampleRate : EXCLUSIVE_SAMPLE_RATES) { - if (testExclusiveFormatPCM(sampleRate, m_config.numOutputChannels, 16, reinterpret_cast(wfFormat))) { - std::cout << "[WASAPI Exclusive] Using fallback 16-bit PCM at " - << sampleRate << " Hz" << std::endl; - return true; - } - } - - // Try common sample rates with 32-bit float - for (uint32_t sampleRate : EXCLUSIVE_SAMPLE_RATES) { - if (testExclusiveFormat(sampleRate, m_config.numOutputChannels, reinterpret_cast(wfFormat))) { - std::cout << "[WASAPI Exclusive] Using fallback 32-bit float at " - << sampleRate << " Hz" << std::endl; - return true; - } - } - - return false; -} - -bool WASAPIExclusiveDriver::testExclusiveFormat(uint32_t sampleRate, uint32_t channels, void** format) { - WAVEFORMATEX** wfFormat = reinterpret_cast(format); - WAVEFORMATEX testFormat = {}; - testFormat.wFormatTag = WAVE_FORMAT_IEEE_FLOAT; - testFormat.nChannels = static_cast(channels); - testFormat.nSamplesPerSec = sampleRate; - testFormat.wBitsPerSample = 32; - testFormat.nBlockAlign = (testFormat.nChannels * testFormat.wBitsPerSample) / 8; - testFormat.nAvgBytesPerSec = testFormat.nSamplesPerSec * testFormat.nBlockAlign; - testFormat.cbSize = 0; - - WAVEFORMATEX* closestMatch = nullptr; - HRESULT hr = reinterpret_cast(m_audioClient)->IsFormatSupported( - AUDCLNT_SHAREMODE_EXCLUSIVE, - &testFormat, - &closestMatch - ); - - // Log failure reasons - if (hr != S_OK) { - Aestra::Log::warning("[WASAPI Exclusive] Float format not supported: " + std::to_string(sampleRate) + "Hz. HR=" + HResultToString(hr)); - } - - if (hr == S_OK) { - // Exact match - allocate and copy - *wfFormat = (WAVEFORMATEX*)CoTaskMemAlloc(sizeof(WAVEFORMATEX)); - std::memcpy(*wfFormat, &testFormat, sizeof(WAVEFORMATEX)); - return true; - } - - if (closestMatch) { - // Use closest match - *wfFormat = closestMatch; - return true; - } - - return false; -} - -bool WASAPIExclusiveDriver::testExclusiveFormatPCM(uint32_t sampleRate, uint32_t channels, uint32_t bitsPerSample, void** format) { - WAVEFORMATEX** wfFormat = reinterpret_cast(format); - WAVEFORMATEX testFormat = {}; - testFormat.wFormatTag = WAVE_FORMAT_PCM; - testFormat.nChannels = static_cast(channels); - testFormat.nSamplesPerSec = sampleRate; - testFormat.wBitsPerSample = static_cast(bitsPerSample); - testFormat.nBlockAlign = (testFormat.nChannels * testFormat.wBitsPerSample) / 8; - testFormat.nAvgBytesPerSec = testFormat.nSamplesPerSec * testFormat.nBlockAlign; - testFormat.cbSize = 0; - - WAVEFORMATEX* closestMatch = nullptr; - HRESULT hr = reinterpret_cast(m_audioClient)->IsFormatSupported( - AUDCLNT_SHAREMODE_EXCLUSIVE, - &testFormat, - &closestMatch - ); - - if (hr == S_OK) { - // Exact match - allocate and copy - *wfFormat = (WAVEFORMATEX*)CoTaskMemAlloc(sizeof(WAVEFORMATEX)); - std::memcpy(*wfFormat, &testFormat, sizeof(WAVEFORMATEX)); - return true; - } - - if (closestMatch) { - // Use closest match - *wfFormat = closestMatch; - return true; - } - - return false; -} - -void WASAPIExclusiveDriver::closeStream() { - closeDevice(); - m_bufferFrameCount = 0; - m_state = DriverState::INITIALIZED; - std::cout << "[WASAPI Exclusive] Stream closed" << std::endl; -} - -bool WASAPIExclusiveDriver::startStream() { - if (m_state != DriverState::STREAM_OPEN) { - setError(DriverError::STREAM_START_FAILED, "Stream not open"); - return false; - } - - // Pre-fill buffer with silence to prevent any initial garbage - BYTE* data = nullptr; - HRESULT hr = reinterpret_cast(m_renderClient)->GetBuffer(m_bufferFrameCount, &data); - if (SUCCEEDED(hr)) { - WAVEFORMATEX* wf = reinterpret_cast(m_waveFormat); - // Zero the entire buffer - std::memset(reinterpret_cast(data), 0, m_bufferFrameCount * wf->nBlockAlign); - reinterpret_cast(m_renderClient)->ReleaseBuffer(m_bufferFrameCount, 0); - std::cout << "[WASAPI Exclusive] Pre-filled buffer with silence (" - << m_bufferFrameCount << " frames)" << std::endl; - } - - // Initialize soft-start ramp (150ms fade-in to prevent pops/clicks) - m_rampDurationSamples = static_cast(m_actualSampleRate * 0.150); // 150ms - m_rampSampleCount = 0; - m_isRamping = true; - std::cout << "[WASAPI Exclusive] Soft-start ramp: " << m_rampDurationSamples - << " samples (" << (m_rampDurationSamples / static_cast(m_actualSampleRate) * 1000.0) - << "ms)" << std::endl; - - m_shouldStop = false; - m_isRunning = true; - - // Start audio client - hr = reinterpret_cast(m_audioClient)->Start(); - if (FAILED(hr)) { - setError(DriverError::STREAM_START_FAILED, "Failed to start audio client: " + HResultToString(hr)); - m_isRunning = false; - return false; - } - - Aestra::Log::info("[WASAPI Exclusive] Stream started successfully at " + std::to_string(m_config.sampleRate) + " Hz"); - - // Start audio thread - m_audioThread = std::thread(&WASAPIExclusiveDriver::audioThreadProc, this); - - m_state = DriverState::STREAM_RUNNING; - std::cout << "[WASAPI Exclusive] Stream started with safety features active" << std::endl; - return true; -} - -void WASAPIExclusiveDriver::stopStream() { - if (!m_isRunning) { - return; - } - - std::cout << "[WASAPI Exclusive] Stopping stream safely..." << std::endl; - - // Signal stop to audio thread - m_shouldStop = true; - - // Trigger event to wake up audio thread immediately - if (m_audioEvent) { - SetEvent(m_audioEvent); - } - - // Wait for audio thread with proper shutdown sequence - if (m_audioThread.joinable()) { - // Wait a short period for thread to exit gracefully - if (m_audioThread.joinable()) { - m_audioThread.join(); // Try normal join first - } - - // If still joinable after a brief wait, it may be hung - detach to prevent std::terminate - if (m_audioThread.joinable()) { - std::cerr << "[WASAPI Exclusive] Warning: Audio thread didn't stop gracefully, detaching" << std::endl; - m_audioThread.detach(); - } else { - // Thread stopped successfully - safe to fill buffer - fillAudioBufferWithSilence(); - } - } else { - // Thread was not joinable (already detached or not started) - fillAudioBufferWithSilence(); - } - - // Stop audio client - this should be silent since we've already stopped the thread - if (m_audioClient) { - reinterpret_cast(m_audioClient)->Stop(); - } - - m_isRunning = false; - m_state = DriverState::STREAM_OPEN; - std::cout << "[WASAPI Exclusive] Stream stopped safely" << std::endl; -} - -void WASAPIExclusiveDriver::fillAudioBufferWithSilence() { - // Fill buffer with silence before stopping to prevent clicks - // Only call this after thread has stopped to avoid race conditions - if (m_renderClient && m_bufferFrameCount > 0 && m_waveFormat) { - BYTE* data = nullptr; - HRESULT hr = reinterpret_cast(m_renderClient)->GetBuffer(m_bufferFrameCount, &data); - if (SUCCEEDED(hr)) { - WAVEFORMATEX* wf = reinterpret_cast(m_waveFormat); - std::memset(reinterpret_cast(data), 0, m_bufferFrameCount * wf->nBlockAlign); - reinterpret_cast(m_renderClient)->ReleaseBuffer(m_bufferFrameCount, AUDCLNT_BUFFERFLAGS_SILENT); - } - } -} - -bool WASAPIExclusiveDriver::initializeSharedFallback() { - m_usingSharedFallback = true; - - HRESULT hr = reinterpret_cast(m_device)->Activate( - IID_IAudioClient, - CLSCTX_ALL, - nullptr, - (void**)&m_audioClient - ); - - if (FAILED(hr)) { - setError(DriverError::STREAM_OPEN_FAILED, "Shared fallback: failed to activate audio client: " + HResultToString(hr)); - return false; - } - - // Get the shared-mode mix format - WAVEFORMATEX* mixFormat = nullptr; - hr = reinterpret_cast(m_audioClient)->GetMixFormat(&mixFormat); - if (FAILED(hr) || !mixFormat) { - setError(DriverError::STREAM_OPEN_FAILED, "Shared fallback: failed to get mix format"); - return false; - } - m_waveFormat = mixFormat; - m_actualSampleRate = reinterpret_cast(m_waveFormat)->nSamplesPerSec; - - // Create event for audio thread - m_audioEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); - if (!m_audioEvent) { - setError(DriverError::STREAM_OPEN_FAILED, "Shared fallback: failed to create audio event"); - return false; - } - - // Initialize in shared mode; let WASAPI pick the buffer duration - hr = reinterpret_cast(m_audioClient)->Initialize( - AUDCLNT_SHAREMODE_SHARED, - AUDCLNT_STREAMFLAGS_EVENTCALLBACK, - 0, - 0, - reinterpret_cast(m_waveFormat), - nullptr - ); - - if (FAILED(hr)) { - setError(DriverError::STREAM_OPEN_FAILED, - "Shared fallback: initialize failed. HRESULT: " + HResultToString(hr)); - return false; - } - - hr = reinterpret_cast(m_audioClient)->GetBufferSize(&m_bufferFrameCount); - if (FAILED(hr)) { - setError(DriverError::STREAM_OPEN_FAILED, "Shared fallback: failed to get buffer size"); - return false; - } - - hr = reinterpret_cast(m_audioClient)->GetService(IID_IAudioRenderClient, (void**)&m_renderClient); - if (FAILED(hr)) { - setError(DriverError::STREAM_OPEN_FAILED, "Shared fallback: failed to get render client"); - return false; - } - - std::cout << "[WASAPI Shared Fallback] Initialized - " - << "Sample Rate: " << m_actualSampleRate << " Hz, " - << "Buffer: " << m_bufferFrameCount << " frames\n"; - return true; -} - -double WASAPIExclusiveDriver::getStreamLatency() const { - if (!m_audioClient || !m_waveFormat) { - return 0.0; - } - - // In exclusive mode, latency is simply buffer size / sample rate - return (m_actualSampleRate > 0) ? static_cast(m_bufferFrameCount) / m_actualSampleRate : 0.0; -} - -void WASAPIExclusiveDriver::audioThreadProc() { - if (!setThreadPriority()) { - std::cerr << "[WASAPI Exclusive] Warning: Failed to set thread priority" << std::endl; - } - - // Set MMCSS Pro Audio scheduling for real-time performance - DWORD taskIndex = 0; - HANDLE avrtHandle = AvSetMmThreadCharacteristicsA("Pro Audio", &taskIndex); - if (!avrtHandle) { - std::cerr << "[WASAPI Exclusive] Warning: Failed to set MMCSS" << std::endl; - } else { - // Set thread priority to critical for lowest possible latency - BOOL prioritySuccess = AvSetMmThreadPriority(avrtHandle, AVRT_PRIORITY_CRITICAL); - if (!prioritySuccess) { - std::cerr << "[WASAPI Exclusive] Warning: Failed to set MMCSS priority to CRITICAL" << std::endl; - } else { - std::cout << "[WASAPI Exclusive] MMCSS enabled: Pro Audio @ CRITICAL priority" << std::endl; - } - } - - std::vector userBuffer(m_bufferFrameCount * m_config.numOutputChannels); - - std::cout << "[WASAPI Exclusive] Audio thread running with " - << m_bufferFrameCount << " frames at " << m_actualSampleRate << " Hz" << std::endl; - - while (!m_shouldStop) { - DWORD waitResult = WaitForSingleObject(m_audioEvent, 2000); - - if (waitResult != WAIT_OBJECT_0) { - if (!m_shouldStop) { - std::cerr << "[WASAPI Exclusive] Event timeout!" << std::endl; - m_statistics.underrunCount++; - - // On timeout/underrun, try to recover by filling silence - BYTE* data = nullptr; - HRESULT hr = reinterpret_cast(m_renderClient)->GetBuffer(m_bufferFrameCount, &data); - if (SUCCEEDED(hr)) { - WAVEFORMATEX* wf = reinterpret_cast(m_waveFormat); - std::memset(reinterpret_cast(data), 0, m_bufferFrameCount * wf->nBlockAlign); - reinterpret_cast(m_renderClient)->ReleaseBuffer(m_bufferFrameCount, 0); - std::cout << "[WASAPI Exclusive] Recovered from timeout with silence" << std::endl; - } - } - continue; - } - - if (m_shouldStop) { - break; - } - - LARGE_INTEGER startTime; - QueryPerformanceCounter(&startTime); - - // Get buffer - BYTE* data = nullptr; - HRESULT hr = reinterpret_cast(m_renderClient)->GetBuffer(m_bufferFrameCount, &data); - if (FAILED(hr)) { - std::cerr << "[WASAPI Exclusive] GetBuffer failed: " << HResultToString(hr) << std::endl; - m_statistics.underrunCount++; - - // Zero user buffer to prevent stale data on next successful callback - std::fill(userBuffer.begin(), userBuffer.end(), 0.0f); - continue; - } - - // Call user callback - double streamTime = static_cast(m_statistics.callbackCount * m_bufferFrameCount) / m_actualSampleRate; - - if (m_userCallback) { - m_userCallback( - userBuffer.data(), - nullptr, - m_bufferFrameCount, - streamTime, - m_userData - ); - } else { - std::fill(userBuffer.begin(), userBuffer.end(), 0.0f); - } - - // Apply soft-start ramp to prevent harsh audio on startup - if (m_isRamping) { - for (uint32_t frame = 0; frame < m_bufferFrameCount; ++frame) { - if (m_rampSampleCount < m_rampDurationSamples) { - // Linear fade-in ramp - float rampGain = static_cast(m_rampSampleCount) / m_rampDurationSamples; - - // Apply ramp to all channels in this frame - for (uint32_t ch = 0; ch < m_config.numOutputChannels; ++ch) { - userBuffer[frame * m_config.numOutputChannels + ch] *= rampGain; - } - - m_rampSampleCount++; - } else { - // Ramp complete - m_isRamping = false; - break; - } - } - } - - // Convert and copy to WASAPI buffer based on format - WAVEFORMATEX* wf = reinterpret_cast(m_waveFormat); - if (wf->wFormatTag == WAVE_FORMAT_IEEE_FLOAT) { - // 32-bit float - direct copy - std::memcpy(reinterpret_cast(data), userBuffer.data(), m_bufferFrameCount * wf->nBlockAlign); - } - else if (wf->wFormatTag == WAVE_FORMAT_PCM) { - // PCM format - need to convert floats to integers with proper clamping - if (wf->wBitsPerSample == 16) { - // 16-bit PCM - int16_t* pcmData = reinterpret_cast(data); - for (uint32_t i = 0; i < m_bufferFrameCount * m_config.numOutputChannels; ++i) { - // Convert float (-1.0 to 1.0) to int16_t (-32768 to 32767) - float sample = userBuffer[i]; - // Clamp to valid range (already done above, but double-check) - if (sample > 1.0f) sample = 1.0f; - if (sample < -1.0f) sample = -1.0f; - // Convert with proper scaling (32767.0f, not 32768.0f to avoid overflow) - pcmData[i] = static_cast(sample * 32767.0f); - } - } - else if (wf->wBitsPerSample == 24) { - // 24-bit PCM (stored in 3 bytes, little-endian) - uint8_t* pcmData = data; - for (uint32_t i = 0; i < m_bufferFrameCount * m_config.numOutputChannels; ++i) { - float sample = userBuffer[i]; - // Clamp to valid range - if (sample > 1.0f) sample = 1.0f; - if (sample < -1.0f) sample = -1.0f; - // Convert to 24-bit (8388607 = 2^23 - 1) - int32_t pcmValue = static_cast(sample * 8388607.0f); - // Write 3 bytes (little-endian) - pcmData[i * 3 + 0] = static_cast(pcmValue & 0xFF); - pcmData[i * 3 + 1] = static_cast((pcmValue >> 8) & 0xFF); - pcmData[i * 3 + 2] = static_cast((pcmValue >> 16) & 0xFF); - } - } - else if (wf->wBitsPerSample == 32) { - // 32-bit PCM - int32_t* pcmData = reinterpret_cast(data); - for (uint32_t i = 0; i < m_bufferFrameCount * m_config.numOutputChannels; ++i) { - float sample = userBuffer[i]; - // Clamp to valid range - if (sample > 1.0f) sample = 1.0f; - if (sample < -1.0f) sample = -1.0f; - // Convert to 32-bit (2147483647 = 2^31 - 1) - pcmData[i] = static_cast(sample * 2147483647.0f); - } - } - else { - // Unknown bit depth - zero the buffer and log warning - Aestra::Log::warning("[WASAPI Exclusive] Unknown PCM bit depth: " + std::to_string(wf->wBitsPerSample) + " bits. Outputting silence."); - std::memset(reinterpret_cast(data), 0, m_bufferFrameCount * wf->nBlockAlign); - } - } - else { - // Unknown format - zero the buffer and log warning - Aestra::Log::warning("[WASAPI Exclusive] Unknown format tag: " + std::to_string(wf->wFormatTag) + ". Outputting silence."); - std::memset(reinterpret_cast(data), 0, m_bufferFrameCount * wf->nBlockAlign); - } - - // Release buffer - hr = reinterpret_cast(m_renderClient)->ReleaseBuffer(m_bufferFrameCount, 0); - if (FAILED(hr)) { - std::cerr << "[WASAPI Exclusive] ReleaseBuffer failed" << std::endl; - } - - // Update statistics - LARGE_INTEGER endTime; - QueryPerformanceCounter(&endTime); - double callbackTimeUs = static_cast(endTime.QuadPart - startTime.QuadPart) * 1000000.0 / static_cast(m_perfFreq); - updateStatistics(callbackTimeUs); - } - - if (avrtHandle) { - AvRevertMmThreadCharacteristics(avrtHandle); - } - - std::cout << "[WASAPI Exclusive] Audio thread exiting" << std::endl; -} - -bool WASAPIExclusiveDriver::setThreadPriority() { - return SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL) != 0; -} - -void WASAPIExclusiveDriver::setError(DriverError error, const std::string& message) { - m_lastError = error; - m_errorMessage = message; - m_state = DriverState::DRIVER_ERROR; - - Aestra::Log::error("[WASAPI Exclusive] Error: " + message); - - if (m_errorCallback) { - m_errorCallback(error, message); - } -} - -void WASAPIExclusiveDriver::updateStatistics(double callbackTimeUs) { - m_statistics.callbackCount++; - - const double alpha = 0.1; - m_statistics.averageCallbackTimeUs = - alpha * callbackTimeUs + (1.0 - alpha) * m_statistics.averageCallbackTimeUs; - - if (callbackTimeUs > m_statistics.maxCallbackTimeUs) { - m_statistics.maxCallbackTimeUs = callbackTimeUs; - } - - if (m_waveFormat && m_bufferFrameCount > 0) { - double bufferDurationUs = static_cast(m_bufferFrameCount) * 1000000.0 / m_actualSampleRate; - m_statistics.cpuLoadPercent = (callbackTimeUs / bufferDurationUs) * 100.0; - } - - m_statistics.actualLatencyMs = getStreamLatency() * 1000.0; -} - -} // namespace Audio -} // namespace Aestra diff --git a/AestraCore/CMakeLists.txt b/AestraCore/CMakeLists.txt index 5dc4611f..b2c45767 100644 --- a/AestraCore/CMakeLists.txt +++ b/AestraCore/CMakeLists.txt @@ -2,8 +2,6 @@ cmake_minimum_required(VERSION 3.22) project(AestraCore VERSION 1.0.0 LANGUAGES CXX) -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) # ============================================================================= # AestraCore Library diff --git a/meta/CHANGELOGS/CHANGELOG_2026Q1.md b/meta/CHANGELOGS/CHANGELOG_2026Q1.md index d819c7e1..2e87a772 100644 --- a/meta/CHANGELOGS/CHANGELOG_2026Q1.md +++ b/meta/CHANGELOGS/CHANGELOG_2026Q1.md @@ -4,6 +4,35 @@ All notable changes for Aestra in Q1 2026 are documented here. ## [Unreleased] +### Maintenance & Infrastructure (March 23, 2026) + +- **CI/CD Improvements:** + - Consolidated build workflows into unified CI pipeline + - Added build timeouts (30m Linux, 45m Windows/macOS) to prevent hanging builds + - Added Discord webhook gating (skips when secret unavailable) + - Fixed all CI build failures + +- **Code Quality:** + - Added `.clang-tidy` configuration for static analysis + - Added pre-commit hook for platform abstraction leak detection + - Removed redundant CMake C++ standard settings + - Removed accidentally committed build artifacts (saved 4MB) + - Strengthened `.gitignore` patterns + +- **Bug Fixes (Qodo Review):** + - Fixed CommandHistory deadlock (execute outside lock) + - Fixed AutosaveManager self-deadlock (check state before locking) + - Fixed CommandTransaction::redo() to call redo() on children + - Fixed DuplicateClipCommand to preserve ID across redo + - Fixed TrimClipCommand negative duration validation + +- **Headless Infrastructure:** + - Added ProjectValidator for safe project loading + - Added AutosaveManager for crash recovery + - Added CommandTransaction for grouped undo + - Added AudioExporter for offline rendering + - Added HeadlessMusicGenerator for programmatic music + ### Internal Arsenal / Rumble milestone - Added a verified internal instrument validation stack around **Aestra Rumble**.