DirectX 因素

使用 XAudio2 的 Windows 8 发声功能

Charles Petzold

下载代码示例

Charles Petzold对于 Windows 8 的 Windows 商店 app 可以播放 MP3 或 WMA 声音文件时轻松地使用 MediaElement — — 你只是给它一个 URI 或流的声音文件。Windows 存储区的应用程序也可以访问 API 发挥到流媒体视频或音频与外部设备。

但是,如果您需要更多先进的音频处理呢?也许您想修改其路上到硬件,音频文件的内容或动态生成的声音。

Windows 应用程序商店还可以执行这些作业通过 DirectX。Windows 8 的声音生成和处理支持两个 DirectX 组件 — — 核心音频和 XAudio2。在宏伟的东西,这些都是相当低级的接口,但核心音频低于 XAudio2 和更适合于需要与音频硬件的紧密连接的应用程序。

XAudio2 是的继任者 DirectSound 和 Xbox XAudio 图书馆,和它可能是您需要做有趣的事情,与声音的 Windows 存储应用程序的最佳匹配。虽然 XAudio2 主要供游戏,但这不应该阻止我们使用它为更严重的目的 — — 例如,使音乐或娱乐业务用户用滑稽的声音。

XAudio2 版本 2.8 是 Windows 8 的内置组件。其余的 DirectX,像 XAudio2 编程接口基于 com虽然理论上可能访问 XAudio2 从 Windows 8 支持任何编程语言,用于 XAudio2 的最自然、 最简单的语言是 c + +。使用声音往往需要高性能代码,因此 c + + 是一个不错的选择,在这方面也。

第一个 XAudio2 程序

让我们开始编写一个程序,它使用 XAudio2 播放简单 5 秒声音在按按钮的响应。因为你可能对 Windows 8 和 DirectX 编程的新,我会采取比较慢一点。

我会认为你有适合于创建 Windows 存储应用程序的安装 Visual Studio 的版本。在新建项目对话框中,选择 Visual c + + 和 Windows 存储在左边和空白 App (XAML) 的可用模板列表中。我给我的项目的名称 SimpleAudio,和这篇文章可以找到该项目之间的可下载代码。

在建立可执行的文件,使用 XAudio2,您需要将程序与 xaudio2.lib 的导入库链接。造就项目属性对话框中,通过选择项目菜单上的最后一项,或通过右击解决方案资源管理器中的项目名称并选择属性。在左边的列中,选择配置属性然后链接器并输入。附加依赖项 (顶部的项目) 和小小的箭头,单击。选择编辑并在框中键入 xaudio2.lib。

你会也想要到的 xaudio2.h 头文件的引用,所以将以下语句添加到 pch.h 中的预编译的头列表:

#include <xaudio2.h>

在 MainPage.xaml 文件中,用于显示程序可能遇到与 XAudio2 API 和一个按钮,用于播放声音的任何错误添加 TextBlock。 如图 1 所示。

图 1 为 SimpleAudio 的 MainPage.xaml 文件

<Page x:Class="SimpleAudio.MainPage" ...
>
  <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
    <TextBlock Name="errorText"
               FontSize="24"
               TextWrapping="Wrap"
               HorizontalAlignment="Center"
               VerticalAlignment="Center" />
    <Button Name="submitButton"
            Content="Submit Audio Button"
            Visibility="Collapsed"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            Click="OnSubmitButtonClick" />
  </Grid>
</Page>

所示的 MainPage.xaml.h 头文件大容量图 2。 我已经删除宣言 》 的 OnNavigatedTo 方法,因为不会使用它。 为该按钮的 Click 处理程序声明,正如 XAudio2 程序的使用与连接的四个字段。

图 2 SimpleAudio 的 MainPage.xaml.h 头文件

namespace SimpleAudio
{
  public ref class MainPage sealed
  {
  private:
    Microsoft::WRL::ComPtr<IXAudio2> pXAudio2;
    IXAudio2MasteringVoice * pMasteringVoice;
    IXAudio2SourceVoice * pSourceVoice;
    byte soundData[2 * 5 * 44100];
  public:
    MainPage();
  private:
    void OnSubmitButtonClick(Platform::Object^ sender,
      Windows::UI::Xaml::RoutedEventArgs^ args);
  };
}

