在C++/Qt中绘制音频波形

3
我有一个大学任务,需要使用C++/Qt显示音频文件的波形。我们应该能够修改用于显示它的比例(以屏幕像素每个音频样本表示)。
到目前为止,我已经能够:
- 打开音频文件 - 读取样本 - 在给定的比例下绘制样本
为了在给定的比例下绘制样本,我尝试了两种策略。假设N是比例的值:
- 对于从0到窗口宽度的i,将第i * N个音频样本绘制在屏幕像素i上。这非常快,并且时间恒定,因为我们总是访问相同数量的音频数据点。然而,它不正确地表示波形,因为我们使用一个点的值来表示N个点。 - 对于从0到N *宽度的i,将第i个音频样本绘制在屏幕位置i /(N *宽度)处,并让Qt找出如何在物理屏幕像素上正确表示它。这会绘制非常漂亮的波形,但访问数据需要很长时间。例如,如果我想要每个像素显示500个样本,并且我的窗口宽度为100像素,则必须访问50,000个点,然后由Qt作为100个物理点(像素)绘制。
那么,如何获得正确的音频数据绘图,而且还能快速计算?我应该为每个物理像素计算N个样本的平均值吗?我应该进行一些曲线拟合吗?
换句话说,在Qt / Matplotlib / Matlab等将数千个数据点绘制到非常有限的物理像素上时涉及哪种操作?

子采样是将音频信号的频率大幅降低的常规方法。(由于混叠,小幅降低更加困难)。对窗口或滑动窗口进行平均处理会改变数据。 - Malcolm McLean
这与我提出的一个问题非常相似。您可能会发现那里的讨论很有趣:https://dev59.com/gpffa4cB1Zd3GeqP-Jdy - Andrew Bainbridge
2个回答

6

因为我知道如何做,并且在stackoverflow上问过类似的问题,所以我会引用这个。稍后我会提供代码。

绘制波形是一个真正的问题。我试图弄清楚这个问题已经半年多了!总结一下:

根据Audacity文档

enter image description here

波形视图使用两种蓝色,一种较深,一种较浅。

  • 波形的深蓝部分显示该像素代表区域中最高的峰值。在默认缩放级别下,Audacity将在该像素宽度内显示许多样本,因此该像素表示组中最响亮的样本的值。
  • 波形的浅蓝部分显示相同样本组的平均RMS(均方根)值。这是一个大致指导,可以判断该区域的声音有多响,但没有办法单独提取或使用这个波形的RMS部分。

因此,您只需尝试从一块数据中获取重要信息。如果您一遍又一遍地做这件事,就会有多个阶段可用于绘制。

我将在此提供一些代码,请耐心等待,因为它还在开发中:

template<typename T>
class CacheHandler {
public:

    std::vector<T> data;
    vector2d<T> min, max, rms;

    CacheHandler(std::vector<T>& data) throw(std::exception);

    void addData(std::vector<T>& samples);

    /*
    irreversible removes data.
    Fails if end index is greater than data length
    */
    void removeData(int endIndex);

    void removeData(int startIndex, int endIndex);
};

使用以下代码:

template<typename T>
inline WaveformPane::CacheHandler<T>::CacheHandler(std::vector<T>& data, int sampleSizeInBits) throw(std::exception)
{
    this->data = data;
    this->sampleSizeInBits = sampleSizeInBits;
    int N = log(data.size()) / log(2);
    rms.resize(N); min.resize(N); max.resize(N);
    rms[0] = calcRMSSegments(data, 2);
    min[0] = getMinPitchSegments(data, 2);
    max[0] = getMaxPitchSegments(data, 2);
    for (int i = 1; i < N; i++) {
        rms[i] = calcRMSSegments(rms[i - 1], 2);
        min[i] = getMinPitchSegments(min[i - 1], 2);
        max[i] = getMaxPitchSegments(max[i - 1], 2);
    }
}

3
我建议的是这样做:
假设你的音频文件中有totalNumSamples个音频样本,显示部件的宽度为widgetWidth像素,你可以计算出每个像素所代表的音频样本:
// Given an x value (in pixels), returns the appropriate corresponding
// offset into the audio-samples array that represents the
// first sample that should be included in that pixel.
int GetFirstSampleIndexForPixel(int x, int widgetWidth, int totalNumSamples)
{
   return (totalNumSamples*x)/widgetWidth;
}

virtual void paintEvent(QPaintEvent * e)
{
   QPainter p(this);
   for (int x=0; x<widgetWidth; x++)
   {
      const int firstSampleIndexForPixel = GetFirstSampleIndexForPixel(x, widgetWidth, totalNumSamples);
      const int lastSampleIndexForPixel = GetFirstSampleIndexForPixel(x+1, widgetWidth, totalNumSamples)-1;
      const int largestSampleValueForPixel = GetMaximumSampleValueInRange(firstSampleIndexForPixel, lastSampleIndexForPixel);
      const int smallestSampleValueForPixel = GetMinimumSampleValueInRange(firstSampleIndexForPixel, lastSampleIndexForPixel);

      // draw a vertical line spanning all sample values that are contained in this pixel
      p.drawLine(x, GetYValueForSampleValue(largestSampleValueForPixel), x, GetYValueForSampleValue(smallestSampleValueForPixel));
   }
}

请注意,我没有包含GetMinimumSampleValueInRange()、GetMaximumSampleValueInRange()或GetYValueForSampleValue()的源代码,因为它们的功能从它们的名称中应该很明显,但如果不明白,请告诉我,我可以解释。
一旦您成功实现了上述内容(即在小部件中绘制显示整个文件的波形),您就可以开始添加缩放和平移功能。水平缩放可以通过修改GetFirstSampleIndexForPixel()的行为来实现,例如:
int GetFirstSampleIndexForPixel(int x, int widgetWidth, int sampleIndexAtLeftEdgeOfWidget, int sampleIndexAfterRightEdgeOfWidget)
{
   int numSamplesToDisplay = sampleIndexAfterRightEdgeOfWidget-sampleIndexAtLeftEdgeOfWidget;
   return sampleIndexAtLeftEdgeOfWidget+((numSamplesToDisplay*x)/widgetWidth);
}

有了这个功能,你可以通过传递不同的值给sampleIndexAtLeftEdgeOfWidgetsampleIndexAfterRightEdgeOfWidget来进行缩放/平移,这两个值共同指示你想要显示的文件子范围。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接