C# 声音时频图绘制

C# 声音时频图绘制

在这里插入图片描述

采集PCM音频数据

音频原来自麦克风

音频源来自录音文件

处理PCM音频数据

使用 FftSharp.FFT 将PCM数据进行傅里叶变换

安装FftSharp框架

在Nuget包管理器中搜索FftSharp并安装在这里插入图片描述

傅里叶变换

将采集到的PCM数据进行傅里叶变换

      // 傅里叶变换
      System.Numerics.Complex[] spectrum = FftSharp.FFT.Forward(audio);
      double[] ys = FftSharp.FFT.Magnitude(spectrum);

绘制时频图

采用自定义控件的方式来绘制时频图,核心代码如下:

/// <summary>
/// 声音时频图
/// </summary>
public class SoundTimeFreqControl : Control
{

    private const int MarginLeft = 40;
    private const int GradientWidth = 16;


    /// <summary>
    /// 一共有多少格
    /// </summary>
    private const int MaxColumn = 128;

    /// <summary>
    /// 权重
    /// </summary>
    private const int Weight = 0;

    private const int Amplitude = 23;

    // 创建一个画笔
    private Pen pen = new Pen(Brushes.Blue, 1);
    public Brush TextBrush = new SolidColorBrush(Colors.Gainsboro);
    public LinearGradientBrush GradientBrush;

    public double MaxFrequency { get; set; } = 24000;


    public SoundTimeFreqControl()
    {
        dbList = new Queue<double[]>(MaxColumn);

        // 创建渐变色值
        GradientBrush = new LinearGradientBrush();
        GradientBrush.StartPoint = new Point(0, 0);
        GradientBrush.EndPoint = new Point(1, 1);

        // 添加渐变色值
        GradientStop gradientStop3 = new GradientStop(Colors.Yellow, 0);
        GradientStop gradientStop2 = new GradientStop(Colors.Red, 0.5);
        GradientStop gradientStop1 = new GradientStop(Colors.Blue, 1);
        GradientBrush.GradientStops.Add(gradientStop1);
        GradientBrush.GradientStops.Add(gradientStop2);
        GradientBrush.GradientStops.Add(gradientStop3);
    }

    public void Clear()
    {

    }

    public void Refresh()
    {
        this.InvalidateVisual();
    }


    // 数据源,用于存储折线图的数据
    private Queue<double[]> dbList;

    public void AddDataList(double[] audio)
    {
        var data = new double[audio.Length / 4];

        int take = 1;
        for (int i = 0; take < audio.Length; i++)
        {
            data[i] = audio.Skip(take).Take(4).Average();
            take += 4;
        }


        var avg = data.Average();
        var buf = data.Select(x => (x - avg) / avg).ToArray();

        var avg2 = buf.Average();

        for (int i = 0; i < buf.Length; i++)
        {
            if (i <= 1 || i == 47)
            {
                buf[i] = avg2;
            }
            else
            {
                if (buf[i] > avg2)
                {
                    buf[i] *= 2.5;
                }
            }
        }

        testMax = Math.Max(data.Max(), testMax);
        testMin = Math.Min(data.Min(), testMin);

        if (dbList.Count >= MaxColumn)
        {
            dbList.Dequeue();
        }
        dbList.Enqueue(buf);
    }

    public double testMax = double.MinValue;
    public double testMin = double.MaxValue;


    protected override void OnRender(DrawingContext drawingContext)
    {
        // 渲染数据
        DrawTimeFrequency(drawingContext, this.ActualWidth, this.ActualHeight);
    }

    private void DrawTimeFrequency(DrawingContext drawingContext, double imageWidth, double imageHeight)
    {
        double width = imageWidth - MarginLeft * 2 - GradientWidth - 1;
        double height = imageHeight - 1;

        var itemHeight = height / 4;

        drawingContext.DrawRectangle(this.Background, null, new Rect(0, 0, imageWidth, imageHeight));

        // 画方框
        drawingContext.DrawRect(this.Foreground, MarginLeft, 1, width, height);

         画竖线
        //for (int i = 1; i < 4; i++)
        //{
        //    var left = i * itemWidth;
        //    drawingContext.DrawLine(this.Foreground, left, 0, left, height);
        //}

        // 画渐变色域
        drawingContext.DrawRectangle(this.GradientBrush, null, new Rect(imageWidth - GradientWidth - MarginLeft - 1, 0, GradientWidth, imageHeight));


        // 画横线
        for (int i = 1; i < 4; i++)
        {
            var top = i * itemHeight;

            drawingContext.DrawLine(this.Foreground, MarginLeft + 1, top, 8 + MarginLeft, top);
            // 画文本
            var freq = (4 - i) * MaxFrequency * 0.25;
            drawingContext.DrawText(FormatUtil.Frequency(freq), this.TextBrush, MarginLeft / 2, top - 1, 13);



            var text = GetGradientText(4 - i);
            var left = (imageWidth - MarginLeft);
            drawingContext.DrawLine(this.Foreground, left, top, 8 + left, top);
            drawingContext.DrawText(text, this.TextBrush, MarginLeft / 2 + left - 2, top - 1, 13);
        }

        // 画折线
        if (dbList.Count > 0)
        {
            DrawPointPath(drawingContext, width, height);
        }
    }

    private string GetGradientText(int index)
    {
        return $"{index * 25}";
    }

