using LibAudioVisualizer; using NAudio.CoreAudioApi; using NAudio.Wave; using System; using System.Collections.Generic; using System.Linq; using System.Numerics; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using System.Windows.Threading; namespace WpfAudioVisualizer { /// /// Interaction logic for MainWindow.xaml /// public partial class MainWindow : Window { WasapiCapture capture; // 音频捕获 Visualizer visualizer; // 可视化 Timer? dataTimer; Timer? drawingTimer; double[]? spectrumData; // 频谱数据 Color[] allColors; // 渐变颜色 public MainWindow() { capture = new WasapiLoopbackCapture(); // 捕获电脑发出的声音 visualizer = new Visualizer(256); // 新建一个可视化器, 并使用 256 个采样进行傅里叶变换 allColors = GetAllHsvColors(); // 获取所有的渐变颜色 (HSV 颜色) capture.WaveFormat = WaveFormat.CreateIeeeFloatWaveFormat(8192, 1); // 指定捕获的格式, 单声道, 32位深度, IeeeFloat 编码, 8192采样率 capture.DataAvailable += Capture_DataAvailable; // 订阅事件 InitializeComponent(); } /// /// 简单的数据模糊 /// /// 数据 /// 模糊半径 /// 结果 private double[] MakeSmooth(double[] data, int radius) { double[] GetWeights(int radius) { double Gaussian(double x) => Math.Pow(Math.E, (-4 * x * x)); // 憨批高斯函数 int len = 1 + radius * 2; // 长度 int end = len - 1; // 最后的索引 double radiusF = (double)radius; // 半径浮点数 double[] weights = new double[len]; // 权重 for (int i = 0; i <= radius; i++) // 先把右边的权重算出来 weights[radius + i] = Gaussian(i / radiusF); for (int i = 0; i < radius; i++) // 把右边的权重拷贝到左边 weights[i] = weights[end - i]; double total = weights.Sum(); for (int i = 0; i < len; i++) // 使权重合为 0 weights[i] = weights[i] / total; return weights; } void ApplyWeights(double[] buffer, double[] weights) { int len = buffer.Length; for (int i = 0; i < len; i++) buffer[i] = buffer[i] * weights[i]; } double[] weights = GetWeights(radius); double[] buffer = new double[1 + radius * 2]; double[] result = new double[data.Length]; if (data.Length < radius) { Array.Fill(result, data.Average()); return result; } for (int i = 0; i < radius; i++) { Array.Fill(buffer, data[i], 0, radius + 1); // 填充缺省 for (int j = 0; j < radius; j++) // { buffer[radius + 1 + j] = data[i + j]; } ApplyWeights(buffer, weights); result[i] = buffer.Sum(); } for (int i = radius; i < data.Length - radius; i++) { for (int j = 0; j < radius; j++) // { buffer[j] = data[i - j]; } buffer[radius] = data[i]; for (int j = 0; j < radius; j++) // { buffer[radius + j + 1] = data[i + j]; } ApplyWeights(buffer, weights); result[i] = buffer.Sum(); } for (int i = data.Length - radius; i < data.Length; i++) { Array.Fill(buffer, data[i], 0, radius + 1); // 填充缺省 for (int j = 0; j < radius; j++) // { buffer[radius + 1 + j] = data[i - j]; } ApplyWeights(buffer, weights); result[i] = buffer.Sum(); } return result; } /// /// 获取 HSV 中所有的基础颜色 (饱和度和明度均为最大值) /// /// 所有的 HSV 基础颜色(共 256 * 6 个, 并且随着索引增加, 颜色也会渐变) private Color[] GetAllHsvColors() { Color[] result = new Color[256 * 6]; for (int i = 0; i <= 255; i++) { result[i] = Color.FromArgb(255, 255, (byte)i, 0); } for (int i = 0; i <= 255; i++) { result[256 + i] = Color.FromArgb(255, (byte)(255 - i), 255, 0); } for (int i = 0; i <= 255; i++) { result[512 + i] = Color.FromArgb(255, 0, 255, (byte)i); } for (int i = 0; i <= 255; i++) { result[768 + i] = Color.FromArgb(255, 0, (byte)(255 - i), 255); } for (int i = 0; i <= 255; i++) { result[1024 + i] = Color.FromArgb(255, (byte)i, 0, 255); } for (int i = 0; i <= 255; i++) { result[1280 + i] = Color.FromArgb(255, 255, 0, (byte)(255 - i)); } return result; } /// /// /// /// /// private void Capture_DataAvailable(object? sender, WaveInEventArgs e) { int length = e.BytesRecorded / 4; // 采样的数量 (每一个采样是 4 字节) double[] result = new double[length]; // 声明结果 for (int i = 0; i < length; i++) result[i] = BitConverter.ToSingle(e.Buffer, i * 4); // 取出采样值 visualizer.PushSampleData(result); // 将新的采样存储到 可视化器 中 } /// /// 用来刷新频谱数据以及实现频谱数据缓动 /// /// /// private void DataTimer_Tick(object? state) { double[] newSpectrumData = visualizer.GetSpectrumData(""); // 从可视化器中获取频谱数据 newSpectrumData = MakeSmooth(newSpectrumData, 2); // 平滑频谱数据 if (spectrumData == null) // 如果已经存储的频谱数据为空, 则把新的频谱数据直接赋值上去 { spectrumData = newSpectrumData; return; } for (int i = 0; i < newSpectrumData.Length; i++) // 计算旧频谱数据和新频谱数据之间的 "中间值" { double oldData = spectrumData[i]; double newData = newSpectrumData[i]; double lerpData = oldData + (newData - oldData) * .2f; // 每一次执行, 频谱值会向目标值移动 20% (如果太大, 缓动效果不明显, 如果太小, 频谱会有延迟的感觉) spectrumData[i] = lerpData; } } /// /// 绘制一个渐变的 波浪 /// /// 绘图目标 /// 下方颜色 /// 上方颜色 /// 频谱数据 /// 波浪中, 点的数量 /// 波浪的宽度 /// 波浪的起始X坐标 /// 波浪的其实Y坐标 /// 频谱的缩放(使用负值可以翻转波浪) private void DrawGradient(Path g, Color down, Color up, double[] spectrumData, int pointCount, double drawingWidth, double xOffset, double yOffset, double scale) { Point[] points = new Point[pointCount + 2]; for (int i = 0; i < pointCount; i++) { double x = i * drawingWidth / pointCount + xOffset; double y = spectrumData[i * spectrumData.Length / pointCount] * scale + yOffset; points[i + 1] = new Point(x, y); } points[0] = new Point(xOffset, yOffset); points[points.Length - 1] = new Point(xOffset + drawingWidth, yOffset); double upP = points.Min(v => v.Y); if (Math.Abs(upP - yOffset) < 1) return; g.Data = new PathGeometry() { Figures = { new PathFigure() { IsFilled = true, Segments = { new PolyLineSegment(points, false) } } } }; g.Fill = new LinearGradientBrush(down, up, new Point(0, yOffset), new Point(0, upP)); } /// /// 绘制渐变的条形 /// /// 绘图目标 /// 下方颜色 /// 上方颜色 /// 频谱数据 /// 条形的数量 /// 绘图的宽度 /// 绘图的起始 X 坐标 /// 绘图的起始 Y 坐标 /// 条形与条形之间的间隔(像素) /// private void DrawGradientStrips(Path g, Color down, Color up, double[] spectrumData, int stripCount, double drawingWidth, double xOffset, double yOffset, double spacing, double scale) { double stripWidth = (drawingWidth - spacing * stripCount) / stripCount; Point[] points = new Point[stripCount]; for (int i = 0; i < stripCount; i++) { double x = stripWidth * i + spacing * i + xOffset; double y = spectrumData[i * spectrumData.Length / stripCount] * scale; // height points[i] = new Point(x, y); } double upP = points.Min(v => v.Y < 0 ? yOffset + v.Y : yOffset); double downP = points.Max(v => v.Y < 0 ? yOffset : yOffset + v.Y); if (downP < yOffset) downP = yOffset; GeometryGroup geo = new GeometryGroup(); Brush brush = new LinearGradientBrush(down, up, new Point(0, downP), new Point(0, upP)); for (int i = 0; i < stripCount; i++) { Point p = points[i]; double y = yOffset; double height = p.Y; if (height < 0) { y += height; height = -height; } Point[] endPoints = new Point[] { new Point(p.X, p.Y), new Point(p.X, p.Y + height), new Point(p.X + stripWidth, p.Y + height), new Point(p.X + stripWidth, p.Y) }; PathFigure fig = new PathFigure(); fig.StartPoint = endPoints[0]; fig.Segments.Add(new PolyLineSegment(endPoints, false)); //fig.IsClosed = true; geo.Children.Add(new PathGeometry() { Figures = { fig } }); } g.Data = geo; g.Fill = brush; } /// /// 画曲线 /// /// /// /// /// /// /// /// /// private void DrawCurve(Path g, Brush brush, double[] spectrumData, int pointCount, double drawingWidth, double xOffset, double yOffset, double scale) { Point[] points = new Point[pointCount]; for (int i = 0; i < pointCount; i++) { double x = i * drawingWidth / pointCount + xOffset; double y = spectrumData[i * spectrumData.Length / pointCount] * scale + yOffset; points[i] = new Point(x, y); } PathFigure fig = new PathFigure(); fig.Segments.Add(new PolyLineSegment(points, true)); g.Data = new PathGeometry() { Figures = { fig } }; g.Stroke = brush; } private void DrawCircleStrips(Path g, Brush brush, double[] spectrumData, int stripCount, double xOffset, double yOffset, double radius, double spacing, double rotation, double scale) { double rotationAngle = Math.PI / 180 * rotation; double blockWidth = MathF.PI * 2 / stripCount; // angle double stripWidth = blockWidth - MathF.PI / 180 * spacing; // angle Point[] points = new Point[stripCount]; for (int i = 0; i < stripCount; i++) { double x = blockWidth * i + rotationAngle; // angle double y = spectrumData[i * spectrumData.Length / stripCount] * scale; // height points[i] = new Point(x, y); } PathGeometry geo = new PathGeometry(); for (int i = 0; i < stripCount; i++) { Point p = points[i]; double sinStart = Math.Sin(p.X); double sinEnd = Math.Sin(p.X + stripWidth); double cosStart = Math.Cos(p.X); double cosEnd = Math.Cos(p.X + stripWidth); Point[] polygon = new Point[] { new Point((cosStart * radius + xOffset), (sinStart * radius + yOffset)), new Point((cosEnd * radius + xOffset), (sinEnd * radius + yOffset)), new Point((cosEnd * (radius + p.Y) + xOffset), (sinEnd * (radius + p.Y) + yOffset)), new Point((cosStart * (radius + p.Y) + xOffset), (sinStart * (radius + p.Y) + yOffset)), }; PathFigure fig = new PathFigure(); fig.IsFilled = true; fig.Segments.Add(new PolyLineSegment(polygon, false)); geo.Figures.Add(fig); } g.Data = geo; g.Fill = brush; } /// /// 画圆环条 /// /// /// /// /// /// /// /// /// /// /// private void DrawCircleGradientStrips(Path g, Color inner, Color outer, double[] spectrumData, int stripCount, double xOffset, double yOffset, double radius, double spacing, double rotation, double scale) { double rotationAngle = Math.PI / 180 * rotation; double blockWidth = Math.PI * 2 / stripCount; // angle double stripWidth = blockWidth - MathF.PI / 180 * spacing; // angle Point[] points = new Point[stripCount]; for (int i = 0; i < stripCount; i++) { double x = blockWidth * i + rotationAngle; // angle double y = spectrumData[i * spectrumData.Length / stripCount] * scale; // height points[i] = new Point(x, y); } double maxHeight = points.Max(v => v.Y); double outerRadius = radius + maxHeight; PathGeometry geo = new PathGeometry(); for (int i = 0; i < stripCount; i++) { Point p = points[i]; double sinStart = Math.Sin(p.X); double sinEnd = Math.Sin(p.X + stripWidth); double cosStart = Math.Cos(p.X); double cosEnd = Math.Cos(p.X + stripWidth); Point[] polygon = new Point[] { new Point((cosStart * radius + xOffset),(sinStart * radius + yOffset)), new Point((cosEnd * radius + xOffset),(sinEnd * radius + yOffset)), new Point((cosEnd * (radius + p.Y) + xOffset), (sinEnd * (radius + p.Y) + yOffset)), new Point((cosStart * (radius + p.Y) + xOffset), (sinStart * (radius + p.Y) + yOffset)) }; PathFigure fig = new PathFigure(); fig.IsFilled = true; fig.Segments.Add(new PolyLineSegment(polygon, false)); geo.Figures.Add(fig); } LinearGradientBrush brush = new LinearGradientBrush( new GradientStopCollection() { new GradientStop(Colors.Transparent, 0), new GradientStop(inner, radius / (radius + maxHeight)), new GradientStop(outer, 1) }, new Point(xOffset, yOffset), new Point(xOffset, yOffset + radius + maxHeight)); g.Data = geo; g.Fill = brush; } private void DrawStrips(Path g, Brush brush, double[] spectrumData, int stripCount, int drawingWidth, float xOffset, float yOffset, float spacing, double scale) { float stripWidth = (drawingWidth - spacing * stripCount) / stripCount; Point[] points = new Point[stripCount]; for (int i = 0; i < stripCount; i++) { double x = stripWidth * i + spacing * i + xOffset; double y = spectrumData[i * spectrumData.Length / stripCount] * scale; // height points[i] = new Point(x, y); } PathGeometry geo = new PathGeometry(); for (int i = 0; i < stripCount; i++) { Point p = points[i]; double y = yOffset; double height = p.Y; if (height < 0) { y += height; height = -height; } Point[] endPoints = new Point[] { new Point(p.X, y), new Point(p.X, y + height), new Point(p.X + stripWidth, y + height), new Point(p.X + stripWidth, y) }; PathFigure fig = new PathFigure(); fig.IsFilled = true; fig.Segments.Add(new PolyLineSegment(endPoints, false)); geo.Figures.Add(fig); } g.Data = geo; g.Fill = brush; } private void DrawGradientBorder( Rectangle upBorder, Rectangle downBorder, Rectangle leftBorder, Rectangle rightBorder, Color inner, Color outer, double scale, double width) { int thickness = (int)(width * scale); upBorder.Height = thickness; downBorder.Height = thickness; leftBorder.Width = thickness; rightBorder.Width = thickness; upBorder.Fill = new LinearGradientBrush(outer, inner, 90); downBorder.Fill = new LinearGradientBrush(inner, outer, 90); leftBorder.Fill = new LinearGradientBrush(outer, inner, 0); rightBorder.Fill = new LinearGradientBrush(inner, outer, 0); } int colorIndex = 0; double rotation = 0; DispatcherOperation? lastInvocation; private void DrawingTimer_Tick(object? state) { if (spectrumData == null) return; if (lastInvocation != null && lastInvocation.Status == DispatcherOperationStatus.Executing) return; lastInvocation = Dispatcher.InvokeAsync(() => { rotation += .1; colorIndex++; Color color1 = allColors[colorIndex % allColors.Length]; Color color2 = allColors[(colorIndex + 200) % allColors.Length]; double[] bassArea = Visualizer.TakeSpectrumOfFrequency(spectrumData, capture.WaveFormat.SampleRate, 250); double bassScale = bassArea.Average() * 100; double extraScale = Math.Min(drawingPanel.ActualHeight, drawingPanel.ActualHeight) / 6; Brush brush = new SolidColorBrush(Colors.Purple); DrawGradientBorder(up, down, left, right, Color.FromArgb(0, color1.R, color1.G, color1.B), color2, bassScale, drawingPanel.ActualWidth / 10); DrawGradientStrips(strips, color1, color2, spectrumData, spectrumData.Length, strips.ActualWidth, 0, strips.ActualHeight, 3, -strips.ActualHeight * 10); DrawCircleGradientStrips(circle, color1, color2, spectrumData, spectrumData.Length, drawingPanel.ActualHeight / 2, drawingPanel.ActualHeight / 2, Math.Min(drawingPanel.ActualHeight, drawingPanel.ActualHeight) / 4 + extraScale * bassScale, 1, rotation, drawingPanel.ActualHeight / 6 * 10); DrawCurve(sampleWave, brush, visualizer.SampleData, visualizer.SampleData.Length, drawingPanel.ActualWidth, 0, drawingPanel.ActualHeight / 2, Math.Min(drawingPanel.ActualHeight / 10, 100)); }); } private void Window_Loaded(object sender, RoutedEventArgs e) { capture.StartRecording(); //dataTimer = new Timer(DataTimer_Tick, null, 30, 30); //drawingTimer = new Timer(DrawingTimer_Tick, null, 30, 30); } private void Window_Closed(object sender, EventArgs e) { Environment.Exit(0); } } }