Визуализация пространственного звука с помощью пространственных звуковых объектов
В этой статье представлены некоторые простые примеры, демонстрирующие реализацию пространственного звука с помощью статических пространственных звуковых объектов, динамических пространственных звуковых объектов и пространственных звуковых объектов, использующих функцию относительной передачи головы (HRTF). Шаги реализации для всех трех этих методов очень похожи, и эта статья содержит аналогичный структурированный пример кода для каждого метода. Полные примеры реализаций пространственного звука в реальном мире см . в репозитории GitHub примеров пространственных звуков Майкрософт. Обзор решения на уровне платформы Windows Sonic для поддержки пространственного звука в Xbox и Windows см. в статье "Пространственный звук".
Визуализация звука с помощью статических пространственных звуковых объектов
Статический звуковой объект используется для отображения звука в одном из 18 статических звуковых каналов, определенных в перечислении AudioObjectType. Каждый из этих каналов представляет реальный или виртуализированный динамик в фиксированной точке в пространстве, который не перемещается со временем. Статические каналы, доступные на определенном устройстве, зависят от используемого пространственного формата звука. Список поддерживаемых форматов и их количества каналов см. в разделе "Пространственный звук".
При инициализации пространственного аудиопотока необходимо указать, какие из доступных статических каналов будет использоваться поток. В следующем примере определения констант можно использовать для указания общих конфигураций динамиков и получения количества каналов, доступных для каждого из них.
const AudioObjectType ChannelMask_Mono = AudioObjectType_FrontCenter;
const AudioObjectType ChannelMask_Stereo = (AudioObjectType)(AudioObjectType_FrontLeft | AudioObjectType_FrontRight);
const AudioObjectType ChannelMask_2_1 = (AudioObjectType)(ChannelMask_Stereo | AudioObjectType_LowFrequency);
const AudioObjectType ChannelMask_Quad = (AudioObjectType)(AudioObjectType_FrontLeft | AudioObjectType_FrontRight | AudioObjectType_BackLeft | AudioObjectType_BackRight);
const AudioObjectType ChannelMask_4_1 = (AudioObjectType)(ChannelMask_Quad | AudioObjectType_LowFrequency);
const AudioObjectType ChannelMask_5_1 = (AudioObjectType)(AudioObjectType_FrontLeft | AudioObjectType_FrontRight | AudioObjectType_FrontCenter | AudioObjectType_LowFrequency | AudioObjectType_SideLeft | AudioObjectType_SideRight);
const AudioObjectType ChannelMask_7_1 = (AudioObjectType)(ChannelMask_5_1 | AudioObjectType_BackLeft | AudioObjectType_BackRight);
const UINT32 MaxStaticObjectCount_7_1_4 = 12;
const AudioObjectType ChannelMask_7_1_4 = (AudioObjectType)(ChannelMask_7_1 | AudioObjectType_TopFrontLeft | AudioObjectType_TopFrontRight | AudioObjectType_TopBackLeft | AudioObjectType_TopBackRight);
const UINT32 MaxStaticObjectCount_7_1_4_4 = 16;
const AudioObjectType ChannelMask_7_1_4_4 = (AudioObjectType)(ChannelMask_7_1_4 | AudioObjectType_BottomFrontLeft | AudioObjectType_BottomFrontRight |AudioObjectType_BottomBackLeft | AudioObjectType_BottomBackRight);
const UINT32 MaxStaticObjectCount_8_1_4_4 = 17;
const AudioObjectType ChannelMask_8_1_4_4 = (AudioObjectType)(ChannelMask_7_1_4_4 | AudioObjectType_BackCenter);
Первым шагом в отрисовке пространственного звука является получение конечной точки звука, в которую будут отправляться звуковые данные. Создайте экземпляр MMDeviceEnumerator и вызовите GetDefaultAudioEndpoint , чтобы получить устройство отрисовки звука по умолчанию.
HRESULT hr;
Microsoft::WRL::ComPtr<IMMDeviceEnumerator> deviceEnum;
Microsoft::WRL::ComPtr<IMMDevice> defaultDevice;
hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), (void**)&deviceEnum);
hr = deviceEnum->GetDefaultAudioEndpoint(EDataFlow::eRender, eMultimedia, &defaultDevice);
При создании пространственного аудиопотока необходимо указать формат звука, который будет использоваться, предоставив структуру WAVEFORMATEX . При воспроизведении звука из файла формат обычно определяется форматом аудиофайла. В этом примере используется формат mono, 32-разрядная, 48Гц.
WAVEFORMATEX format;
format.wFormatTag = WAVE_FORMAT_IEEE_FLOAT;
format.wBitsPerSample = 32;
format.nChannels = 1;
format.nSamplesPerSec = 48000;
format.nBlockAlign = (format.wBitsPerSample >> 3) * format.nChannels;
format.nAvgBytesPerSec = format.nBlockAlign * format.nSamplesPerSec;
format.cbSize = 0;
Следующим шагом в отрисовке пространственного звука является инициализация пространственного аудиопотока. Сначала получите экземпляр ISpatialAudioClient, вызвав IMMDevice::Activate. Вызовите ISpatialAudioClient::IsAudioObjectFormatSupported , чтобы убедиться, что используется звуковой формат, который вы используете. Создайте событие, которое будет использоваться в звуковом конвейере, чтобы уведомить приложение о том, что оно готово для получения дополнительных звуковых данных.
Заполните структуру SpatialAudioObjectRenderStreamActivationParams , которая будет использоваться для инициализации пространственного аудиопотока. В этом примере поле StaticObjectTypeMask имеет значение ChannelMask_Stereo константы, определенной ранее в этой статье, то есть только передние и левые каналы могут использоваться аудиопотоком. Так как в этом примере используются только статические звуковые объекты и не используются динамические объекты, поле MaxDynamicObjectCount имеет значение 0. В поле "Категория " задан элемент перечисления AUDIO_STREAM_CATEGORY , который определяет, как система смешает звук из этого потока с другими источниками звука.
Вызовите ISpatialAudioClient::ActivateSpatialAudioStream для активации потока.
Microsoft::WRL::ComPtr<ISpatialAudioClient> spatialAudioClient;
// Activate ISpatialAudioClient on the desired audio-device
hr = defaultDevice->Activate(__uuidof(ISpatialAudioClient), CLSCTX_INPROC_SERVER, nullptr, (void**)&spatialAudioClient);
hr = spatialAudioClient->IsAudioObjectFormatSupported(&format);
// Create the event that will be used to signal the client for more data
HANDLE bufferCompletionEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
SpatialAudioObjectRenderStreamActivationParams streamParams;
streamParams.ObjectFormat = &format;
streamParams.StaticObjectTypeMask = ChannelMask_Stereo;
streamParams.MinDynamicObjectCount = 0;
streamParams.MaxDynamicObjectCount = 0;
streamParams.Category = AudioCategory_SoundEffects;
streamParams.EventHandle = bufferCompletionEvent;
streamParams.NotifyObject = nullptr;
PROPVARIANT activationParams;
PropVariantInit(&activationParams);
activationParams.vt = VT_BLOB;
activationParams.blob.cbSize = sizeof(streamParams);
activationParams.blob.pBlobData = reinterpret_cast<BYTE *>(&streamParams);
Microsoft::WRL::ComPtr<ISpatialAudioObjectRenderStream> spatialAudioStream;
hr = spatialAudioClient->ActivateSpatialAudioStream(&activationParams, __uuidof(spatialAudioStream), (void**)&spatialAudioStream);
Примечание.
При использовании интерфейсов ISpatialAudioClient в заголовке набора средств разработки Xbox One (XDK) необходимо сначала вызвать EnableSpatialAudio перед вызовом IMMDeviceEnumerator::EnumAudioEndpoints или IMMDeviceEnumerator::GetDefaultAudioEndpoint. Сбой этого приведет к ошибке E_NOINTERFACE, возвращаемой из вызова активации. EnableSpatialAudio доступен только для заголовков XDK и не требуется вызывать для универсальная платформа Windows приложений, работающих на Xbox One, или для любых устройств, отличных от Xbox One.
Объявите указатель на ISpatialAudioObject, который будет использоваться для записи звуковых данных в статический канал. Типичные приложения будут использовать объект для каждого канала, указанного в поле StaticObjectTypeMask . Для простоты в этом примере используется только один статический звуковой объект.
// In this simple example, one object will be rendered
Microsoft::WRL::ComPtr<ISpatialAudioObject> audioObjectFrontLeft;
Перед вводом цикла отрисовки звука вызовите ISpatialAudioObjectRenderStream::Start , чтобы конвейер мультимедиа начал запрашивать звуковые данные. В этом примере счетчик используется для остановки отрисовки звука через 5 секунд.
В цикле отрисовки дождитесь события завершения буфера, предоставленного при инициализации пространственного звукового потока. При ожидании события следует задать разумный предел времени ожидания, например 100 мс, так как любое изменение типа отрисовки или конечной точки приведет к тому, что это событие никогда не будет сигнализировать. В этом случае можно вызвать ISpatialAudioObjectRenderStream::Reset , чтобы попытаться сбросить пространственный аудиопоток.
Затем вызовите ISpatialAudioObjectRenderStream::BeginUpdatingAudioObjects , чтобы сообщить системе, что вы готовы заполнить буферы звуковых объектов данными. Этот метод возвращает количество доступных динамических звуковых объектов, не используемых в этом примере, и количество кадров буфера для звуковых объектов, отображаемых этим потоком.
Если статический пространственный звуковой объект еще не создан, создайте один или несколько путем вызова ISpatialAudioObjectRenderStream::ActivateSpatialAudioObject, передавая значение из перечисления AudioObjectType , указывающего статический канал, на который отображается звук объекта.
Затем вызовите ISpatialAudioObject::GetBuffer , чтобы получить указатель на звуковой буфер пространственного звукового объекта. Этот метод также возвращает размер буфера в байтах. В этом примере используется вспомогательный метод WriteToAudioObjectBuffer для заполнения буфера звуковыми данными. Этот метод показан далее в этой статье. После записи в буфер пример проверка, чтобы узнать, достигнуто ли 5-секундное время существования объекта, и если да, вызывается метод ISpatialAudioObject::SetEndOfStream, чтобы сообщить звуковому конвейеру, что больше звука не будет записано с помощью этого объекта, и объект имеет значение NULLptr, чтобы освободить свои ресурсы.
После записи данных во все звуковые объекты вызовите ISpatialAudioObjectRenderStream::EndUpdatingAudioObjects , чтобы сообщить системе, что данные готовы к отрисовке. Вы можете вызывать GetBuffer только между вызовом BeginUpdatingAudioObjects и EndUpdatingAudioObjects.
// Start streaming / rendering
hr = spatialAudioStream->Start();
// This example will render 5 seconds of audio samples
UINT totalFrameCount = format.nSamplesPerSec * 5;
bool isRendering = true;
while (isRendering)
{
// Wait for a signal from the audio-engine to start the next processing pass
if (WaitForSingleObject(bufferCompletionEvent, 100) != WAIT_OBJECT_0)
{
hr = spatialAudioStream->Reset();
if (hr != S_OK)
{
// handle the error
break;
}
}
UINT32 availableDynamicObjectCount;
UINT32 frameCount;
// Begin the process of sending object data and metadata
// Get the number of dynamic objects that can be used to send object-data
// Get the frame count that each buffer will be filled with
hr = spatialAudioStream->BeginUpdatingAudioObjects(&availableDynamicObjectCount, &frameCount);
BYTE* buffer;
UINT32 bufferLength;
if (audioObjectFrontLeft == nullptr)
{
hr = spatialAudioStream->ActivateSpatialAudioObject(AudioObjectType::AudioObjectType_FrontLeft, &audioObjectFrontLeft);
if (hr != S_OK) break;
}
// Get the buffer to write audio data
hr = audioObjectFrontLeft->GetBuffer(&buffer, &bufferLength);
if (totalFrameCount >= frameCount)
{
// Write audio data to the buffer
WriteToAudioObjectBuffer(reinterpret_cast<float*>(buffer), frameCount, 200.0f, format.nSamplesPerSec);
totalFrameCount -= frameCount;
}
else
{
// Write audio data to the buffer
WriteToAudioObjectBuffer(reinterpret_cast<float*>(buffer), totalFrameCount, 750.0f, format.nSamplesPerSec);
// Set end of stream for the last buffer
hr = audioObjectFrontLeft->SetEndOfStream(totalFrameCount);
audioObjectFrontLeft = nullptr; // Release the object
isRendering = false;
}
// Let the audio engine know that the object data are available for processing now
hr = spatialAudioStream->EndUpdatingAudioObjects();
};
После завершения отрисовки пространственного звука остановите пространственный аудиопоток, вызвав ISpatialAudioObjectRenderStream::Stop. Если вы еще не собираетесь использовать поток, освободите ресурсы, вызвав ISpatialAudioObjectRenderStream::Reset.
// Stop the stream
hr = spatialAudioStream->Stop();
// Don't want to start again, so reset the stream to free its resources
hr = spatialAudioStream->Reset();
CloseHandle(bufferCompletionEvent);
Вспомогательный метод WriteToAudioObjectBuffer записывает полный буфер примеров или количество оставшихся выборок, указанных нашим приложением. Это также можно определить, например, по количеству примеров, оставшихся в исходном звуковом файле. Простая волна греха, частота которой масштабируется параметром ввода частоты , создается и записывается в буфер.
void WriteToAudioObjectBuffer(FLOAT* buffer, UINT frameCount, FLOAT frequency, UINT samplingRate)
{
const double PI = 4 * atan2(1.0, 1.0);
static double _radPhase = 0.0;
double step = 2 * PI * frequency / samplingRate;
for (UINT i = 0; i < frameCount; i++)
{
double sample = sin(_radPhase);
buffer[i] = FLOAT(sample);
_radPhase += step; // next frame phase
if (_radPhase >= 2 * PI)
{
_radPhase -= 2 * PI;
}
}
}
Визуализация звука с помощью динамических пространственных звуковых объектов
Динамические объекты позволяют отображать звук из произвольной позиции в пространстве относительно пользователя. Положение и громкость динамического звукового объекта можно изменить с течением времени. Игры обычно используют положение трехмерного объекта в игровом мире, чтобы указать положение динамического звукового объекта, связанного с ним. В следующем примере используется простая структура My3dObject для хранения минимального набора данных, необходимого для представления объекта. Эти данные включают указатель на ISpatialAudioObject, позицию, скорость, громкость и частоту тона для объекта, а также значение, в котором хранится общее количество кадров, для которых объект должен отображать звук.
struct My3dObject
{
Microsoft::WRL::ComPtr<ISpatialAudioObject> audioObject;
Windows::Foundation::Numerics::float3 position;
Windows::Foundation::Numerics::float3 velocity;
float volume;
float frequency; // in Hz
UINT totalFrameCount;
};
Шаги реализации динамических звуковых объектов в значительной степени совпадают с инструкциями по статическим звуковым объектам, описанным выше. Сначала получите конечную точку звука.
HRESULT hr;
Microsoft::WRL::ComPtr<IMMDeviceEnumerator> deviceEnum;
Microsoft::WRL::ComPtr<IMMDevice> defaultDevice;
hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), (void**)&deviceEnum);
hr = deviceEnum->GetDefaultAudioEndpoint(EDataFlow::eRender, eMultimedia, &defaultDevice);
Затем инициализируйте пространственный аудиопоток. Получите экземпляр ISpatialAudioClient, вызвав IMMDevice::Activate. Вызовите ISpatialAudioClient::IsAudioObjectFormatSupported , чтобы убедиться, что используется звуковой формат, который вы используете. Создайте событие, которое будет использоваться в звуковом конвейере, чтобы уведомить приложение о том, что оно готово для получения дополнительных звуковых данных.
Вызовите ISpatialAudioClient::GetMaxDynamicObjectCount , чтобы получить количество динамических объектов, поддерживаемых системой. Если этот вызов возвращает значение 0, динамические пространственные звуковые объекты не поддерживаются или включены на текущем устройстве. Сведения о включении пространственного звука и подробных сведений о количестве динамических звуковых объектов, доступных для различных пространственных форматов звука, см. в разделе "Пространственный звук".
При заполнении структуры SpatialAudioObjectRenderStreamActivationParams задайте для поля MaxDynamicObjectCount максимальное количество динамических объектов, которые будет использоваться приложением.
Вызовите ISpatialAudioClient::ActivateSpatialAudioStream для активации потока.
// Activate ISpatialAudioClient on the desired audio-device
Microsoft::WRL::ComPtr<ISpatialAudioClient> spatialAudioClient;
hr = defaultDevice->Activate(__uuidof(ISpatialAudioClient), CLSCTX_INPROC_SERVER, nullptr, (void**)&spatialAudioClient);
hr = spatialAudioClient->IsAudioObjectFormatSupported(&format);
// Create the event that will be used to signal the client for more data
HANDLE bufferCompletionEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
UINT32 maxDynamicObjectCount;
hr = spatialAudioClient->GetMaxDynamicObjectCount(&maxDynamicObjectCount);
if (maxDynamicObjectCount == 0)
{
// Dynamic objects are unsupported
return;
}
// Set the maximum number of dynamic audio objects that will be used
SpatialAudioObjectRenderStreamActivationParams streamParams;
streamParams.ObjectFormat = &format;
streamParams.StaticObjectTypeMask = AudioObjectType_None;
streamParams.MinDynamicObjectCount = 0;
streamParams.MaxDynamicObjectCount = min(maxDynamicObjectCount, 4);
streamParams.Category = AudioCategory_GameEffects;
streamParams.EventHandle = bufferCompletionEvent;
streamParams.NotifyObject = nullptr;
PROPVARIANT pv;
PropVariantInit(&pv);
pv.vt = VT_BLOB;
pv.blob.cbSize = sizeof(streamParams);
pv.blob.pBlobData = (BYTE *)&streamParams;
Microsoft::WRL::ComPtr<ISpatialAudioObjectRenderStream> spatialAudioStream;;
hr = spatialAudioClient->ActivateSpatialAudioStream(&pv, __uuidof(spatialAudioStream), (void**)&spatialAudioStream);
Ниже приведен некоторый код, предназначенный для конкретного приложения для поддержки этого примера, который будет динамически создавать случайным образом размещенные звуковые объекты и хранить их в векторе.
// Used for generating a vector of randomized My3DObject structs
std::vector<My3dObject> objectVector;
std::default_random_engine gen;
std::uniform_real_distribution<> pos_dist(-25, 25); // uniform distribution for random position
std::uniform_real_distribution<> vel_dist(-1, 1); // uniform distribution for random velocity
std::uniform_real_distribution<> vol_dist(0.5, 1.0); // uniform distribution for random volume
std::uniform_real_distribution<> pitch_dist(40, 400); // uniform distribution for random pitch
int spawnCounter = 0;
Перед вводом цикла отрисовки звука вызовите ISpatialAudioObjectRenderStream::Start , чтобы конвейер мультимедиа начал запрашивать звуковые данные.
В цикле отрисовки дождитесь события завершения буфера, предоставленного при инициализации пространственного звукового потока. При ожидании события следует задать разумный предел времени ожидания, например 100 мс, так как любое изменение типа отрисовки или конечной точки приведет к тому, что это событие никогда не будет сигнализировать. В этом случае можно вызвать ISpatialAudioObjectRenderStream::Reset , чтобы попытаться сбросить пространственный аудиопоток.
Затем вызовите ISpatialAudioObjectRenderStream::BeginUpdatingAudioObjects , чтобы сообщить системе, что вы готовы заполнить буферы звуковых объектов данными. Этот метод возвращает количество доступных динамических звуковых объектов и количество кадров буфера для звуковых объектов, отображаемых этим потоком.
Всякий раз, когда счетчик разрежений достигает указанного значения, мы активируем новый динамический звуковой объект путем вызова ISpatialAudioObjectRenderStream::ActivateSpatialAudioObject, указывающего AudioObjectType_Dynamic. Если все доступные динамические звуковые объекты уже выделены, этот метод вернет SPLAUDCLNT_E_NO_MORE_OBJECTS. В этом случае можно выбрать выпуск одного или нескольких ранее активированных звуковых объектов на основе приоритета для конкретного приложения. После создания динамического звукового объекта он добавляется в новую структуру My3dObject с случайными значениями позиции, скорости, громкости и частоты, которая затем добавляется в список активных объектов.
Затем выполните итерацию по всем активным объектам, представленным в этом примере с определяемой приложением структурой My3dObject . Для каждого звукового объекта вызовите ISpatialAudioObject::GetBuffer , чтобы получить указатель на звуковой буфер пространственного звукового объекта. Этот метод также возвращает размер буфера в байтах. Вспомогательный метод WriteToAudioObjectBuffer для заполнения буфера звуковыми данными. После записи в буфер пример обновляет положение динамического звукового объекта путем вызова ISpatialAudioObject::SetPosition. Громкость звукового объекта также может быть изменена путем вызова SetVolume. Если вы не обновляете позицию или том объекта, она будет хранить позицию и том с момента последнего задания. Если достигнуто время существования приложения, определяемое приложением, вызывается ISpatialAudioObject::SetEndOfStream, чтобы сообщить звуковому конвейеру, что больше звука не будет записано с помощью этого объекта, и объект имеет значение NULLptr, чтобы освободить свои ресурсы.
После записи данных во все звуковые объекты вызовите ISpatialAudioObjectRenderStream::EndUpdatingAudioObjects , чтобы сообщить системе, что данные готовы к отрисовке. Вы можете вызывать GetBuffer только между вызовом BeginUpdatingAudioObjects и EndUpdatingAudioObjects.
// Start streaming / rendering
hr = spatialAudioStream->Start();
do
{
// Wait for a signal from the audio-engine to start the next processing pass
if (WaitForSingleObject(bufferCompletionEvent, 100) != WAIT_OBJECT_0)
{
break;
}
UINT32 availableDynamicObjectCount;
UINT32 frameCount;
// Begin the process of sending object data and metadata
// Get the number of active objects that can be used to send object-data
// Get the frame count that each buffer will be filled with
hr = spatialAudioStream->BeginUpdatingAudioObjects(&availableDynamicObjectCount, &frameCount);
BYTE* buffer;
UINT32 bufferLength;
// Spawn a new dynamic audio object every 200 iterations
if (spawnCounter % 200 == 0 && spawnCounter < 1000)
{
// Activate a new dynamic audio object
Microsoft::WRL::ComPtr<ISpatialAudioObject> audioObject;
hr = spatialAudioStream->ActivateSpatialAudioObject(AudioObjectType::AudioObjectType_Dynamic, &audioObject);
// If SPTLAUDCLNT_E_NO_MORE_OBJECTS is returned, there are no more available objects
if (SUCCEEDED(hr))
{
// Init new struct with the new audio object.
My3dObject obj = {
audioObject,
Windows::Foundation::Numerics::float3(static_cast<float>(pos_dist(gen)), static_cast<float>(pos_dist(gen)), static_cast<float>(pos_dist(gen))),
Windows::Foundation::Numerics::float3(static_cast<float>(vel_dist(gen)), static_cast<float>(vel_dist(gen)), static_cast<float>(vel_dist(gen))),
static_cast<float>(static_cast<float>(vol_dist(gen))),
static_cast<float>(static_cast<float>(pitch_dist(gen))),
format.nSamplesPerSec * 5 // 5 seconds of audio samples
};
objectVector.insert(objectVector.begin(), obj);
}
}
spawnCounter++;
// Loop through all dynamic audio objects
std::vector<My3dObject>::iterator it = objectVector.begin();
while (it != objectVector.end())
{
it->audioObject->GetBuffer(&buffer, &bufferLength);
if (it->totalFrameCount >= frameCount)
{
// Write audio data to the buffer
WriteToAudioObjectBuffer(reinterpret_cast<float*>(buffer), frameCount, it->frequency, format.nSamplesPerSec);
// Update the position and volume of the audio object
it->audioObject->SetPosition(it->position.x, it->position.y, it->position.z);
it->position += it->velocity;
it->audioObject->SetVolume(it->volume);
it->totalFrameCount -= frameCount;
++it;
}
else
{
// If the audio object reaches its lifetime, set EndOfStream and release the object
// Write audio data to the buffer
WriteToAudioObjectBuffer(reinterpret_cast<float*>(buffer), it->totalFrameCount, it->frequency, format.nSamplesPerSec);
// Set end of stream for the last buffer
hr = it->audioObject->SetEndOfStream(it->totalFrameCount);
it->audioObject = nullptr; // Release the object
it->totalFrameCount = 0;
it = objectVector.erase(it);
}
}
// Let the audio-engine know that the object data are available for processing now
hr = spatialAudioStream->EndUpdatingAudioObjects();
} while (objectVector.size() > 0);
После завершения отрисовки пространственного звука остановите пространственный аудиопоток, вызвав ISpatialAudioObjectRenderStream::Stop. Если вы еще не собираетесь использовать поток, освободите ресурсы, вызвав ISpatialAudioObjectRenderStream::Reset.
// Stop the stream
hr = spatialAudioStream->Stop();
// We don't want to start again, so reset the stream to free it's resources.
hr = spatialAudioStream->Reset();
CloseHandle(bufferCompletionEvent);
Визуализация звука с помощью динамических пространственных звуковых объектов для HRTF
Другой набор API, ISpatialAudioRenderStreamForHrtf и ISpatialAudioObjectForHrtf, позволяет пространственному звуку, использующему функцию передачи (HRTF) Корпорации Майкрософт для определения положения имитирования положения эмитатора в пространстве относительно пользователя, который может быть изменен с течением времени. Помимо положения, звуковые объекты HRTF позволяют указать ориентацию в пространстве, прямость, в которой создается звук, например конус или карта ioid фигура, и модель распада, когда объект перемещается ближе и дальше от виртуального прослушивателя. Обратите внимание, что эти интерфейсы HRTF доступны только в том случае, если пользователь выбрал Windows Sonic для наушников в качестве пространственного звукового модуля для устройства. Сведения о настройке устройства для использования Windows Sonic для наушников см. в разделе "Пространственный звук".
API ISpatialAudioRenderStreamForHrtf и ISpatialAudioObjectForHrtf позволяют приложению явно использовать путь отрисовки Windows Sonic для наушников напрямую. Эти API не поддерживают форматы пространственного звука, такие как Dolby Atmos для домашнего театра или Dolby Atmos для наушников, а также не формат вывода, контролируемый потребителем, через панель управления звуком , или воспроизведение через динамики. Эти интерфейсы предназначены для использования в приложениях Windows Смешанная реальность, которые хотят использовать специальные возможности Windows Sonic для наушников (например, предустановки среды и откат на основе расстояния, указанные программным способом за пределами стандартных конвейеров разработки контента). Большинство игр и сценариев виртуальной реальности предпочитают использовать ISpatialAudioClient . Шаги реализации для обоих наборов API практически идентичны, поэтому можно реализовать как технологии, так и переключиться во время выполнения в зависимости от того, какая функция доступна на текущем устройстве.
Приложения смешанной реальности обычно используют положение трехмерного объекта в виртуальном мире, чтобы указать положение динамического звукового объекта, связанного с ним. В следующем примере используется простая структура My3dObjectForHrtf для хранения минимального набора данных, необходимого для представления объекта. Эти данные включают указатель на ISpatialAudioObjectForHrtf, позицию, ориентацию, скорость и частоту тона для объекта, а также значение, в котором хранится общее количество кадров, для которых объект должен отображать звук.
struct My3dObjectForHrtf
{
Microsoft::WRL::ComPtr<ISpatialAudioObjectForHrtf> audioObject;
Windows::Foundation::Numerics::float3 position;
Windows::Foundation::Numerics::float3 velocity;
float yRotationRads;
float deltaYRotation;
float frequency; // in Hz
UINT totalFrameCount;
};
Шаги реализации для динамических звуковых объектов HRTF в значительной степени совпадают с шагами для динамических звуковых объектов, описанных в предыдущем разделе. Сначала получите конечную точку звука.
HRESULT hr;
Microsoft::WRL::ComPtr<IMMDeviceEnumerator> deviceEnum;
Microsoft::WRL::ComPtr<IMMDevice> defaultDevice;
hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), (void**)&deviceEnum);
hr = deviceEnum->GetDefaultAudioEndpoint(EDataFlow::eRender, eMultimedia, &defaultDevice);
Затем инициализируйте пространственный аудиопоток. Получите экземпляр ISpatialAudioClient, вызвав IMMDevice::Activate. Вызовите ISpatialAudioClient::IsAudioObjectFormatSupported , чтобы убедиться, что используется звуковой формат, который вы используете. Создайте событие, которое будет использоваться в звуковом конвейере, чтобы уведомить приложение о том, что оно готово для получения дополнительных звуковых данных.
Вызовите ISpatialAudioClient::GetMaxDynamicObjectCount , чтобы получить количество динамических объектов, поддерживаемых системой. Если этот вызов возвращает значение 0, динамические пространственные звуковые объекты не поддерживаются или включены на текущем устройстве. Сведения о включении пространственного звука и подробных сведений о количестве динамических звуковых объектов, доступных для различных пространственных форматов звука, см. в разделе "Пространственный звук".
При заполнении структуры SpatialAudioHrtfActivationParams задайте для поля MaxDynamicObjectCount максимальное количество динамических объектов, которые будет использоваться приложением. Параметры активации для HRTF поддерживают несколько дополнительных параметров, таких как SpatialAudioHrtfDistanceDecay, SpatialAudioHrtfDirectivityUnion, SpatialAudioHrtfEnvironmentType и SpatialAudioHrtfOrientation, которые указывают значения по умолчанию этих параметров для новых объектов, созданных из потока. Эти параметры являются необязательными. Задайте поля nullptr для предоставления значений по умолчанию.
Вызовите ISpatialAudioClient::ActivateSpatialAudioStream для активации потока.
// Activate ISpatialAudioClient on the desired audio-device
Microsoft::WRL::ComPtr<ISpatialAudioClient> spatialAudioClient;
hr = defaultDevice->Activate(__uuidof(ISpatialAudioClient), CLSCTX_INPROC_SERVER, nullptr, (void**)&spatialAudioClient);
Microsoft::WRL::ComPtr<ISpatialAudioObjectRenderStreamForHrtf> spatialAudioStreamForHrtf;
hr = spatialAudioClient->IsSpatialAudioStreamAvailable(__uuidof(spatialAudioStreamForHrtf), NULL);
hr = spatialAudioClient->IsAudioObjectFormatSupported(&format);
// Create the event that will be used to signal the client for more data
HANDLE bufferCompletionEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
UINT32 maxDynamicObjectCount;
hr = spatialAudioClient->GetMaxDynamicObjectCount(&maxDynamicObjectCount);
SpatialAudioHrtfActivationParams streamParams;
streamParams.ObjectFormat = &format;
streamParams.StaticObjectTypeMask = AudioObjectType_None;
streamParams.MinDynamicObjectCount = 0;
streamParams.MaxDynamicObjectCount = min(maxDynamicObjectCount, 4);
streamParams.Category = AudioCategory_GameEffects;
streamParams.EventHandle = bufferCompletionEvent;
streamParams.NotifyObject = NULL;
SpatialAudioHrtfDistanceDecay decayModel;
decayModel.CutoffDistance = 100;
decayModel.MaxGain = 3.98f;
decayModel.MinGain = float(1.58439 * pow(10, -5));
decayModel.Type = SpatialAudioHrtfDistanceDecayType::SpatialAudioHrtfDistanceDecay_NaturalDecay;
decayModel.UnityGainDistance = 1;
streamParams.DistanceDecay = &decayModel;
SpatialAudioHrtfDirectivity directivity;
directivity.Type = SpatialAudioHrtfDirectivityType::SpatialAudioHrtfDirectivity_Cone;
directivity.Scaling = 1.0f;
SpatialAudioHrtfDirectivityCone cone;
cone.directivity = directivity;
cone.InnerAngle = 0.1f;
cone.OuterAngle = 0.2f;
SpatialAudioHrtfDirectivityUnion directivityUnion;
directivityUnion.Cone = cone;
streamParams.Directivity = &directivityUnion;
SpatialAudioHrtfEnvironmentType environment = SpatialAudioHrtfEnvironmentType::SpatialAudioHrtfEnvironment_Large;
streamParams.Environment = &environment;
SpatialAudioHrtfOrientation orientation = { 1,0,0,0,1,0,0,0,1 }; // identity matrix
streamParams.Orientation = &orientation;
PROPVARIANT pv;
PropVariantInit(&pv);
pv.vt = VT_BLOB;
pv.blob.cbSize = sizeof(streamParams);
pv.blob.pBlobData = (BYTE *)&streamParams;
hr = spatialAudioClient->ActivateSpatialAudioStream(&pv, __uuidof(spatialAudioStreamForHrtf), (void**)&spatialAudioStreamForHrtf);
Ниже приведен некоторый код, предназначенный для конкретного приложения для поддержки этого примера, который будет динамически создавать случайным образом размещенные звуковые объекты и хранить их в векторе.
// Used for generating a vector of randomized My3DObject structs
std::vector<My3dObjectForHrtf> objectVector;
std::default_random_engine gen;
std::uniform_real_distribution<> pos_dist(-10, 10); // uniform distribution for random position
std::uniform_real_distribution<> vel_dist(-.02, .02); // uniform distribution for random velocity
std::uniform_real_distribution<> yRotation_dist(-3.14, 3.14); // uniform distribution for y-axis rotation
std::uniform_real_distribution<> deltaYRotation_dist(.01, .02); // uniform distribution for y-axis rotation
std::uniform_real_distribution<> pitch_dist(40, 400); // uniform distribution for random pitch
int spawnCounter = 0;
Перед вводом цикла отрисовки звука вызовите ISpatialAudioObjectRenderStreamForHrtf::Start , чтобы поручить конвейеру мультимедиа начать запрос звуковых данных.
В цикле отрисовки дождитесь события завершения буфера, предоставленного при инициализации пространственного звукового потока. При ожидании события следует задать разумный предел времени ожидания, например 100 мс, так как любое изменение типа отрисовки или конечной точки приведет к тому, что это событие никогда не будет сигнализировать. В этом случае можно вызвать ISpatialAudioRenderStreamForHrtf::Reset , чтобы попытаться сбросить пространственный аудиопоток.
Затем вызовите ISpatialAudioRenderStreamForHrtf::BeginUpdatingAudioObjects , чтобы сообщить системе, что вы готовы заполнить буферы звуковых объектов данными. Этот метод возвращает количество доступных динамических звуковых объектов, не используемых в этом примере, и количество кадров буфера для звуковых объектов, отображаемых этим потоком.
Всякий раз, когда счетчик разрежений достигает указанного значения, мы активируем новый динамический звуковой объект, вызвав ISpatialAudioRenderStreamForHrtf::ActivateSpatialAudioObjectForHrtf, указав AudioObjectType_Dynamic. Если все доступные динамические звуковые объекты уже выделены, этот метод вернет SPLAUDCLNT_E_NO_MORE_OBJECTS. В этом случае можно выбрать выпуск одного или нескольких ранее активированных звуковых объектов на основе приоритета для конкретного приложения. После создания динамического звукового объекта он добавляется в новую структуру My3dObjectForHrtf с случайным положением, поворотом, скоростью, объемом и значениями частоты, которые затем добавляются в список активных объектов.
Затем выполните итерацию по всем активным объектам, представленным в этом примере с определяемой приложением структурой My3dObjectForHrtf . Для каждого звукового объекта вызовите ISpatialAudioObjectForHrtf::GetBuffer , чтобы получить указатель на звуковой буфер пространственного звукового объекта. Этот метод также возвращает размер буфера в байтах. Вспомогательный метод WriteToAudioObjectBuffer, указанный ранее в этой статье, для заполнения буфера звуковыми данными. После записи в буфер пример обновляет положение и ориентацию звукового объекта HRTF путем вызова ISpatialAudioObjectForHrtf::SetPosition и ISpatialAudioObjectForHrtf::SetOrientation. В этом примере вспомогательный метод CalculateEmitterConeOrientationMatrix используется для вычисления матрицы ориентации с учетом направления, на который указывает трехмерный объект. Реализация этого метода показана ниже. Громкость звукового объекта также может быть изменена путем вызова ISpatialAudioObjectForHrtf::SetGain. Если вы не обновляете положение, ориентацию или том объекта, он будет хранить положение, ориентацию и том с момента последнего задания. Если достигнуто время существования, определяемое приложением объекта, вызывается ISpatialAudioObjectForHrtf::SetEndOfStream, чтобы сообщить звуковому конвейеру, что больше звука не будет записано с помощью этого объекта, и объект имеет значение NULLPTR, чтобы освободить свои ресурсы.
После записи данных во все звуковые объекты вызовите ISpatialAudioRenderStreamForHrtf::EndUpdatingAudioObjects , чтобы сообщить системе, что данные готовы к отрисовке. Вы можете вызывать GetBuffer только между вызовом BeginUpdatingAudioObjects и EndUpdatingAudioObjects.
// Start streaming / rendering
hr = spatialAudioStreamForHrtf->Start();
do
{
// Wait for a signal from the audio-engine to start the next processing pass
if (WaitForSingleObject(bufferCompletionEvent, 100) != WAIT_OBJECT_0)
{
break;
}
UINT32 availableDynamicObjectCount;
UINT32 frameCount;
// Begin the process of sending object data and metadata
// Get the number of active objects that can be used to send object-data
// Get the frame count that each buffer will be filled with
hr = spatialAudioStreamForHrtf->BeginUpdatingAudioObjects(&availableDynamicObjectCount, &frameCount);
BYTE* buffer;
UINT32 bufferLength;
// Spawn a new dynamic audio object every 200 iterations
if (spawnCounter % 200 == 0 && spawnCounter < 1000)
{
// Activate a new dynamic audio object
Microsoft::WRL::ComPtr<ISpatialAudioObjectForHrtf> audioObject;
hr = spatialAudioStreamForHrtf->ActivateSpatialAudioObjectForHrtf(AudioObjectType::AudioObjectType_Dynamic, &audioObject);
// If SPTLAUDCLNT_E_NO_MORE_OBJECTS is returned, there are no more available objects
if (SUCCEEDED(hr))
{
// Init new struct with the new audio object.
My3dObjectForHrtf obj = { audioObject,
Windows::Foundation::Numerics::float3(static_cast<float>(pos_dist(gen)), static_cast<float>(pos_dist(gen)), static_cast<float>(pos_dist(gen))),
Windows::Foundation::Numerics::float3(static_cast<float>(vel_dist(gen)), static_cast<float>(vel_dist(gen)), static_cast<float>(vel_dist(gen))),
static_cast<float>(static_cast<float>(yRotation_dist(gen))),
static_cast<float>(static_cast<float>(deltaYRotation_dist(gen))),
static_cast<float>(static_cast<float>(pitch_dist(gen))),
format.nSamplesPerSec * 5 // 5 seconds of audio samples
};
objectVector.insert(objectVector.begin(), obj);
}
}
spawnCounter++;
// Loop through all dynamic audio objects
std::vector<My3dObjectForHrtf>::iterator it = objectVector.begin();
while (it != objectVector.end())
{
it->audioObject->GetBuffer(&buffer, &bufferLength);
if (it->totalFrameCount >= frameCount)
{
// Write audio data to the buffer
WriteToAudioObjectBuffer(reinterpret_cast<float*>(buffer), frameCount, it->frequency, format.nSamplesPerSec);
// Update the position and volume of the audio object
it->audioObject->SetPosition(it->position.x, it->position.y, it->position.z);
it->position += it->velocity;
Windows::Foundation::Numerics::float3 emitterDirection = Windows::Foundation::Numerics::float3(cos(it->yRotationRads), 0, sin(it->yRotationRads));
Windows::Foundation::Numerics::float3 listenerDirection = Windows::Foundation::Numerics::float3(0, 0, 1);
DirectX::XMFLOAT4X4 rotationMatrix;
DirectX::XMMATRIX rotation = CalculateEmitterConeOrientationMatrix(emitterDirection, listenerDirection);
XMStoreFloat4x4(&rotationMatrix, rotation);
SpatialAudioHrtfOrientation orientation = {
rotationMatrix._11, rotationMatrix._12, rotationMatrix._13,
rotationMatrix._21, rotationMatrix._22, rotationMatrix._23,
rotationMatrix._31, rotationMatrix._32, rotationMatrix._33
};
it->audioObject->SetOrientation(&orientation);
it->yRotationRads += it->deltaYRotation;
it->totalFrameCount -= frameCount;
++it;
}
else
{
// If the audio object reaches its lifetime, set EndOfStream and release the object
// Write audio data to the buffer
WriteToAudioObjectBuffer(reinterpret_cast<float*>(buffer), it->totalFrameCount, it->frequency, format.nSamplesPerSec);
// Set end of stream for the last buffer
hr = it->audioObject->SetEndOfStream(it->totalFrameCount);
it->audioObject = nullptr; // Release the object
it->totalFrameCount = 0;
it = objectVector.erase(it);
}
}
// Let the audio-engine know that the object data are available for processing now
hr = spatialAudioStreamForHrtf->EndUpdatingAudioObjects();
} while (objectVector.size() > 0);
После завершения отрисовки пространственного звука остановите пространственный аудиопоток, вызвав ISpatialAudioRenderStreamForHrtf::Stop. Если вы еще не собираетесь использовать поток, освободив свои ресурсы, вызвав ISpatialAudioRenderStreamForHrtf::Reset.
// Stop the stream
hr = spatialAudioStreamForHrtf->Stop();
// We don't want to start again, so reset the stream to free it's resources.
hr = spatialAudioStreamForHrtf->Reset();
CloseHandle(bufferCompletionEvent);
В следующем примере кода показана реализация вспомогательного метода CalculateEmitterConeOrientationMatrix , который использовался в приведенном выше примере для вычисления матрицы ориентации с указанием направления 3D-объекта.
DirectX::XMMATRIX CalculateEmitterConeOrientationMatrix(Windows::Foundation::Numerics::float3 listenerOrientationFront, Windows::Foundation::Numerics::float3 emitterDirection)
{
DirectX::XMVECTOR vListenerDirection = DirectX::XMLoadFloat3(&listenerOrientationFront);
DirectX::XMVECTOR vEmitterDirection = DirectX::XMLoadFloat3(&emitterDirection);
DirectX::XMVECTOR vCross = DirectX::XMVector3Cross(vListenerDirection, vEmitterDirection);
DirectX::XMVECTOR vDot = DirectX::XMVector3Dot(vListenerDirection, vEmitterDirection);
DirectX::XMVECTOR vAngle = DirectX::XMVectorACos(vDot);
float angle = DirectX::XMVectorGetX(vAngle);
// The angle must be non-zero
if (fabsf(angle) > FLT_EPSILON)
{
// And less than PI
if (fabsf(angle) < DirectX::XM_PI)
{
return DirectX::XMMatrixRotationAxis(vCross, angle);
}
// If equal to PI, find any other non-collinear vector to generate the perpendicular vector to rotate about
else
{
DirectX::XMFLOAT3 vector = { 1.0f, 1.0f, 1.0f };
if (listenerOrientationFront.x != 0.0f)
{
vector.x = -listenerOrientationFront.x;
}
else if (listenerOrientationFront.y != 0.0f)
{
vector.y = -listenerOrientationFront.y;
}
else // if (_listenerOrientationFront.z != 0.0f)
{
vector.z = -listenerOrientationFront.z;
}
DirectX::XMVECTOR vVector = DirectX::XMLoadFloat3(&vector);
vVector = DirectX::XMVector3Normalize(vVector);
vCross = DirectX::XMVector3Cross(vVector, vEmitterDirection);
return DirectX::XMMatrixRotationAxis(vCross, angle);
}
}
// If the angle is zero, use an identity matrix
return DirectX::XMMatrixIdentity();
}
См. также