【问题标题】:Fast display of waveform in C/C++用 C/C++ 快速显示波形
【发布时间】:2016-09-29 23:01:44
【问题描述】:

我有兴趣在 Windows 和 Linux 上用 C 或 C++ 实现音频编辑器。我不知道如何在完全缩小的视图中足够快地显示波形。我不是在寻找有关快速帧缓冲技术的信息。这是一个关于算法和数据结构的问题,以有效地确定要显示的内容。

假设我希望能够编辑 2 小时长的 5 通道、48 KHz、24 位声音。那是 5 GB 的样本数据。我希望能够从每个样本一个像素一直缩小,直到所有样本数据一次可见。我希望应用程序即使在慢速机器上也能感觉响应,例如,为了争论,1 GHz Atom。当我说响应时,我希望 GUI 更新通常发生在用户输入的 1/30 秒内。

在决定为完全缩小的视图渲染什么时,一个简单的实现会扫描整个波形中的每个样本 - 它需要找到显示的每个像素宽度“覆盖”的所有样本的最大和最小样本值.我编写了一个简单的应用程序来测试这种方法的速度。我在我的 2015 3.5 GHz Xeon 上使用 1 小时长的单声道 16 位 44.1 KHz 样本进行了测试。它需要 0.12 秒。这太慢了数百倍。

您可以想象维护一个缩小数据的缓存,但我不知道如何避免在大多数插入或删除后重新计算整个缓存。感觉一定有更好的办法。

这是一个图表,显示了我想要实现的目标:

这是目前大多数可用音频编辑器中的显示方式。用户可能会期待这种行为。我用 Audacity 进行了测试,它以这种方式工作(尽管它也以较浅的颜色显示样本的平均值)。它可以处理任意插入到大声音中,看似即时。我不会阅读 75 兆字节的源代码来了解它是如何做到的。

编辑:

许多人提出了在显示缩小视图时只考虑样本子集的方案。我得出的结论是我不想这样做,因为它丢失了太多有用的信息。例如,如果您正在寻找声音中的故障(例如乙烯基转换中的咔嗒声),则包括所有样本很重要。在最坏的情况下,如果故障只有一个样本长,我仍然希望保证它显示在完全缩小的视图中。

【问题讨论】:

  • 下采样!下采样! 1/30 秒在旧机器上是一个梦想,但你可以通过更好的硬件获得好的结果。还要记住,除非您非常小心,否则图形和 IO 将是缓慢的部分。未压缩的波可以寻求读取每个 n 的 1 个样本(不准确但反应灵敏),然后在后台读取和下采样。在其他所有事情完成后缓存并接受你不会有你想要的性能。这不是一个算法或一个数据结构,而是一堆技术来给出印象
  • 下采样是什么意思?根据我对术语的理解,如果我对 20 KHz 方波进行下采样,我会得到一个直流信号。而显示器仍应显示全幅信号。我可以看到,每 n 个样本中读取 1 个以获得不准确但响应式的显示是对我最初问题的可能答案。顺便说一句,我已经可以将数据快速绘制到屏幕上。我只是将所有的线条画到 RAM 中的位图中,然后将整个东西 blit 到屏幕上。
  • 反对票有什么解释吗?
  • 如何更新 GUI?您使用的是什么 GUI 框架?
  • 你的程序是多线程的吗?例如,一个线程处理输入而另一个线程正在绘制?

标签: c++ algorithm performance data-structures


【解决方案1】:

当缩放处于每个像素有多个样本的位置时,不值得准确计算每个像素的平均样本值。用户无法在该缩放级别准确对齐 GUI 工具,因此没有任何好处。用户只需要定性视图。

我只会为窗口区域的每个屏幕像素选择一个样本,跳过不必要的样本。

类似这样的代码完全未经测试

std::vector<double> samples(1024*1024); // [-1.0 < s < 1.0]

int window_x = 1024; // window size in pixels
int window_y = 768; // window size in pixels

