又写了一堆屎山

This commit is contained in:
lichx
2024-04-19 10:00:37 +08:00
parent 7cf048fd7c
commit b0efa940c1
81 changed files with 2827 additions and 260 deletions
@@ -0,0 +1,77 @@
namespace AudioVisualizer
{
partial class MainWindow
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
components = new System.ComponentModel.Container();
dataTimer = new System.Windows.Forms.Timer(components);
drawingPanel = new Panel();
drawingTimer = new System.Windows.Forms.Timer(components);
SuspendLayout();
//
// dataTimer
//
dataTimer.Interval = 30;
dataTimer.Tick += DataTimer_Tick;
//
// drawingPanel
//
drawingPanel.Dock = DockStyle.Fill;
drawingPanel.Location = new Point(0, 0);
drawingPanel.Name = "drawingPanel";
drawingPanel.Size = new Size(880, 432);
drawingPanel.TabIndex = 0;
drawingPanel.MouseDoubleClick += DrawingPanel_MouseDoubleClick;
//
// drawingTimer
//
drawingTimer.Interval = 30;
drawingTimer.Tick += DrawingTimer_Tick;
//
// MainWindow
//
AutoScaleDimensions = new SizeF(11F, 24F);
AutoScaleMode = AutoScaleMode.Font;
BackColor = Color.Black;
ClientSize = new Size(880, 432);
Controls.Add(drawingPanel);
FormBorderStyle = FormBorderStyle.None;
Name = "MainWindow";
Text = "Music Visualizer";
FormClosed += MainWindow_FormClosed;
Load += MainWindow_Load;
ResumeLayout(false);
}
#endregion
private System.Windows.Forms.Timer dataTimer;
private Panel drawingPanel;
private System.Windows.Forms.Timer drawingTimer;
}
}
@@ -0,0 +1,466 @@
using LibAudioVisualizer;
using NAudio.CoreAudioApi;
using NAudio.Wave;
using System.Drawing.Drawing2D;
using System.Numerics;
namespace AudioVisualizer
{
public partial class MainWindow : Form
{
WasapiCapture capture; // 音频捕获
Visualizer visualizer; // 可视化
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>
/// 获取 HSV 中所有的基础颜色 (饱和度和明度均为最大值)
/// </summary>
/// <returns>所有的 HSV 基础颜色(共 256 * 6 个, 并且随着索引增加, 颜色也会渐变)</returns>
private Color[] GetAllHsvColors()
{
Color[] result = new Color[256 * 6];
for (int i = 0; i < 256; i++)
{
result[i] = Color.FromArgb(255, i, 0);
}
for (int i = 0; i < 256; i++)
{
result[256 + i] = Color.FromArgb(255 - i, 255, 0);
}
for (int i = 0; i < 256; i++)
{
result[512 + i] = Color.FromArgb(0, 255, i);
}
for (int i = 0; i < 256; i++)
{
result[768 + i] = Color.FromArgb(0, 255 - i, 255);
}
for (int i = 0; i < 256; i++)
{
result[1024 + i] = Color.FromArgb(i, 0, 255);
}
for (int i = 0; i < 256; i++)
{
result[1280 + i] = Color.FromArgb(255, 0, 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? sender, EventArgs e)
{
double[] newSpectrumData = visualizer.GetSpectrumData(); // 从可视化器中获取频谱数据
newSpectrumData = Visualizer.GetBlurry(newSpectrumData, 2); // 平滑频谱数据
spectrumData = newSpectrumData;
}
/// <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(Graphics g, Color down, Color up, double[] spectrumData, int pointCount, int drawingWidth, float xOffset, float yOffset, double scale)
{
GraphicsPath path = new GraphicsPath();
PointF[] points = new PointF[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 PointF((float)x, (float)y);
}
points[0] = new PointF(xOffset, yOffset);
points[points.Length - 1] = new PointF(xOffset + drawingWidth, yOffset);
path.AddCurve(points);
float upP = (float)points.Min(v => v.Y);
if (Math.Abs(upP - yOffset) < 1)
return;
using Brush brush = new LinearGradientBrush(new PointF(0, yOffset), new PointF(0, upP), down, up);
g.FillPath(brush, path);
}
/// <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(Graphics g, Color down, Color up, double[] spectrumData, int stripCount, int drawingWidth, float xOffset, float yOffset, float spacing, double scale)
{
float stripWidth = (drawingWidth - spacing * stripCount) / stripCount;
PointF[] points = new PointF[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 PointF((float)x, (float)y);
}
float upP = (float)points.Min(v => v.Y < 0 ? yOffset + v.Y : yOffset);
float downP = (float)points.Max(v => v.Y < 0 ? yOffset : yOffset + v.Y);
if (downP < yOffset)
downP = yOffset;
if (Math.Abs(upP - downP) < 1)
return;
using Brush brush = new LinearGradientBrush(new PointF(0, downP), new PointF(0, upP), down, up);
for (int i = 0; i < stripCount; i++)
{
PointF p = points[i];
float y = yOffset;
float height = p.Y;
if (height < 0)
{
y += height;
height = -height;
}
g.FillRectangle(brush, new RectangleF(p.X, y, stripWidth, height));
}
}
/// <summary>
/// 画曲线
/// </summary>
/// <param name="g"></param>
/// <param name="pen"></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(Graphics g, Pen pen, double[] spectrumData, int pointCount, int drawingWidth, double xOffset, double yOffset, double scale)
{
PointF[] points = new PointF[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 PointF((float)x, (float)y);
}
g.DrawCurve(pen, points);
}
/// <summary>
/// 画简单的圆环线条
/// </summary>
/// <param name="g"></param>
/// <param name="brush"></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="rotation"></param>
/// <param name="scale"></param>
private void DrawCircleStrips(Graphics 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
PointF[] points = new PointF[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 PointF((float)x, (float)y);
}
for (int i = 0; i < stripCount; i++)
{
PointF 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);
PointF[] polygon = new PointF[]
{
new PointF((float)(cosStart * radius + xOffset), (float)(sinStart * radius + yOffset)),
new PointF((float)(cosEnd * radius + xOffset), (float)(sinEnd * radius + yOffset)),
new PointF((float)(cosEnd * (radius + p.Y) + xOffset), (float)(sinEnd * (radius + p.Y) + yOffset)),
new PointF((float)(cosStart * (radius + p.Y) + xOffset), (float)(sinStart * (radius + p.Y) + yOffset)),
};
g.FillPolygon(brush, polygon);
}
}
/// <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(Graphics 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
PointF[] points = new PointF[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 PointF((float)x, (float)y);
}
double maxHeight = points.Max(v => v.Y);
double outerRadius = radius + maxHeight;
PointF[] polygon = new PointF[4];
for (int i = 0; i < stripCount; i++)
{
PointF 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);
PointF
p1 = new PointF((float)(cosStart * radius + xOffset),(float)(sinStart * radius + yOffset)),
p2 = new PointF((float)(cosEnd * radius + xOffset),(float)(sinEnd * radius + yOffset)),
p3 = new PointF((float)(cosEnd * (radius + p.Y) + xOffset), (float)(sinEnd * (radius + p.Y) + yOffset)),
p4 = new PointF((float)(cosStart * (radius + p.Y) + xOffset), (float)(sinStart * (radius + p.Y) + yOffset));
polygon[0] = p1;
polygon[1] = p2;
polygon[2] = p3;
polygon[3] = p4;
PointF innerP = new PointF((p1.X + p2.X) / 2, (p1.Y + p2.Y) / 2);
PointF outerP = new PointF((p3.X + p4.X) / 2, (p3.Y + p4.Y) / 2);
Vector2 offset = new Vector2(outerP.X - innerP.X, outerP.Y - innerP.Y);
if (MathF.Sqrt(offset.X * offset.X + offset.Y * offset.Y) < 1) // 渐变笔刷两点之间距离不能太小
continue;
try
{
using LinearGradientBrush brush = new LinearGradientBrush(innerP, outerP, inner, outer); // 这里有玄学 bug, 这个 线性笔刷会 OutMemoryException
g.FillPolygon(brush, polygon); // 但是实际上不应该有这个异常...
}
catch { }
}
}
/// <summary>
/// 画简单的线条
/// </summary>
/// <param name="g"></param>
/// <param name="brush"></param>
/// <param name="spectrumData"></param>
/// <param name="stripCount"></param>
/// <param name="drawingWidth"></param>
/// <param name="xOffset"></param>
/// <param name="yOffset"></param>
/// <param name="spacing"></param>
/// <param name="scale"></param>
private void DrawStrips(Graphics g, Brush brush, double[] spectrumData, int stripCount, int drawingWidth, float xOffset, float yOffset, float spacing, double scale)
{
float stripWidth = (drawingWidth - spacing * stripCount) / stripCount;
PointF[] points = new PointF[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 PointF((float)x, (float)y);
}
for (int i = 0; i < stripCount; i++)
{
PointF p = points[i];
float y = yOffset;
float height = p.Y;
if (height < 0)
{
y += height;
height = -height;
}
g.FillRectangle(brush, new RectangleF(p.X, y, stripWidth, height));
}
}
/// <summary>
/// 画渐变的边框
/// </summary>
/// <param name="g"></param>
/// <param name="inner"></param>
/// <param name="outer"></param>
/// <param name="area"></param>
/// <param name="scale"></param>
/// <param name="width"></param>
private void DrawGradientBorder(Graphics g, Color inner, Color outer, Rectangle area, double scale, float width)
{
int thickness = (int)(width * scale);
if (thickness < 1)
return;
Rectangle rect = new Rectangle(area.X, area.Y, area.Width, area.Height);
Rectangle up = new Rectangle(rect.Location, new Size(rect.Width, thickness));
Rectangle down = new Rectangle(new Point(rect.X, (int)(rect.X + rect.Height - scale * width)), new Size(rect.Width, thickness));
Rectangle left = new Rectangle(rect.Location, new Size(thickness, rect.Height));
Rectangle right = new Rectangle(new Point((int)(rect.X + rect.Width - scale * width), rect.Y), new Size(thickness, rect.Height));
LinearGradientBrush upB = new LinearGradientBrush(up, outer, inner, LinearGradientMode.Vertical);
LinearGradientBrush downB = new LinearGradientBrush(down, inner, outer, LinearGradientMode.Vertical);
LinearGradientBrush leftB = new LinearGradientBrush(left, outer, inner, LinearGradientMode.Horizontal);
LinearGradientBrush rightB = new LinearGradientBrush(right, inner, outer, LinearGradientMode.Horizontal);
upB.WrapMode = downB.WrapMode = leftB.WrapMode = rightB.WrapMode = WrapMode.TileFlipXY;
g.FillRectangle(upB, up);
g.FillRectangle(downB, down);
g.FillRectangle(leftB, left);
g.FillRectangle(rightB, right);
}
int colorIndex = 0;
double rotation = 0;
BufferedGraphics? oldBuffer;
private void DrawingTimer_Tick(object? sender, EventArgs e)
{
if (spectrumData == null)
return;
rotation += 0.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.Width, drawingPanel.Height) / 6; // 低音导致的缩放 (乘上窗口大小)
Rectangle border = new Rectangle(Point.Empty, drawingPanel.Size);
BufferedGraphics buffer = BufferedGraphicsManager.Current.Allocate(drawingPanel.CreateGraphics(), drawingPanel.ClientRectangle);
Graphics g = buffer.Graphics;
if (oldBuffer != null)
{
//oldBuffer.Render(buffer.Graphics); // 如果你想要实现 "留影" 效果, 就取消注释这段代码, 并且将 g.Clear 改为 g.FillRectange(xxx, 半透明的黑色)
oldBuffer.Dispose();
}
using Pen pen = new Pen(Color.Pink); // 画音频采样波形用的笔
g.SmoothingMode = SmoothingMode.HighQuality; // 嗨嗨害, 那必须得是高质量绘图
g.Clear(drawingPanel.BackColor);
//DrawGradientBorder(g, Color.FromArgb(0, color1), color2, border, bassScale, drawingPanel.Width / 10);
DrawGradientStrips(g, color1, color2, spectrumData, spectrumData.Length, drawingPanel.Width, 0, drawingPanel.Height, 3, -drawingPanel.Height * 50);
//DrawCircleGradientStrips(g, color1, color2, spectrumData, spectrumData.Length, drawingPanel.Width / 2, drawingPanel.Height / 2, MathF.Min(drawingPanel.Width, drawingPanel.Height) / 4 + extraScale * bassScale, 1, rotation, drawingPanel.Width / 6 * 10);
//DrawCurve(g, pen, visualizer.SampleData, visualizer.SampleData.Length, drawingPanel.Width, 0, drawingPanel.Height / 2, MathF.Min(drawingPanel.Height / 10, 100));
buffer.Render();
oldBuffer = buffer; // 保存一下 buffer (之所以不全局只使用一个 Buffer 是因为,,, 用户可能调整窗口大小, 所以每一帧都必须适应)
}
private void MainWindow_Load(object sender, EventArgs e)
{
capture.StartRecording();
dataTimer.Start();
drawingTimer.Start();
}
private void MainWindow_FormClosed(object sender, FormClosedEventArgs e)
{
Environment.Exit(0);
}
private void DrawingPanel_MouseDoubleClick(object sender, MouseEventArgs e)
{
WindowState = WindowState != FormWindowState.Maximized ? FormWindowState.Maximized : FormWindowState.Normal;
FormBorderStyle = WindowState == FormWindowState.Maximized ? FormBorderStyle.None : FormBorderStyle.Sizable;
}
}
}
@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="dataTimer.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
<metadata name="drawingTimer.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>197, 17</value>
</metadata>
</root>
@@ -0,0 +1,106 @@
namespace LibDynamics
{
public class SecondOrderDynamics
{
private double xp;// previous input
private double y, yd; // state variables
private double _w, _z, _d, k1, k2, k3; // dynamics constants
private double _r;
private double _f;
/// <summary>
/// 频率
/// - 即速度, 单位是赫兹(Hz)
/// - 不会影响输出结果的形状, 会影响 '震荡频率'
/// </summary>
public double F
{
get => _f; set
{
_f = value;
InitMotionValues(_f, _z, _r);
}
}
/// <summary>
/// 阻尼 <br />
/// - 当为 0 时, 输出将永远震荡不衰减 <br />
/// - 当大于 0 小于 1 时, 输出会超出结果, 并逐渐趋于目标 <br />
/// - 当为 1 时, 输出的曲线是趋向结果, 并正好在指定频率对应时间内抵达结果 <br />
/// - 当大于 1 时, 输出值同样时取向结果, 但速度会更慢, 无法在指定频率对应时间内抵达结果 <br />
/// </summary>
public double Z
{
get => _z; set
{
_z = value;
InitMotionValues(_f, _z, _r);
}
}
/// <summary>
/// 初始响应
/// - 当为 0 时, 数据需要进行 '加速' 来开始运动 <br />
/// - 当为 1 时, 数据会立即开始响应 <br />
/// - 当大于 1 时, 输出会因为 '速度过快' 而超出目标结果 <br />
/// - 当小于 0 时, 输出会 '预测运动', 即 '抬手动作'. 例如目标是 '加' 时, 输出会先进行 '减', 再进行 '加',
/// - 当运动目标为机械时, 通常取值为 2
/// </summary>
public double R
{
get => _r; set
{
_r = value;
InitMotionValues(_f, _z, _r);
}
}
public SecondOrderDynamics(double f, double z, double r, double x0)
{
//compute constants
InitMotionValues(f, z, r);
// initialize variables
xp = x0;
y = x0;
yd = 0;
}
private void InitMotionValues(double f, double z, double r)
{
_w = 2 * Math.PI * f;
_z = z;
_d = _w * Math.Sqrt(Math.Abs(z * z - 1));
k1 = z / (Math.PI * f);
k2 = 1 / ((2 * Math.PI * f) * (2 * Math.PI * f));
k3 = r * z / (2 * Math.PI * f);
}
public double Update(double deltaTime, double x)
{
double xd = (x - xp) / deltaTime;
double k1_stable, k2_stable;
if (_w * deltaTime < _z)
{
k1_stable = k1;
k2_stable = Math.Max(Math.Max(k2, deltaTime * deltaTime / 2 + deltaTime * k1 / 2), deltaTime * k1);
}
else
{
double t1 = Math.Exp(-_z * _w * deltaTime);
double alpha = 2 * t1 * (_z <= 1 ? Math.Cos(deltaTime * _d) : Math.Cosh(deltaTime * _d));
double beta = t1 * t1;
double t2 = deltaTime / (1 + beta - alpha);
k1_stable = (1 - beta) * t2;
k2_stable = deltaTime * t2;
}
y = y + deltaTime * yd;
yd = yd + deltaTime * (x + k3 * xd - y - k1_stable * yd) / k2_stable;
xp = x;
return y;
}
}
}
@@ -0,0 +1,126 @@
namespace LibDynamics
{
public class SecondOrderDynamicsForArray
{
private double[] xps, xds;// previous input
private double[] ys, yds; // state variables
private double _w, _z, _d, k1, k2, k3; // dynamics constants
private double _r;
private double _f;
/// <summary>
/// 频率
/// - 即速度, 单位是赫兹(Hz)
/// - 不会影响输出结果的形状, 会影响 '震荡频率'
/// </summary>
public double F
{
get => _f; set
{
_f = value;
InitMotionValues(_f, _z, _r);
}
}
/// <summary>
/// 阻尼 <br />
/// - 当为 0 时, 输出将永远震荡不衰减 <br />
/// - 当大于 0 小于 1 时, 输出会超出结果, 并逐渐趋于目标 <br />
/// - 当为 1 时, 输出的曲线是趋向结果, 并正好在指定频率对应时间内抵达结果 <br />
/// - 当大于 1 时, 输出值同样时取向结果, 但速度会更慢, 无法在指定频率对应时间内抵达结果 <br />
/// </summary>
public double Z
{
get => _z; set
{
_z = value;
InitMotionValues(_f, _z, _r);
}
}
/// <summary>
/// 初始响应
/// - 当为 0 时, 数据需要进行 '加速' 来开始运动 <br />
/// - 当为 1 时, 数据会立即开始响应 <br />
/// - 当大于 1 时, 输出会因为 '速度过快' 而超出目标结果 <br />
/// - 当小于 0 时, 输出会 '预测运动', 即 '抬手动作'. 例如目标是 '加' 时, 输出会先进行 '减', 再进行 '加',
/// - 当运动目标为机械时, 通常取值为 2
/// </summary>
public double R
{
get => _r; set
{
_r = value;
InitMotionValues(_f, _z, _r);
}
}
/// <summary>
///
/// </summary>
/// <param name="f"></param>
/// <param name="z"></param>
/// <param name="r"></param>
/// <param name="x0"></param>
/// <param name="size">Array size</param>
public SecondOrderDynamicsForArray(double f, double z, double r, double x0, int size)
{
//compute constants
InitMotionValues(f, z, r);
// initialize variables
xps = new double[size];
ys = new double[size];
xds = new double[size];
yds = new double[size];
Array.Fill(xps, x0);
Array.Fill(ys, x0);
}
private void InitMotionValues(double f, double z, double r)
{
_w = 2 * Math.PI * f;
_z = z;
_d = _w * Math.Sqrt(Math.Abs(z * z - 1));
k1 = z / (Math.PI * f);
k2 = 1 / ((2 * Math.PI * f) * (2 * Math.PI * f));
k3 = r * z / (2 * Math.PI * f);
}
public double[] Update(double deltaTime, double[] xs)//xps p for past,xds d for delta,xs current val
{
if (xs.Length != xps.Length)
throw new ArgumentException();
for (int i = 0; i < xds.Length; i++)
xds[i] = (xs[i] - xps[i]) / deltaTime;
double k1_stable, k2_stable;
if (_w * deltaTime < _z)
{
k1_stable = k1;
k2_stable = Math.Max(Math.Max(k2, deltaTime * deltaTime / 2 + deltaTime * k1 / 2), deltaTime * k1);
}
else
{
double t1 = Math.Exp(-_z * _w * deltaTime);
double alpha = 2 * t1 * (_z <= 1 ? Math.Cos(deltaTime * _d) : Math.Cosh(deltaTime * _d));
double beta = t1 * t1;
double t2 = deltaTime / (1 + beta - alpha);
k1_stable = (1 - beta) * t2;
k2_stable = deltaTime * t2;
}
for (int i = 0; i < ys.Length; i++)
{
ys[i] = ys[i] + deltaTime * yds[i];
yds[i] = yds[i] + deltaTime * (xs[i] + k3 * xds[i] - ys[i] - k1_stable * yds[i]) / k2_stable;
}
for (int i = 0; i < xps.Length; i++)
xps[i] = xs[i];
return ys;
}
}
}
@@ -0,0 +1,227 @@
using LibDynamics;
using FftComplex = FftSharp.Complex;
using FftTransform = FftSharp.Transform;
namespace LibAudioVisualizer
{
public class Visualizer
{
//private int _m;
private double[] _sampleData;
private DateTime _lastTime;
private SecondOrderDynamicsForArray _dynamics;
private int _size;
/// <summary>
/// 采样数据
/// </summary>
public double[] SampleData => _sampleData;
/// <summary>
/// 尺寸
/// </summary>
public int Size
{
get => _size; set
{
if (!(Get2Flag(value)))
throw new ArgumentException("长度必须是 2 的 n 次幂");
_size = value;
_sampleData = new double[value];
_dynamics = new SecondOrderDynamicsForArray(1, 1, 1, 0, value / 2);
}
}
public int OutputSize => Size / 2;
public Visualizer(int size)
{
if (!(Get2Flag(size)))
throw new ArgumentException("大小必须是 2 的 n 次幂", nameof(size));
_lastTime = DateTime.Now;
_sampleData = new double[size];
_dynamics = new SecondOrderDynamicsForArray(1, 1, 1, 0, size / 2);
}
/// <summary>
/// 判断是否是 2 的整数次幂
/// </summary>
/// <param name="num"></param>
/// <returns></returns>
private bool Get2Flag(int num)
{
if (num < 1)
return false;
return (num & num - 1) == 0;
}
public void PushSampleData(double[] waveData)
{
if (waveData.Length > _sampleData.Length)
{
Array.Copy(waveData, waveData.Length - _sampleData.Length, _sampleData, 0, _sampleData.Length);
}
else
{
Array.Copy(_sampleData, waveData.Length, _sampleData, 0, _sampleData.Length - waveData.Length);
Array.Copy(waveData, 0, _sampleData, _sampleData.Length - waveData.Length, waveData.Length);
}
}
public void PushSampleData(double[] waveData, int count)
{
if (count > _sampleData.Length)
{
Array.Copy(waveData, count - _sampleData.Length, _sampleData, 0, _sampleData.Length);
}
else
{
Array.Copy(_sampleData, count, _sampleData, 0, _sampleData.Length - count);
Array.Copy(waveData, 0, _sampleData, _sampleData.Length - count, count);
}
}
/// <summary>
/// 获取频谱数据 (数据已经删去共轭部分)
/// </summary>
/// <returns></returns>
public double[] GetSpectrumData()
{
DateTime now = DateTime.Now;
double deltaTime = (now - _lastTime).TotalSeconds;
_lastTime = now;
int len = _sampleData.Length;
FftComplex[] data = new FftComplex[len];
for (int i = 0; i < len; i++)
data[i] = new FftComplex(_sampleData[i], 0);
FftTransform.FFT(data);
int halfLen = len / 2;
double[] spectrum = new double[halfLen]; // 傅里叶变换结果左右对称, 只需要取一半
for (int i = 0; i < halfLen; i++)
spectrum[i] = data[i].Magnitude / len;
var window = new FftSharp.Windows.Bartlett();
window.Create(halfLen);
window.ApplyInPlace(spectrum, false);
//return spectrum;
return _dynamics.Update(deltaTime, spectrum);
}
/// <summary>
/// 取指定频率内的频谱数据
/// </summary>
/// <param name="spectrum">源频谱数据</param>
/// <param name="sampleRate">采样率</param>
/// <param name="frequency">目标频率</param>
/// <returns></returns>
public static double[] TakeSpectrumOfFrequency(double[] spectrum, double sampleRate, double frequency)
{
double frequencyPerSampe = sampleRate / spectrum.Length;
int lengthInNeed = (int)(Math.Min(frequency / frequencyPerSampe, spectrum.Length));
double[] result = new double[lengthInNeed];
Array.Copy(spectrum, 0, result, 0, lengthInNeed);
return result;
}
/// <summary>
/// 简单的数据模糊
/// </summary>
/// <param name="data">数据</param>
/// <param name="radius">模糊半径</param>
/// <returns>结果</returns>
public static double[] GetBlurry(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;
}
}
}