fec77632ef
* Fix issues with seedlist for AutomatedTesting Fixes error reporting so it will show the file hint in the tool. Removes any missing assets from the .seed file. Remove an unnecessary dependency from AutomatedTesting dependencies file. Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com> * Make ArchiveComponent use AZ::IO::IArchive Initial changes that will get the sychronous calls in ArchiveComponent to use IArchive interface rather than external zip/7z tools. Some of the asynchronous api are still in place, anything that wasn't being used has been removed for now. This may change later if we move towards all the api being asynchronous. Until then, we can't remove the reliance upon the external archive tools completely. Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com> * Updates AZStd::thread constructors Adds a variadic constructor which forwards args to the functor. Because of our thread_desc extension, there was confusion on the arugments, so the args were reordered to take the thread_desc first, before the functor and args. Also the thread_desc is taken as reference rather than by pointer. Update callsites to account for this change. Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com> * Async operation of ArchiveComponent api This sets up the ArchiveComponent to operate asynchronously. It uses promise/future to transfer results to caller. This is still broken, there's a few things that need to get fixed up, but this is a good checkpoint for the work as it solidifies the api, cleans up a bunch of unused code, and compiles. Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com> * Removes the platform-specific ArchiveComponen These are no longer needed, as they control the direct interaction with host OS tools like 7za.exe or /bin/zip. Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com> * Removes the platform-specific files from cmake Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com> * Removes the 7za.exe (and legal notice) This tool is no longer needed in the repo. Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com> * Fixes usage of IArchive::GetFullPath() This changed to return a PathView, updated to reflect that. Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com> * Fix promises and threads Make sure promises are only set exactly once. This meant reworking some of the initial error checking. Detach threads when created. Adds [[nodiscard]] to the functions that return a future. Since threads are detached, the future is the main way to get communication from the thread. Clean up interface, add comments. Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com> * More edits to thread creation Changes to thread construction to account for parameter change. Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com> * Fix some remaining issues with ArchiveComponent Put created threads inside a container, then join them at Deactivate. Fix asset bundler case when injecting a file with no working directory. Fix thread constructor that applies args to a function. Fix lambdas to take string args by value rather than reference. Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com> * Fixes some remaining bugs in ArchiveComponent Open archive as read-only during extract & list operations. Fixes paths issues. Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com> * Fix initialize of opaque thread handle in thread_UnixLike.h Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> * Removed unused variable in AssetBundleComponent.cpp to fix compiler warning Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> * Fix some issues with archives File paths in the CDR and the local headers need to match, but there were issues with path separators and case that made it possible to get invalid headers errors in some archives. Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com> * Adds some new ArchiveComponent unit tests Adds new tests for extraction of archive and adding files from a file list to an archive. Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com> * Fix file data offset issues when opening archives When opening an INestedArchive it would run through the CDR headers to create file entries in the zip cache. The offsets to the compressed data were being calculated incorrectly because they were using the CDR headers rather than jumping to the local file headers and getting offsets from those sizes. Removed and refactored some archive validation flow and zip cache factory init methods to either init default or init w/ additional validation checks. Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com> * Addresses PR feedback Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com> * Address more points of feedback in PR Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com> * Address additional PR feedback Fixes up some error checks and uses of strings vs paths. Enable archive component tests on Linux so we can see if they will pass. Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com> * Address PR feedback Change the INestedArchive interface to list files as AZ::IO::Path. Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com> * Disabling the ArchiveComponent tests on Linux They failed so we will revisit them to attempt a fix. Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com> * Rename a member variable to be more accurate Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com> * Address feedback on PR Bump version of Archive Components for serialize context. Improve error messages during archive open and validation. Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com> * Revert recent changes Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com> Co-authored-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com>
735 lines
31 KiB
C++
735 lines
31 KiB
C++
/*
|
|
* Copyright (c) Contributors to the Open 3D Engine Project.
|
|
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
*
|
|
*/
|
|
|
|
#include "MicrophoneSystemComponent.h"
|
|
|
|
#include <audioclient.h>
|
|
#include <mmdeviceapi.h>
|
|
#include <functiondiscoverykeys_devpkey.h>
|
|
|
|
#include <AzCore/std/chrono/chrono.h>
|
|
#include <AzCore/std/parallel/atomic.h>
|
|
#include <AzCore/std/parallel/thread.h>
|
|
#include <AzCore/std/smart_ptr/unique_ptr.h>
|
|
|
|
#include <AudioRingBuffer.h>
|
|
#include <IAudioInterfacesCommonData.h>
|
|
|
|
#if defined(USE_LIBSAMPLERATE)
|
|
#include <samplerate.h>
|
|
#endif // USE_LIBSAMPLERATE
|
|
|
|
namespace Audio
|
|
{
|
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
class MicrophoneSystemComponentWindows : public MicrophoneSystemComponent::Implementation
|
|
{
|
|
public:
|
|
AZ_CLASS_ALLOCATOR(MicrophoneSystemComponentWindows, AZ::SystemAllocator, 0);
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////
|
|
bool InitializeDevice() override
|
|
{
|
|
AZ_TracePrintf("WindowsMicrophone", "Initializing Microphone device - Windows!!\n");
|
|
|
|
// Assert: m_enumerator, m_device, m_audioClient, m_audioCaptureClient are all nullptr!
|
|
AZ_Assert(!m_enumerator && !m_device && !m_audioClient && !m_audioCaptureClient, "InitializeDevice - One or more pointers are not null before init!\n");
|
|
|
|
// This Gem initializes very early, before Qt application is booted.
|
|
// Qt calls OleInitialize internally, which initializes COM with Apartment Threaded model.
|
|
// To avoid errors, we initialize COM here with the same model.
|
|
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
|
|
|
const CLSID CLSID_MMDeviceEnumerator_UUID = __uuidof(MMDeviceEnumerator);
|
|
const IID IID_IMMDeviceEnumerator_UUID = __uuidof(IMMDeviceEnumerator);
|
|
HRESULT hresult = CoCreateInstance(
|
|
CLSID_MMDeviceEnumerator_UUID, nullptr,
|
|
CLSCTX_ALL, IID_IMMDeviceEnumerator_UUID,
|
|
reinterpret_cast<void**>(&m_enumerator)
|
|
);
|
|
|
|
if (FAILED(hresult))
|
|
{
|
|
AZ_Error("WindowsMicrophone", false, "Failed to create an MMDeviceEnumerator!\n");
|
|
return false;
|
|
}
|
|
|
|
if (m_enumerator)
|
|
{
|
|
hresult = m_enumerator->GetDefaultAudioEndpoint(EDataFlow::eCapture, ERole::eConsole, &m_device);
|
|
|
|
if (!m_device || hresult == ERROR_NOT_FOUND)
|
|
{
|
|
AZ_Warning("WindowsMicrophone", false, "No Microphone Device found!\n");
|
|
return false;
|
|
}
|
|
|
|
if (FAILED(hresult))
|
|
{
|
|
AZ_Error("WindowsMicrophone", false, "HRESULT %d received while getting the default endpoint!\n", hresult);
|
|
return false;
|
|
}
|
|
|
|
IPropertyStore* m_deviceProps = nullptr;
|
|
hresult = m_device->OpenPropertyStore(STGM_READ, &m_deviceProps);
|
|
|
|
if (FAILED(hresult))
|
|
{
|
|
AZ_Warning("WindowsMicrophone", false, "Failed to open the enpoint device's properties!\n");
|
|
// not a full failure
|
|
}
|
|
|
|
PROPVARIANT endpointName;
|
|
PropVariantInit(&endpointName);
|
|
hresult = m_deviceProps->GetValue(PKEY_Device_FriendlyName, &endpointName);
|
|
|
|
if (FAILED(hresult))
|
|
{
|
|
AZ_Warning("WindowsMicrophone", false, "Failed to get the endpoint device's friendly name!\n");
|
|
// not a full failure
|
|
}
|
|
else
|
|
{
|
|
AZ_TracePrintf("WindowsMicrophone", "Microphone Endpoint Device Initialized: %S\n", endpointName.pwszVal);
|
|
m_deviceName = endpointName.pwszVal;
|
|
}
|
|
|
|
PropVariantClear(&endpointName);
|
|
if (m_deviceProps)
|
|
{
|
|
m_deviceProps->Release();
|
|
m_deviceProps = nullptr;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////
|
|
void ShutdownDevice() override
|
|
{
|
|
AZ_TracePrintf("WindowsMicrophone", "Shutting down Microphone device - Windows!\n");
|
|
|
|
// Assert: m_audioClient and m_audioCaptureClient are both nullptr! (i.e. the capture thread is not running)
|
|
AZ_Assert(!m_audioClient && !m_audioCaptureClient, "ShutdownDevice - Audio Client pointers are not null! You need to call EndSession first!\n");
|
|
|
|
if (m_device)
|
|
{
|
|
m_device->Release();
|
|
m_device = nullptr;
|
|
}
|
|
if (m_enumerator)
|
|
{
|
|
m_enumerator->Release();
|
|
m_enumerator = nullptr;
|
|
}
|
|
|
|
CoUninitialize();
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////
|
|
bool StartSession() override
|
|
{
|
|
AZ_TracePrintf("WindowsMicrophone", "Starting Microphone session - Windows!\n");
|
|
AZ_Assert(m_device != nullptr, "Attempting to start a Microphone session while the device is uninitialized - Windows!\n");
|
|
|
|
// Get the IAudioClient from the device
|
|
const IID IID_IAudioClient_UUID = __uuidof(IAudioClient);
|
|
HRESULT hresult = m_device->Activate(IID_IAudioClient_UUID, CLSCTX_ALL, nullptr, reinterpret_cast<void**>(&m_audioClient));
|
|
|
|
if (FAILED(hresult))
|
|
{
|
|
AZ_Error("WindowsMicrophone", false, "Failed to get an IAudioClient on the device - Windows!\n");
|
|
return false;
|
|
}
|
|
|
|
// Get the mix format of the IAudioClient
|
|
hresult = m_audioClient->GetMixFormat(&m_streamFormat);
|
|
|
|
if (FAILED(hresult))
|
|
{
|
|
AZ_Error("WindowsMicrophone", false, "Failed to get the mix format from the IAudioClient - Windows!\n");
|
|
return false;
|
|
}
|
|
|
|
// Initialize the IAudioClient
|
|
// Note: REFERENCE_TIME = 100 nanoseconds (1e2)
|
|
// --> 1e9 ns = 1 sec
|
|
// --> 1e7 (REFTIMES_PER_SEC) * 1e2 (REFERENCE_TIME) ns = 1 sec
|
|
const AZ::u64 REFTIMES_PER_SEC = 10000000;
|
|
REFERENCE_TIME duration = REFTIMES_PER_SEC;
|
|
hresult = m_audioClient->Initialize(
|
|
AUDCLNT_SHAREMODE_SHARED, // Share Mode
|
|
0, // Stream Flags
|
|
duration, // Buffer Duration
|
|
0, // Periodicity
|
|
m_streamFormat, // Wave Format Ex
|
|
nullptr // Audio Session GUID
|
|
);
|
|
|
|
if (FAILED(hresult))
|
|
{
|
|
AZ_Error("WindowsMicrophone", false, "Failed to initialize the IAudioClient - Windows!\n");
|
|
return false;
|
|
}
|
|
|
|
// Get size of the allocated buffer
|
|
hresult = m_audioClient->GetBufferSize(&m_bufferFrameCount);
|
|
|
|
if (FAILED(hresult))
|
|
{
|
|
AZ_Error("WindowsMicrophone", false, "Failed to get the buffer size of the IAudioClient - Windows!\n");
|
|
return false;
|
|
}
|
|
|
|
// Get the IAudioCaptureClient
|
|
const IID IID_IAudioCaptureClient_UUID = __uuidof(IAudioCaptureClient);
|
|
hresult = m_audioClient->GetService(IID_IAudioCaptureClient_UUID, reinterpret_cast<void**>(&m_audioCaptureClient));
|
|
|
|
if (FAILED(hresult))
|
|
{
|
|
// Some possible results: (hresult == E_NOINTERFACE || hresult == AUDCLNT_E_NOT_INITIALIZED || hresult == AUDCLNT_E_WRONG_ENDPOINT_TYPE)
|
|
AZ_Error("WindowsMicrophone", false, "Failed to get an IAudioCaptureClient service interface - Windows!\n");
|
|
return false;
|
|
}
|
|
|
|
// Set format for internal sink
|
|
SetFormatInternal(m_bufferFrameCount);
|
|
|
|
if (!ValidateFormatInternal())
|
|
{
|
|
AZ_Error("WindowsMicrophone", false, "Failed to set a supported format - Windows!\n");
|
|
return false;
|
|
}
|
|
|
|
AllocateBuffersInternal();
|
|
|
|
m_bufferDuration = static_cast<double>(REFTIMES_PER_SEC) * (m_bufferFrameCount / m_streamFormat->nSamplesPerSec);
|
|
|
|
// Start recording!
|
|
hresult = m_audioClient->Start();
|
|
|
|
if (FAILED(hresult))
|
|
{
|
|
AZ_Error("WindowsMicrophone", false, "Failed to start Microphone recording - Windows!\n");
|
|
return false;
|
|
}
|
|
|
|
// Spawn a new thread with the basic capture loop: [GetNextPacketSize, GetBuffer, CopyData, ReleaseBuffer]
|
|
m_capturing = true;
|
|
AZStd::thread_desc threadDesc;
|
|
threadDesc.m_name = "MicrophoneCapture-WASAPI";
|
|
auto captureFunc = AZStd::bind(&MicrophoneSystemComponentWindows::RunAudioCapture, this);
|
|
m_captureThread = AZStd::thread(threadDesc, captureFunc);
|
|
|
|
return true;
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////
|
|
void RunAudioCapture()
|
|
{
|
|
const AZ::u64 REFTIMES_PER_MILLISEC = 10000;
|
|
AZ::u32 numFramesAvailable = 0;
|
|
AZ::u32 packetLength = 0;
|
|
DWORD bufferFlags = 0;
|
|
|
|
AZ::u8* data = nullptr;
|
|
|
|
while (m_capturing)
|
|
{
|
|
AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(static_cast<AZ::u64>(m_bufferDuration) / REFTIMES_PER_MILLISEC / 2));
|
|
|
|
HRESULT hresult = m_audioCaptureClient->GetNextPacketSize(&packetLength);
|
|
|
|
if (FAILED(hresult))
|
|
{
|
|
AZ_Error("WindowsMicrophone", false, "Failed to GetNextPacketSize, ending thread - Windows!\n");
|
|
m_capturing = false;
|
|
continue;
|
|
}
|
|
|
|
while (m_capturing && packetLength != 0)
|
|
{
|
|
bufferFlags = 0;
|
|
hresult = m_audioCaptureClient->GetBuffer(&data, &numFramesAvailable, &bufferFlags, nullptr, nullptr);
|
|
|
|
if (FAILED(hresult))
|
|
{
|
|
AZ_Error("WindowsMicrophone", false, "Failed to GetBuffer, ending thread - Windows!\n");
|
|
packetLength = 0;
|
|
m_capturing = false;
|
|
continue;
|
|
}
|
|
|
|
if (bufferFlags & AUDCLNT_BUFFERFLAGS_SILENT)
|
|
{
|
|
data = nullptr; // signals internal buffer to write silence
|
|
}
|
|
|
|
if (!CopyDataInternal(data, numFramesAvailable))
|
|
{
|
|
numFramesAvailable = 0;
|
|
}
|
|
|
|
hresult = m_audioCaptureClient->ReleaseBuffer(numFramesAvailable);
|
|
|
|
if (FAILED(hresult))
|
|
{
|
|
AZ_Error("WindowsMicrophone", false, "Failed to ReleaseBuffer, ending thread - Windows!\n");
|
|
packetLength = 0;
|
|
m_capturing = false;
|
|
continue;
|
|
}
|
|
|
|
hresult = m_audioCaptureClient->GetNextPacketSize(&packetLength);
|
|
|
|
if (FAILED(hresult))
|
|
{
|
|
AZ_Error("WindowsMicrophone", false, "Failed to GetNextPacketSize, ending thread - Windows!\n");
|
|
packetLength = 0;
|
|
m_capturing = false;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Any post-thread cleanup?
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////
|
|
void EndSession() override
|
|
{
|
|
AZ_TracePrintf("WindowsMicrophone", "Ending Microphone session - Windows!\n");
|
|
|
|
// Signal thread to end
|
|
m_capturing = false;
|
|
// Thread join
|
|
if (m_captureThread.joinable())
|
|
{
|
|
m_captureThread.join();
|
|
m_captureThread = AZStd::thread(); // destroy
|
|
}
|
|
|
|
AZ_TracePrintf("WindowsMicrophone", "Microphone capture thread ended - Windows!");
|
|
|
|
if (m_audioClient)
|
|
{
|
|
// Stop recording!
|
|
HRESULT hresult = m_audioClient->Stop();
|
|
|
|
if (FAILED(hresult))
|
|
{
|
|
AZ_Error("WindowsMicrophone", false, "Failed to stop Microphone recording - Windows!\n");
|
|
}
|
|
}
|
|
|
|
if (m_audioCaptureClient)
|
|
{
|
|
m_audioCaptureClient->Release();
|
|
m_audioCaptureClient = nullptr;
|
|
}
|
|
if (m_audioClient)
|
|
{
|
|
m_audioClient->Release();
|
|
m_audioClient = nullptr;
|
|
}
|
|
CoTaskMemFree(m_streamFormat);
|
|
m_streamFormat = nullptr;
|
|
|
|
DeallocateBuffersInternal();
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////
|
|
bool IsCapturing() override
|
|
{
|
|
return m_capturing;
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////
|
|
SAudioInputConfig GetFormatConfig() const override
|
|
{
|
|
return m_config;
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////
|
|
// Returns the number of sample frames obtained
|
|
AZStd::size_t GetData(void** outputData, AZStd::size_t numFrames, const SAudioInputConfig& targetConfig, bool shouldDeinterleave) override
|
|
{
|
|
bool changeSampleType = (targetConfig.m_sampleType != m_config.m_sampleType);
|
|
bool changeSampleRate = (targetConfig.m_sampleRate != m_config.m_sampleRate);
|
|
bool changeNumChannels = (targetConfig.m_numChannels != m_config.m_numChannels);
|
|
|
|
#if defined(USE_LIBSAMPLERATE)
|
|
bool micFormatIsInt = (m_config.m_sampleType == AudioInputSampleType::Int);
|
|
bool targetFormatIsInt = (targetConfig.m_sampleType == AudioInputSampleType::Int);
|
|
bool stereoToMono = (targetConfig.m_numChannels == 1 && m_config.m_numChannels == 2);
|
|
|
|
// Handle the default no-change case
|
|
if (!(changeSampleType || changeSampleRate || changeNumChannels))
|
|
{
|
|
return m_captureData->ConsumeData(outputData, numFrames, m_config.m_numChannels, shouldDeinterleave);
|
|
}
|
|
|
|
// Consume mic data into the 'working' conversion buffer (In)...
|
|
numFrames = m_captureData->ConsumeData(reinterpret_cast<void**>(&m_conversionBufferIn.m_data), numFrames, m_config.m_numChannels, false);
|
|
|
|
if (micFormatIsInt && (changeSampleType || changeSampleRate || changeNumChannels))
|
|
{
|
|
// Do a prep [Int]-->[Float] conversion...
|
|
src_short_to_float_array(
|
|
reinterpret_cast<AZ::s16*>(m_conversionBufferIn.m_data),
|
|
reinterpret_cast<float*>(m_conversionBufferOut.m_data),
|
|
static_cast<int>(numFrames * m_config.m_numChannels)
|
|
);
|
|
|
|
// Swap to move the 'working' buffer back to the 'In' buffer.
|
|
AZStd::swap(m_conversionBufferIn.m_data, m_conversionBufferOut.m_data);
|
|
}
|
|
|
|
if (changeSampleRate)
|
|
{
|
|
if (m_srcState && targetConfig.m_sampleRate < m_config.m_sampleRate)
|
|
{
|
|
// Setup Conversion Data
|
|
m_srcData.end_of_input = 0;
|
|
m_srcData.input_frames = static_cast<long>(numFrames);
|
|
m_srcData.output_frames = static_cast<long>(numFrames);
|
|
m_srcData.data_in = reinterpret_cast<float*>(m_conversionBufferIn.m_data);
|
|
m_srcData.data_out = reinterpret_cast<float*>(m_conversionBufferOut.m_data);
|
|
|
|
// Conversion ratio is output_sample_rate / input_sample_rate
|
|
m_srcData.src_ratio = static_cast<double>(targetConfig.m_sampleRate) / static_cast<double>(m_config.m_sampleRate);
|
|
|
|
// Process
|
|
int error = src_process(m_srcState, &m_srcData);
|
|
if (error != 0)
|
|
{
|
|
AZ_TracePrintf("WindowsMicrophone", "SRC(src_process): %s - Windows!\n", src_strerror(error));
|
|
}
|
|
|
|
AZ_Warning("WindowsMicrophone", numFrames == m_srcData.input_frames_used,
|
|
"SRC(src_process): Num Frames requested (%u) was different than Num Frames processed (%u) - Windows!\n",
|
|
numFrames, m_srcData.input_frames_used);
|
|
|
|
numFrames = m_srcData.output_frames_gen;
|
|
|
|
// Swap to move the 'working' buffer back to the 'In' buffer.
|
|
AZStd::swap(m_conversionBufferIn.m_data, m_conversionBufferOut.m_data);
|
|
}
|
|
else
|
|
{
|
|
// Unable to continue: Either upsampling is requested or there is no resampler state.
|
|
// todo: Upsampling could be done if we reallocate the conversion buffers. Right now we assume a max size of the buffer
|
|
// based on the size of the ringbuffer. If samplerate is to increase, those buffers would need to increase in size
|
|
// too. GetData is the only point which we know the target samplerate.
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
if (changeNumChannels)
|
|
{
|
|
if (stereoToMono)
|
|
{
|
|
// Samples are interleaved now, copy only left channel to the output
|
|
float* bufferInputData = reinterpret_cast<float*>(m_conversionBufferIn.m_data);
|
|
float* bufferOutputData = reinterpret_cast<float*>(m_conversionBufferOut.m_data);
|
|
for (AZ::u32 frame = 0; frame < numFrames; ++frame)
|
|
{
|
|
bufferOutputData[frame] = *bufferInputData++;
|
|
++bufferInputData;
|
|
}
|
|
}
|
|
else // monoToStereo
|
|
{
|
|
// Split single samples to both left and right channels
|
|
if (shouldDeinterleave)
|
|
{
|
|
float* bufferInputData = reinterpret_cast<float*>(m_conversionBufferIn.m_data);
|
|
float** bufferOutputData = reinterpret_cast<float**>(m_conversionBufferOut.m_data);
|
|
for (AZ::u32 frame = 0; frame < numFrames; ++frame)
|
|
{
|
|
bufferOutputData[0][frame] = bufferOutputData[1][frame] = bufferInputData[frame];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
float* bufferInputData = reinterpret_cast<float*>(m_conversionBufferIn.m_data);
|
|
float* bufferOutputData = reinterpret_cast<float*>(m_conversionBufferOut.m_data);
|
|
for (AZ::u32 frame = 0; frame < numFrames; ++frame)
|
|
{
|
|
*bufferOutputData++ = bufferInputData[frame];
|
|
*bufferOutputData++ = bufferInputData[frame];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Swap to move the 'working' buffer back to the 'In' buffer.
|
|
AZStd::swap(m_conversionBufferIn.m_data, m_conversionBufferOut.m_data);
|
|
}
|
|
|
|
if (targetFormatIsInt)
|
|
{
|
|
// Do a final [Float]-->[Int] conversion...
|
|
src_float_to_short_array(
|
|
reinterpret_cast<float*>(m_conversionBufferIn.m_data),
|
|
*reinterpret_cast<AZ::s16**>(outputData),
|
|
static_cast<int>(numFrames * m_config.m_numChannels)
|
|
);
|
|
}
|
|
else
|
|
{
|
|
// Otherwise, we're done, just memcpy the 'working' buffer to the output.
|
|
::memcpy(outputData, m_conversionBufferIn.m_data, numFrames * targetConfig.m_numChannels * (targetConfig.m_bitsPerSample >> 3));
|
|
}
|
|
|
|
return numFrames;
|
|
|
|
#else
|
|
|
|
if (changeSampleType || changeSampleRate || changeNumChannels)
|
|
{
|
|
// Without the SRC library, any change is unsupported!
|
|
return 0;
|
|
}
|
|
else
|
|
{
|
|
// No change to the data from Input to Output
|
|
return m_captureData->ConsumeData(outputData, numFrames, m_config.m_numChannels, shouldDeinterleave);
|
|
}
|
|
|
|
#endif // USE_LIBSAMPLERATE
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////
|
|
void SetFormatInternal(AZ::u32 bufferFrameCount)
|
|
{
|
|
if (m_streamFormat->wFormatTag == WAVE_FORMAT_EXTENSIBLE)
|
|
{
|
|
auto streamFormatExt = reinterpret_cast<WAVEFORMATEXTENSIBLE*>(m_streamFormat);
|
|
if (streamFormatExt)
|
|
{
|
|
if (streamFormatExt->SubFormat == KSDATAFORMAT_SUBTYPE_PCM)
|
|
{
|
|
AZ_TracePrintf("WindowsMicrophone", "PCM Format - Windows!\n");
|
|
m_config.m_sampleType = AudioInputSampleType::Int;
|
|
}
|
|
else if (streamFormatExt->SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT)
|
|
{
|
|
AZ_TracePrintf("WindowsMicrophone", "IEEE Float Format - Windows!\n");
|
|
m_config.m_sampleType = AudioInputSampleType::Float;
|
|
}
|
|
else
|
|
{
|
|
m_config.m_sampleType = AudioInputSampleType::Unsupported;
|
|
}
|
|
|
|
if (streamFormatExt->dwChannelMask == KSAUDIO_SPEAKER_MONO)
|
|
{
|
|
AZ_TracePrintf("WindowsMicrophone", "Channel Format: Mono - Windows!\n");
|
|
m_config.m_numChannels = 1;
|
|
}
|
|
else if (streamFormatExt->dwChannelMask == KSAUDIO_SPEAKER_STEREO)
|
|
{
|
|
AZ_TracePrintf("WindowsMicrophone", "Channel Format: Stereo - Windows!\n");
|
|
m_config.m_numChannels = 2;
|
|
}
|
|
else
|
|
{
|
|
AZ_Error("WindowsMicrophone", false, "Only Mono and Stereo microphone inputs are supported - Windows!\n");
|
|
m_config.m_numChannels = 0;
|
|
}
|
|
|
|
m_config.m_sampleRate = m_streamFormat->nSamplesPerSec;
|
|
m_config.m_bitsPerSample = m_streamFormat->wBitsPerSample;
|
|
|
|
AZ_TracePrintf("WindowsMicrophone", "Sample Rate: %u - Windows!\n", m_config.m_sampleRate);
|
|
AZ_TracePrintf("WindowsMicrophone", "Bits Per Sample: %u - Windows!\n", m_config.m_bitsPerSample);
|
|
|
|
m_config.m_sourceType = Audio::AudioInputSourceType::Microphone;
|
|
m_config.SetBufferSizeFromFrameCount(bufferFrameCount);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Untested code path.
|
|
// Every device I tested went through the wave format extensible path...
|
|
if (m_streamFormat->wFormatTag == WAVE_FORMAT_PCM)
|
|
{
|
|
m_config.m_sampleType = AudioInputSampleType::Int;
|
|
}
|
|
else if (m_streamFormat->wFormatTag == WAVE_FORMAT_IEEE_FLOAT)
|
|
{
|
|
m_config.m_sampleType = AudioInputSampleType::Float;
|
|
}
|
|
else
|
|
{
|
|
m_config.m_sampleType = AudioInputSampleType::Unsupported;
|
|
}
|
|
|
|
m_config.m_numChannels = m_streamFormat->nChannels;
|
|
m_config.m_sampleRate = m_streamFormat->nSamplesPerSec;
|
|
m_config.m_bitsPerSample = m_streamFormat->wBitsPerSample;
|
|
m_config.SetBufferSizeFromFrameCount(bufferFrameCount);
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////
|
|
bool ValidateFormatInternal()
|
|
{
|
|
bool valid = true;
|
|
if (m_config.m_numChannels < 1 || m_config.m_numChannels > 2)
|
|
{
|
|
AZ_TracePrintf("WindowsMicrophone", "Only Mono and Stereo Microphone inputs are supported - Windows!\n");
|
|
valid = false;
|
|
}
|
|
|
|
if (m_config.m_sampleType == AudioInputSampleType::Unsupported)
|
|
{
|
|
AZ_TracePrintf("WindowsMicrophone", "Unsupported sample format detected - Windows!\n");
|
|
valid = false;
|
|
}
|
|
|
|
if (m_config.m_sampleType == AudioInputSampleType::Int && m_config.m_bitsPerSample != 16)
|
|
{
|
|
AZ_TracePrintf("WindowsMicrophone", "Only bitdepths of 16 bits are supported with integer samples - Windows!\n");
|
|
valid = false;
|
|
}
|
|
|
|
if (m_config.m_bufferSize == 0)
|
|
{
|
|
AZ_TracePrintf("WindowsMicrophone", "Buffer size for the Microphone input has not been set - Windows!\n");
|
|
valid = false;
|
|
}
|
|
|
|
return valid;
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////
|
|
void AllocateBuffersInternal()
|
|
{
|
|
AZ_Assert(m_config.m_bufferSize > 0, "Format was checked already, but buffer size of the Microhpone input is zero - Windows!\n");
|
|
|
|
DeallocateBuffersInternal();
|
|
|
|
if (m_config.m_sampleType == AudioInputSampleType::Float)
|
|
{
|
|
AZ_Assert(m_config.m_bitsPerSample == 32, "Format was checked already, but non-32-bit float samples are detected - Windows!\n");
|
|
m_captureData.reset(aznew Audio::RingBuffer<float>(m_config.GetSampleCountFromBufferSize()));
|
|
}
|
|
else if (m_config.m_sampleType == AudioInputSampleType::Int)
|
|
{
|
|
AZ_Assert(m_config.m_bitsPerSample == 16, "Format was checked already, but non-16-bit integer samples are detected - Windows!\n");
|
|
m_captureData.reset(aznew Audio::RingBuffer<AZ::s16>(m_config.GetSampleCountFromBufferSize()));
|
|
}
|
|
|
|
#if defined(USE_LIBSAMPLERATE)
|
|
// New SRC State
|
|
if (!m_srcState)
|
|
{
|
|
int error = 0;
|
|
m_srcState = src_new(SRC_SINC_MEDIUM_QUALITY, m_config.m_numChannels, &error);
|
|
if (m_srcState)
|
|
{
|
|
AZStd::size_t conversionBufferMaxSize = m_config.GetSampleCountFromBufferSize() * sizeof(float); // Use this because float is the biggest sample type.
|
|
|
|
m_conversionBufferIn.m_data = static_cast<AZ::u8*>(azmalloc(conversionBufferMaxSize, MEMORY_ALLOCATION_ALIGNMENT, AZ::SystemAllocator));
|
|
m_conversionBufferIn.m_sizeBytes = conversionBufferMaxSize;
|
|
|
|
m_conversionBufferOut.m_data = static_cast<AZ::u8*>(azmalloc(conversionBufferMaxSize, MEMORY_ALLOCATION_ALIGNMENT, AZ::SystemAllocator));
|
|
m_conversionBufferOut.m_sizeBytes = conversionBufferMaxSize;
|
|
|
|
::memset(m_conversionBufferIn.m_data, 0, m_conversionBufferIn.m_sizeBytes);
|
|
::memset(m_conversionBufferOut.m_data, 0, m_conversionBufferOut.m_sizeBytes);
|
|
}
|
|
else
|
|
{
|
|
AZ_TracePrintf("WindowsMicrophone", "SRC(src_new): %s - Windows!\n", src_strerror(error));
|
|
}
|
|
}
|
|
#endif // USE_LIBSAMPLERATE
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////
|
|
void DeallocateBuffersInternal()
|
|
{
|
|
if (m_captureData)
|
|
{
|
|
m_captureData.reset();
|
|
}
|
|
|
|
#if defined(USE_LIBSAMPLERATE)
|
|
// Cleanup SRC State
|
|
m_srcState = src_delete(m_srcState);
|
|
m_srcState = nullptr;
|
|
|
|
if (m_conversionBufferIn.m_data)
|
|
{
|
|
azfree(m_conversionBufferIn.m_data, AZ::SystemAllocator);
|
|
m_conversionBufferIn.m_data = nullptr;
|
|
m_conversionBufferIn.m_sizeBytes = 0;
|
|
}
|
|
|
|
if (m_conversionBufferOut.m_data)
|
|
{
|
|
azfree(m_conversionBufferOut.m_data, AZ::SystemAllocator);
|
|
m_conversionBufferOut.m_data = nullptr;
|
|
m_conversionBufferOut.m_sizeBytes = 0;
|
|
}
|
|
#endif // USE_LIBSAMPLERATE
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////
|
|
bool CopyDataInternal(const AZ::u8* inputData, AZStd::size_t numFrames)
|
|
{
|
|
// This function should return false if unable to copy all the frames! That way we can pass 0 to ReleaseBuffer to signal
|
|
// that the buffer was not consumed yet. The api states that between GetBuffer and corresponding ReleaseBuffer calls, you
|
|
// need to read all of it or none of it. If unable to copy the entire buffer, calling ReleaseBuffer with 0 means that the
|
|
// next call to GetBuffer will return the same buffer that wasn't consumed.
|
|
|
|
AZStd::size_t numFramesCopied = m_captureData->AddData(inputData, numFrames, m_config.m_numChannels);
|
|
|
|
return numFramesCopied > 0;
|
|
}
|
|
|
|
private:
|
|
// Device and format data...
|
|
IAudioClient* m_audioClient = nullptr;
|
|
IAudioCaptureClient* m_audioCaptureClient = nullptr;
|
|
IMMDevice* m_device = nullptr;
|
|
IMMDeviceEnumerator* m_enumerator = nullptr;
|
|
WAVEFORMATEX* m_streamFormat = nullptr;
|
|
|
|
// Thread data...
|
|
AZStd::atomic<bool> m_capturing = false;
|
|
AZStd::wstring m_deviceName;
|
|
AZStd::thread m_captureThread;
|
|
|
|
// Wasapi buffer data...
|
|
double m_bufferDuration = 0; // in REFTIMES_PER_SEC (1e7)
|
|
AZ::u32 m_bufferFrameCount = 0; // number of frames the Wasapi buffer holds
|
|
|
|
Audio::SAudioInputConfig m_config; // the format configuration
|
|
|
|
AZStd::unique_ptr<Audio::RingBufferBase> m_captureData = nullptr;
|
|
|
|
#if defined(USE_LIBSAMPLERATE)
|
|
// Sample Rate Converter data...
|
|
SRC_STATE* m_srcState = nullptr;
|
|
SRC_DATA m_srcData;
|
|
AudioStreamData m_conversionBufferIn;
|
|
AudioStreamData m_conversionBufferOut;
|
|
#endif // USE_LIBSAMPLERATE
|
|
};
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
MicrophoneSystemComponent::Implementation* MicrophoneSystemComponent::Implementation::Create()
|
|
{
|
|
return aznew MicrophoneSystemComponentWindows();
|
|
}
|
|
} // namespace Audio
|