【问题标题】:How to draw an audio waveform to a bitmap如何将音频波形绘制到位图
【发布时间】:2016-09-20 23:12:43
【问题描述】:

我正在尝试提取 wav 文件的音频内容并将生成的波形导出为图像 (bmp/jpg/png)。

所以我找到了以下代码,它可以绘制正弦波并按预期工作:

    string filename = @"C:\0\test.bmp";
    int width = 640;
    int height = 480;
    Bitmap b = new Bitmap(width, height);

    for (int i = 0; i < width; i++)
    {
        int y = (int)((Math.Sin((double)i * 2.0 * Math.PI / width) + 1.0) * (height - 1) / 2.0);
        b.SetPixel(i, y, Color.Black);
    }
    b.Save(filename);

这完全符合预期,我想做的是替换

int y = (int)((Math.Sin((double)i * 2.0 * Math.PI / width) + 1.0) * (height - 1) / 2.0);

类似的东西

int y = converted and scaled float from monoWaveFileFloatValues

那么,我最好如何以最简单的方式来做这件事呢?

我有 2 个基本问题需要处理(我认为)

  1. 以不丢失信息的方式将 float 转换为 int,这是由于 SetPixel(i, y, Color.Black); 其中 x 和 y 都是 int
  2. 在 x 轴上跳过样本,以便波形适合定义的空间 audio length / image width 给出样本数以平均由单个像素表示的强度

其他选项是找到另一种绘制波形的方法,该方法不依赖于上述方法。 Using a chart 可能是个好方法,但如果可能的话我希望能够直接渲染图像

这一切都是从控制台应用程序运行的,并且我的音频数据(减去标题)已经在一个浮点数组中。


更新 1

以下代码使我能够使用System.Windows.Forms.DataVisualization.Charting 绘制所需的输出,但处理 27776 个样本需要大约 30 秒,虽然它确实可以满足我的需求,但速度太慢而无法使用。所以我仍在寻找一种直接绘制位图的解决方案。

    System.Windows.Forms.DataVisualization.Charting.Chart chart = new System.Windows.Forms.DataVisualization.Charting.Chart();
    chart.Size = new System.Drawing.Size(640, 320);
    chart.ChartAreas.Add("ChartArea1");
    chart.Legends.Add("legend1");

    // Plot {sin(x), 0, 2pi} 
    chart.Series.Add("sin");
    chart.Series["sin"].LegendText = args[0];
    chart.Series["sin"].ChartType = System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Spline;

    //for (double x = 0; x < 2 * Math.PI; x += 0.01)
    for (int x = 0; x < audioDataLength; x ++)
    {
        //chart.Series["sin"].Points.AddXY(x, Math.Sin(x));
        chart.Series["sin"].Points.AddXY(x, leftChannel[x]);
    }

    // Save sin_0_2pi.png image file
    chart.SaveImage(@"c:\tmp\example.png", System.Drawing.Imaging.ImageFormat.Png);

输出如下:

【问题讨论】:

  • 那么你有什么代码可以读取音频文件吗?评估标题然后检查数据?这应该是你的开始;绘图只是在.. 而且,不,绘制图表的点数并不是一个好主意,imo
  • @TaW - “我的音频数据(减去标题)已经在浮点数组中。”所以我正在寻找下一步。

标签: c# audio visualization


【解决方案1】:

所以我设法使用代码示例 found here 来解决这个问题,尽管我对与它的交互方式做了一些小改动。