任何程序都希望使用 XAudio2 必须创建一个实现 IXAudio2 接口的对象。 (您将看到如何做到这一点不久)。IXAudio2 从在 COM 中,著名 IUnknown 类派生,它是本质上的引用计数,这意味着它将删除其自己的资源,当它不再被引用的程序。 Microsoft::WRL 命名空间中的 ComPtr (COM 指针) 类对 COM 对象的指针变成"智能指针",用于跟踪其自身的引用。 这是推荐用于处理在 Windows 应用程序商店中的 COM 对象的方法。

任何非平凡的 XAudio2 程序还需要到实现了 IXAudio2MasteringVoice 和 IXaudio2SourceVoice 接口的对象的指针。 在 XAudio2 用语中,"声音"是生成或修改音频数据的对象。 掌握声音就是从概念上讲,召集所有的个别声音并准备他们的声音代硬件混音器。 你只能得其中一个,但您可能会有多源声音生成单独的声音。 (也有向源声音应用筛选器或影响方法。)

IXAudio2MasterVoice 和 IXAudio2SourceVoice 的指针不是引用计数 ; 其生存期受的 IXAudio2 对象。

我的声音数据值得 5 秒钟还包含了一个大数组:

byte soundData[5 * 2 * 44100];

在实际的程序中,您会想要在运行时分配此大小的数组 — — 和摆脱它时你不需要它 — — 但你很快就会看到为什么去做这种方式。

如何计算该数组的大小? 虽然 XAudio2 支持压缩的音频,产生声音的大多数程序将坚持与称为脉冲代码调制或 PCM 格式。 在 PCM 声音波形由固定的采样率在固定大小的值表示。 在光盘上的音乐,采样速率每秒,44,100 次是音频的与每个立体声,共 176,400 字节的数据为 1 秒采样的 2 个字节。 (当在应用程序中嵌入的声音,压缩被建议。 XAudio2 支持 ADPCM ; WMA 和 MP3 还支持媒体基础引擎。)

对于此程序,我也选择要使用的 44,100 的采样率与每个样本的 2 个字节。 在 c + +,每个示例都因此短。 我永远坚持带单声道声音现在,因此 88,200 字节需要每秒的音频。 在数组分配,即 5 秒乘以 5。

创建对象

多的 MainPage.xaml.cpp 文件所示图 3。 所有 XAudio2 初始化是首页构造函数中执行的。 它始于对 XAudio2Create 的调用以获取实现 IXAudio2 接口的对象的指针。 这是使用 XAudio2 的第一步。 与某些 COM 接口,不同的是没有对 CoCreateInstance 的调用是必需的。

图 3 MainPage.xaml.cpp

MainPage::MainPage()
{
  InitializeComponent();
  // Create an IXAudio2 object
  HRESULT hr = XAudio2Create(&pXAudio2);
  if (FAILED(hr))
  {
    errorText->Text = "XAudio2Create failure: " + hr.ToString();
    return;
  }
  // Create a mastering voice
  hr = pXAudio2->CreateMasteringVoice(&pMasteringVoice);
  if (FAILED(hr))
  {
    errorText->Text = "CreateMasteringVoice failure: " + hr.ToString();
    return;
  }
  // Create a source voice
  WAVEFORMATEX waveformat;
  waveformat.wFormatTag = WAVE_FORMAT_PCM;
  waveformat.
nChannels = 1;
  waveformat.
nSamplesPerSec = 44100;
  waveformat.
nAvgBytesPerSec = 44100 * 2;
  waveformat.
nBlockAlign = 2;
  waveformat.wBitsPerSample = 16;
  waveformat.cbSize = 0;
  hr = pXAudio2->CreateSourceVoice(&pSourceVoice, &waveformat);
  if (FAILED(hr))
  {
    errorText->Text = "CreateSourceVoice failure: " + hr.ToString();
    return;
  }
  // Start the source voice
  hr = pSourceVoice->Start();
  if (FAILED(hr))
  {
    errorText->Text = "Start failure: " + hr.ToString();
    return;
  }
  // Fill the array with sound data
  for (int index = 0, second = 0; second < 5; second++)
  {
    for (int cycle = 0; cycle < 441; cycle++)
    {
      for (int sample = 0; sample < 100; sample++)
      {
        short value = sample < 50 ?
32767 : -32768;
        soundData[index++] = value & 0xFF;
        soundData[index++] = (value >> 8) & 0xFF;
      }
    }
  }
  // Make the button visible
  submitButton->Visibility = Windows::UI::Xaml::Visibility::Visible;
}
void MainPage::OnSubmitButtonClick(Object^ sender, 
  RoutedEventArgs^ args)
{
  // Create a button to reference the byte array
  XAUDIO2_BUFFER buffer = { 0 };
  buffer.AudioBytes = 2 * 5 * 44100;
  buffer.pAudioData = soundData;
  buffer.Flags = XAUDIO2_END_OF_STREAM;
  buffer.PlayBegin = 0;
  buffer.PlayLength = 5 * 44100;
  // Submit the buffer
  HRESULT hr = pSourceVoice->SubmitSourceBuffer(&buffer);
  if (FAILED(hr))
  {
    errorText->Text = "SubmitSourceBuffer failure: " + hr.ToString();
    submitButton->Visibility = 
      Windows::UI::Xaml::Visibility::Collapsed;
    return;
  }
}

