Files
RhythmicWallpaper/WpfAudioVisualizer/MainWindow.xaml.cs
2024-10-17 23:10:02 +08:00

573 lines
22 KiB
C#

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
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
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();
}
/// <summary>
/// 简单的数据模糊
/// </summary>
/// <param name="data">数据</param>
/// <param name="radius">模糊半径</param>
/// <returns>结果</returns>
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;
}
/// <summary>
/// 获取 HSV 中所有的基础颜色 (饱和度和明度均为最大值)
/// </summary>
/// <returns>所有的 HSV 基础颜色(共 256 * 6 个, 并且随着索引增加, 颜色也会渐变)</returns>
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;
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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); // 将新的采样存储到 可视化器 中
}
/// <summary>
/// 用来刷新频谱数据以及实现频谱数据缓动
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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;
}
}
/// <summary>
/// 绘制一个渐变的 波浪
/// </summary>
/// <param name="g">绘图目标</param>
/// <param name="down">下方颜色</param>
/// <param name="up">上方颜色</param>
/// <param name="spectrumData">频谱数据</param>
/// <param name="pointCount">波浪中, 点的数量</param>
/// <param name="drawingWidth">波浪的宽度</param>
/// <param name="xOffset">波浪的起始X坐标</param>
/// <param name="yOffset">波浪的其实Y坐标</param>
/// <param name="scale">频谱的缩放(使用负值可以翻转波浪)</param>
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));
}
/// <summary>
/// 绘制渐变的条形
/// </summary>
/// <param name="g">绘图目标</param>
/// <param name="down">下方颜色</param>
/// <param name="up">上方颜色</param>
/// <param name="spectrumData">频谱数据</param>
/// <param name="stripCount">条形的数量</param>
/// <param name="drawingWidth">绘图的宽度</param>
/// <param name="xOffset">绘图的起始 X 坐标</param>
/// <param name="yOffset">绘图的起始 Y 坐标</param>
/// <param name="spacing">条形与条形之间的间隔(像素)</param>
/// <param name="scale"></param>
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;
}
/// <summary>
/// 画曲线
/// </summary>
/// <param name="g"></param>
/// <param name="brush"></param>
/// <param name="spectrumData"></param>
/// <param name="pointCount"></param>
/// <param name="drawingWidth"></param>
/// <param name="xOffset"></param>
/// <param name="yOffset"></param>
/// <param name="scale"></param>
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;
}
/// <summary>
/// 画圆环条
/// </summary>
/// <param name="g"></param>
/// <param name="inner"></param>
/// <param name="outer"></param>
/// <param name="spectrumData"></param>
/// <param name="stripCount"></param>
/// <param name="xOffset"></param>
/// <param name="yOffset"></param>
/// <param name="radius"></param>
/// <param name="spacing"></param>
/// <param name="scale"></param>
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);
}
}
}