2016-05-31 226 views
3

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

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

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

你可以想象维护一个缩小数据的缓存,但我看不到如何避免在大多数插入或删除操作后重新计算整个缓存。感觉好像有一个更好的方法。

这里展示我想达到什么图:

enter image description here

这是如何在最流行可用音频编辑作品的显示。用户可能会期待这种行为。我使用Audacity进行了测试,并且以这种方式工作(尽管它也显示了颜色较浅的样本的平均值)。它可以处理任意插入大声音,看似瞬间。我不打算阅读75兆字节的源代码来了解它是如何实现的。

编辑:

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

+2

降采样!采样! 1/30秒是旧机器上的梦想,但你可以用更好的硬件获得好的结果。另外请记住,图形和IO将是缓慢的部分,除非你非常小心。未压缩的波可以试图读取每个n的1个样本(不准确但响应),然后在背景读取和下采样。一切完成后缓存并接受你将不会有你想要的性能。这不是一种算法或一种数据结构,而是一系列给予*印象*的技术。 –

+0

下采样是什么意思?根据我从这个术语的理解,如果我下取样20 KHz方波,我会得到一个直流信号。而显示器仍应显示全幅信号。我可以看到,读取n个样本中的1个以获得不准确但响应性很高的显示是我原始问题的可能答案。顺便说一句,我已经可以快速将数据绘制到屏幕上了。我只是将所有的行绘制到RAM中的位图中,然后将整个事件呈现给屏幕。 –

+0

反对票的任何解释? –

回答

0

在阅读Peter Stock的回答后,我提出了以下计划。我认为将允许显示计算速度比原始方案快大约500倍,并且不应该增加任何显着的成本来插入或删除。内存开销小于1%。

声音数据将以131072个采样的块分配,因此插入和删除操作不需要重新分配和复制整个声音。当第一次加载声音时,每个块将被完全填充(除了最后一块)。插入和删除会导致一种分裂。为了简单起见,我将安排每个块的开始始终包含有效的样本数据,并且任何间隙将位于块的末尾。

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

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

Display calculation for one pixel width (no fragmentation)

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

Display calculation for one pixel width (with fragmentation)

+0

我的实现是https://github.com/abainbridge/sound_shovel/blob/4c442871ad530748dac9444f3f9b23ace6275ba7/src/main.cpp。在此阶段,它只创建一个2小时48 kHz 16位音频的单声道缓冲区。在基于i3-6100U的机器上计算800像素完全缩小视图的显示缓冲区大约需要2ms。但是,在这个阶段很容易理解。如果/当我使它更加全面时,我会再次发布。 –

+0

我现在添加了WAV加载和交互式滚动和缩放。 GitHub上也有一个win32二进制文件 - https://github.com/abainbridge/sound_shovel/releases –

2

当变焦在其中有多个样品每像素这是不值得准确计算每个像素的平均样本值的点。用户无法在缩放级别准确对齐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); 
} 

很明显,你还需要考虑窗口滚动等...

+0

我认为这与Adriano在我的问题的评论中所说的类似。这绝对有一定的价值。根据你的方案,当声音是正弦波,频率为samples.size()/ window_x时,我担心病态。然后循环的每次迭代将得到s的相同值。我认为Adriano建议通过考虑每个像素的越来越多的采样作为后台处理来逐渐改进这种近似。 –

+0

@AndrewBainbridge在考虑邻近样本时,来自发生器的纯正弦波也可能会继续表现出相同的效果,因为它们在每种情况下都是相同的。所以他们会平均相同。至少在我选择将波形显示为垂直线的方式下,显示在您的场景中仍然是正确的。然而,它代表非常低的频率会遇到麻烦。我想你可以引入随机抖动来准确地从邻居中选择哪个样本?还是迭代屏幕像素位置的变量的进展? – Galik

+0

@AndrewBainbridge是的,我会用第一遍这样的东西(但不要在内存中做 - 它几乎是无用的 - 甚至不需要从磁盘读取不需要的样本!)它看起来不错(请参阅截图)有人造信号?不,当然不是,但它足够用_true_信号和... **然后使用所有样本在背景中进行适当的抽取**(minimax)。如果感知速度不够,那么你可以做块抽取来呈现(并保持UI响应)。这是_effect_我几乎总是看到声音编辑器程序。 –

2

也许你可以使用从图形MIP映射技术,贸易使用更多的内存更快的速度?

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

的可视指导,用.表示存储的数据点(最小/最大采样值)和_由先前.覆盖的样品:

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

然后,只需查询适当的电平的mip-map为缩放级别。

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

但也许内存使用情况使这不适合你?


编辑

如果添加和删除是频繁操作,使高速缓存不可取的重新计算(和你想对间隔精确的采样,而不是仅仅在单个点),那么你可以更改mip映射方法以存储与本地最小/最大采样点对齐的数据,而不是基于时间的网格。

使用--------|--------表示在时间间隔的局部最大/最小值,这里有一个图形表示:

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

然后添加和只去除需要立即局部区域的开始和结束的重新计算添加/删除部分。

您可能想要索引本地最小/最大值,因此您不需要进行大量搜索。一个更复杂的实施方案 - 可能不值得你呢?

+0

是的,我一直在思考这些问题。鉴于每帧扫描每个样本的速度只有几百倍,所以我试图只使用一个mip-map级别。数组中的某个项目代表2^15个样本(因为我期望我需要支持的最大声音大约为2^30个样本)。这种方案的内存使用量很小。我的问题是,是否有任何方法可以避免在每次插入/删除后重新计算整个mip-map。我可以想出一些方法来制作它,所以你只需要重新计算1/4的平均值,但对我而言仍然很差。 –

+0

您编辑的解决方案看起来很适合我。我不明白你是如何决定从哪里开始和结束每个“本地最大”搜索的范围。我发布了一个类似于您的答案,但是对于每个固定的1024个采样范围计算“局部最大值”,并在1024个采样边界上对齐。这对你来说似乎合理吗?或者我错过了一个诀窍? –

+0

我只是在猜测 - 我没有实际的经历,所以我没有更多的提供!是的,有一些细节需要填写:有固定长度的“本地最大”区域吗?让他们重叠或不重合?我的例子我把它们集中在本地最大样本上,虽然看最右边的最大值,但它实际上并不是最大样本 - 只是最大样本没有涵盖我的“本地最大值”。我会从最大的未覆盖样本开始分配它们,并继续进行,直到所有样本都被本地最大值覆盖。 –