diff options
Diffstat (limited to 'audio/SoundWire/Samples/SdcaVad/SdcaVDsp/KeywordDetector.cpp')
| -rw-r--r-- | audio/SoundWire/Samples/SdcaVad/SdcaVDsp/KeywordDetector.cpp | 1053 |
1 files changed, 1053 insertions, 0 deletions
diff --git a/audio/SoundWire/Samples/SdcaVad/SdcaVDsp/KeywordDetector.cpp b/audio/SoundWire/Samples/SdcaVad/SdcaVDsp/KeywordDetector.cpp new file mode 100644 index 00000000..3611a2c1 --- /dev/null +++ b/audio/SoundWire/Samples/SdcaVad/SdcaVDsp/KeywordDetector.cpp @@ -0,0 +1,1053 @@ +/*++ + +Copyright (c) Microsoft Corporation. All rights reserved. + + THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY + KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR + PURPOSE. + +Module Name: + + KeywordDetector.cpp + +Abstract: + + Sample keyword detector management. + +Environment: + + Kernel mode + +--*/ + +#include "private.h" +#include "stdunk.h" +#include <ks.h> +#include <mmsystem.h> +#include <ksmedia.h> +#include "streamengine.h" + +#include "KeywordDetector.h" + +#ifndef __INTELLISENSE__ +#include "KeywordDetector.tmh" +#endif + + +#pragma code_seg("PAGE") +CKeywordDetector::CKeywordDetector( + _In_ WDFDEVICE Device, + _In_ ACXCIRCUIT Circuit, + _In_ WAVEFORMATEXTENSIBLE *DetectionFormat +) + : + m_streamRunning(FALSE), + m_qpcStartCapture(0), + m_nLastQueuedPacket(-1), + m_SoundDetectorArmed1(FALSE), + m_SoundDetectorArmed2(FALSE), + m_SoundDetectorData1(0), + m_SoundDetectorData2(0), + m_ullKeywordStartTimestamp(0), + m_ullKeywordStopTimestamp(0), + m_Device(Device), + m_Circuit(Circuit), + m_Prepared(FALSE), + m_Suspended(FALSE), + m_dispatchThread(nullptr), + m_FunctionInformation(nullptr), + m_Initialized(FALSE) +{ + PAGED_CODE(); + DSP_CIRCUIT_CONTEXT *circuitCtx; + + memcpy(&(m_PrepareParams.DetectionFormat), DetectionFormat, sizeof(WAVEFORMATEXTENSIBLE)); + circuitCtx = GetDspCircuitContext(m_Circuit); + m_PrepareParams.EndpointId = circuitCtx->EndpointId; + + // Assume streaming (bypass) mode + m_PrepareParams.VadMode = VadModeStreaming; + + KeInitializeEvent(&(m_Events.Suspend), SynchronizationEvent, FALSE); + KeInitializeEvent(&(m_Events.Resume), SynchronizationEvent, FALSE); + + // Initialize our pool of packets and the list structures + // The packet spin locks protect the producer/consumer relationship + // between the dpc routine and GetReadPacket + KeInitializeSpinLock(&m_PacketPoolSpinLock); + KeInitializeSpinLock(&m_PacketFifoSpinLock); + + // The buffering state spin lock protects the state variables + // shared between the arm/disarm and the dpc routine + KeInitializeSpinLock(&m_BufferingStateSpinLock); + + // current state is disarmed + // reset fifo and buffering state + UpdateBufferingState(); +} + +#pragma code_seg("PAGE") +CKeywordDetector::~CKeywordDetector() +{ + PAGED_CODE(); + + m_threadExitEvent.set(); + m_threadExitedEvent.wait(); + + if (m_FunctionInformation) + { + ExFreePool(m_FunctionInformation); + } + + m_Initialized = FALSE; +} + + +#pragma code_seg("PAGE") +_IRQL_requires_max_(PASSIVE_LEVEL) +NTSTATUS CKeywordDetector::Initialize() +{ + PAGED_CODE(); + HANDLE handle; + LARGE_INTEGER qpcFrequency; + + KeQueryPerformanceCounter(&qpcFrequency); + m_qpcFrequency = qpcFrequency.QuadPart; + + // TODO: currently ignoring the results of these calls as to + // not break anything (since they all currently fail) + + // Retrieve capabilities to know ivad/evad, and + // entity id's + RETURN_NTSTATUS_IF_FAILED(GetDeviceKwsCapabilityDescriptor(&m_CapabilityDescriptor)); + RETURN_NTSTATUS_IF_FAILED(GetDeviceFunctionInformation(&m_FunctionInformation)); + RETURN_NTSTATUS_IF_TRUE(0 == m_CapabilityDescriptor.DataPathsSupported, STATUS_NOT_SUPPORTED); + + RETURN_NTSTATUS_IF_FAILED(GetVadDescriptor(&m_VadDescriptor)); + RETURN_NTSTATUS_IF_FAILED(GetVadEntities(&m_VadEntities)); + + // create worker thread to handle suspended access + RETURN_NTSTATUS_IF_FAILED(PsCreateSystemThread(&handle, THREAD_ALL_ACCESS, 0, 0, 0, CKeywordDetector::s_HandleNotifications, this)); + + auto scope_exit([&handle]() { + ZwClose(handle); + }); + + RETURN_NTSTATUS_IF_FAILED(ObReferenceObjectByHandleWithTag(handle, THREAD_ALL_ACCESS, nullptr, KernelMode, KEYWORDDETECTOR_POOLTAG, (PVOID*)&m_dispatchThread, nullptr)); + + // set notification events + RETURN_NTSTATUS_IF_FAILED(SetSuspendAccessEvent(&m_Events)); + + + return STATUS_SUCCESS; +} + +_Use_decl_annotations_ +void CKeywordDetector::s_HandleNotifications(PVOID context) +{ + PAGED_CODE(); + auto kws = static_cast<CKeywordDetector*>(context); + kws->HandleNotifications(); +} + +_IRQL_requires_(PASSIVE_LEVEL) +void +CKeywordDetector::HandleNotifications() +{ + PAGED_CODE(); + NTSTATUS status{ STATUS_SUCCESS }; + PVOID waitObjects[] = { &m_Events.Suspend, &m_Events.Resume, m_threadExitEvent.get() }; + + // start with the even reset to indicate that the thread is running + m_threadExitedEvent.clear(); + while (true) + { + status = KeWaitForMultipleObjects(3, waitObjects, WaitAny, Executive, KernelMode, FALSE, nullptr, nullptr); + if (STATUS_WAIT_0 == status) + { + auto lock = m_csLock.acquire(); +#pragma prefast(suppress:__WARNING_NEED_NO_COMPETING_THREAD, "wil::fast_mutex lacks required SAL annotation, lock is held") + m_Suspended = TRUE; +#pragma prefast(suppress:__WARNING_CALLER_FAILING_TO_HOLD, "wil::fast_mutex lacks required SAL annotation, lock is held") + UpdateVadStreamState(); + continue; + } + if (STATUS_WAIT_1 == status) + { + auto lock = m_csLock.acquire(); +#pragma prefast(suppress:__WARNING_NEED_NO_COMPETING_THREAD, "wil::fast_mutex lacks required SAL annotation, lock is held") + m_Suspended = FALSE; +#pragma prefast(suppress:__WARNING_CALLER_FAILING_TO_HOLD, "wil::fast_mutex lacks required SAL annotation, lock is held") + UpdateVadStreamState(); + continue; + } + + else // consider as exit event + { + break; + } + } + m_threadExitedEvent.set(); + PsTerminateSystemThread(status); +} + + +#pragma code_seg("PAGE") +_IRQL_requires_max_(PASSIVE_LEVEL) +NTSTATUS CKeywordDetector::ReadKeywordTimestampRegistry() +{ + PAGED_CODE(); + + UNICODE_STRING parametersPath; + + RTL_QUERY_REGISTRY_TABLE paramTable[] = { + // QueryRoutine Flags Name EntryContext DefaultType DefaultData DefaultLength + { NULL, RTL_QUERY_REGISTRY_DIRECT | RTL_QUERY_REGISTRY_TYPECHECK, L"KeywordDetectorStartTimestamp", &m_ullKeywordStartTimestamp, (REG_QWORD << RTL_QUERY_REGISTRY_TYPECHECK_SHIFT) | REG_QWORD, &m_ullKeywordStartTimestamp, sizeof(ULONGLONG) }, + { NULL, RTL_QUERY_REGISTRY_DIRECT | RTL_QUERY_REGISTRY_TYPECHECK, L"KeywordDetectorStopTimestamp", &m_ullKeywordStopTimestamp, (REG_QWORD << RTL_QUERY_REGISTRY_TYPECHECK_SHIFT) | REG_QWORD, &m_ullKeywordStopTimestamp, sizeof(ULONGLONG) }, + { NULL, 0, NULL, NULL, 0, NULL, 0 } + }; + + RtlInitUnicodeString(¶metersPath, NULL); + + // The sizeof(WCHAR) is added to the maximum length, for allowing a space for null termination of the string. + parametersPath.MaximumLength = + g_RegistryPath.Length + sizeof(L"\\Parameters") + sizeof(WCHAR); + +#pragma prefast(suppress:__WARNING_ALIASED_MEMORY_LEAK, "memory is freed by scope_exit") + parametersPath.Buffer = (PWCH)ExAllocatePool2(PagedPool, parametersPath.MaximumLength, KEYWORDDETECTOR_POOLTAG); + RETURN_NTSTATUS_IF_TRUE(parametersPath.Buffer == NULL, STATUS_INSUFFICIENT_RESOURCES); + auto parametersPath_free = scope_exit([¶metersPath]() { + PAGED_CODE(); + ExFreePool(parametersPath.Buffer); + }); + + RtlAppendUnicodeToString(¶metersPath, g_RegistryPath.Buffer); + RtlAppendUnicodeToString(¶metersPath, L"\\Parameters"); + + RETURN_NTSTATUS_IF_FAILED(RtlQueryRegistryValues( + RTL_REGISTRY_ABSOLUTE | RTL_REGISTRY_OPTIONAL, + parametersPath.Buffer, + ¶mTable[0], + NULL, + NULL + )); + + return STATUS_SUCCESS; +} + +#pragma code_seg("PAGE") +_IRQL_requires_max_(PASSIVE_LEVEL) +NTSTATUS CKeywordDetector::ResetDetector(_In_ GUID eventId) +{ + PAGED_CODE(); + + RETURN_NTSTATUS_IF_TRUE(eventId != CONTOSO_KEYWORD1 && + eventId != CONTOSO_KEYWORD2 && + eventId != GUID_NULL, + STATUS_INVALID_PARAMETER); + + // Initialize detector on first use, which is going to be the + // initial reset of the detector. + if(!m_Initialized) + { + RETURN_NTSTATUS_IF_FAILED(Initialize()); + m_Initialized = TRUE; + } + + auto lock = m_csLock.acquire(); + + if (eventId == CONTOSO_KEYWORD1) + { + m_SoundDetectorData1 = 0; +#pragma prefast(suppress:__WARNING_NEED_NO_COMPETING_THREAD, "wil::fast_mutex lacks required SAL annotation, lock is held") + m_SoundDetectorArmed1 = FALSE; + } + else if(eventId == CONTOSO_KEYWORD2) + { + m_SoundDetectorData2 = 0; +#pragma prefast(suppress:__WARNING_NEED_NO_COMPETING_THREAD, "wil::fast_mutex lacks required SAL annotation, lock is held") + m_SoundDetectorArmed2 = FALSE; + } + else if(eventId == GUID_NULL) + { + // When DownloadDetectorData is called to set the pattern for multiple keywords + // at once, all keyword detectors must be reset. Also used during keyword detector + // initialization and cleanup to restore it back to initial state and power down. + m_SoundDetectorData1 = 0; +#pragma prefast(suppress:__WARNING_NEED_NO_COMPETING_THREAD, "wil::fast_mutex lacks required SAL annotation, lock is held") + m_SoundDetectorArmed1 = FALSE; + m_SoundDetectorData2 = 0; +#pragma prefast(suppress:__WARNING_NEED_NO_COMPETING_THREAD, "wil::fast_mutex lacks required SAL annotation, lock is held") + m_SoundDetectorArmed2 = FALSE; + } + +#pragma prefast(suppress:__WARNING_CALLER_FAILING_TO_HOLD, "wil::fast_mutex lacks required SAL annotation, lock is held") + RETURN_NTSTATUS_IF_FAILED(UpdateVadStreamState()); + + return STATUS_SUCCESS; +} + +#pragma code_seg("PAGE") +_IRQL_requires_max_(PASSIVE_LEVEL) +NTSTATUS CKeywordDetector::DownloadDetectorData(_In_ GUID eventId, _In_ LONGLONG Data) +{ + PAGED_CODE(); + + RETURN_NTSTATUS_IF_TRUE(eventId != CONTOSO_KEYWORD1 && + eventId != CONTOSO_KEYWORD2 && + eventId != GUID_NULL, + STATUS_INVALID_PARAMETER); + + + // reset the detector for this event Id + ResetDetector(eventId); + + // In this example, the driver supports detection data + // set with a single call for both detectors, or each + // detector set individually. + if (eventId == CONTOSO_KEYWORD1) + { + m_SoundDetectorData1 = Data; + } + else if(eventId == CONTOSO_KEYWORD2) + { + m_SoundDetectorData2 = Data; + } + else if(eventId == GUID_NULL) + { + // in this simplified example "Data" is set on both detectors, + // however in a real system "Data" could be a data structure which + // contains different values for each detector. + m_SoundDetectorData1 = m_SoundDetectorData2 = Data; + } + + return STATUS_SUCCESS; +} + +// The following function is only applicable to single keyword detection systems, +// and assumes keyword detector #1. +#pragma code_seg("PAGE") +_IRQL_requires_max_(PASSIVE_LEVEL) +NTSTATUS CKeywordDetector::GetDetectorData(_In_ GUID eventId, _Out_ LONGLONG *Data) +{ + PAGED_CODE(); + + RETURN_NTSTATUS_IF_TRUE(eventId != CONTOSO_KEYWORD1 && + eventId != CONTOSO_KEYWORD2 && + eventId != GUID_NULL, + STATUS_INVALID_PARAMETER); + + *Data = 0; + + if (eventId == CONTOSO_KEYWORD1) + { + *Data = m_SoundDetectorData1; + } + else if(eventId == CONTOSO_KEYWORD2) + { + *Data = m_SoundDetectorData2; + } + + return STATUS_SUCCESS; +} + +#pragma code_seg("PAGE") +_IRQL_requires_max_(PASSIVE_LEVEL) +ULONGLONG CKeywordDetector::GetStartTimestamp() +{ + PAGED_CODE(); + + return m_ullKeywordStartTimestamp; +} + +#pragma code_seg("PAGE") +_IRQL_requires_max_(PASSIVE_LEVEL) +ULONGLONG CKeywordDetector::GetStopTimestamp() +{ + PAGED_CODE(); + + return m_ullKeywordStopTimestamp; +} + +#pragma code_seg("PAGE") +_IRQL_requires_max_(PASSIVE_LEVEL) +NTSTATUS CKeywordDetector::SetArmed(_In_ GUID eventId, _In_ BOOLEAN Arm) +{ + PAGED_CODE(); + + BOOLEAN previousDetector1State = FALSE; + BOOLEAN previousDetector2State = FALSE; + + RETURN_NTSTATUS_IF_TRUE(eventId != CONTOSO_KEYWORD1 && + eventId != CONTOSO_KEYWORD2 && + eventId != GUID_NULL, + STATUS_INVALID_PARAMETER); + + // lock scope enter + { + auto lock = m_csLock.acquire(); + + // the previous state is "armed" if either detector is armed. + // this reflects the fact that both detectors are sharing the + // same stream. +#pragma prefast(suppress:__WARNING_NEED_NO_COMPETING_THREAD, "wil::fast_mutex lacks required SAL annotation, lock is held") + previousDetector1State = m_SoundDetectorArmed1; +#pragma prefast(suppress:__WARNING_NEED_NO_COMPETING_THREAD, "wil::fast_mutex lacks required SAL annotation, lock is held") + previousDetector2State = m_SoundDetectorArmed2; + + auto revertOnFailure = scope_exit([&]() { + PAGED_CODE(); +#pragma prefast(suppress:__WARNING_NEED_NO_COMPETING_THREAD, "wil::fast_mutex lacks required SAL annotation, lock is held") + m_SoundDetectorArmed1 = previousDetector1State; +#pragma prefast(suppress:__WARNING_NEED_NO_COMPETING_THREAD, "wil::fast_mutex lacks required SAL annotation, lock is held") + m_SoundDetectorArmed2 = previousDetector2State; +#pragma prefast(suppress:__WARNING_CALLER_FAILING_TO_HOLD, "wil::fast_mutex lacks required SAL annotation, lock is held") + UpdateVadStreamState(); + }); + + if (eventId == CONTOSO_KEYWORD1) + { +#pragma prefast(suppress:__WARNING_NEED_NO_COMPETING_THREAD, "wil::fast_mutex lacks required SAL annotation, lock is held") + m_SoundDetectorArmed1 = Arm; + } + else if(eventId == CONTOSO_KEYWORD2) + { +#pragma prefast(suppress:__WARNING_NEED_NO_COMPETING_THREAD, "wil::fast_mutex lacks required SAL annotation, lock is held") + m_SoundDetectorArmed2 = Arm; + } + +#pragma prefast(suppress:__WARNING_CALLER_FAILING_TO_HOLD, "wil::fast_mutex lacks required SAL annotation, lock is held") + RETURN_NTSTATUS_IF_FAILED(UpdateVadStreamState()); + + revertOnFailure.release(); + } + + // Change buffering state if needed + UpdateBufferingState(); + + return STATUS_SUCCESS; +} + +#pragma code_seg("PAGE") +_IRQL_requires_max_(PASSIVE_LEVEL) +NTSTATUS CKeywordDetector::GetArmed(_In_ GUID eventId, _Out_ BOOLEAN *Arm) +{ + PAGED_CODE(); + + RETURN_NTSTATUS_IF_TRUE(eventId != CONTOSO_KEYWORD1 && + eventId != CONTOSO_KEYWORD2 && + eventId != GUID_NULL, + STATUS_INVALID_PARAMETER); + + auto lock = m_csLock.acquire(); + + *Arm = FALSE; + + if (eventId == CONTOSO_KEYWORD1) + { +#pragma prefast(suppress:__WARNING_NEED_NO_COMPETING_THREAD, "wil::fast_mutex lacks required SAL annotation, lock is held") + *Arm = m_SoundDetectorArmed1; + } + else if(eventId == CONTOSO_KEYWORD2) + { +#pragma prefast(suppress:__WARNING_NEED_NO_COMPETING_THREAD, "wil::fast_mutex lacks required SAL annotation, lock is held") + *Arm = m_SoundDetectorArmed2; + } + + return STATUS_SUCCESS; +} + +#pragma code_seg("PAGE") +_IRQL_requires_max_(PASSIVE_LEVEL) +VOID CKeywordDetector::Run() +{ + PAGED_CODE(); + m_streamRunning = TRUE; + UpdateBufferingState(); +} + +#pragma code_seg("PAGE") +_IRQL_requires_max_(PASSIVE_LEVEL) +VOID CKeywordDetector::Stop() +{ + PAGED_CODE(); + m_streamRunning = FALSE; + UpdateBufferingState(); +} + +#pragma code_seg() +_IRQL_requires_max_(PASSIVE_LEVEL) +VOID CKeywordDetector::UpdateBufferingState() +{ + BOOL armed = FALSE; + KIRQL irql = PASSIVE_LEVEL; + + { + auto lock = m_csLock.acquire(); +#pragma prefast(suppress:__WARNING_RACE_CONDITION, "wil::fast_mutex lacks required SAL annotation, lock is held") + armed = m_SoundDetectorArmed1 | m_SoundDetectorArmed2; + } + + // acquire buffering state spin lock to synchronize state changes with the running dpc routine + KeAcquireSpinLock(&m_BufferingStateSpinLock, &irql); + + if (armed || m_streamRunning) + { + // if we're armed or stream running, and not buffering, start buffering + // if m_qpcStartCapture is not 0, then it's already buffering + if (m_qpcStartCapture == 0) + { + m_qpcStartCapture = KeQueryPerformanceCounter(NULL).QuadPart; + } + } + else + { + // if we're disarmed and no stream running, reset buffering + m_qpcStartCapture = 0; + m_nLastQueuedPacket = (-1); + InitializeListHead(&m_PacketPoolHead); + InitializeListHead(&m_PacketFifoHead); + + for (int i = 0; i < ARRAYSIZE(m_PacketPool); i++) + { + InsertTailList(&m_PacketPoolHead, &m_PacketPool[i].ListEntry); + } + } + + KeReleaseSpinLock(&m_BufferingStateSpinLock, irql); + + return; +} + +#pragma code_seg() +_IRQL_requires_max_(PASSIVE_LEVEL) +VOID CKeywordDetector::NotifyDetection() +{ + KIRQL irql = PASSIVE_LEVEL; + + // Because we are modifying shared buffer state to simulate a notification, + // we need to acquire the spin lock to synchronize this with the dpc routine + KeAcquireSpinLock(&m_BufferingStateSpinLock, &irql); + + // A detection will only happen if armed and the + // stream is already running. If there isn't a client + // running, then set the stream start time to align + // with this detection. + if (!m_streamRunning) + { + // the start capture time is now. + m_qpcStartCapture = KeQueryPerformanceCounter(NULL).QuadPart; + + // The following code is for testing purposes only. + // m_qpcFrequency is defined to be the number of ticks in 1 second. + // Use the stream start time (the current time retrieved in StartBufferStream) to + // mark when the keyword ended, and the start time minus 1 second worth of ticks + // to mark when the keyword started. Also, adjust the stream start time to align + // to this new keyword start time, so that the simulated stream contains the full keyword. + + m_ullKeywordStopTimestamp = m_qpcStartCapture; // stop time is the current time + m_qpcStartCapture = m_qpcStartCapture - m_qpcFrequency; // buffer start time is 1 second ago + m_ullKeywordStartTimestamp = m_qpcStartCapture; // buffer start time = keyword start time + + } + else + { + // The following code is for testing purposes only. + // If the stream is running, we cannot modify qpcStartCapture to be in + // the past, so instead make the keyword start & stop times fit within the + // time period that the keyword has been running. If it has been running + // for more than 1 second, then set the keyword start time to be 1 second back + // into the stream, as though we just figured out there was a keyword there. + // If it has been running less than one second, then the keyword size ends + // up being however long the stream has been running. + + LARGE_INTEGER qpc; + qpc = KeQueryPerformanceCounter(NULL); + + m_ullKeywordStopTimestamp = qpc.QuadPart; // stop time is the current time + + if (m_qpcStartCapture < (qpc.QuadPart - m_qpcFrequency)) + { + m_ullKeywordStartTimestamp = (qpc.QuadPart - m_qpcFrequency); + } + else + { + m_ullKeywordStartTimestamp = m_qpcStartCapture; + } + } + + KeReleaseSpinLock(&m_BufferingStateSpinLock, irql); + + return; +} + +#pragma code_seg() +_IRQL_requires_min_(DISPATCH_LEVEL) +VOID CKeywordDetector::DpcRoutine( + _In_ LONGLONG PerformanceCounter, + _In_ LONGLONG PerformanceFrequency, + _Out_ BOOLEAN *isRealtime, + _Out_ LONGLONG *NewPacketNumber, + _Out_ ULONGLONG *NewPerformanceCount) +{ + LONGLONG currentPacket; + LONGLONG packetsToQueue; + + KIRQL irql = PASSIVE_LEVEL; + + // used to synchronize buffering state variables with stream state changes, + // arming changes, etc. + KeAcquireSpinLock(&m_BufferingStateSpinLock, &irql); + + *isRealtime = FALSE; + *NewPacketNumber = 0; + *NewPerformanceCount = 0; + + // TODO: the timer only runs when the stream is open, but really for KWS it should be building up a collection of burst data + // in the queue from 1.5 sec before the trigger happens. Is there some way to simulate that behavior here? Without doing that, + // there isn't really a burst that happens, just a trickle because while the timestamps will be right, the queue won't contain + // anything until the timer fires at the normal rate. + + if (m_qpcStartCapture > 0) + { + currentPacket = (PerformanceCounter - m_qpcStartCapture) * (SamplesPerSecond / SamplesPerPacket) / PerformanceFrequency; + packetsToQueue = currentPacket - m_nLastQueuedPacket; + + // If the fifo is empty, and we're going to add something, then we are realtime + *isRealtime = IsListEmpty(&m_PacketFifoHead) && packetsToQueue > 0; + + *NewPacketNumber = m_nLastQueuedPacket+1; + *NewPerformanceCount = m_qpcStartCapture + (*NewPacketNumber * m_qpcFrequency * SamplesPerPacket / SamplesPerSecond); + + while (packetsToQueue > 0) + { + LIST_ENTRY* packetListEntry; + PACKET_ENTRY* packetEntry; + + do + { + packetListEntry = ExInterlockedRemoveHeadList(&m_PacketPoolHead, &m_PacketPoolSpinLock); + if (packetListEntry != NULL) break; + + // Pool is empty, no room to buffer more, an overrun is occurring. Drop and reuse the + // oldest packet from head of fifo. + + // Since the pool is empty, the fifo should be full. However, although unlikely, the + // driver might empty the fifo before this routine removes a packet. In that case, the + // pool should have packets available again. Therefore this is a retry loop. + packetListEntry = ExInterlockedRemoveHeadList(&m_PacketFifoHead, &m_PacketFifoSpinLock); + if (packetListEntry != NULL) break; + } while (TRUE); + + packetEntry = CONTAINING_RECORD(packetListEntry, PACKET_ENTRY, ListEntry); + + packetEntry->PacketNumber = ++m_nLastQueuedPacket; + packetEntry->QpcWhenSampled = m_qpcStartCapture + (packetEntry->PacketNumber * PerformanceFrequency * SamplesPerPacket / SamplesPerSecond); + + // TODO: this should really put something real in the buffer. Use the sine tone generator maybe? + RtlZeroMemory(&packetEntry->Samples[0], sizeof(packetEntry->Samples)); + + ExInterlockedInsertTailList(&m_PacketFifoHead, packetListEntry, &m_PacketFifoSpinLock); + + packetsToQueue -= 1; + } + } + + KeReleaseSpinLock(&m_BufferingStateSpinLock, irql); +} + +#pragma code_seg() +_IRQL_requires_max_(PASSIVE_LEVEL) +NTSTATUS CKeywordDetector::GetFifoStart(_Out_ ULONG *PacketNumber, _Out_ ULONGLONG *PerformanceCount) +{ + NTSTATUS status = STATUS_DEVICE_NOT_READY; + KIRQL irql = PASSIVE_LEVEL; + + // acquire the fifo spin lock in order to safely inspect the head of the fifo + KeAcquireSpinLock(&m_PacketFifoSpinLock, &irql); + + // peek at the first entry in the list, and retrieve the required packet number and qpc for it + if (!IsListEmpty(m_PacketFifoHead.Flink)) + { + PACKET_ENTRY *packetEntry; + + packetEntry = CONTAINING_RECORD(m_PacketFifoHead.Flink, PACKET_ENTRY, ListEntry); + + status = RtlLongLongToULong(packetEntry->PacketNumber, PacketNumber); + if (NT_SUCCESS(status)) + { + *PerformanceCount = packetEntry->QpcWhenSampled; + status = STATUS_SUCCESS; + } + } + + KeReleaseSpinLock(&m_PacketFifoSpinLock, irql); + + return status; +} + +#pragma code_seg() +_IRQL_requires_max_(PASSIVE_LEVEL) +NTSTATUS CKeywordDetector::GetReadPacket +( + _In_ ULONG PacketCount, + _In_ ULONG PacketSize, + _In_reads_(PacketSize) PVOID *Packets, + _Out_ ULONG *PacketNumber, + _Out_ ULONG64 *PerformanceCounterValue, + _Out_ BOOLEAN *MoreData, + _Out_ ULONG *NextPacketNumber, + _Out_ ULONGLONG *NextPerformanceCount +) +{ + NTSTATUS status = STATUS_DEVICE_NOT_READY; + LIST_ENTRY *packetListEntry = NULL; + + // This call is synchronized with the dpc routine through the packet list + // spin locks, as a producer consumer relationship. + // buffering state variables are not available, and taking the buffering state + // lock here would introduce lock contention between the producer and the consumer. + *PacketNumber = 0; + *PerformanceCounterValue = 0; + *MoreData = FALSE; + *NextPacketNumber = 0; + *NextPerformanceCount = 0; + + packetListEntry = ExInterlockedRemoveHeadList(&m_PacketFifoHead, &m_PacketFifoSpinLock); + if (packetListEntry != NULL) + { + BYTE *packetData; + PACKET_ENTRY *packetEntry; + + packetEntry = CONTAINING_RECORD(packetListEntry, PACKET_ENTRY, ListEntry); + + status = RtlLongLongToULong(packetEntry->PacketNumber, PacketNumber); + if (NT_SUCCESS(status)) + { + packetData = (PBYTE) Packets[(*PacketNumber) % PacketCount]; + + *PerformanceCounterValue = packetEntry->QpcWhenSampled; + + if (NT_SUCCESS(GetFifoStart(NextPacketNumber, NextPerformanceCount))) + { + *MoreData = TRUE; + } + + // TODO: the packet size here needs to line up to the packet size allocated. + // Also, handle the first packet offset + RtlCopyMemory(packetData, packetEntry->Samples, min(sizeof(packetEntry->Samples), PacketSize)); + } + + ExInterlockedInsertTailList(&m_PacketPoolHead, packetListEntry, &m_PacketPoolSpinLock); + } + + return STATUS_SUCCESS; +} + +PAGED_CODE_SEG +NTSTATUS +CKeywordDetector::SendPropertyTo +( + _In_ GUID PropertySet, + _In_ ULONG PropertyId, + _In_ ACX_PROPERTY_VERB Verb, + _In_ PVOID Control, + _In_ ULONG ControlCb, + _Inout_ PVOID Value, + _In_ ULONG ValueCb, + _Out_ ULONG_PTR* Information +) +{ + PAGED_CODE(); + + ACXPIN pin; + pin = AcxCircuitGetPinById(m_Circuit, DspCapturePinTypeBridge); + RETURN_NTSTATUS_IF_TRUE(pin == NULL, STATUS_INVALID_PARAMETER); + + DSP_PIN_CONTEXT* pinCtx = GetDspPinContext(pin); + RETURN_NTSTATUS_IF_TRUE(pinCtx == NULL, STATUS_INVALID_PARAMETER); + + RETURN_NTSTATUS_IF_TRUE(pinCtx->TargetCircuit == NULL, STATUS_INVALID_DEVICE_STATE); + + ACX_REQUEST_PARAMETERS requestParams; + ACX_REQUEST_PARAMETERS_INIT_PROPERTY( + &requestParams, + PropertySet, + PropertyId, + Verb, + AcxItemTypeCircuit, + 0, + Control, ControlCb, + Value, ValueCb + ); + + WDFREQUEST request; + WDF_OBJECT_ATTRIBUTES attributes; + WDF_OBJECT_ATTRIBUTES_INIT(&attributes); + attributes.ParentObject = m_Device; + RETURN_NTSTATUS_IF_FAILED(WdfRequestCreate(&attributes, AcxTargetCircuitGetWdfIoTarget(pinCtx->TargetCircuit), &request)); + + auto request_free = scope_exit([&request]() { + WdfObjectDelete(request); + }); + + RETURN_NTSTATUS_IF_FAILED(AcxTargetCircuitFormatRequestForProperty(pinCtx->TargetCircuit, request, &requestParams)); + + WDF_REQUEST_SEND_OPTIONS sendOptions; + WDF_REQUEST_SEND_OPTIONS_INIT(&sendOptions, WDF_REQUEST_SEND_OPTION_SYNCHRONOUS); + WDF_REQUEST_SEND_OPTIONS_SET_TIMEOUT(&sendOptions, WDF_REL_TIMEOUT_IN_SEC(5)); + + RETURN_NTSTATUS_IF_TRUE(!WdfRequestSend(request, AcxTargetCircuitGetWdfIoTarget(pinCtx->TargetCircuit), &sendOptions), STATUS_INVALID_DEVICE_REQUEST); + + NTSTATUS status = (WdfRequestGetStatus(request)); + + if (Information) + { + *Information = WdfRequestGetInformation(request); + } + if (status == STATUS_BUFFER_OVERFLOW && ValueCb == 0) + { + // Don't trace this error, it's normal + return status; + } + + RETURN_NTSTATUS_IF_FAILED(status); + + return STATUS_SUCCESS; +} + +PAGED_CODE_SEG +NTSTATUS +CKeywordDetector::GetDeviceFunctionInformation +( + _Out_ PSDCA_FUNCTION_INFORMATION_LIST *FunctionInfo +) +{ + PAGED_CODE(); + NTSTATUS status = STATUS_SUCCESS; + ULONG_PTR requiredBufferSize = 0; + + RETURN_NTSTATUS_IF_TRUE(nullptr == FunctionInfo, STATUS_INVALID_PARAMETER); + + status = SendPropertyTo(KSPROPERTYSETID_Sdca, + KSPROPERTY_SDCA_FUNCTION_INFORMATION, + AcxPropertyVerbGet, + nullptr, 0, + nullptr, 0, + &requiredBufferSize); + + if (status == STATUS_BUFFER_OVERFLOW) + { + // expect a buffer overflow error, confirm size is valid + if (requiredBufferSize >= sizeof(SDCA_FUNCTION_INFORMATION_LIST)) + { + // size is valid, allocate and retrieve + *FunctionInfo = (PSDCA_FUNCTION_INFORMATION_LIST) ExAllocatePool2(POOL_FLAG_NON_PAGED, requiredBufferSize, DRIVER_TAG); + RETURN_NTSTATUS_IF_TRUE(nullptr == *FunctionInfo, STATUS_INSUFFICIENT_RESOURCES); + status = SendPropertyTo(KSPROPERTYSETID_Sdca, + KSPROPERTY_SDCA_FUNCTION_INFORMATION, + AcxPropertyVerbGet, + nullptr, 0, + *FunctionInfo, sizeof(SDCA_FUNCTION_INFORMATION_LIST), + nullptr); + } + else + { + // correct buffer overflow error, but size is wrong + RETURN_NTSTATUS_IF_FAILED(STATUS_UNSUCCESSFUL); + } + } + else if (NT_SUCCESS(status)) + { + // call should not succeeded with a null buffer pointer + RETURN_NTSTATUS_IF_FAILED(STATUS_INVALID_DEVICE_REQUEST); + } + + RETURN_NTSTATUS_IF_FAILED(status); + + return STATUS_SUCCESS; +} + +PAGED_CODE_SEG +NTSTATUS +CKeywordDetector::GetDeviceKwsCapabilityDescriptor +( + _Out_ PDEVICE_KWS_CAPABILITY_DESCRIPTOR Descriptor +) +{ + PAGED_CODE(); + + memset(Descriptor, 0, sizeof(DEVICE_KWS_CAPABILITY_DESCRIPTOR)); + RETURN_NTSTATUS_IF_FAILED(SendPropertyTo(KSPROPERTYSETID_SdcaKws, + KSPROPERTY_SDCAKWS_DEVICE_CAPABILITY, + AcxPropertyVerbGet, + nullptr, 0, + Descriptor, sizeof(DEVICE_KWS_CAPABILITY_DESCRIPTOR), + nullptr)); + + return STATUS_SUCCESS; +} + +PAGED_CODE_SEG +NTSTATUS +CKeywordDetector::GetVadDescriptor( + _Out_ PVAD_DESCRIPTOR_FORMAT Descriptor + ) +{ + PAGED_CODE(); + + // For simplicity, this sample code uses a static-sized VAD_DESCRIPTOR + // with room for 11 total formats. This still has the potential to + // fail if the target device supports more than that many formats for VAD + memset(Descriptor, 0, sizeof(VAD_DESCRIPTOR_FORMAT)); + RETURN_NTSTATUS_IF_FAILED(SendPropertyTo(KSPROPERTYSETID_SdcaKws, + KSPROPERTY_SDCAKWS_VAD_CAPABILITY, + AcxPropertyVerbGet, + nullptr, 0, + Descriptor, sizeof(VAD_DESCRIPTOR_FORMAT), + nullptr)); + + return STATUS_SUCCESS; +} + +PAGED_CODE_SEG +NTSTATUS +CKeywordDetector::GetVadEntities( + _Out_ PVAD_ENTITIES_EXTRA Entities + ) +{ + PAGED_CODE(); + + // For simplicity, this sample code uses a static-sized VAD_ENTITIES + // with room for 25 total elements. This still has the potential to + // fail if the target device has more than 25 elements. + memset(Entities, 0, sizeof(VAD_ENTITIES_EXTRA)); + RETURN_NTSTATUS_IF_FAILED(SendPropertyTo(KSPROPERTYSETID_SdcaKws, + KSPROPERTY_SDCAKWS_VAD_ENTITIES, + AcxPropertyVerbGet, + nullptr, 0, + Entities, sizeof(VAD_ENTITIES_EXTRA), + nullptr)); + + return STATUS_SUCCESS; +} + +PAGED_CODE_SEG +NTSTATUS +CKeywordDetector::SetSuspendAccessEvent +( + _In_ PSDCA_KWS_NOTIFICATIONS Events +) +{ + PAGED_CODE(); + + RETURN_NTSTATUS_IF_FAILED(SendPropertyTo(KSPROPERTYSETID_SdcaKws, + KSPROPERTY_SDCAKWS_ACCESS_EVENTS, + AcxPropertyVerbSet, + nullptr, 0, + Events, sizeof(SDCA_KWS_NOTIFICATIONS), + nullptr)); + + return STATUS_SUCCESS; +} + +// There are three states. Disarmed, Armed and Suspended, +// and Armed and Prepared. + +// If we're Disarmed, we need to clean up the vad stream, suspend/resume +// state doesn't matter. + +// if we're Armed and Suspended, we should be detecting but are experiencing +// a period of deafness due to the codec driver needing to access the hardware. +// So, we need to clean up the vad stream and wait for the resume notification +// to recreate the vad stream + +// if we're Armed and Prepared, then we're actively detecting, so we +// need the stream prepared. +PAGED_CODE_SEG +_Requires_lock_held_(m_csLock) +NTSTATUS +CKeywordDetector::UpdateVadStreamState() +{ + PAGED_CODE(); + + if (m_SoundDetectorArmed1 || m_SoundDetectorArmed2) + { + // if we're armed, not prepared, and not suspended, then + // we need to move to the armed and prepared state. + if (!m_Prepared && !m_Suspended) + { + // To move into the Armed and Prepared state + // we need to prepare the vad stream + RETURN_NTSTATUS_IF_FAILED(ConfigureVadPort(&m_PrepareParams)); + } + // if we are armed, prepared, and suspended, then + // we need to move to the armed and suspended state + else if (m_Prepared && m_Suspended) + { + // To move into the Armed and Suspended state + // we need to cleanup the VAD stream + RETURN_NTSTATUS_IF_FAILED(CleanupVadPort()); + } + // else + // if we are armed, prepared, and not suspended, then we are in + // the armed and prepared state, nothing else to do. + + // Or, if we are armed, not prepared, and suspended, then we are in + // the armed and suspended state, nothing else to do. + } + else + { + if (m_Prepared) + { + // moving into the disarmed state + RETURN_NTSTATUS_IF_FAILED(CleanupVadPort()); + } + } + + return STATUS_SUCCESS; +} + + +PAGED_CODE_SEG +_Requires_lock_held_(m_csLock) +NTSTATUS +CKeywordDetector::ConfigureVadPort +( + _In_ PSDCA_KWS_PREPARE_PARAMS PrepareParams +) +{ + PAGED_CODE(); + + RETURN_NTSTATUS_IF_FAILED(SendPropertyTo(KSPROPERTYSETID_SdcaKws, + KSPROPERTY_SDCAKWS_CONFIGURE_VAD_PORT, + AcxPropertyVerbSet, + nullptr, 0, + PrepareParams, sizeof(SDCA_KWS_PREPARE_PARAMS), + nullptr)); + + m_Prepared = TRUE; + + return STATUS_SUCCESS; +} + +PAGED_CODE_SEG +_Requires_lock_held_(m_csLock) +NTSTATUS +CKeywordDetector::CleanupVadPort( ) +{ + PAGED_CODE(); + + RETURN_NTSTATUS_IF_FAILED(SendPropertyTo(KSPROPERTYSETID_SdcaKws, + KSPROPERTY_SDCAKWS_CLEANUP_VAD_PORT, + AcxPropertyVerbSet, + nullptr, 0, + NULL, 0, + nullptr)); + + m_Prepared = FALSE; + + return STATUS_SUCCESS; +} + |