一旦创建 IXAudio2 对象时,CreateMasteringVoice 和 CreateSourceVoice 方法获取指向作为头文件中的字段定义的其他两个接口的指针。

CreateSourceVoice 调用需要一个 WAVEFORMATEX 结构,这将是熟悉的人曾与 Win32 API 中的音频。 这种结构定义将在您使用此特定声音的音频数据的性质。 (不同的源的声音可以使用不同的格式)。对于 PCM,只有三个数字为真正相关:采样率 (在此示例中的 44,100),每个样本 (2 个字节或 16 位) 和渠道 (1 这里) 的数的大小。 这些根据其他字段:NBlockAlign 字段除以 8,nChannels 倍 wBitsPerSample 和 nAvgBytesPerSec 字段是 nSamplesPerSec 和 nBlockAlign 的产品。 但在此示例中,我展示了所有字段的显式值。

一旦获得的 IXAudio2SourceVoice 对象,可以对它调用 Start 方法。 此时,在概念上播放 XAudio2,但我们其实还没有给它任何要播放的音频数据。 Stop 方法也是可用的和一个真正的程序将使用这两种方法来控制时声音应该和不应该玩。

这些调用的所有四个不是那么简单,他们在此代码中出现的 ! 他们都有额外的参数,但方便默认值定义,我只是选择现在接受这些默认设置。

DirectX 中的几乎所有函数和方法调用都返回 HRESULT 值,以指示成功或失败。 有不同的策略来处理这些错误。 我选择只是为了显示使用中的 XAML 文件和停止进一步处理定义的 TextBlock 的错误代码。

PCM 音频数据

构造函数通过填满的 soundData 数组与音频数据,得出结论,但数组不实际上分叉以上到 IXAudio2SourceVoice 直到按下的按钮。

声音是振动,和人类是对 20 Hz (或每秒周期数) 到 20,000 Hz 的大致范围中空气中振动敏感。 在钢琴上的中央 C 是约 261.6 Hz。

假设您正在使用 44,100 Hz 和 16 位样品的采样率和您想要生成 4,410 Hz,这只是超出了钢琴上的最关键的频率的波形音频数据。 这种波形的每个周期需要 10 个样本的有符号的 16 位值。 这些值 10 时将予以重复,每秒钟的声音 4,410 倍。

441 Hz 的频率的波形 — — 非常接近 440 Hz,对应于上面中间 C 用作一种优化的标准 A — — 呈现与 100 个样本。 这种循环会对声音的每一秒 441 次重复。

因为 PCM 涉及恒定采样率,低频率的声音似乎被采样,并呈现在分辨率较高比高频率的声音。 这不是一个问题吗? 只是 10 个样本呈现 4,410 Hz 波形没有相当数量的失真和 441 Hz 的波形进行比较吗?

