From 05e0efccf6fda26201c620ffd0a13321b5a6770b Mon Sep 17 00:00:00 2001 From: Kostas Giannakakis Date: Sun, 4 Aug 2024 16:00:09 +0300 Subject: [PATCH] Update 5.4.3b --- .../UnrealEditor-WindowsAudioCapture.dll | Bin 112640 -> 143872 bytes .../Private/AudioCaptureWorker.cpp | 211 +++++++----- .../Private/AudioListener.cpp | 228 +++++++------ .../WindowsAudioCapture/Private/AudioSink.cpp | 123 ++++--- .../Private/WindowsAudioCaptureComponent.cpp | 318 ++++++++++++++---- .../Public/AudioCaptureWorker.h | 124 +++---- .../Public/AudioListener.h | 40 ++- .../WindowsAudioCapture/Public/AudioSink.h | 32 +- .../Public/WindowsAudioCaptureComponent.h | 262 ++++++++++----- 9 files changed, 862 insertions(+), 476 deletions(-) diff --git a/WindowsAudioCapture/Binaries/Win64/UnrealEditor-WindowsAudioCapture.dll b/WindowsAudioCapture/Binaries/Win64/UnrealEditor-WindowsAudioCapture.dll index 53f5f8077749160600b720a085c68c0c8949bd1c..9f9e838ee49c312f2891caa69dd3bbd0deb5d1e1 100644 GIT binary patch literal 143872 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

