使用空间音频对象呈现空间声音

本文提供了一些简单示例,演示如何使用静态空间音频对象、动态空间音频对象和使用 Microsoft 头部相对传输函数的空间音频对象 (HRTF) 实现空间声音。 这三种技术的实现步骤非常相似,本文为每个技术提供了类似的结构化代码示例。 有关实际空间音频实现的完整端到端示例,请参阅 Microsoft 空间声音示例 github 存储库。 有关 Windows Sonic 的概述,Microsoft 的 Xbox 和 Windows空间声音支持平台级解决方案,请参阅空间声音

使用静态空间音频对象呈现音频

静态音频对象用于将声音呈现为 AudioObjectType 枚举中定义的 18 个静态音频通道之一。 其中每个通道都表示在空间中不随时间移动的固定点上的实际或虚拟化扬声器。 在特定设备上可用的静态通道取决于所使用的空间声音格式。 有关支持的格式及其通道计数的列表,请参阅 空间声音

初始化空间音频流时,必须指定流将使用哪些可用静态通道。 以下示例常量定义可用于指定常见说话人配置,并获取每个通道可用的通道数。

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);

创建空间音频流时,必须指定流将通过提供 波形图X 结构的音频格式。 如果要从文件播放音频,则格式通常由音频文件格式确定。 此示例使用单声道 32 位 48Hz 格式。

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;

呈现空间音频的下一步是初始化空间音频流。 首先,通过调用 IMMDevice::Activate 获取 ISpatialAudioClient 的实例。 调用 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);

注意

在Xbox One开发工具包上使用 ISpatialAudioClient 接口 (XDK) 游戏时,必须先在调用 IMMDeviceEnumerator::EnumAudioEndpointsIMMDeviceEnumerator::GetDefaultAudioEndpoint 之前调用 EnableSpatialAudio。 未能执行此操作将导致从调用激活返回E_NOINTERFACE错误。 EnableSpatialAudio 仅适用于 XDK 游戏,无需为在Xbox One上运行的通用 Windows 平台应用调用,也不需要为任何非Xbox One设备调用。

 

声明用于将音频数据写入静态通道的 ISpatialAudioObject 的指针。 典型应用将针对 StaticObjectTypeMask 字段中指定的每个通道使用对象。 为简单起见,此示例仅使用单个静态音频对象。

// In this simple example, one object will be rendered
Microsoft::WRL::ComPtr<ISpatialAudioObject> audioObjectFrontLeft;

在进入音频呈现循环之前,调用 ISpatialAudioObjectRenderStream::"开始"菜单指示媒体管道开始请求音频数据。 此示例使用计数器在 5 秒后停止音频的呈现。

在呈现循环中,等待初始化空间音频流时提供的缓冲区完成事件发出信号。 等待事件时,应设置合理的超时限制(如 100 毫秒),因为对呈现类型或终结点所做的任何更改都将导致该事件永远不会发出信号。 在这种情况下,可以调用 ISpatialAudioObjectRenderStream::Reset 以尝试重置空间音频流。

接下来,调用 ISpatialAudioObjectRenderStream::BeginUpdatingAudioObjects ,让系统知道你要用数据填充音频对象的缓冲区。 此方法返回在此示例中未使用的可用动态音频对象数,以及此流呈现的音频对象的缓冲区帧计数。

如果尚未创建静态空间音频对象,请通过调用 ISpatialAudioObjectRenderStream::ActivateSpatialAudioObject 创建一个或多个对象,从 AudioObjectType 枚举传入一个值,指示对象音频呈现到的静态通道。

接下来,调用 ISpatialAudioObject::GetBuffer 以获取指向空间音频对象的音频缓冲区的指针。 此方法还返回缓冲区的大小(以字节为单位)。 此示例使用帮助程序方法 WriteToAudioObjectBuffer 填充缓冲区中的音频数据。 此方法将在本文后面部分进行演示。 写入缓冲区后,该示例将检查是否已达到对象的 5 秒生存期,如果是这样,将调用 ISpatialAudioObject::SetEndOfStream ,让音频管道知道不会使用此对象写入更多音频,并且该对象设置为 nullptr 以释放其资源。

将数据写入所有音频对象后,调用 ISpatialAudioObjectRenderStream::EndUpdatingAudioObjects ,让系统知道数据已准备好呈现。 只能在对 BeginUpdatingAudioObjectsEndUpdatingAudioObjects 的调用之间调用 GetBuffer