    private void DrawPointPath(DrawingContext drawingContext, double width, double height)
    {
        var itemWidth = width / MaxColumn;


        var brush = Brushes.Yellow;
        //brush.Opacity = 1;

        int index = dbList.Count;
        foreach (var item in dbList)
        {
            var left = (itemWidth) * index + MarginLeft;
            var itemHeight = height / item.Length;
            for (int i = 0; i < item.Length; i++)
            {

                var volume = item[i];

                // 固定范围在 0-100
                if (volume > 0)
                {
                    var value = volume / Amplitude * 100;

                    value = Math.Min(100, value + Weight);

                    if (value > 0)
                    {
                        var color = GetColorByValue((int)value);
                        var mPaintDottLine = new SolidColorBrush(color);
                        mPaintDottLine.Opacity = value / 100+0.15;
                        var top = (item.Length - i) * itemHeight;
                        drawingContext.FillEllipse(mPaintDottLine, left, top, itemWidth, itemHeight);
                    }

                }
            }

            index--;
        }
    }

    public static Color GetColorByValue(int value)
    {
        // 固定范围在 0-100
        value = Math.Max(0, Math.Min(100, value));

        if (value <= 50)
        {
            double ratio = (double)value / 50;
            byte r = (byte)(255 * ratio);
            byte g = (byte)(0);
            byte b = (byte)(178 - (178 * ratio));

            return System.Windows.Media.Color.FromRgb(r, g, b);
        }
        else
        {
            double ratio = (value - 50) / 50f;
            byte r = (byte)(255);
            byte g = (byte)(255 * ratio);
            byte b = (byte)(0);

            return System.Windows.Media.Color.FromRgb(r, g, b);
        }
    }

    /// <summary>
    /// 截图
    /// </summary>
    /// <param name="fileName"></param>
    /// <param name="imageWidth"></param>
    /// <param name="imageHeight"></param>
    /// <param name="dpi"></param>
    public string Screenshot(string fileName, int imageWidth = 800, int imageHeight = 800, double dpi = 96)
    {
        // 创建DrawingVisual对象
        DrawingVisual drawingVisual = new DrawingVisual();

        // 获取DrawingContext以绘制
        using (DrawingContext drawingContext = drawingVisual.RenderOpen())
        {
            DrawTimeFrequency(drawingContext, imageWidth, imageHeight);
        }

        // 创建RenderTargetBitmap以保存绘制内容
        RenderTargetBitmap renderTargetBitmap = new RenderTargetBitmap(
            Convert.ToInt32(dpi / 96 * imageWidth),  // 图片宽度和高度
            Convert.ToInt32(dpi / 96 * imageHeight),
            dpi, dpi,            // DPI设置
        PixelFormats.Pbgra32);

        // 渲染DrawingVisual到RenderTargetBitmap
        renderTargetBitmap.Render(drawingVisual);

        // 创建一个BitmapEncoder(例如PngBitmapEncoder)来保存图像
        PngBitmapEncoder bitmapEncoder = new PngBitmapEncoder();
        bitmapEncoder.Frames.Add(BitmapFrame.Create(renderTargetBitmap));


        // 判断路径是否存在
        var floder = Path.GetDirectoryName(fileName);
        if (!Directory.Exists(floder))
        {
            Directory.CreateDirectory(floder);
        }

        // 保存图像到文件
        using (var fileStream = File.Create(fileName))
        {
            bitmapEncoder.Save(fileStream);
        }

        return fileName;
    }

}

其他拓展类

FormatUtil

internal class FormatUtil
 {
     public static string Frequency(double freq)
     {
         if (freq < 1000)
         {
             return string.Format("{0}Hz", (int)freq);
         }
         else
         {
             var value = Math.Floor(freq / 1000);
             return string.Format("{0}kHz", value);
         }

     }
 }

DrawingContextExt

public static class DrawingContextExt
{
    public static void DrawRect(this DrawingContext drawingContext, Brush color, double x, double y, double w, double h)
    {
        drawingContext.DrawRectangle(null, new Pen(color, 1), new System.Windows.Rect(x, y, w, h));
    }

    public static void DrawLine(this DrawingContext drawingContext, Brush color, double x, double y, double x2, double y2)
    {
        drawingContext.DrawLine(new Pen(color, 1), new Point(x, y), new Point(x2, y2));
    }





    public static void FillEllipse(this DrawingContext drawingContext, Brush brush, double x, double y, double w, double h)
    {
        var radiusX = w / 2;
        var radiusY = h / 2;
        drawingContext.DrawEllipse(brush, null, new Point(x - radiusX, y - radiusY), radiusX, radiusY);
    }



    public static void DrawText(this DrawingContext drawingContext, string data, Brush brush, double x, double y, double emSize = 10)
    {
        // 创建FormattedText对象以设置文字的样式、位置和对齐方式
        FormattedText formattedText = new FormattedText(
            data,
            System.Globalization.CultureInfo.CurrentCulture,
            FlowDirection.LeftToRight,
            new Typeface("Arial"),
            emSize, brush);

        // 设置文字在 (50, 50) 的位置水平和垂直居中

        // 计算绘制点的坐标,使文本居中绘制
        Point drawPoint = new Point(x - formattedText.Width / 2, y - formattedText.Height / 2);

        // 绘制文字
        drawingContext.DrawText(formattedText, drawPoint);

    }
}

相关推荐

最近更新

  1. TCP协议是安全的吗?

    2024-06-06 08:24:04       16 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-06-06 08:24:04       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-06-06 08:24:04       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-06-06 08:24:04       18 阅读

热门阅读

  1. 【Vue】水果购物车-基本渲染

    2024-06-06 08:24:04       9 阅读
  2. Go 错误处理

    2024-06-06 08:24:04       6 阅读
  3. 【C++】6-7 你好,输出的格式控制(三角形)

    2024-06-06 08:24:04       7 阅读
  4. HarmonyOS NEXT Push接入

    2024-06-06 08:24:04       6 阅读
  5. 解决VIvado编程中遇到的bug 2

    2024-06-06 08:24:04       7 阅读
  6. spark相关知识

    2024-06-06 08:24:04       6 阅读