原来任何量化失真的 PCM 发生频率大于一半的采样率。 (这称为奈奎斯特频率后贝尔实验室工程师 Harry Nyquist)。44,100 Hz 采样频率被选为 CD 音频的原因之一是奈奎斯特频率是 22,050 Hz,上限是人类听觉 224 个约 20,000 hz 的频率。 换句话说,在 44,100 Hz 采样率、 量化失真是听不到人类。

SimpleAudio 程序生成通过算法简单的波形 — — 方波 441 Hz 的频率。 有 100 个样本,每个周期。 在每个周期首 50 最大正值 (32,767 处理短整数时) 下, 一步的 50 最大负值 (-32768)。 请注意这些短值必须首先具有的低位字节的字节数组中存储:

soundData[index + 0] = value & 0xFF;
soundData[index + 1] = (value >> 8) & 0xFF;

到目前为止,没有什么实际发挥。 为该按钮的 Click 处理程序中发生这种情况。 XAUDIO2_BUFFER 结构用于引用计数字节和持续时间指定为样本的数目的字节数组。 此缓冲区传递给 IXAudio2SourceVoice 对象的 SubmitSourceBuffer 方法。 如果 (在此示例中一样) 已经调用 Start 方法然后声音立即开始播放。

我怀疑我不必提及异步播放声音。 SubmitSourceBuffer 的调用返回立即虽然单独的线程献给铲到声音硬件数据的实际进程。 可以在调用后放弃,传递给 SubmitSourceBuffer 的 XAUDIO2_BUFFER — — 因为它是在此程序中的 Click 处理程序退出和本地变量超出范围时 — — 但实际的字节数组必须保留在可访问的内存中。 事实上,您的程序可以处理这些字节,如播放声音。 但是,有很多更好技术 (涉及回调方法),允许您动态地生成声音数据的程序。

如果不使用回调来确定声音已完成时,此程序需要该程序的持续时间内保留 soundData 数组。

您可以按按钮多次,和每个调用进行有效地排队,另一个缓冲区,当以前的缓冲区完成播放。 如果该程序将移至背景,为静音,但它继续发挥在沉默中。 换句话说,如果您单击按钮,并至少 5 秒钟将程序移动到背景,什么将发挥时,程序返回到前台。

特征的声音

很多我们在日常生活中听到的声音同时来自各种不同来源,因此,是相当复杂。 然而,在某些情况下 — — 尤其是当处理音乐的声音 — — 个别色调可以定义几个特色:

  • 振幅,这由我们的感官解释为卷。
  • 频率,被解释为螺距。
  • 空间,可以在多个扬声器的音频播放中模仿。
  • 音色与感知之间的差额的小号和钢琴演奏,例如有关色彩的声音的混合。

SoundCharacteristics 项目演示这些孤立的四个特点。 它备有 44,100 的采样率以及 16 位的 SimpleAudio 项目的样本,但生成立体声声音。 为两个通道的声音,必须交叉存取数据:左声道,右声道 16 位样本跟 16 位示例。

SoundCharacteristics 的 MainPage.xaml.h 头文件定义了一些常量:

static const int sampleRate = 44100;
static const int seconds = 5;
static const int channels = 2;
static const int samples = seconds * sampleRate;

它还定义了四个数组为声音数据,但这些类型而不是字节短的是:

short volumeSoundData[samples * channels];
short pitchSoundData[samples * channels];
short spaceSoundData[samples * channels];
short timbreSoundData[samples * channels];

使用短数组使初始化更容易,因为 16 位波形值不需要一半被打碎。 简单强制转换允许要提交声音数据时由 XAUDIO2_BUFFER 引用的数组。 因为我在此程序中使用立体声,这些数组作为数组在 SimpleAudio 中有双字节数。

这些阵列的所有四个首页构造函数中初始化。 为卷演示、 441 Hz 方波仍然参与,但它起价的容积为零、 获取过去前 2 秒内,逐渐响亮,然后在卷中下降,在最后 2 秒。 图 4 显示的代码来初始化 volumeSoundData。

图 4 声音数据的变化量为 SoundCharacteristics

