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

本文提供一些简单示例,演示如何使用静态空间音频对象、动态空间音频对象以及空间音频对象通过 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);

创建空间音频流时,必须通过提供 WAVEFORMATEX 结构指定流将使用的音频格式。 如果要从文件播放音频,则格式通常由音频文件格式确定。 此示例使用单通道 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 开发工具包 (XDK) 游戏的 ISpatialAudioClient 接口时,必须先调用 EnableSpatialAudio,然后再调用 IMMDeviceEnumerator::EnumAudioEndpointsIMMDeviceEnumerator::GetDefaultAudioEndpoint。 否则将导致 Activate 调用返回 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::Start 以指示媒体管道开始请求音频数据。 此示例使用计数器在 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 帮助程序方法会写入样本的完整缓冲区或应用定义的时间限制指定的剩余样本数。 也可以通过源音频文件中剩余的样本数等方式来确定这一信息。 生成一个频率根据频率输入参数调整的简单正弦波,并写入缓冲区。

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::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,让系统知道可随时呈现数据。 只能在对 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::Start 以指示媒体管道开始请求音频数据。

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

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

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

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

将数据写入所有音频对象后,调用 ISpatialAudioRenderStreamForHrtf::EndUpdatingAudioObjects,让系统知道可随时呈现数据。 只能在对 BeginUpdatingAudioObjectsEndUpdatingAudioObjects 的调用之间调用 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