// visit every window pixel
for(int x = 0; x < window_x; ++x)
{
    // select relevant sample for the current screen pixel x
    double s = samples[(x * samples.size()) / window_x];

    int y = (window_y / 2) * s; // get y size for sample value

    // draw sample point/line at coordinate (x, f(y))
    gd.draw_line(x, (window_y / 2) - y, x, (window_y / 2) + y);
}

显然您还需要考虑窗口滚动等...

【讨论】:

  • 我认为这类似于阿德里亚诺在 cmets 中对我的问题所说的话。这肯定有一些价值。使用您的方案,我担心声音是频率为 samples.size() / window_x 的正弦波时的病态情况。然后循环的每次迭代都将获得相同的 s 值。我认为 Adriano 建议通过将每像素越来越多的样本视为后台进程来逐步改进这种近似。
  • @AndrewBainbridge 来自发生器的纯正弦波在考虑相邻样本时也可能继续表现出相同的效果,因为它们在每种情况下都是相同的。所以他们的平均值是一样的。至少在我选择将波浪显示为垂直线的方式中,在您的场景中显示将继续正确。但是,它在表示非常低的频率时会遇到麻烦。我想您可以将随机抖动引入从其邻居中精确选择的样本?还是迭代屏幕像素位置的变量的进展?
  • @AndrewBainbridge 是的,我会在第一遍使用这样的东西(但不要在内存中这样做 - 它几乎没用 - 甚至不要 read from磁盘不需要的样本!)它看起来不错(请参阅您的屏幕截图)带有人造信号吗?不,当然不是,但使用 true 信号和... 然后使用所有样本在背景中进行适当的抽取(极小值)就足够了。如果感知速度还不够,那么您可以在块中进行抽取以呈现(并保持 UI 响应)。这是我几乎总是在声音编辑器程序中看到的效果
  • 当然你可能会做得更好(也可以处理正弦波),但它会变得更加复杂,因为你必须优化你的 I/O、缓存和尽可能多地异步执行(再次印象是瞬间的。
  • @Adriano - 我同意大多数真实世界的信号在这种近似下看起来会比正弦波更好。你关于甚至不从磁盘加载样本的观点很有趣。但是,我认为我可以通过相当简单的代码实现我想要的性能并显示完美的结果。一旦我绘制了一些图表,我将很快发布我的解决方案。不过,我必须将所有样本加载到 RAM 中。
【解决方案2】:

也许您可以使用图形中的 mip-mapping 技术,使用更多内存换取更快的速度?

如果您有 32 个样本,请维护一个缩小的 x2、x4、x8 的缓存...存储此数据将再次占用与原始数据相同的空间(16 + 8 + 4 + 2 + 1 个样本)。

视觉指南,. 表示存储的数据点(最小/最大样本值),_ 表示之前的. 覆盖的样本:

1st level: ................
2nd level: ._._._._._._._._
3rd level: .___.___.___.___
4th level: ._______._______
5th level: ._______________

然后只需查询相应级别的 mip-map 即可获得缩放级别。

是的,当您插入/删除样本时,您必须重新创建 mip-map 缓存(或其中的一部分)。

但也许内存使用量使这不适合您?


编辑

如果添加和删除是一种频繁的操作,并且不希望重新计算缓存(并且您希望在间隔内而不是在单个点上进行准确的下采样),那么您可以更改 mip-mapping 方法以存储对齐的数据到本地最小/最大样本点,而不是基于时间的网格。

使用--------|-------- 表示区间内的局部最小值/最大值,这是一个图形表示:

                             --------|--------
   --------|--------
                   --------|--------
                                     --------|--
------|--------
                                     .
           .                        . .
.     .   . .   .          .       .   .     .
 . ... . .   . . .   . .. . .     .     .   . .
  .     .     .   . . .  .   .   .       . .   .
                   .          . .         .
                               .
--------|--------
           --------|--------
                                  --------|-----
                       --------|--------

然后添加和删除只需要在添加/删除部分的开始和结束处重新计算直接局部区域。

您可能希望索引本地最小值/最大值,因此您无需进行大量搜索。实施更复杂的方案 - 对您来说可能不值得?

【讨论】:

  • 是的,我一直在考虑这些问题。鉴于每帧扫描每个样本的速度只有几百倍,我很想只使用一个 mip-map 级别。数组中的一项代表 2^15 个样本(因为我预计我需要支持的最大声音将是 2^30 个样本左右)。这种方案的内存使用量很小。我的问题是,有什么方法可以避免在每次插入/删除后重新计算整个 mip-map。我可以想出一些方法来做到这一点,所以你只需要平均重新计算 1/4,但这对我来说仍然很差。
  • 您编辑的解决方案对我来说看起来很合适。我不太明白您如何决定从哪里开始和结束每个“局部最大值”搜索的范围。我已经发布了一个与您类似的答案,但“局部最大值”是针对每个固定的 1024 样本范围计算的,并在 1024 样本边界上对齐。你觉得这合理吗?还是我错过了一个技巧?
  • 我只是在猜测——我没有实际做过这件事的经验,所以我没有更多的东西可以提供!是的,有一些细节需要填写:是否有固定长度的“本地最大”区域?是否允许它们重叠?在我的示例中,我将它们集中在本地最大样本上,尽管查看最右边的最大值,它实际上并不是最大样本 - 只是尚未覆盖我的“局部最大值”值的最大样本。我会从最大的未覆盖样本开始分配它们,直到所有样本都被局部最大值覆盖。
  • 我刚刚做了一些计算。如果在每个 mip-map “步骤”处将分辨率减半,则总共需要 ~98% 更多 内存。但是,如果将分辨率除以 4,则额外的内存成本会急剧下降,达到约 39%。更进一步,除以 8 将产生约 14% 的总内存成本。因此,在这种情况下,您可以尝试使用除法量,因为内存效率会扩展。即使在 JavaScript 中,我也获得了可靠的 60 fps 可视化(尽管在我的案例中使用了 很多 的 GPU 加速涂层)。
【解决方案3】:

在阅读了 Peter Stock 的回答后,我想出了以下方案。我认为它允许显示计算比简单方案快约 500 倍,并且不应该为插入或删除增加任何明显的成本。内存开销小于 1%。

声音数据将分配在 131072 个样本的块中,因此插入和删除不需要重新分配和复制整个声音。首次加载声音时,每个块都将被完全填充(可能最后一个除外)。插入和删除会导致一种碎片化。为简单起见,我将安排每个块的开头始终包含有效的样本数据,并且任何间隙都将位于块的末尾。

每个块都有两个与之关联的查找表,一个用于最大值,一个用于最小值。查找表中的每一项对应 1024 个样本。

下图显示了如何计算显示器的一个像素宽度的最大值。它显示了一些与计算相关的块。它假设没有“碎片化”。

插入后,情况稍微复杂一些。两个块的末端现在有无效区域。最大查找表中的条目现在对应于样本的部分空白区域。这些条目的值仅通过获取存在的样本的最大值来找到。

【讨论】:

  • 我的实现是github.com/abainbridge/sound_shovel/blob/…。在这个阶段,它只创建一个 2 小时 48 kHz 16 位音频的单声道缓冲区。在我基于 i3-6100U 的机器上计算 800 像素完全缩小视图的显示缓冲区大约需要 2 毫秒。但是,在这个阶段很容易理解。如果/当我使其功能更全面时,我会再次发布。
  • 我现在添加了 WAV 加载和交互式滚动和缩放。现在 GitHub 上也有一个 win32 二进制文件 - github.com/abainbridge/sound_shovel/releases
猜你喜欢
  • 1970-01-01
  • 2011-03-26
  • 2016-01-02
  • 2016-06-05
  • 2011-01-03
  • 1970-01-01
  • 2011-01-11
  • 2022-09-28
  • 2012-05-07
相关资源
最近更新 更多