for (int index = 0, sample = 0; sample < samples; sample++)
{
  double t = 1;
  if (sample < 2 * samples / 5)
    t = sample / (2.0 * samples / 5);
  else if (sample > 3 * samples / 5)
    t = (samples - sample) / (2.0 * samples / 5);
  double amplitude = pow(2, 15 * t) - 1;
  short waveform = sample % 100 < 50 ?
1 : -1;
  short value = short(amplitude * waveform);
  volumeSoundData[index++] = value;
  volumeSoundData[index++] = value;
}

人类感知是卷的对数:每增加一倍的波形振幅等同于卷 6 dB 增加。 (用于 CD 音频的 16 位振幅具有 96 分贝的动态范围)。在所示的代码图 4 首先改变卷计算从 0 线性增加到 1 的 t 的值和跌幅然后回 0。 振幅变量的计算使用的 pow 函数和从 0 到 32767 的范围。 这被乘以一个方形波具有 1 和-1 的值。 结果是两次添加到数组:第一次为左声道,然后为右声道。

人类感知也是频率的对数的。 世界音乐的大部分组织各地区间的八度是频率增加一倍的螺距。 前两个注释"在某个地方上彩虹"是合唱的八度飞跃是否这合唱的唱的低音或女高音。 图 5 显示在两个八度音阶的音域范围内不同螺距的代码:从 220 到 880 基于 t 中 (如卷示例) 是从 0 到 1 的值,然后调回到 0。

图 5 声音数据的变化频率为 SoundCharacteristics

double angle = 0;
for (int index = 0, sample = 0; sample < samples; sample++)
{
  double t = 1;
  if (sample < 2 * samples / 5)
    t = sample / (2.0 * samples / 5);
  else if (sample > 3 * samples / 5)
    t = (samples - sample) / (2.0 * samples / 5);
  double frequency = 220 * pow(2, 2 * t);
  double angleIncrement = 360 * frequency / waveformat.
nSamplesPerSec;
  angle += angleIncrement;
  while (angle > 360)
    angle -= 360;
  short value = angle < 180 ?
32767 : -32767;
  pitchSoundData[index++] = value;
  pitchSoundData[index++] = value;
}

在前面的示例中我选择了 441 赫兹频率,因为它干净地分为 44,100 的采样率。 一般情况下,采样率不是整数倍的频率,并因此不能有样品每个周期的整数值。 相反,此程序维护一个浮动的点 angleIncrement 变量,是频率成正比,用于增加一个范围从 0 到 360 的角度值。 然后,此角度值用于构造波形。

空间示范移动声音从中心到左通道,然后向右,然后回中心。 音色演示,波形开头 441 hz 的正弦曲线。 正弦曲线是振动的最基本类型的数学表述 — — 力在哪里位移成反比的差分方程的解。 所有其他定期波形包含谐波,这也是正弦波但与基频的整数倍数为单位的频率。 音色演示更改波形顺利从正弦波到三角波,方波,锯齿波,同时增加谐波含量的声音。

更大的图片

虽然我刚刚证明可以如何通过生成一个 IXAudio2SourceVoice 对象的声音数据控制音量、 音调、 空间和音色,对象本身包括方法来更改音量和空间,以及甚至的频率。 ("3D"空间设施是也支持。虽然它是可能生成复合的声音数据,它一堆个别的色调结合单源的声音,可以创建多个 IXAudio2SourceVoice 对象并通过相同的掌握语音一起发挥他们。

此外,XAudio2 定义允许您定义的筛选器和其他效果,如混响或回声的 IXAudioSubmixVoice。 筛选器有能力动态地更改现有色调的音色,可以极大地有助于创造有趣和现实的音乐声。

也许以后我在这两个程序中证明了什么最重要加强要求与 XAudio2 回调函数的工作。 而不是分配和初始化大块大块的声音数据,这两个程序一样,它使得程序来动态生成声音数据,因为它正在发挥的更多意义。

Charles Petzold 是 MSDN 杂志和"编程窗口,第 6 版"的作者长期贡献 (O'Reilly 媒体,2012年),一本关于编写应用程序的 Windows 8 书。 他的网站是 charlespetzold.com

衷心感谢以下技术专家对本文的审阅: Scott Selfon