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