// 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 帮助程序方法可编写样本的完整缓冲区或由应用定义的时间限制指定的剩余样本数。 例如,也可以通过源音频文件中剩余的样本数来确定这一点。 一个简单的 sin wave,其 频率由频率 输入参数缩放,生成并写入缓冲区。

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;
        }
    }
}

使用动态空间音频对象呈现音频

动态对象允许从空间中任意位置相对于用户呈现音频。 动态音频对象的位置和音量可以随时间变化。 游戏通常使用游戏世界中 3D 对象的位置来指定与其关联的动态音频对象的位置。 以下示例将使用简单的结构 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);

接下来,初始化空间音频流。 通过调用 IMMDevice::Activate 获取 ISpatialAudioClient 的实例。 调用 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::"开始"菜单指示媒体管道开始请求音频数据。

在呈现循环中,等待初始化空间音频流以发出信号时提供的缓冲区完成事件。 等待事件时,应设置合理的超时限制(如 100 毫秒),因为对呈现类型或终结点所做的任何更改都将导致该事件永远不会发出信号。 在这种情况下,可以调用 ISpatialAudioObjectRenderStream::Reset 以尝试重置空间音频流。

接下来,调用 ISpatialAudioObjectRenderStream::BeginUpdatingAudioObjects ,让系统知道你要用数据填充音频对象的缓冲区。 此方法返回可用动态音频对象的数量,以及此流呈现的音频对象的缓冲区帧计数。

每当生成计数器达到指定值时,我们将通过调用指定AudioObjectType_DynamicISpatialAudioObjectRenderStream::ActivateSpatialAudioObject 来激活新的动态音频对象。 如果已分配所有可用的动态音频对象,此方法将返回 SPLAUDCLNT_E_NO_MORE_OBJECTS。 在这种情况下,可以选择根据特定于应用的优先级释放一个或多个以前激活的音频对象。 创建动态音频对象后,会将其添加到新的 My3dObject 结构,其中包含随机位置、速度、音量和频率值,然后添加到活动对象列表中。

接下来,使用应用定义的 My3dObject 结构循环访问此示例中表示的所有活动对象。 对于每个音频对象,调用 ISpatialAudioObject::GetBuffer 以获取指向空间音频对象的音频缓冲区的指针。 此方法还返回缓冲区的大小(以字节为单位)。 帮助程序方法 WriteToAudioObjectBuffer,用于使用音频数据填充缓冲区。 写入缓冲区后,该示例通过调用 ISpatialAudioObject::SetPosition 更新动态音频对象的位置。 还可以通过调用 SetVolume 来修改音频对象的音量。 如果不更新对象的位置或卷,它将保留上次设置该对象的位置和卷。 如果已达到对象的应用定义生存期,将调用 ISpatialAudioObject::SetEndOfStream ,让音频管道知道不会使用此对象编写更多音频,并且该对象设置为 nullptr 以释放其资源。

将数据写入所有音频对象后,调用 ISpatialAudioObjectRenderStream::EndUpdatingAudioObjects ,让系统知道数据已准备好呈现。 只能在对 BeginUpdatingAudioObjectsEndUpdatingAudioObjects 的调用之间调用 GetBuffer

// 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 (ISpatialAudioRenderStreamForHrtfISpatialAudioObjectForHrtf)启用空间音频,该音频使用 Microsoft 的头相对传输函数 (HRTF) 来衰减声音,以模拟相对于用户在空间中的位置,而用户可能会随时间变化。 除了位置之外,HRTF 音频对象还允许你在空间中指定方向、发出声音(如锥形或心形)的定向性,以及随着对象在距离虚拟侦听器更近和更远的位置移动的衰减模型。 请注意,仅当用户选择用于耳机的 Windows Sonic作为设备的空间音频引擎时,这些 HRTF 接口才可用。 有关将设备配置为使用用于耳机的 Windows Sonic的信息,请参阅空间声音

ISpatialAudioRenderStreamForHrtfISpatialAudioObjectForHrtf API 允许应用程序直接使用用于耳机的 Windows Sonic呈现路径。 这些 API 不支持空间声音格式,例如Dolby Atmos for Home Theater或Dolby Atmos for Headphones,也不支持通过声音控制面板切换使用者控制的输出格式,也不支持通过扬声器播放。 这些接口适用于Windows Mixed Reality应用程序,这些应用程序希望使用特定于用于耳机的 Windows Sonic的功能 (,例如以编程方式指定的环境预设和基于距离的滚动更新,) 的典型内容创作管道之外。 大多数游戏和虚拟现实方案宁愿改用 ISpatialAudioClient 。 这两个 API 集的实现步骤几乎相同,因此,可以根据当前设备上可用的功能在运行时实现技术和切换。

