带时间戳的事件

合成器计时的总体情况是,每个音符都有时间戳并放在缓冲区中,而不是在需要播放的确切时间发送音符。 然后,在时间戳指定的时间的两毫秒内处理和播放该缓冲区。 (尽管计时分辨率以数百纳秒为单位,但我们将以毫秒为单位进行讨论,这是本讨论中更方便的时间单位。)

由于系统通过延迟时钟知道延迟,因此时间戳事件可以在缓冲区中等待在适当的时间播放,而不仅仅是将事件丢弃到队列中,并且希望延迟较低。

主时钟实现 COM IReferenceClock 接口(在 Microsoft Windows SDK 文档中介绍)。 系统上的所有设备都使用此参考时间。

Microsoft 的波形接收器实现生成每 20 毫秒唤醒一次的线程。 线程的工作是创建另一个缓冲区并将其交给 DirectSound。 为了创建该缓冲区,它调用合成器,并要求它呈现指定数量的音乐数据。 要求的数量由线程唤醒的实际时间决定,这不太可能正好是 20 毫秒。

实际传递到合成器的只是一个指向内存中开始将数据写入 PCM 缓冲区的位置的指针,以及一个指定要写入多少数据的长度参数。 然后,合成器可以将 PCM 数据写入此缓冲区,并将其填充到指定的量。 也就是说,从起始地址开始呈现,直到达到指定的长度。 该内存块可以是 DirectSoundBuffer(这是默认情况),但也可以是 DirectShow 图形或波形接收器定义的一些其他目标。

PCM 缓冲区在概念上是循环的(即,不断循环)。 合成器将描述声音的 16 位数字呈现到缓冲区的连续切片中。 每次线程唤醒时,切片大小略有不同,因为接收器不能精确地每 20 毫秒唤醒一次。 因此,每次线程唤醒时,都会进行追赶,以确定在返回睡眠之前应该在缓冲区中前进多远。

从应用程序的角度来看,合成器端口驱动程序本身具有 IDirectMusicSynth::GetLatencyClock 函数,该函数从波形接收器获取时钟。 因此有两个时钟:

  • 每一个(包括波形接收器)都侦听的主时钟。

  • 由波形接收器实现的延迟时钟,应用程序将其视为提供延迟时钟的 DirectMusic 端口。

换句话说,应用程序要求延迟时钟,但认为时钟来自 DirectMusic 端口抽象,而不是来自波形接收器。

此延迟时钟返回的时间是可以呈现缓冲区的最早时间,因为合成器已经呈现到缓冲区中的那个点。 如果合成器在最后一次写入时提供了更小的缓冲区,那么延迟也会更小。

因此,波形接收器在合成器上调用 IDirectMusicSynth::Render,显示缓冲区并请求用呈现数据填充缓冲区。 如下图所示,合成器接收作为 IDirectMusicSynth::PlayBuffer 函数调用的结果而传入的所有带有时间戳的事件。

Diagram illustrating the queuing process of time-stamped messages in a synthesizer.

每个输入缓冲区都包含带有时间戳的消息。 这些消息中的每一条都被放入队列中,以便在其时间戳指定的时间呈现到缓冲区中。

这个模型的一个重要之处在于,除了时间戳之外,没有其他特定的顺序。 这些事件将流式传输到队列中,因此可以在呈现之前随时将其添加到队列中。 一切都是以时间为基础的。 例如,如果参考时间当前处于 400 个时间单位,那么时间戳为在时间 400 发生的所有事情现在都在发生。 时间戳为从现在起 10 个单位发生的事件将在时间 410 发生。