【问题标题】:Plot an audio waveform in C++/Qt在 C++/Qt 中绘制音频波形
【发布时间】:2017-01-23 19:32:18
【问题描述】:

我有一个大学作业,其中包括使用 C++/Qt 显示音频文件的波形。我们应该能够修改用于显示它的比例(以每个屏幕像素的音频样本数表示)。

到目前为止,我能够:

  • 打开音频文件
  • 阅读示例
  • 以给定的比例绘制样本

为了以给定的比例绘制样本,我尝试了两种策略。假设 N 是刻度的值:

  • 对于 i 从 0 到我的窗口宽度,在屏幕像素 i 处绘制第 i * N 个音频样本时间>。这在时间上非常快速且恒定,因为我们总是访问相同数量的音频数据点。
    但是,它不能正确表示波形,因为我们仅使用 1 个点的值来表示 N 分。

  • 对于 i 从 0 到 N * width,在屏幕位置绘制第 i 个音频样本 i / (N * width) 并让 Qt 弄清楚如何在物理屏幕像素上正确表示它。
    这绘制了非常漂亮的波形,但访问数据需要花费大量时间。例如,如果我想每个像素显示 500 个样本,并且我的窗口宽度为 100 像素,我必须访问 50 000 个点,然后 Qt 将这些点绘制为 100 个物理点(像素)。

那么,我怎样才能得到可以快速计算的正确音频数据图?我应该计算每个物理像素的 N 个样本的平均值吗?我应该做一些曲线拟合吗?
换句话说,当Qt/Matplotlib/Matlab/etc 将数千个数据点绘制到数量非常有限的物理像素上时,涉及到什么样的操作?

【问题讨论】:

  • 二次采样是大量降低音频信号频率的常规方法。 (由于混叠,少量更困难)。平均一个窗口或一个滑动窗口会改变数据。
  • 这与我提出的问题非常相似。您可能会发现那里的讨论很有趣:stackoverflow.com/questions/37554058/…

标签: c++ qt plot real-time


【解决方案1】:

仅仅因为我知道怎么做,而且我已经在 stackoverflow 上问过类似的问题,所以我将引用 this。稍后我会提供代码。

绘制波形是一个真正的问题。我想了半年多! 总结一下:

根据Audacity Documentation

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

  • 波形的深蓝色部分显示像素所代表区域中的最高峰值。在默认缩放级别 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);
    }
}

【讨论】:

    【解决方案2】:

    我的建议是这样的:

    给定音频文件中的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 传递不同的值来缩放/平移,这些值共同指示您要显示的文件的子范围。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-12-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-06-14
      • 1970-01-01
      相关资源
      最近更新 更多