2

我正在为我的毕业写一个声音编辑器。我正在使用BASS从 MP3、WAV、OGG 等文件中提取样本,并添加回声、镶边等 DSP 效果。简单地说,我制作了我的框架,将效果从位置 1 应用到位置 2,剪切/粘贴管理。

现在我的问题是我想创建一个与Cool Edit Pro中的控件类似的控件,该控件绘制歌曲的波形表示,并能够放大/缩小波形的选择部分等。选择后我可以做类似的事情:

TInterval EditZone = WaveForm->GetSelection();

其中 TInterval 具有这种形式:

struct TInterval
{
    long Start;
    long End;
}

当涉及到复杂的绘图时,我是一个初学者,所以任何关于如何使用 BASS 返回的样本数据创建歌曲的波形表示,并具有放大/缩小能力的提示将不胜感激。

我正在用 C++ 编写我的项目,但我可以理解 C#、Delphi 代码,所以如果你愿意,你也可以用最后两种语言发布代码片段:)

感谢 DrOptix

4

4 回答 4

6

通过缩放,我认为您的意思是水平缩放而不是垂直缩放。音频编辑器这样做的方式是扫描波形,将其分解为时间窗口,其中 X 中的每个像素代表一定数量的样本。它可以是一个小数,但您可以不使用小数缩放比例,而不会过多地惹恼用户。缩小一点后,最大值始终为正整数,最小值始终为负整数。

对于屏幕上的每个像素,您需要知道该像素的最小采样值和最大采样值。因此,您需要一个函数来扫描块中的波形数据并跟踪该块的累积最大值和最小值。

这是一个缓慢的过程,因此专业的音频编辑器会以某个固定的缩放比例保留预先计算的最小值和最大值表。它可能位于 512/1 或 1024/1。当您以 > 1024 个样本/像素的缩放比率进行绘图时,您将使用预先计算的表格。如果您低于该比率,您将直接从文件中获取数据。如果你不这样做,你会发现当你缩小时你绘制代码变得太慢了。

做这个扫描的时候写一次处理文件所有通道的代码是值得的,这里的慢会让你的整个程序感觉迟钝,这里重要的是磁盘IO,CPU跟上没有问题,所以简单的 C++ 代码可以很好地构建最小/最大表,但您不想多次浏览文件,而是希望按顺序执行。

一旦你有了最小/最大表,就把它们放在身边。您希望尽可能少地返回磁盘,并且想要重新绘制窗口的许多原因不需要您重新扫描最小/最大表。与最初构建它们的磁盘 io 成本相比,持有它们的内存成本并不高。

然后,通过在该像素表示的时间的最大值和最小值之间绘制一系列 1 像素宽的垂直线来绘制波形。如果您从预先构建的最小/最大表中绘图,这应该会非常快。

于 2010-02-21T08:49:00.860 回答
2

我最近自己做了这个。正如 Marius 建议的那样,您需要计算出每列像素有多少样本。然后计算出最小值和最大值,然后绘制一条从最大值到最小值的垂直线。

作为第一次通过,这似乎工作正常。您会遇到的问题是,当您缩小时,从磁盘中检索样本将开始花费太长时间。作为解决方案,我在音频文件旁边构建了一个“峰值”文件。峰值文件存储 n 个样本组的最小/最大对。玩 n 直到你得到正确的数量取决于你。我个人发现 128 个样本是大小和速度之间的一个很好的权衡。还值得记住的是,除非您绘制的控件尺寸大于 65536 像素,否则您不需要将此峰值信息存储为超过 16 位的值,这样可以节省一点空间。

于 2010-02-21T08:50:15.223 回答
0

您不只是在 2 画布上绘制样本点吗?您应该知道文件每秒有多少样本(从标题中读取),然后在 y 轴上绘制值。由于您希望能够放大和缩小,因此您需要控制每个像素的样本数(缩放级别)。接下来,您取每个像素的样本点的平均值(例如,如果每个像素有 5 个样本,则取每 5 个点的平均值。然后您可以使用 2d 绘图 api 在点之间绘制线条。

于 2010-02-21T08:25:07.923 回答
0

使用开源 NAudio 包 -

public class WavReader2
{
    private readonly WaveFileReader _objStream;

    public WavReader2(String sPath)
    {
        _objStream = new WaveFileReader(sPath);
    }

    public List<SampleRangeValue> GetPixelGraph(int iSamplesPerPixel)
    {
        List<SampleRangeValue> colOutputValues = new List<SampleRangeValue>();

        if (_objStream != null)
        {
            _objStream.Position = 0;
            int iBytesPerSample = (_objStream.WaveFormat.BitsPerSample / 8) * _objStream.WaveFormat.Channels;
            int iNumPixels = (int)Math.Ceiling(_objStream.SampleCount/(double)iSamplesPerPixel);

            byte[] aryWaveData = new byte[iSamplesPerPixel * iBytesPerSample];
            _objStream.Position = 0; // startPosition + (e.ClipRectangle.Left * iBytesPerSample * iSamplesPerPixel);

            for (float iPixelNum = 0; iPixelNum < iNumPixels; iPixelNum += 1)
            {
                short iCurrentLowValue = 0;
                short iCurrentHighValue = 0;
                int iBytesRead = _objStream.Read(aryWaveData, 0, iSamplesPerPixel * iBytesPerSample);
                if (iBytesRead == 0)
                    break;

                List<short> colValues = new List<short>();
                for (int n = 0; n < iBytesRead; n += 2)
                {
                    short iSampleValue = BitConverter.ToInt16(aryWaveData, n);
                    colValues.Add(iSampleValue);
                }

                float fLowPercent =  (float)((float)colValues.Min() /ushort.MaxValue);
                float fHighPercent = (float)((float)colValues.Max() / ushort.MaxValue);

                colOutputValues.Add(new SampleRangeValue(fHighPercent, fLowPercent));
            }
        }

        return colOutputValues;
    }
}

public struct SampleRangeValue
{
    public float HighPercent;
    public float LowPercent;
    public SampleRangeValue(float fHigh, float fLow)
    {
        HighPercent = fHigh;
        LowPercent = fLow;
    }
}
于 2012-02-16T04:22:08.973 回答