public static Bitmap DrawNormalizedAudio(List<float> data, Color foreColor, Color backColor, Size imageSize, string imageFilename)
{
    Bitmap bmp = new Bitmap(imageSize.Width, imageSize.Height);

    int BORDER_WIDTH = 0;
    float width = bmp.Width - (2 * BORDER_WIDTH);
    float height = bmp.Height - (2 * BORDER_WIDTH);

    using (Graphics g = Graphics.FromImage(bmp))
    {
        g.Clear(backColor);
        Pen pen = new Pen(foreColor);
        float size = data.Count;
        for (float iPixel = 0; iPixel < width; iPixel += 1)
        {
            // determine start and end points within WAV
            int start = (int)(iPixel * (size / width));
            int end = (int)((iPixel + 1) * (size / width));
            if (end > data.Count)
                end = data.Count;

            float posAvg, negAvg;
            averages(data, start, end, out posAvg, out negAvg);

            float yMax = BORDER_WIDTH + height - ((posAvg + 1) * .5f * height);
            float yMin = BORDER_WIDTH + height - ((negAvg + 1) * .5f * height);

            g.DrawLine(pen, iPixel + BORDER_WIDTH, yMax, iPixel + BORDER_WIDTH, yMin);
        }
    }
    bmp.Save(imageFilename);
    bmp.Dispose();
    return null;
}


private static void averages(List<float> data, int startIndex, int endIndex, out float posAvg, out float negAvg)
{
    posAvg = 0.0f;
    negAvg = 0.0f;

    int posCount = 0, negCount = 0;

    for (int i = startIndex; i < endIndex; i++)
    {
        if (data[i] > 0)
        {
            posCount++;
            posAvg += data[i];
        }
        else
        {
            negCount++;
            negAvg += data[i];
        }
    }

    if (posCount > 0)
       posAvg /= posCount;
    if (negCount > 0)
       negAvg /= negCount;
}

为了让它工作,在调用 DrawNormalizedAudio 方法之前我必须做几件事,你可以在下面看到我需要做的事情:

    Size imageSize = new Size();
    imageSize.Width = 1000;
    imageSize.Height = 500;
    List<float> lst = leftChannel.OfType<float>().ToList(); //change float array to float list - see link below
    DrawNormalizedAudio(lst, Color.Red, Color.White, imageSize, @"c:\tmp\example2.png");

* change float array to float list

这样的结果如下,一个手拍wav样本的波形表示:

我很确定需要对代码进行一些更新/修订,但这是一个开始,希望这将有助于其他尝试做与我相同的事情的人。

如果您发现任何可以改进的地方,请告诉我。


更新

  1. cmets 中提到的 NaN 问题现已解决,上面的代码已更新。
  2. 已更新波形图像以表示通过删除第 1 点中所述的 NaN 值修复的输出。

更新 1

平均电平(不是 RMS)是通过将每个采样点的最大电平相加并除以采样总数来确定的。这方面的例子可以在下面看到:

静音 Wav 文件:

拍手 Wav 文件:

布朗、粉红和白噪声 Wav 文件:

【讨论】:

  • 很高兴你能走到这一步!如果您首先创建 List:var points = data.ToList().Select((y, x) =&gt; new { x, y }).Select(p =&gt; new PointF(p.x, p.y)).ToList();,则可以使用 DrawLines 方法一次性绘制线条(推荐用于速度和质量)。您可能还想使用Graphics.ScaleTranform 进行所有缩放而不是缩放所有坐标。像g.ScaleTransform(0.1f, 0.1f);(或更少)这样的东西将是一个开始,但您应该使用var xScale = (data.Max() - data.Min()) / imageSize.Width;etc 进行计算。另外:你需要Dispose()的Bitmap!
  • @TaW 你提到的第一部分我将不得不与缩放一起研究,更详细地确保我理解这个过程。我现在将使用 Dispose() 更新我的代码。在我做任何进一步的事情之前,我得到了我需要处理的 NaN 值。不知道为什么,但一旦我弄清楚了,也会更新代码。
  • 不确定我是否理解平均水平线。删除 NaN(i.r. 溢出)数据点也不是处理这些值的正确方法。难道他们错过了求和但计算在除法器中,从而改变了平均值?为什么什么东西会溢出?
【解决方案2】:

这是您可能想要学习的一个变体。它缩放 Graphics 对象,因此它可以直接使用float 数据。

请注意我如何将绘图区域平移(即移动)两次,以便更方便地进行绘图!

它还使用DrawLines 方法进行绘图。除了速度之外的好处是线条可能是半透明的或比一个像素粗,而不会在关节处出现伪影。你可以看到中心线穿过。