混合现实应用通常使用虚拟世界中 3D 对象的位置来指定与之关联的动态音频对象的位置。 以下示例使用简单的结构 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);

接下来,初始化空间音频流。 通过调用 IMMDevice::Activate 获取 ISpatialAudioClient 的实例。 调用 ISpatialAudioClient::IsAudioObjectFormatSupported 以确保支持所使用的音频格式。 创建音频管道将用于通知应用已准备好获取更多音频数据的事件。

调用 ISpatialAudioClient::GetMaxDynamicObjectCount 以检索系统支持的动态对象数。 如果此调用返回 0,则当前设备上不支持或启用动态空间音频对象。 有关启用空间音频以及可用于不同空间音频格式的动态音频对象数量的详细信息,请参阅 空间声音

填充 SpatialAudioHrtfActivationParams 结构时,将 MaxDynamicObjectCount 字段设置为应用将使用的最大动态对象数。 HRTF 的激活参数支持一些附加参数,例如 SpatialAudioHrtfDistanceDecaySpatialAudioHrtfDirectivityUnionSpatialAudioHrtfEnvironmentTypeSpatialAudioHrtfOrientation,用于指定从流创建的新对象的这些设置的默认值。 这些参数是可选的。 将字段设置为 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::"开始"菜单,以指示媒体管道开始请求音频数据。

在呈现循环中,等待初始化空间音频流以发出信号时提供的缓冲区完成事件。 在等待事件时,应设置合理的超时限制(如 100 毫秒),因为对呈现类型或终结点的任何更改都会导致该事件永远不会发出信号。 在这种情况下,可以调用 ISpatialAudioRenderStreamForHrtf::Reset 以尝试重置空间音频流。

接下来,调用 ISpatialAudioRenderStreamForHrtf::BeginUpdatingAudioObjects ,让系统知道你要用数据填充音频对象的缓冲区。 此方法返回在此示例中不使用的可用动态音频对象数,以及此流呈现的音频对象的缓冲区帧计数。

每当生成计数器达到指定值时,我们将通过调用 ISpatialAudioRenderStreamForHrtf::ActivateSpatialAudioObjectForHrtfAudioObjectType_Dynamic 来激活新的动态音频对象。 如果已分配所有可用的动态音频对象,此方法将返回 SPLAUDCLNT_E_NO_MORE_OBJECTS。 在这种情况下,可以选择根据特定于应用的优先顺序释放一个或多个以前激活的音频对象。 创建动态音频对象后,它将添加到新的 My3dObjectForHrtf 结构,其中包含随机位置、旋转、速度、音量和频率值,然后添加到活动对象列表中。

接下来,使用应用定义的 My3dObjectForHrtf 结构循环访问此示例中表示的所有活动对象。 对于每个音频对象,调用 ISpatialAudioObjectForHrtf::GetBuffer 以获取指向空间音频对象的音频缓冲区的指针。 此方法还返回缓冲区的大小(以字节为单位)。 在本文前面列出的 Helper 方法 WriteToAudioObjectBuffer 中,使用音频数据填充缓冲区。 写入缓冲区后,该示例通过调用 ISpatialAudioObjectForHrtf::SetPositionISpatialAudioObjectForHrtf::SetOrientation 来更新 HRTF 音频对象的位置和方向。 在此示例中,Helper 方法 CalculateEmitterConeOrientationMatrix 用于计算方向矩阵,给定 3D 对象指向的方向。 此方法的实现如下所示。 还可以通过调用 ISpatialAudioObjectForHrtf::SetGain 来修改音频对象的音量。 如果不更新对象的位置、方向或卷,它将保留上次设置的位置、方向和卷。 如果已达到对象的应用定义生存期,将调用 ISpatialAudioObjectForHrtf::SetEndOfStream ,让音频管道知道不会使用此对象编写更多音频,并且该对象设置为 nullptr 以释放其资源。

将数据写入所有音频对象后,调用 ISpatialAudioRenderStreamForHrtf::EndUpdatingAudioObjects ,让系统知道数据已准备好呈现。 只能在对 BeginUpdatingAudioObjects 和 EndUpdatingAudioObjects 的调用之间调用 GetBuffer

// 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();
}

空间音效

ISpatialAudioClient

ISpatialAudioObject