!<`Odt7EQiDkALyam2nU{KwHjJG&Lg?9aqohac{ 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 mZ%Z-n9h8o01$A!MCzJVf|sTPG+qmXUG +#include + +std::unique_ptr FAudioCaptureWorker::Runnable = NULL; +std::atomic FAudioCaptureWorker::ThreadCounter(0); +std::mutex FAudioCaptureWorker::workerMutex; + +FAudioCaptureWorker::FAudioCaptureWorker() + : m_listener(16, WAVE_FORMAT_PCM, 4, 0) + , m_sink() + , bIsFinished(false) + , Thread(NULL) { // Higher overall ThreadCounter to avoid duplicated names FAudioCaptureWorker::ThreadCounter++; - Thread = FRunnableThread::Create(this, *FString::Printf(TEXT("FAudioCaptureWorker%d"), FAudioCaptureWorker::ThreadCounter), 0, EThreadPriority::TPri_Normal); + Thread = FRunnableThread::Create(this, *FString::Printf(TEXT("FAudioCaptureWorker%d"), FAudioCaptureWorker::ThreadCounter.load()), 0, EThreadPriority::TPri_Normal); + if (Thread) + { + UE_LOG(LogTemp, Log, TEXT("FAudioCaptureWorker created with thread ID: %d"), Thread->GetThreadID()); + } + else + { + UE_LOG(LogTemp, Error, TEXT("Failed to create FAudioCaptureWorker thread")); + } } FAudioCaptureWorker::~FAudioCaptureWorker() { - // Make sure to mark Thread as finished + // Ensure to mark Thread as finished bIsFinished = true; - delete Thread; - Thread = NULL; + if (Thread) + { + UE_LOG(LogTemp, Log, TEXT("FAudioCaptureWorker thread ID: %d is being destroyed"), Thread->GetThreadID()); + delete Thread; + Thread = NULL; + } + UE_LOG(LogTemp, Log, TEXT("FAudioCaptureWorker destroyed.")); } FAudioCaptureWorker* FAudioCaptureWorker::InitializeWorker() { - Runnable = new FAudioCaptureWorker(); - - return Runnable; + std::lock_guard lock(workerMutex); + if (!Runnable) + { + Runnable = std::make_unique(); + } + UE_LOG(LogTemp, Log, TEXT("FAudioCaptureWorker initialized.")); + return Runnable.get(); } bool FAudioCaptureWorker::Init() { - // Make sure the Worker is marked is not finished + // Mark Worker as not finished bIsFinished = false; - + UE_LOG(LogTemp, Log, TEXT("FAudioCaptureWorker initialized successfully.")); return true; } uint32 FAudioCaptureWorker::Run() { + if (Thread) + { + UE_LOG(LogTemp, Log, TEXT("FAudioCaptureWorker thread ID: %d started running."), Thread->GetThreadID()); + } m_listener.RecordAudioStream(&m_sink, bIsFinished); - + if (Thread) + { + UE_LOG(LogTemp, Log, TEXT("FAudioCaptureWorker thread ID: %d stopped running."), Thread->GetThreadID()); + } return 0; } void FAudioCaptureWorker::Stop() { StopTaskCounter.Increment(); + if (Thread) + { + UE_LOG(LogTemp, Log, TEXT("FAudioCaptureWorker thread ID: %d stop requested."), Thread->GetThreadID()); + } } void FAudioCaptureWorker::ShutdownWorker() { + std::lock_guard lock(workerMutex); if (Runnable) { Runnable->EnsureCompletion(); - delete Runnable; - Runnable = NULL; + Runnable.reset(); } + UE_LOG(LogTemp, Log, TEXT("FAudioCaptureWorker shut down.")); } void FAudioCaptureWorker::Exit() { - // Make sure to mark Thread as finished + // Mark Thread as finished bIsFinished = true; + if (Thread) + { + UE_LOG(LogTemp, Log, TEXT("FAudioCaptureWorker thread ID: %d exited."), Thread->GetThreadID()); + } } void FAudioCaptureWorker::EnsureCompletion() { Stop(); - // Make sure to mark Thread as finished + // Mark Thread as finished bIsFinished = true; - if (Thread != NULL) + if (Thread) { Thread->WaitForCompletion(); - } + } + if (Thread) + { + UE_LOG(LogTemp, Log, TEXT("FAudioCaptureWorker thread ID: %d ensured completion."), Thread->GetThreadID()); + } } -TArray FAudioCaptureWorker::GetFrequencyArray(float FreqLogBase, float FreqMultiplier, float FreqPower, float FreqOffset) +void FAudioCaptureWorker::GetFrequencyArray(float FreqLogBase, float FreqMultiplier, float FreqPower, float FreqOffset, TArray& OutFrequencies, TArray& OutLeftChannelFrequencies, TArray& OutRightChannelFrequencies) { TArray freqs; + TArray leftChannelFreqs; + TArray rightChannelFreqs; AudioChunk chunk; if (!m_sink.Dequeue(chunk)) { - // Log that no audio chunk is available // UE_LOG(LogTemp, Warning, TEXT("No audio chunk available.")); - return freqs; + return; } - if (chunk.size <= 0 || chunk.chunk == nullptr) + if (chunk.size <= 0 || chunk.chunk == NULL) { - // Log invalid chunk size or data UE_LOG(LogTemp, Warning, TEXT("Invalid audio chunk: size=%d"), chunk.size); - return freqs; + return; } // Calculate Frequency Values - CalculateFrequencySpectrum(chunk.chunk, 2, chunk.size, FreqLogBase, FreqMultiplier, FreqPower, FreqOffset, freqs); - - // Empty chunk's trash - m_sink.EmptyQueue(chunk); - - TArray resultFloats; - float count = freqs.Num() / 2 - 1; - - resultFloats.Reserve(count); + CalculateFrequencySpectrum(chunk.chunk, 2, chunk.size, FreqLogBase, FreqMultiplier, FreqPower, FreqOffset, OutFrequencies, OutLeftChannelFrequencies, OutRightChannelFrequencies); - for (float i = 1; i < freqs.Num() / 2; i++) - { - if (freqs[i] < 0) - { - freqs[i] = 0; - } - - resultFloats.Add(freqs[i]); - } - - return resultFloats; + // Empty the queue + m_sink.EmptyQueue(); } float GetTheFFTInValue(const int16 InSampleValue, const int16 InSampleIndex, const int16 InSampleCount) { float FFTValue = InSampleValue; - FFTValue *= 0.5f * (1 - FMath::Cos(2 * PI * InSampleIndex / (InSampleCount - 1))); - return FFTValue; } -void FAudioCaptureWorker::CalculateFrequencySpectrum -( +void FAudioCaptureWorker::CalculateFrequencySpectrum( int16* SamplePointer, const int32 NumChannels, const int32 NumAvailableSamples, @@ -140,11 +164,14 @@ void FAudioCaptureWorker::CalculateFrequencySpectrum float FreqMultiplier, float FreqPower, float FreqOffset, - TArray& OutFrequencies -) + TArray& OutFrequencies, + TArray& OutLeftChannelFrequencies, + TArray& OutRightChannelFrequencies) { - // Clear the Array before continuing + // Clear the Arrays before continuing OutFrequencies.Empty(); + OutLeftChannelFrequencies.Empty(); + OutRightChannelFrequencies.Empty(); // Make sure the Number of Channels is correct if (NumChannels <= 0 || NumChannels > 2) @@ -154,7 +181,7 @@ void FAudioCaptureWorker::CalculateFrequencySpectrum } // Check if we actually have a Buffer to work with - if (SamplePointer == nullptr) + if (SamplePointer == NULL) { UE_LOG(LogTemp, Warning, TEXT("SamplePointer is null")); return; @@ -178,7 +205,6 @@ void FAudioCaptureWorker::CalculateFrequencySpectrum // Shift the window enough so that we get a PowerOfTwo. FFT works better with that int32 PoT = 2; - while (SamplesToRead > PoT) { PoT *= 2; @@ -187,22 +213,22 @@ void FAudioCaptureWorker::CalculateFrequencySpectrum // Now we have a good PowerOfTwo to work with SamplesToRead = PoT; - // Create two 2-dim Arrays for complex numbers | Buffer and Output - kiss_fft_cpx* Buffer[2] = { 0 }; - kiss_fft_cpx* Output[2] = { 0 }; + // Create unique pointers for complex numbers | Buffer and Output + std::unique_ptr Buffer[2] = { NULL }; + std::unique_ptr Output[2] = { NULL }; // Create 1-dim Array with one slot for SamplesToRead int32 Dims[1] = { SamplesToRead }; - kiss_fftnd_cfg STF = kiss_fftnd_alloc(Dims, 1, 0, nullptr, nullptr); + kiss_fftnd_cfg STF = kiss_fftnd_alloc(Dims, 1, 0, NULL, NULL); int16* SamplePtr = SamplePointer; // Allocate space in the Buffer and Output Arrays for all the data that FFT returns for (int32 ChannelIndex = 0; ChannelIndex < NumChannels; ChannelIndex++) { - Buffer[ChannelIndex] = (kiss_fft_cpx*)KISS_FFT_MALLOC(sizeof(kiss_fft_cpx) * SamplesToRead); - Output[ChannelIndex] = (kiss_fft_cpx*)KISS_FFT_MALLOC(sizeof(kiss_fft_cpx) * SamplesToRead); + Buffer[ChannelIndex].reset(new kiss_fft_cpx[SamplesToRead]); + Output[ChannelIndex].reset(new kiss_fft_cpx[SamplesToRead]); } // Shift our SamplePointer to the Current "FirstSample" @@ -239,34 +265,67 @@ void FAudioCaptureWorker::CalculateFrequencySpectrum { if (Buffer[ChannelIndex]) { - kiss_fftnd(STF, Buffer[ChannelIndex], Output[ChannelIndex]); + kiss_fftnd(STF, Buffer[ChannelIndex].get(), Output[ChannelIndex].get()); } } - OutFrequencies.AddZeroed(SamplesToRead); + OutFrequencies.AddZeroed(SamplesToRead / 2); + OutLeftChannelFrequencies.AddZeroed(SamplesToRead / 2); + OutRightChannelFrequencies.AddZeroed(SamplesToRead / 2); - for (int32 SampleIndex = 0; SampleIndex < SamplesToRead; ++SampleIndex) + for (int32 SampleIndex = 0; SampleIndex < SamplesToRead / 2; ++SampleIndex) { double ChannelSum = 0.0f; + double LeftChannelSum = 0.0f; + double RightChannelSum = 0.0f; for (int32 ChannelIndex = 0; ChannelIndex < NumChannels; ++ChannelIndex) { if (Output[ChannelIndex]) { - // With this we get the actual Frequency value for the frequencies from 0hz to ~22000hz - ChannelSum += FMath::Sqrt(FMath::Square(Output[ChannelIndex][SampleIndex].r) + FMath::Square(Output[ChannelIndex][SampleIndex].i)); + double Magnitude = FMath::Sqrt(FMath::Square(Output[ChannelIndex][SampleIndex].r) + FMath::Square(Output[ChannelIndex][SampleIndex].i)); + ChannelSum += Magnitude; + + if (ChannelIndex == 0) + { + LeftChannelSum += Magnitude; + } + else if (ChannelIndex == 1) + { + RightChannelSum += Magnitude; + } } } - OutFrequencies[SampleIndex] = FMath::Pow((FMath::LogX(FreqLogBase, ChannelSum / NumChannels) * FreqMultiplier), FreqPower) + FreqOffset; - } + // Average the ChannelSum across channels and normalize + ChannelSum /= NumChannels; - // Make sure to free up the FFT stuff - KISS_FFT_FREE(STF); + // Apply a compensation factor to counteract the roll-off + ChannelSum *= (1.0f + SampleIndex / float(SamplesToRead / 2)); - for (int32 ChannelIndex = 0; ChannelIndex < NumChannels; ++ChannelIndex) - { - KISS_FFT_FREE(Buffer[ChannelIndex]); - KISS_FFT_FREE(Output[ChannelIndex]); + // Logarithmic scaling (if needed) + OutFrequencies[SampleIndex] = FMath::Pow((FMath::LogX(FreqLogBase, ChannelSum) * FreqMultiplier), FreqPower) + FreqOffset; + + // Zero-out channels with no audio + if (LeftChannelSum == 0.0f) + { + OutLeftChannelFrequencies[SampleIndex] = 0.0f; + } + else + { + OutLeftChannelFrequencies[SampleIndex] = FMath::Pow((FMath::LogX(FreqLogBase, LeftChannelSum) * FreqMultiplier), FreqPower) + FreqOffset; + } + + if (RightChannelSum == 0.0f) + { + OutRightChannelFrequencies[SampleIndex] = 0.0f; + } + else + { + OutRightChannelFrequencies[SampleIndex] = FMath::Pow((FMath::LogX(FreqLogBase, RightChannelSum) * FreqMultiplier), FreqPower) + FreqOffset; + } } + + // Free up the FFT stuff + kiss_fft_free(STF); } diff --git a/WindowsAudioCapture/Source/WindowsAudioCapture/Private/AudioListener.cpp b/WindowsAudioCapture/Source/WindowsAudioCapture/Private/AudioListener.cpp index cab5bd9..b641957 100644 --- a/WindowsAudioCapture/Source/WindowsAudioCapture/Private/AudioListener.cpp +++ b/WindowsAudioCapture/Source/WindowsAudioCapture/Private/AudioListener.cpp @@ -1,112 +1,136 @@ -//Windows Audio Capture (WAC) by KwstasG (Kostas Giannakakis) +// Windows Audio Capture (WAC) by KwstasG (Kostas Giannakakis) #include "AudioListener.h" #include "WindowsAudioCapture.h" - -HRESULT ThrowOrExit(HRESULT hr) -{ - return hr; -} +#include AudioListener::AudioListener(int BitsPerSample, int FormatTag, int BlockAlign, int XSize) { - // This might break if done more than once - CoInitialize(nullptr); - - HRESULT hr; - REFERENCE_TIME hnsRequestedDuration = m_refTimesPerSec; - - hr = CoCreateInstance(m_CLSID_MMDeviceEnumerator, NULL,CLSCTX_ALL, m_IID_IMMDeviceEnumerator,(void**)&m_pEnumerator); - - if (hr) throw hr; - - hr = m_pEnumerator->GetDefaultAudioEndpoint( - //eCapture, eConsole, &pDevice); //set this to capture from default recording device instead of render device - eRender, eConsole, &m_pDevice); - if (hr) throw hr; - - hr = m_pDevice->Activate(m_IID_IAudioClient, CLSCTX_ALL,NULL, (void**)&m_pAudioClient); - if (hr) throw hr; - - hr = m_pAudioClient->GetMixFormat(&m_pwfx); - if (hr) throw hr; - - m_pwfx->wBitsPerSample = BitsPerSample; - m_pwfx->nBlockAlign = BlockAlign; - m_pwfx->wFormatTag = FormatTag; - m_pwfx->cbSize = XSize; - m_pwfx->nAvgBytesPerSec = m_pwfx->nSamplesPerSec * m_pwfx->nBlockAlign; - - hr = m_pAudioClient->Initialize( - //AUDCLNT_SHAREMODE_EXCLUSIVE, - AUDCLNT_SHAREMODE_SHARED, - //0, //set this instead of loopback to capture from default recording device - AUDCLNT_STREAMFLAGS_LOOPBACK, - hnsRequestedDuration, - 0, - m_pwfx, - NULL); - if (hr) throw hr; - - // Get the size of the allocated buffer. - hr = m_pAudioClient->GetBufferSize(&m_bufferFrameCount); - if (hr) throw hr; - - hr = m_pAudioClient->GetService(m_IID_IAudioCaptureClient,(void**)&m_pCaptureClient); - if (hr) throw hr; - - - // Calculate the actual duration of the allocated buffer. - m_hnsActualDuration = (double)m_refTimesPerSec * - m_bufferFrameCount / m_pwfx->nSamplesPerSec; + // This might break if done more than once + HRESULT hr = CoInitialize(nullptr); + if (FAILED(hr)) + { + throw std::runtime_error("Failed to initialize COM library"); + } + + REFERENCE_TIME hnsRequestedDuration = m_refTimesPerSec; + + hr = CoCreateInstance(m_CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, m_IID_IMMDeviceEnumerator, (void**)&m_pEnumerator); + if (FAILED(hr)) + { + throw std::runtime_error("Failed to create instance of MMDeviceEnumerator"); + } + + hr = m_pEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, &m_pDevice); + if (FAILED(hr)) + { + throw std::runtime_error("Failed to get default audio endpoint"); + } + + hr = m_pDevice->Activate(m_IID_IAudioClient, CLSCTX_ALL, NULL, (void**)&m_pAudioClient); + if (FAILED(hr)) + { + throw std::runtime_error("Failed to activate audio client"); + } + + hr = m_pAudioClient->GetMixFormat(&m_pwfx); + if (FAILED(hr)) + { + throw std::runtime_error("Failed to get mix format"); + } + + m_pwfx->wBitsPerSample = BitsPerSample; + m_pwfx->nBlockAlign = BlockAlign; + m_pwfx->wFormatTag = FormatTag; + m_pwfx->cbSize = XSize; + m_pwfx->nAvgBytesPerSec = m_pwfx->nSamplesPerSec * m_pwfx->nBlockAlign; + + hr = m_pAudioClient->Initialize( + AUDCLNT_SHAREMODE_SHARED, + AUDCLNT_STREAMFLAGS_LOOPBACK, + hnsRequestedDuration, + 0, + m_pwfx, + NULL); + if (FAILED(hr)) + { + throw std::runtime_error("Failed to initialize audio client"); + } + + hr = m_pAudioClient->GetBufferSize(&m_bufferFrameCount); + if (FAILED(hr)) + { + throw std::runtime_error("Failed to get buffer size"); + } + + hr = m_pAudioClient->GetService(m_IID_IAudioCaptureClient, (void**)&m_pCaptureClient); + if (FAILED(hr)) + { + throw std::runtime_error("Failed to get audio capture client service"); + } + + m_hnsActualDuration = (double)m_refTimesPerSec * m_bufferFrameCount / m_pwfx->nSamplesPerSec; } + AudioListener::~AudioListener() { + if (m_pAudioClient) + { + m_pAudioClient->Stop(); + } + CoUninitialize(); } -HRESULT AudioListener::RecordAudioStream(IAudioSink* Sink, bool& Done) + +HRESULT AudioListener::RecordAudioStream(IAudioSink* Sink, std::atomic& Done) { - HRESULT hr; - BYTE *pData; - DWORD flags; - UINT32 packetLength = 0; - UINT32 numFramesAvailable; - - hr = m_pAudioClient->Start(); // Start recording. - if (hr) throw hr; - - // Each loop fills about half of the shared buffer. - while (!Done) - { - // Sleep for half the buffer duration. - Sleep(m_hnsActualDuration / m_refTimesPerMS / 2); - - hr = m_pCaptureClient->GetNextPacketSize(&packetLength); - if (hr) - return ThrowOrExit(hr); - - while (packetLength != 0) - { - // Get the available data in the shared buffer. - hr = m_pCaptureClient->GetBuffer(&pData,&numFramesAvailable,&flags, NULL, NULL); - if (hr) throw hr; - - //if (flags & AUDCLNT_BUFFERFLAGS_SILENT) - //{ - // pData = NULL; // Tell CopyData to write silence. - //} - - // Copy the available capture data to the audio sink. - hr = Sink->CopyData(pData, numFramesAvailable); - if (hr) throw hr; - - hr = m_pCaptureClient->ReleaseBuffer(numFramesAvailable); - if (hr) throw hr; - - hr = m_pCaptureClient->GetNextPacketSize(&packetLength); - if (hr) throw hr; - } - } - - hr = m_pAudioClient->Stop(); // Stop recording. - if (hr) throw hr; - return hr; -} \ No newline at end of file + HRESULT hr = m_pAudioClient->Start(); // Start recording. + if (FAILED(hr)) + { + return hr; + } + + BYTE *pData = nullptr; + DWORD flags = 0; + UINT32 packetLength = 0; + UINT32 numFramesAvailable = 0; + + while (!Done) + { + Sleep(static_cast(m_hnsActualDuration / m_refTimesPerMS / 2)); + + hr = m_pCaptureClient->GetNextPacketSize(&packetLength); + if (FAILED(hr)) + { + return hr; + } + + while (packetLength != 0) + { + hr = m_pCaptureClient->GetBuffer(&pData, &numFramesAvailable, &flags, NULL, NULL); + if (FAILED(hr)) + { + return hr; + } + + hr = Sink->CopyData(pData, numFramesAvailable); + if (FAILED(hr)) + { + return hr; + } + + hr = m_pCaptureClient->ReleaseBuffer(numFramesAvailable); + if (FAILED(hr)) + { + return hr; + } + + hr = m_pCaptureClient->GetNextPacketSize(&packetLength); + if (FAILED(hr)) + { + return hr; + } + } + } + + hr = m_pAudioClient->Stop(); + return hr; +} diff --git a/WindowsAudioCapture/Source/WindowsAudioCapture/Private/AudioSink.cpp b/WindowsAudioCapture/Source/WindowsAudioCapture/Private/AudioSink.cpp index f90f227..d24b8d8 100644 --- a/WindowsAudioCapture/Source/WindowsAudioCapture/Private/AudioSink.cpp +++ b/WindowsAudioCapture/Source/WindowsAudioCapture/Private/AudioSink.cpp @@ -1,76 +1,95 @@ -//Windows Audio Capture (WAC) by KwstasG (Kostas Giannakakis) +// Windows Audio Capture (WAC) by KwstasG (Kostas Giannakakis) #include "AudioSink.h" #include "WindowsAudioCapture.h" +#include -AudioSink::AudioSink() +AudioSink::AudioSink(int bitDepth, int nChannels) + : m_bitDepth(bitDepth), m_nChannels(nChannels), m_chunkSize(0) { + Initialize(); } AudioSink::~AudioSink() { + std::lock_guard lock(m_mutex); + while (!m_queue.empty()) + { + delete[] m_queue.front().chunk; + m_queue.pop(); + } } -bool AudioSink::Dequeue(AudioChunk& Chunk) +void AudioSink::Initialize() { - std::lock_guard lock(m_mutex); + m_maxSampleVal = (1 << (m_bitDepth - 1)) - 1; +} - if (m_queue.size() == 0) - return false; +bool AudioSink::Dequeue(AudioChunk& Chunk) +{ + std::lock_guard lock(m_mutex); - Chunk = m_queue.front(); - m_queue.pop(); - return true; + if (m_queue.empty()) + return false; + Chunk = m_queue.front(); + m_queue.pop(); + return true; } -bool AudioSink::EmptyQueue(AudioChunk& Chunk) +bool AudioSink::EmptyQueue() { - //std::lock_guard lock(m_mutex); - - while (m_queue.size() > 0) - { - Chunk = m_queue.front(); - m_queue.pop(); - } + std::lock_guard lock(m_mutex); - return true; + while (!m_queue.empty()) + { + delete[] m_queue.front().chunk; + m_queue.pop(); + } + return true; } int AudioSink::CopyData(const BYTE* Data, const int NumFramesAvailable) { - std::lock_guard lock(m_mutex); - - AudioChunk chunk; - if (Data == NULL) - { - chunk.size = 0; - m_queue.push(chunk); - return 0; - } - chunk.size = NumFramesAvailable * 2; - chunk.chunk = new int16[chunk.size]; - int multiplier = sizeof(int16) / sizeof(unsigned char); - std::memcpy(chunk.chunk, Data, chunk.size * multiplier); - bool nonZero = false; - - for (int i = 0; i < chunk.size; i++) - { - if (chunk.chunk[i] == -1 || chunk.chunk[i] == 1) - { - chunk.chunk[i] = 0; - } - if (chunk.chunk[i] != 0) - { - nonZero = true; - //UE_LOG(LogTemp, Log, TEXT("NumFramesAvailable: %d, Sample number %d, Sample value %hi"), NumFramesAvailable, i, chunk.chunk[i]); - } - } - - if (nonZero) - { - m_queue.push(chunk); - } - - return 0; + std::lock_guard lock(m_mutex); + + if (Data == nullptr) + { + AudioChunk chunk; + chunk.size = 0; + chunk.chunk = nullptr; + m_queue.push(chunk); + return 0; + } + + int chunkSize = NumFramesAvailable * m_nChannels; + AudioChunk chunk; + chunk.size = chunkSize; + chunk.chunk = new int16[chunkSize]; + std::memcpy(chunk.chunk, Data, chunkSize * sizeof(int16)); + + bool nonZero = false; + + for (int i = 0; i < chunkSize; ++i) + { + if (chunk.chunk[i] == -1 || chunk.chunk[i] == 1) + { + chunk.chunk[i] = 0; + } + if (chunk.chunk[i] != 0) + { + nonZero = true; + } + } + + if (nonZero) + { + m_queue.push(chunk); + } + else + { + delete[] chunk.chunk; + } + + return 0; } diff --git a/WindowsAudioCapture/Source/WindowsAudioCapture/Private/WindowsAudioCaptureComponent.cpp b/WindowsAudioCapture/Source/WindowsAudioCapture/Private/WindowsAudioCaptureComponent.cpp index 4649471..7190d71 100644 --- a/WindowsAudioCapture/Source/WindowsAudioCapture/Private/WindowsAudioCaptureComponent.cpp +++ b/WindowsAudioCapture/Source/WindowsAudioCapture/Private/WindowsAudioCaptureComponent.cpp @@ -1,114 +1,312 @@ -//Windows Audio Capture (WAC) by KwstasG (Kostas Giannakakis) +// Windows Audio Capture (WAC) by KwstasG (Kostas Giannakakis) #include "WindowsAudioCaptureComponent.h" #include "WindowsAudioCapture.h" +#include // Sets default values for this component's properties UWindowsAudioCaptureComponent::UWindowsAudioCaptureComponent() { - PrimaryComponentTick.bCanEverTick = false; + PrimaryComponentTick.bCanEverTick = true; } UWindowsAudioCaptureComponent::~UWindowsAudioCaptureComponent() { - if (!FAudioCaptureWorker::Runnable == NULL) - { - FAudioCaptureWorker::Runnable->ShutdownWorker(); - - } + if (FAudioCaptureWorker::Runnable != NULL) + { + FAudioCaptureWorker::Runnable->ShutdownWorker(); + } } // Called when the game starts void UWindowsAudioCaptureComponent::BeginPlay() { - Super::BeginPlay(); - - if (FAudioCaptureWorker::Runnable == NULL) - { - // Init new Worker - FAudioCaptureWorker::Runnable->InitializeWorker(); - } + Super::BeginPlay(); + if (FAudioCaptureWorker::Runnable == NULL) + { + // Init new Worker + FAudioCaptureWorker::Runnable->InitializeWorker(); + } } // Called every frame void UWindowsAudioCaptureComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) { - Super::TickComponent(DeltaTime, TickType, ThisTickFunction); + Super::TickComponent(DeltaTime, TickType, ThisTickFunction); } -// This function will return an Array of Frequencies. -TArray UWindowsAudioCaptureComponent::BP_GetFrequencyArray(float inFreqLogBase, float inFreqMultiplier, float inFreqPower, float inFreqOffset) +// Validates frequency calculation parameters +bool UWindowsAudioCaptureComponent::ValidateFrequencyParameters(float& inFreqLogBase, float& inFreqMultiplier, float& inFreqPower, float& inFreqOffset) { - TArray FrequencyArray; + bool isValid = true; + + if (inFreqLogBase <= 0) + { + UE_LOG(LogTemp, Warning, TEXT("Invalid inFreqLogBase: %f. It must be greater than 0. Using default value 10."), inFreqLogBase); + inFreqLogBase = 10.0f; + isValid = false; + } + + if (inFreqMultiplier <= 0) + { + UE_LOG(LogTemp, Warning, TEXT("Invalid inFreqMultiplier: %f. It must be greater than 0. Using default value 0.25."), inFreqMultiplier); + inFreqMultiplier = 0.25f; + isValid = false; + } + + if (inFreqPower <= 0) + { + UE_LOG(LogTemp, Warning, TEXT("Invalid inFreqPower: %f. It must be greater than 0. Using default value 6."), inFreqPower); + inFreqPower = 6.0f; + isValid = false; + } + + if (inFreqOffset < 0) + { + UE_LOG(LogTemp, Warning, TEXT("Invalid inFreqOffset: %f. It must be 0 or greater. Using default value 0."), inFreqOffset); + inFreqOffset = 0.0f; + isValid = false; + } - if (FAudioCaptureWorker::Runnable) - { - FrequencyArray = FAudioCaptureWorker::Runnable->GetFrequencyArray(inFreqLogBase, inFreqMultiplier, inFreqPower, inFreqOffset); - } + return isValid; +} - return FrequencyArray; +// This function will return Arrays of Frequencies for all channels, left channel, and right channel. +void UWindowsAudioCaptureComponent::BP_GetFrequencyArray(TArray& OutFrequencies, TArray& OutLeftChannelFrequencies, TArray& OutRightChannelFrequencies, float inFreqLogBase, float inFreqMultiplier, float inFreqPower, float inFreqOffset) +{ + if (ValidateFrequencyParameters(inFreqLogBase, inFreqMultiplier, inFreqPower, inFreqOffset)) + { + if (FAudioCaptureWorker::Runnable) + { + FAudioCaptureWorker::Runnable->GetFrequencyArray(inFreqLogBase, inFreqMultiplier, inFreqPower, inFreqOffset, OutFrequencies, OutLeftChannelFrequencies, OutRightChannelFrequencies); + } + } } -// This function will return the value of a specific frequency. -void UWindowsAudioCaptureComponent::BP_GetSpecificFrequencyValue(TArray InFrequencies, int32 InWantedFrequency, float& OutFrequencyValue) +// This function will return the value of a specific frequency and its corresponding value in decibels. +void UWindowsAudioCaptureComponent::BP_GetSpecificFrequencyValue(TArray InFrequencies, int32 InWantedFrequency, float& OutFrequencyValue, float& OutFrequencyValueDB) { - // Init the Return Value - OutFrequencyValue = 0.0f; + // Initialize the return values + OutFrequencyValue = 0.0f; + OutFrequencyValueDB = 0.0f; + + // Check if the input frequency array is empty + if (InFrequencies.Num() == 0) + { + return; + } + + // Total number of samples corresponds to the size of InFrequencies array times 2 + int32 TotalSamples = InFrequencies.Num() * 2; + + // Nyquist frequency (half of the total samples, assuming the sample rate is twice the highest frequency in the array) + float NyquistFrequency = TotalSamples / 2.0f; + + // Calculate the index in the frequency array + int32 FrequencyIndex = static_cast(InWantedFrequency / NyquistFrequency * InFrequencies.Num()); - if (InWantedFrequency < 0 || InWantedFrequency > 22000) - return; + // Handle out-of-range indices by clamping + FrequencyIndex = FMath::Clamp(FrequencyIndex, 0, InFrequencies.Num() - 1); - if (InFrequencies.Num() > 0 && (int32)(InWantedFrequency * InFrequencies.Num() * 2 / 48000) < InFrequencies.Num()) - { - OutFrequencyValue = InFrequencies[(int32)(InWantedFrequency * InFrequencies.Num() * 2 / 48000)]; - } + // Retrieve the specific frequency value + if (InFrequencies.Num() > 0) + { + OutFrequencyValue = InFrequencies[FrequencyIndex]; + + // Calculate the frequency value in DB + if (OutFrequencyValue > 0) + { + OutFrequencyValueDB = 20.0f * FMath::LogX(10.0f, OutFrequencyValue); + } + } } -// This function will return the average value for SubBass -void UWindowsAudioCaptureComponent::BP_GetAverageSubBassValue(TArray InFrequencies, float& OutAverageSubBass) +// This function will return the average value for SubBass (20 to 60 Hz) and its corresponding value in decibels. +void UWindowsAudioCaptureComponent::BP_GetAverageSubBassValue(TArray InFrequencies, float& OutAverageSubBass, float& OutAverageSubBassDB) { - BP_GetAverageFrequencyValueInRange(InFrequencies, 20, 60, OutAverageSubBass); + BP_GetAverageFrequencyValueInRange(InFrequencies, 20, 60, OutAverageSubBass, OutAverageSubBassDB); } - -// This function will return the average value for Bass (60 to 250hz) -void UWindowsAudioCaptureComponent::BP_GetAverageBassValue(TArray InFrequencies,float& OutAverageBass) +// This function will return the average value for Bass (60 to 250 Hz) and its corresponding value in decibels. +void UWindowsAudioCaptureComponent::BP_GetAverageBassValue(TArray InFrequencies, float& OutAverageBass, float& OutAverageBassDB) { - BP_GetAverageFrequencyValueInRange(InFrequencies, 60, 250, OutAverageBass); + BP_GetAverageFrequencyValueInRange(InFrequencies, 60, 250, OutAverageBass, OutAverageBassDB); } +// This function will return the average value for Mid frequencies (250 to 2000 Hz) and its corresponding value in decibels. +void UWindowsAudioCaptureComponent::BP_GetAverageMidValue(TArray InFrequencies, float& OutAverageMid, float& OutAverageMidDB) +{ + BP_GetAverageFrequencyValueInRange(InFrequencies, 250, 2000, OutAverageMid, OutAverageMidDB); +} +// This function will return the average value for High frequencies (2000 to 22000 Hz) and its corresponding value in decibels. +void UWindowsAudioCaptureComponent::BP_GetAverageHighValue(TArray InFrequencies, float& OutAverageHigh, float& OutAverageHighDB) +{ + BP_GetAverageFrequencyValueInRange(InFrequencies, 2000, 22000, OutAverageHigh, OutAverageHighDB); +} +// This function will return the average value for a given frequency input range e.g., 20 to 60 (SubBass), and its corresponding value in decibels. void UWindowsAudioCaptureComponent::BP_GetAverageFrequencyValueInRange ( - TArray InFrequencies, - int32 InStartFrequency, - int32 InEndFrequency, - float& OutAverageFrequency + TArray InFrequencies, + int32 InStartFrequency, + int32 InEndFrequency, + float& OutAverageFrequency, + float& OutAverageFrequencyDB ) { - // Init the Return Value - OutAverageFrequency = 0.0f; + // Initialize the return values + OutAverageFrequency = 0.0f; + OutAverageFrequencyDB = 0.0f; - if (InStartFrequency >= InEndFrequency || InStartFrequency < 0 || InEndFrequency > 22000) - return; + // Check if the input frequency array is empty + if (InFrequencies.Num() == 0) + { + return; + } - int32 FStart = (int32)(InStartFrequency * InFrequencies.Num() * 2 / 48000); - int32 FEnd = (int32)(InEndFrequency * InFrequencies.Num() * 2 / 48000); + // Total number of samples corresponds to the size of InFrequencies array times 2 + int32 TotalSamples = InFrequencies.Num() * 2; - if (FStart < 0 || FEnd >= InFrequencies.Num()) - return; + // Nyquist frequency (half of the total samples, assuming the sample rate is twice the highest frequency in the array) + float NyquistFrequency = TotalSamples / 2.0f; - int32 NumberOfFrequencies = 0; + // Calculate start and end indices in the frequency array + int32 FStart = static_cast(InStartFrequency / NyquistFrequency * InFrequencies.Num()); + int32 FEnd = static_cast(InEndFrequency / NyquistFrequency * InFrequencies.Num()); - float ValueSum = 0.0f; + // Handle out-of-range indices by clamping + FStart = FMath::Clamp(FStart, 0, InFrequencies.Num() - 1); + FEnd = FMath::Clamp(FEnd, 0, InFrequencies.Num() - 1); - for (int i = FStart; i <= FEnd; i++) - { - NumberOfFrequencies++; + if (FStart > FEnd) + { + Swap(FStart, FEnd); + } - ValueSum += InFrequencies[i]; - } + int32 NumberOfFrequencies = FEnd - FStart + 1; + + // Sum the values within the specified frequency range + float ValueSum = 0.0f; + for (int32 i = FStart; i <= FEnd; ++i) + { + ValueSum += InFrequencies[i]; + } + + // Calculate the average frequency value + OutAverageFrequency = (NumberOfFrequencies > 0) ? ValueSum / NumberOfFrequencies : 0.0f; + + // Calculate the average frequency value in DB + if (OutAverageFrequency > 0) + { + OutAverageFrequencyDB = 20.0f * FMath::LogX(10.0f, OutAverageFrequency); + } +} + +// This function will return the total energy of the audio signal across all frequencies and its corresponding value in decibels. +void UWindowsAudioCaptureComponent::BP_GetTotalEnergy(TArray InFrequencies, float& OutTotalEnergy, float& OutTotalEnergyDB) +{ + // Initialize the return values + OutTotalEnergy = 0.0f; + OutTotalEnergyDB = 0.0f; - OutAverageFrequency = ValueSum / NumberOfFrequencies; + // Check if the input frequency array is empty + if (InFrequencies.Num() == 0) + { + return; + } + + // Sum the values of all frequencies + for (float FrequencyValue : InFrequencies) + { + OutTotalEnergy += FrequencyValue; + } + + // Calculate the total energy in DB + if (OutTotalEnergy > 0) + { + OutTotalEnergyDB = 20.0f * FMath::LogX(10.0f, OutTotalEnergy); + } +} + +// This function will return the peak frequency and its value in decibels. +void UWindowsAudioCaptureComponent::BP_GetPeakFrequency(TArray InFrequencies, float& OutPeakFrequency, float& OutPeakFrequencyDB) +{ + // Initialize the return values + OutPeakFrequency = 0.0f; + OutPeakFrequencyDB = 0.0f; + + // Check if the input frequency array is empty + if (InFrequencies.Num() == 0) + { + return; + } + + float PeakValue = 0.0f; + + // Find the peak frequency value + for (int32 i = 0; i < InFrequencies.Num(); ++i) + { + if (InFrequencies[i] > PeakValue) + { + PeakValue = InFrequencies[i]; + OutPeakFrequency = static_cast(i); + } + } + + // Calculate the peak frequency value in DB + if (PeakValue > 0) + { + OutPeakFrequencyDB = 20.0f * FMath::LogX(10.0f, PeakValue); + } +} + +// This function normalizes the frequency array to a specified range. +void UWindowsAudioCaptureComponent::BP_NormalizeFrequencyArray(TArray InFrequencies, TArray& OutNormalizedFrequencies, float RangeMin, float RangeMax) +{ + OutNormalizedFrequencies.Empty(InFrequencies.Num()); + + float MinValue = TNumericLimits::Max(); + float MaxValue = TNumericLimits::Lowest(); + + // Find the min and max values in the array + for (float FrequencyValue : InFrequencies) + { + if (FrequencyValue < MinValue) MinValue = FrequencyValue; + if (FrequencyValue > MaxValue) MaxValue = FrequencyValue; + } + + // Normalize the values to the specified range + for (float FrequencyValue : InFrequencies) + { + float NormalizedValue = (FrequencyValue - MinValue) / (MaxValue - MinValue) * (RangeMax - RangeMin) + RangeMin; + OutNormalizedFrequencies.Add(NormalizedValue); + } +} + +// This function applies dynamic range compression to the frequency array. +void UWindowsAudioCaptureComponent::BP_ApplyDynamicRangeCompression(TArray InFrequencies, TArray& OutCompressedFrequencies, float Threshold, float Ratio) +{ + OutCompressedFrequencies.Empty(InFrequencies.Num()); + + float LinearThreshold = FMath::Pow(10.0f, Threshold / 20.0f); + + // Apply compression + for (float FrequencyValue : InFrequencies) + { + float CompressedValue = FrequencyValue; + if (FrequencyValue > LinearThreshold) + { + CompressedValue = LinearThreshold + (FrequencyValue - LinearThreshold) / Ratio; + } + OutCompressedFrequencies.Add(CompressedValue); + } +} + +// This function performs interpolation of a value over time. +void UWindowsAudioCaptureComponent::BP_InterpolateValue(float InputValue, float InterpSpeed, float DeltaTime, float& OutInterpolatedValue) +{ + static float CurrentInterpolatedValue = 0.0f; + CurrentInterpolatedValue = FMath::FInterpTo(CurrentInterpolatedValue, InputValue, DeltaTime, InterpSpeed); + OutInterpolatedValue = CurrentInterpolatedValue; } diff --git a/WindowsAudioCapture/Source/WindowsAudioCapture/Public/AudioCaptureWorker.h b/WindowsAudioCapture/Source/WindowsAudioCapture/Public/AudioCaptureWorker.h index 3e30032..77f8bbc 100644 --- a/WindowsAudioCapture/Source/WindowsAudioCapture/Public/AudioCaptureWorker.h +++ b/WindowsAudioCapture/Source/WindowsAudioCapture/Public/AudioCaptureWorker.h @@ -1,9 +1,12 @@ -//Windows Audio Capture (WAC) by KwstasG (Kostas Giannakakis) +// Windows Audio Capture (WAC) by KwstasG (Kostas Giannakakis) #pragma once #include "Engine.h" #include "AudioSink.h" #include "AudioListener.h" +#include +#include +#include // KISS Headers #include "ThirdParty/Kiss_FFT/kiss_fft129/kiss_fft.h" @@ -11,71 +14,70 @@ class FAudioCaptureWorker : public FRunnable { - public: + // Singleton instance, can access the thread any time via static accessor, if it is active! + static std::unique_ptr Runnable; - //Singleton instance, can access the thread any time via static accessor, if it is active! - static FAudioCaptureWorker* Runnable; - - //Thread to run the worker FRunnable on - FRunnableThread* Thread; + // Thread to run the worker FRunnable on + FRunnableThread* Thread; - //TArray GetFrequencies(); - TArray GetFrequencyArray(float FreqLogBase, float FreqMultiplier, float FreqPower, float FreqOffset); + //TArray GetFrequencies(); + void GetFrequencyArray(float FreqLogBase, float FreqMultiplier, float FreqPower, float FreqOffset, TArray& OutFrequencies, TArray& OutLeftChannelFrequencies, TArray& OutRightChannelFrequencies); private: - - //Stop this thread? Uses Thread Safe Counter - FThreadSafeCounter StopTaskCounter; - - // Bool to check if the thread is running - bool bIsFinished; - - // Counter for the ThreadNames - static int32 ThreadCounter; - - // Function to calculate the frequency spectrum - void CalculateFrequencySpectrum - ( - int16* SamplePointer, - const int32 NumChannels, - const int32 NumAvailableSamples, - float FreqLogBase, - float FreqMultiplier, - float FreqPower, - float FreqOffset, - TArray& OutFrequencies - ); - - AudioListener m_listener; - AudioSink m_sink; + // Stop this thread? Uses Thread Safe Counter + FThreadSafeCounter StopTaskCounter; + + // Bool to check if the thread is running + std::atomic bIsFinished; + + // Counter for the ThreadNames + static std::atomic ThreadCounter; + + // Function to calculate the frequency spectrum + void CalculateFrequencySpectrum + ( + int16* SamplePointer, + const int32 NumChannels, + const int32 NumAvailableSamples, + float FreqLogBase, + float FreqMultiplier, + float FreqPower, + float FreqOffset, + TArray& OutFrequencies, + TArray& OutLeftChannelFrequencies, + TArray& OutRightChannelFrequencies + ); + + AudioListener m_listener; + AudioSink m_sink; + + // Mutex for thread safety + static std::mutex workerMutex; protected: - - public: - - //Constructor / Destructor - FAudioCaptureWorker(); - ~FAudioCaptureWorker(); - - // Custom Init function - static FAudioCaptureWorker* InitializeWorker(); - - // Custom Shutdown function - static void ShutdownWorker(); - - // Start FRunnable Interface - virtual bool Init(); - virtual uint32 Run(); - virtual void Stop(); - virtual void Exit(); - // End FRunnable Interface - - // Make sure Thread completed - void EnsureCompletion(); - - bool IsFinished() const { - return bIsFinished; - } -}; \ No newline at end of file + // Constructor / Destructor + FAudioCaptureWorker(); + ~FAudioCaptureWorker(); + + // Custom Init function + static FAudioCaptureWorker* InitializeWorker(); + + // Custom Shutdown function + static void ShutdownWorker(); + + // Start FRunnable Interface + virtual bool Init() override; + virtual uint32 Run() override; + virtual void Stop() override; + virtual void Exit() override; + // End FRunnable Interface + + // Make sure Thread completed + void EnsureCompletion(); + + bool IsFinished() const { + return bIsFinished.load(); + } +}; diff --git a/WindowsAudioCapture/Source/WindowsAudioCapture/Public/AudioListener.h b/WindowsAudioCapture/Source/WindowsAudioCapture/Public/AudioListener.h index 7f59f36..97e3b29 100644 --- a/WindowsAudioCapture/Source/WindowsAudioCapture/Public/AudioListener.h +++ b/WindowsAudioCapture/Source/WindowsAudioCapture/Public/AudioListener.h @@ -1,4 +1,4 @@ -//Windows Audio Capture (WAC) by KwstasG (Kostas Giannakakis) +// Windows Audio Capture (WAC) by KwstasG (Kostas Giannakakis) #pragma once #define WIN32_LEAN_AND_MEAN @@ -6,33 +6,31 @@ #include #include "IAudioSink.h" #include +#include class AudioListener { public: - AudioListener(int BitsPerSample, int FormatTag, int BlockAlign, int XSize); - ~AudioListener(); - HRESULT RecordAudioStream(IAudioSink*, bool&); + AudioListener(int BitsPerSample, int FormatTag, int BlockAlign, int XSize); + ~AudioListener(); + HRESULT RecordAudioStream(IAudioSink*, std::atomic&); private: -//#define SAFE_RELEASE(punk) \ -// if ((punk) != NULL) \ -// { (punk)->Release(); (punk) = NULL; } - WAVEFORMATEX* m_pwfx = NULL; - IAudioClient* m_pAudioClient = NULL; - IAudioCaptureClient* m_pCaptureClient = NULL; - IMMDeviceEnumerator* m_pEnumerator = NULL; - IMMDevice* m_pDevice = NULL; + WAVEFORMATEX* m_pwfx = NULL; + Microsoft::WRL::ComPtr m_pAudioClient; + Microsoft::WRL::ComPtr m_pCaptureClient; + Microsoft::WRL::ComPtr m_pEnumerator; + Microsoft::WRL::ComPtr m_pDevice; - UINT32 m_bufferFrameCount; - REFERENCE_TIME m_hnsActualDuration; + UINT32 m_bufferFrameCount = 0; + REFERENCE_TIME m_hnsActualDuration = 0; - const int m_refTimesPerMS = 1000; - const int m_refTimesPerSec = 1000; + const int m_refTimesPerMS = 1000; + const int m_refTimesPerSec = 1000; - const CLSID m_CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator); - const IID m_IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator); - const IID m_IID_IAudioClient = __uuidof(IAudioClient); - const IID m_IID_IAudioCaptureClient = __uuidof(IAudioCaptureClient); -}; \ No newline at end of file + const CLSID m_CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator); + const IID m_IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator); + const IID m_IID_IAudioClient = __uuidof(IAudioClient); + const IID m_IID_IAudioCaptureClient = __uuidof(IAudioCaptureClient); +}; diff --git a/WindowsAudioCapture/Source/WindowsAudioCapture/Public/AudioSink.h b/WindowsAudioCapture/Source/WindowsAudioCapture/Public/AudioSink.h index c2c20db..357ece8 100644 --- a/WindowsAudioCapture/Source/WindowsAudioCapture/Public/AudioSink.h +++ b/WindowsAudioCapture/Source/WindowsAudioCapture/Public/AudioSink.h @@ -1,4 +1,4 @@ -//Windows Audio Capture (WAC) by KwstasG (Kostas Giannakakis) +// Windows Audio Capture (WAC) by KwstasG (Kostas Giannakakis) #pragma once #include "IAudioSink.h" @@ -7,24 +7,26 @@ struct AudioChunk { - int16* chunk; - int size; + int16* chunk; + int size; }; class AudioSink : public IAudioSink { public: - bool Dequeue(AudioChunk& Chunk); - bool EmptyQueue(AudioChunk& Chunk); - int CopyData(const BYTE* Data, const int NumFramesAvailable) override; - AudioSink(); - ~AudioSink(); + bool Dequeue(AudioChunk& Chunk); + bool EmptyQueue(); + int CopyData(const BYTE* Data, const int NumFramesAvailable) override; + AudioSink(int bitDepth = 16, int nChannels = 2); + ~AudioSink(); + private: - std::queue m_queue; - int m_bitDepth; - int m_maxSampleVal; - int m_nChannels; - int m_chunkSize; - std::mutex m_mutex; -}; + std::queue m_queue; + int m_bitDepth; + int m_maxSampleVal; + int m_nChannels; + int m_chunkSize; + std::mutex m_mutex; + void Initialize(); +}; diff --git a/WindowsAudioCapture/Source/WindowsAudioCapture/Public/WindowsAudioCaptureComponent.h b/WindowsAudioCapture/Source/WindowsAudioCapture/Public/WindowsAudioCaptureComponent.h index da7b349..7640369 100644 --- a/WindowsAudioCapture/Source/WindowsAudioCapture/Public/WindowsAudioCaptureComponent.h +++ b/WindowsAudioCapture/Source/WindowsAudioCapture/Public/WindowsAudioCaptureComponent.h @@ -1,4 +1,4 @@ -//Windows Audio Capture (WAC) by KwstasG (Kostas Giannakakis) +// Windows Audio Capture (WAC) by KwstasG (Kostas Giannakakis) #pragma once #include "Components/ActorComponent.h" @@ -12,98 +12,182 @@ UCLASS(ClassGroup = (Audio), meta = (BlueprintSpawnableComponent), meta = (DisplayName = "Windows Audio Capture")) class WINDOWSAUDIOCAPTURE_API UWindowsAudioCaptureComponent : public UActorComponent { - GENERATED_BODY() + GENERATED_BODY() public: - // Sets default values for this component's properties - UWindowsAudioCaptureComponent(); - - // Cleans up stuff - ~UWindowsAudioCaptureComponent(); - - // Called every frame - virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; - - - /** - * This function will return an Array of Frequencies. Frequency = (LogX(FreqLogBase, Frequency) * FreqMultiplier)^FreqPower + FreqOffset. e.g.: Frequency = (LogX(10, Frequency) * 0.25)^6 + 0.0 - * - * @param inFreqLogBase Log Base of the Result Frequency. Default: 10 - * @param inFreqMultiplier Multiplier of the Result Frequency. Default: 0.25 - * @param inFreqPower Power of the Result Frequency. Default: 6 - * @param inFreqOffset Offset of the Result Frequency. Default: 0.0 - * - */ - UFUNCTION(BlueprintCallable, meta = (DisplayName = "Get Frequency Array", Keywords = "Get Frequency Array"), Category = "WindowsAudioCapture | Frequency Array") - static TArray BP_GetFrequencyArray - ( - float inFreqLogBase = 10.0, - float inFreqMultiplier = 0.25, - float inFreqPower = 6.0, - float inFreqOffset = 0.0 - ); - - - /** - * This function will return the value of a specific frequency. It's needs a Frequency Array from the "Get Frequency Array" function. - * - * @param InFrequencies Array of float values for different frequencies from 0 to 22000. Can be get by using the "Get Frequency Array" function. - * @param InWantedFrequency The specific frequency you want to keep from the Frequency Array. - * @param OutFrequencyValue Float value of the requested frequency. - * - */ - UFUNCTION(BlueprintPure, meta = (DisplayName = "Get Specific Freq Value", Keywords = "Get Specific Freq Value"), Category = "WindowsAudioCapture | Frequency Values") - static void BP_GetSpecificFrequencyValue(TArray InFrequencies, int32 InWantedFrequency, float& OutFrequencyValue); - - - /** - * This function will return the average value for SubBass (20 to 60hz) - * - * @param InFrequencies Array of float values for different frequencies from 0 to 22000. Can be get by using the "Get Frequency Array" function. - * @param OutAverageSubBass Average value of the frequencies from 20 to 60. - * - */ - UFUNCTION(BlueprintPure, meta = (DisplayName = "Get Average Subbass Value", Keywords = "Get Average Subbass Value"), Category = "WindowsAudioCapture | Frequency Values") - static void BP_GetAverageSubBassValue(TArray InFrequencies, float& OutAverageSubBass); - - - /** - * This function will return the average value for Bass (60 to 250hz) - * - * @param InFrequencies Array of float values for different frequencies from 0 to 22000. Can be get by using the "Get Frequency Array" function. - * @param OutAverageBass Average value of the frequencies from 60 to 250. - * - */ - UFUNCTION(BlueprintPure, meta = (DisplayName = "Get Average Bass Value", Keywords = "Get Average Bass Value"), Category = "WindowsAudioCapture | Frequency Values") - static void BP_GetAverageBassValue(TArray InFrequencies,float& OutAverageBass); - - - /** - * This function will return the average value for a given frequency input range e.g.: 20 to 60 (SubBass) - * - * @param InFrequencies Array of float values for different frequencies from 0 to 22000. Can be get by using the "Get Frequency Array" function. - * @param InStartFrequency Start Frequency of the Frequency interval. - * @param InEndFrequency End Frequency of the Frequency interval. - * @param OutAverageFrequency Average value of the requested frequency interval. - * - */ - UFUNCTION(BlueprintPure, meta = (DisplayName = "Get Average Freq Value In Range", Keywords = "Get Average Freq Value In Range"), Category = "WindowsAudioCapture | Frequency Values") - static void BP_GetAverageFrequencyValueInRange - ( - TArray InFrequencies, - int32 InStartFrequency, - int32 InEndFrequency, - float& OutAverageFrequency - ); - + // Sets default values for this component's properties + UWindowsAudioCaptureComponent(); + + // Cleans up resources + ~UWindowsAudioCaptureComponent(); + + // Called every frame + virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; + + /** + * This function will return an Array of Frequencies for all channels, left channel, and right channel. + * These parameters allow you to customize how the frequencies are calculated and visualized. + * + * @param OutFrequencies Combined frequencies array. + * @param OutLeftChannelFrequencies Left channel frequencies array. + * @param OutRightChannelFrequencies Right channel frequencies array. + * @param inFreqLogBase This controls the base of the logarithm used in the calculation. Increasing this value can make the frequency spectrum appear more compressed. Default: 10 + * @param inFreqMultiplier This scales the frequency values. Increasing this value will spread out the frequencies more, making it easier to see differences between them. Default: 0.25 + * @param inFreqPower This raises the frequency values to the specified power. Higher values can exaggerate differences between frequencies. Default: 6 + * @param inFreqOffset This adds an offset to the frequency values. It can be used to adjust the baseline of the spectrum. Default: 0.0 + * + */ + UFUNCTION(BlueprintCallable, meta = (DisplayName = "Get Frequency Array", Keywords = "Get Frequency Array"), Category = "WindowsAudioCapture | Frequency Array") + static void BP_GetFrequencyArray + ( + TArray& OutFrequencies, + TArray& OutLeftChannelFrequencies, + TArray& OutRightChannelFrequencies, + float inFreqLogBase = 10.0f, + float inFreqMultiplier = 0.25f, + float inFreqPower = 6.0f, + float inFreqOffset = 0.0f + ); + + /** + * This function will return the value of a specific frequency and its corresponding value in decibels. + * + * @param InFrequencies Array of float values for different frequencies. Can be obtained by using the "Get Frequency Array" function. + * @param InWantedFrequency The specific frequency you want to keep from the Frequency Array. + * @param OutFrequencyValue Float value of the requested frequency. + * @param OutFrequencyValueDB Float value of the requested frequency in decibels. + * + */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Get Specific Freq Value", Keywords = "Get Specific Freq Value"), Category = "WindowsAudioCapture | Frequency Values") + static void BP_GetSpecificFrequencyValue(TArray InFrequencies, int32 InWantedFrequency, float& OutFrequencyValue, float& OutFrequencyValueDB); + + /** + * This function will return the average value for SubBass (20 to 60hz) and its corresponding value in decibels. + * + * @param InFrequencies Array of float values for different frequencies. Can be obtained by using the "Get Frequency Array" function. + * @param OutAverageSubBass Average value of the frequencies from 20 to 60. + * @param OutAverageSubBassDB Average value of the frequencies from 20 to 60 in decibels. + * + */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Get Average Subbass Value", Keywords = "Get Average Subbass Value"), Category = "WindowsAudioCapture | Frequency Values") + static void BP_GetAverageSubBassValue(TArray InFrequencies, float& OutAverageSubBass, float& OutAverageSubBassDB); + + /** + * This function will return the average value for Bass (60 to 250hz) and its corresponding value in decibels. + * + * @param InFrequencies Array of float values for different frequencies. Can be obtained by using the "Get Frequency Array" function. + * @param OutAverageBass Average value of the frequencies from 60 to 250. + * @param OutAverageBassDB Average value of the frequencies from 60 to 250 in decibels. + * + */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Get Average Bass Value", Keywords = "Get Average Bass Value"), Category = "WindowsAudioCapture | Frequency Values") + static void BP_GetAverageBassValue(TArray InFrequencies, float& OutAverageBass, float& OutAverageBassDB); + + /** + * This function will return the average value for Mid frequencies (250 to 2000hz) and its corresponding value in decibels. + * + * @param InFrequencies Array of float values for different frequencies. Can be obtained by using the "Get Frequency Array" function. + * @param OutAverageMid Average value of the frequencies from 250 to 2000. + * @param OutAverageMidDB Average value of the frequencies from 250 to 2000 in decibels. + * + */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Get Average Mid Value", Keywords = "Get Average Mid Value"), Category = "WindowsAudioCapture | Frequency Values") + static void BP_GetAverageMidValue(TArray InFrequencies, float& OutAverageMid, float& OutAverageMidDB); + + /** + * This function will return the average value for High frequencies (2000 to 22000hz) and its corresponding value in decibels. + * + * @param InFrequencies Array of float values for different frequencies. Can be obtained by using the "Get Frequency Array" function. + * @param OutAverageHigh Average value of the frequencies from 2000 to 22000. + * @param OutAverageHighDB Average value of the frequencies from 2000 to 22000 in decibels. + * + */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Get Average High Value", Keywords = "Get Average High Value"), Category = "WindowsAudioCapture | Frequency Values") + static void BP_GetAverageHighValue(TArray InFrequencies, float& OutAverageHigh, float& OutAverageHighDB); + + /** + * This function will return the average value for a given frequency input range e.g., 20 to 60 (SubBass), and its corresponding value in decibels. + * + * @param InFrequencies Array of float values for different frequencies. Can be obtained by using the "Get Frequency Array" function. + * @param InStartFrequency Start Frequency of the Frequency interval. + * @param InEndFrequency End Frequency of the Frequency interval. + * @param OutAverageFrequency Average value of the requested frequency interval. + * @param OutAverageFrequencyDB Average value of the requested frequency interval in decibels. + * + */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Get Average Freq Value In Range", Keywords = "Get Average Freq Value In Range"), Category = "WindowsAudioCapture | Frequency Values") + static void BP_GetAverageFrequencyValueInRange + ( + TArray InFrequencies, + int32 InStartFrequency, + int32 InEndFrequency, + float& OutAverageFrequency, + float& OutAverageFrequencyDB + ); + + /** + * This function will return the total energy of the audio signal across all frequencies and its corresponding value in decibels. + * + * @param InFrequencies Array of float values for different frequencies. Can be obtained by using the "Get Frequency Array" function. + * @param OutTotalEnergy Total energy of the audio signal. + * @param OutTotalEnergyDB Total energy of the audio signal in decibels. + * + */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Get Total Energy", Keywords = "Get Total Energy"), Category = "WindowsAudioCapture | Energy") + static void BP_GetTotalEnergy(TArray InFrequencies, float& OutTotalEnergy, float& OutTotalEnergyDB); + + /** + * This function will return the peak frequency and its value in decibels. + * + * @param InFrequencies Array of float values for different frequencies. Can be obtained by using the "Get Frequency Array" function. + * @param OutPeakFrequency Frequency with the highest amplitude. + * @param OutPeakFrequencyDB Value of the peak frequency in decibels. + * + */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Get Peak Frequency", Keywords = "Get Peak Frequency"), Category = "WindowsAudioCapture | Frequency Values") + static void BP_GetPeakFrequency(TArray InFrequencies, float& OutPeakFrequency, float& OutPeakFrequencyDB); + + /** + * This function normalizes the frequency array to a specified range. + * + * @param InFrequencies Array of float values for different frequencies. Can be obtained by using the "Get Frequency Array" function. + * @param OutNormalizedFrequencies Normalized frequency array. + * @param RangeMin Minimum value of the normalized range. + * @param RangeMax Maximum value of the normalized range. + * + */ + UFUNCTION(BlueprintCallable, meta = (DisplayName = "Normalize Frequency Array", Keywords = "Normalize Frequency Array"), Category = "WindowsAudioCapture | Frequency Array") + static void BP_NormalizeFrequencyArray(TArray InFrequencies, TArray& OutNormalizedFrequencies, float RangeMin = 0.0f, float RangeMax = 1.0f); + + /** + * This function applies dynamic range compression to the frequency array. + * + * @param InFrequencies Array of float values for different frequencies. Can be obtained by using the "Get Frequency Array" function. + * @param OutCompressedFrequencies Compressed frequency array. + * @param Threshold Threshold level for compression. + * @param Ratio Compression ratio. + * + */ + UFUNCTION(BlueprintCallable, meta = (DisplayName = "Apply Dynamic Range Compression", Keywords = "Apply Dynamic Range Compression"), Category = "WindowsAudioCapture | Frequency Array") + static void BP_ApplyDynamicRangeCompression(TArray InFrequencies, TArray& OutCompressedFrequencies, float Threshold = -24.0f, float Ratio = 4.0f); + + /** + * This function performs interpolation of a value over time. + * + * @param InputValue The input value of the float. + * @param InterpSpeed The interpolation speed lower=smoother, higher=more responsive, if set to 0 returns the input value. + * @param DeltaTime The time since the last tick. + * @param OutInterpolatedValue The interpolated float value. + * + */ + UFUNCTION(BlueprintCallable, meta = (DisplayName = "Interpolate Value", Keywords = "Interpolate Value"), Category = "WindowsAudioCapture | Utilities") + static void BP_InterpolateValue(float InputValue, float InterpSpeed, float DeltaTime, float& OutInterpolatedValue); protected: - - // Called when the game starts - virtual void BeginPlay() override; - + // Called when the game starts + virtual void BeginPlay() override; private: - - + // Validates frequency calculation parameters + static bool ValidateFrequencyParameters(float& inFreqLogBase, float& inFreqMultiplier, float& inFreqPower, float& inFreqOffset); };