为此,我使用一点 Linq magick 将浮点数据转换为 List&lt;PointF&gt;

我还确保将我创建的所有 GDI+ 对象放在 using 子句中,以便正确处理它们。

...
using System.Windows.Forms;
using System.IO;
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Drawing2D;
..
..
class Program
{
    static void Main(string[] args)
    {
        float[] data = initData(10000);
        Size imgSize = new Size(1000, 400);
        Bitmap bmp = drawGraph(data, imgSize , Color.Green, Color.Black);
        bmp.Save("D:\\wave.png", ImageFormat.Png);
    }

    static float[] initData(int count)
    {
        float[] data = new float[count];

        for (int i = 0; i < count; i++ )
        {
            data[i] = (float) ((Math.Sin(i / 12f) * 880 + Math.Sin(i / 15f) * 440
                              + Math.Sin(i / 66) * 110) / Math.Pow( (i+1), 0.33f));
        }
        return data;
    }

    static Bitmap drawGraph(float[] data, Size size, Color ForeColor, Color BackColor)
    {
        Bitmap bmp = new System.Drawing.Bitmap(size.Width, size.Height, 
                                PixelFormat.Format32bppArgb);
        Padding borders = new Padding(20, 20, 10, 50);
        Rectangle plotArea = new Rectangle(borders.Left, borders.Top,
                       size.Width - borders.Left - borders.Right, 
                       size.Height - borders.Top - borders.Bottom);
        using (Graphics g = Graphics.FromImage(bmp))
        using (Pen pen = new Pen(Color.FromArgb(224, ForeColor),1.75f))
        {
            g.SmoothingMode = SmoothingMode.AntiAlias;
            g.Clear(Color.Silver);
            using (SolidBrush brush = new SolidBrush(BackColor))
                g.FillRectangle(brush, plotArea);
            g.DrawRectangle(Pens.LightGoldenrodYellow, plotArea);

            g.TranslateTransform(plotArea.Left, plotArea.Top);

            g.DrawLine(Pens.White, 0, plotArea.Height / 2,
                   plotArea.Width,  plotArea.Height / 2);


            float dataHeight = Math.Max( data.Max(), - data.Min()) * 2;
            float yScale = 1f * plotArea.Height / dataHeight;
            float xScale = 1f * plotArea.Width / data.Length;


            g.ScaleTransform(xScale, yScale);
            g.TranslateTransform(0, dataHeight / 2);

            var points = data.ToList().Select((y, x) => new { x, y })
                             .Select(p => new PointF(p.x, p.y)).ToList();

            g.DrawLines(pen, points.ToArray());

            g.ResetTransform();
            g.DrawString(data.Length.ToString("###,###,###,##0") + " points plotted.", 
                new Font("Consolas", 14f), Brushes.Black, 
                plotArea.Left, plotArea.Bottom + 2f);
        }
        return bmp;
    }
}

【讨论】:

  • 我喜欢你在这里所做的@Taw!我一定会更详细地看看这个,谢谢!直接绘制浮点数是一个好主意,而且我只能在使用图表方法时才能做到这一点,这真的很慢,所以这会很好解决。
  • 这看起来很棒,我很高兴尝试它,但是.. 看起来这些示例是在小段音频上。我在一个 78 分钟的波形文件上试过它,但它从未完成。它窒息 g.DrawLines(pen, points.ToArray());我的波浪数据是8300万点;
  • 我并不感到惊讶。 8000 万以上的积分远远超出您的想象;事实上,它可能会遇到 gdi+ 数学的精确障碍。
  • 我确实使用上述 majikais 代码的变体使其工作。它不会导致任何错误,因为文件是从流中读取的,并且绘制线调用是按顺序发生的。一个核心 i7 和 80 分钟的波形文件大约需要 3 秒。同样对于 bsckground 你可以使用 Color.Transparent
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-12-03
  • 1970-01-01
  • 1970-01-01
  • 2016-06-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多