如何将音频波形绘制到位图

时间:2016-09-20 23:12:43

标签: c# audio visualization

我正在尝试提取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

那么我怎么能以最简单的方式做到这一点呢?

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

  1. 以不会丢失信息的方式将float转换为int,这归因于SetPixel(i, y, Color.Black);其中x&amp; y都是int
  2. 在x轴上跳过样本,使波形适合定义的空间audio length / image width,给出平均输出强度的样本数,用单个像素表示
  3. 其他选项是找到另一种绘制波形的方法,该方法不依赖于上述方法。 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);
    

    输出如下所示: enter image description here

2 个答案:

答案 0 :(得分:2)

所以我设法使用代码示例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样本的波形表示: enter image description here

我很确定需要对代码进行一些更新/修订,但这是一个开始,希望这会帮助那些试图做同样事情的其他人。

如果您能看到任何可以改进的地方,请与我们联系。

<强>更新

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

    平均水平(非RMS)通过将每个样本点的最大水平相加并除以样本总数来确定。这方面的例子如下:

    无声的Wav档案: enter image description here

    Hand Clap Wav文件: enter image description here

    布朗,粉红&amp;白噪声Wav文件: enter image description here

答案 1 :(得分:1)

以下是您可能希望学习的变体。 扩展 Graphics对象,以便它可以直接使用float数据。

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

它还使用DrawLines方法进行绘制。除速度之外的好处是线可以是半透明的或比一个像素更厚而不会在关节处产生伪影。你可以看到中心线闪耀。

为此,我使用一点List<PointF> magick将浮点数据转换为Linq

我还确保将我创建的所有GDI +对象放在using子句中,这样它们就会被正确处理掉。

enter image description here

...
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;
    }
}