在 C++/Qt 中绘制音频波形
Plot an audio waveform in C++/Qt
我的大学作业是使用 C++/Qt 显示音频文件的波形。我们应该能够修改我们用来显示它的scale(以每个屏幕像素的音频样本表示)。
到目前为止,我能够:
- 打开音频文件
- 阅读样本
- 以给定比例绘制样本
为了以给定比例绘制样本,我尝试了两种策略。假设 N 是比例的值:
对于 i 从 0 到我的 window 的宽度,绘制 i * N 屏幕像素 i 处的第 ] 个音频样本。这是非常快且时间恒定的,因为我们总是访问相同数量的音频数据点。
然而,它并不能正确表示波形,因为我们只使用1个点的值来表示N个点。
对于 i 从 0 到 N * width,绘制 ith 音频样本在屏幕位置 i / (N * width) 让 Qt 弄清楚如何在物理屏幕像素上正确表示它。
绘制的波形非常漂亮,但访问数据要花很多时间。例如,如果我想每个像素显示 500 个样本并且我的 window 的宽度是 100px,我必须访问 50 000 个点,然后 Qt 将其绘制为 100 个物理点(像素)。
那么,我怎样才能得到正确的音频数据图,并且可以快速计算出来呢?我应该计算每个物理像素的 N 个样本的平均值吗?我应该做一些曲线拟合吗?
换句话说,当Qt/Matplotlib/Matlab/etc将数千个数据点绘制到非常有限的物理像素时,涉及什么样的操作?
因为我知道怎么做,而且我已经在 Whosebug 上问过类似的问题,所以我将参考 this。稍后我会提供代码。
绘制波形是一个真正的问题。我试图弄清楚这个问题超过半年!
总结一下:
The waveform view uses two shades of blue, one darker and one lighter.
- The dark blue part of the waveform displays the tallest peak in the area that pixel represents. At default zoom level Audacity will
display many samples within that pixel width, so this pixel represents
the value of the loudest sample in the group.
- The light blue part of the waveform displays the average RMS (Root Mean Square) value for the same group of samples. This is a rough
guide to how loud this area might sound, but there is no way to
extract or use this RMS part of the waveform separately.
因此,您只是尝试从一大块数据中获取重要信息。如果你一遍又一遍地这样做,你将拥有多个可用于绘图的阶段。
我将在这里提供一些代码,请耐心等待它正在开发中:
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);
}
}
我的建议是这样的:
给定音频文件中的 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() 的源代码,因为希望它们的作用从它们的名称中显而易见,但如果没有,请告诉我,我可以解释他们。
一旦您的上述工作相当顺利(即绘制一个波形,将整个文件显示到您的小部件中),您就可以开始添加 zoom-and-pan 功能。可以通过修改 GetFirstSampleIndexForPixel() 的行为来实现水平缩放,例如:
int GetFirstSampleIndexForPixel(int x, int widgetWidth, int sampleIndexAtLeftEdgeOfWidget, int sampleIndexAfterRightEdgeOfWidget)
{
int numSamplesToDisplay = sampleIndexAfterRightEdgeOfWidget-sampleIndexAtLeftEdgeOfWidget;
return sampleIndexAtLeftEdgeOfWidget+((numSamplesToDisplay*x)/widgetWidth);
}
有了它,您可以 zoom/pan 只需为 sampleIndexAtLeftEdgeOfWidget
和 sampleIndexAfterRightEdgeOfWidget
传递不同的值,它们一起表示您要显示的文件的子范围。
我的大学作业是使用 C++/Qt 显示音频文件的波形。我们应该能够修改我们用来显示它的scale(以每个屏幕像素的音频样本表示)。
到目前为止,我能够:
- 打开音频文件
- 阅读样本
- 以给定比例绘制样本
为了以给定比例绘制样本,我尝试了两种策略。假设 N 是比例的值:
对于 i 从 0 到我的 window 的宽度,绘制 i * N 屏幕像素 i 处的第 ] 个音频样本。这是非常快且时间恒定的,因为我们总是访问相同数量的音频数据点。
然而,它并不能正确表示波形,因为我们只使用1个点的值来表示N个点。对于 i 从 0 到 N * width,绘制 ith 音频样本在屏幕位置 i / (N * width) 让 Qt 弄清楚如何在物理屏幕像素上正确表示它。
绘制的波形非常漂亮,但访问数据要花很多时间。例如,如果我想每个像素显示 500 个样本并且我的 window 的宽度是 100px,我必须访问 50 000 个点,然后 Qt 将其绘制为 100 个物理点(像素)。
那么,我怎样才能得到正确的音频数据图,并且可以快速计算出来呢?我应该计算每个物理像素的 N 个样本的平均值吗?我应该做一些曲线拟合吗?
换句话说,当Qt/Matplotlib/Matlab/etc将数千个数据点绘制到非常有限的物理像素时,涉及什么样的操作?
因为我知道怎么做,而且我已经在 Whosebug 上问过类似的问题,所以我将参考 this。稍后我会提供代码。
绘制波形是一个真正的问题。我试图弄清楚这个问题超过半年! 总结一下:
The waveform view uses two shades of blue, one darker and one lighter.
- The dark blue part of the waveform displays the tallest peak in the area that pixel represents. At default zoom level Audacity will display many samples within that pixel width, so this pixel represents the value of the loudest sample in the group.
- The light blue part of the waveform displays the average RMS (Root Mean Square) value for the same group of samples. This is a rough guide to how loud this area might sound, but there is no way to extract or use this RMS part of the waveform separately.
因此,您只是尝试从一大块数据中获取重要信息。如果你一遍又一遍地这样做,你将拥有多个可用于绘图的阶段。
我将在这里提供一些代码,请耐心等待它正在开发中:
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);
}
}
我的建议是这样的:
给定音频文件中的 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() 的源代码,因为希望它们的作用从它们的名称中显而易见,但如果没有,请告诉我,我可以解释他们。
一旦您的上述工作相当顺利(即绘制一个波形,将整个文件显示到您的小部件中),您就可以开始添加 zoom-and-pan 功能。可以通过修改 GetFirstSampleIndexForPixel() 的行为来实现水平缩放,例如:
int GetFirstSampleIndexForPixel(int x, int widgetWidth, int sampleIndexAtLeftEdgeOfWidget, int sampleIndexAfterRightEdgeOfWidget)
{
int numSamplesToDisplay = sampleIndexAfterRightEdgeOfWidget-sampleIndexAtLeftEdgeOfWidget;
return sampleIndexAtLeftEdgeOfWidget+((numSamplesToDisplay*x)/widgetWidth);
}
有了它,您可以 zoom/pan 只需为 sampleIndexAtLeftEdgeOfWidget
和 sampleIndexAfterRightEdgeOfWidget
传递不同的值,它们一起表示您要显示的文件的子范围。