Ghost1372

everything can be handy

SpectrumVisualizer

Lightweight real-time audio visualizer capturing system or mic input, displaying dynamic waveforms and spectrum effects.

Property

Name
TitleFontWeight
TitleHorizontalAlignment
TitleWordWrapping
TitleTrimmingSign
TitleTrimmingGranularity
ArtistFontWeight
ArtistHorizontalAlignment
ArtistWordWrapping
ArtistTrimmingSign
ArtistTrimmingGranularity
ShowAlbumArt
ShowTitle
ShowArtist
SpectrumType
RotationSpeed
CoverOpacity
SpectrumOpacity
FontOpacity
SmoothingFactor
Sensitivity
ColorType
Analyzer

Methods

Name
InitializeSMTCAsync
GetCurrentMediaInfo
Dispose

Example

First you need to implement ISpectrumAnalyzer and define your analyzer, you can use NAudio library which does not support AOT or AudioGraph which does support AOT to analyze audio files.

AudioGraphSpectrumAnalyzer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
public sealed partial class AudioGraphSpectrumAnalyzer : ISpectrumAnalyzer
{
public event Action<float[]> SpectrumDataUpdated;

private AudioGraph _graph;
private AudioDeviceInputNode _input;
private AudioFrameOutputNode _output;

private const int FftLength = 2048;

private readonly float[] _left = new float[FftLength];
private readonly float[] _right = new float[FftLength];
private readonly Complex[] _fftL = new Complex[FftLength];
private readonly Complex[] _fftR = new Complex[FftLength];

private float[] _spectrum;
private double[] _window;

private int _sampleRate;
private bool _running;

public void Start()
{
_ = StartInternalAsync();
}

public void Stop()
{
_running = false;
_graph?.Stop();
}

public void Dispose()
{
Stop();
_graph?.Dispose();
}

[GeneratedComInterface]
[Guid("5B0D3235-4DBA-4D44-865E-8F1D0E4FD04D")]
public unsafe partial interface IMemoryBufferByteAccess
{
void GetBuffer(out byte* buffer, out uint capacity);
}


private async Task StartInternalAsync()
{
if (_running)
return;

_window = new double[FftLength];
for (int i = 0; i < FftLength; i++)
_window[i] = 0.54 - 0.46 * Math.Cos(2 * Math.PI * i / (FftLength - 1));

var settings = new AudioGraphSettings(AudioRenderCategory.Media)
{
QuantumSizeSelectionMode = QuantumSizeSelectionMode.ClosestToDesired,
DesiredSamplesPerQuantum = 1024
};

var graphResult = await AudioGraph.CreateAsync(settings);
if (graphResult.Status != AudioGraphCreationStatus.Success)
return;

_graph = graphResult.Graph;
_sampleRate = (int)_graph.EncodingProperties.SampleRate;

var inputResult = await _graph.CreateDeviceInputNodeAsync(
MediaCategory.Other,
_graph.EncodingProperties);

if (inputResult.Status != AudioDeviceNodeCreationStatus.Success)
return;

_input = inputResult.DeviceInputNode;

_output = _graph.CreateFrameOutputNode();
_input.AddOutgoingConnection(_output);

_graph.QuantumProcessed += OnQuantumProcessed;
_running = true;
_graph.Start();
}

private unsafe void OnQuantumProcessed(AudioGraph sender, object args)
{
if (!_running)
return;

using var frame = _output.GetFrame();
using var buffer = frame.LockBuffer(AudioBufferAccessMode.Read);
using var reference = buffer.CreateReference();

((IMemoryBufferByteAccess)reference).GetBuffer(out byte* data, out uint capacity);

int floatCount = (int)(capacity / sizeof(float));
if (floatCount < FftLength * 2)
return;

float* samples = (float*)data;

for (int i = 0; i < FftLength; i++)
{
_left[i] = samples[i * 2];
_right[i] = samples[i * 2 + 1];
}

for (int i = 0; i < FftLength; i++)
{
float w = (float)_window[i];
_fftL[i] = new Complex(_left[i] * w, 0);
_fftR[i] = new Complex(_right[i] * w, 0);
}

FFT(_fftL);
FFT(_fftR);

int bins = FftLength / 2;
_spectrum ??= new float[bins * 2];

for (int i = 0; i < bins; i++)
{
float freq = i * _sampleRate / (float)FftLength;
float gain = Compensation(freq);

_spectrum[i] = (float)_fftL[i].Magnitude * gain;
_spectrum[i + bins] = (float)_fftR[i].Magnitude * gain;
}

SpectrumDataUpdated?.Invoke(_spectrum);
}

private static void FFT(Complex[] buffer)
{
int n = buffer.Length;
int bits = (int)Math.Log2(n);

for (int j = 1, i = 0; j < n; j++)
{
int bit = n >> 1;
for (; (i & bit) != 0; bit >>= 1)
i &= ~bit;
i |= bit;

if (j < i)
(buffer[j], buffer[i]) = (buffer[i], buffer[j]);
}

for (int len = 2; len <= n; len <<= 1)
{
double ang = -2 * Math.PI / len;
Complex wlen = new(Math.Cos(ang), Math.Sin(ang));

for (int i = 0; i < n; i += len)
{
Complex w = Complex.One;
for (int j = 0; j < len / 2; j++)
{
var u = buffer[i + j];
var v = buffer[i + j + len / 2] * w;
buffer[i + j] = u + v;
buffer[i + j + len / 2] = u - v;
w *= wlen;
}
}
}
}

private static float Compensation(float freq)
{
float[] f = { 20, 50, 100, 200, 500, 1000, 2000, 4000, 8000, 16000, 20000 };
float[] g = { 0.5f, 0.3f, 0.4f, 0.6f, 0.8f, 1.0f, 1.2f, 1.3f, 1.1f, 0.9f, 0.8f };

if (freq <= f[0]) return g[0];
if (freq >= f[^1]) return g[^1];

int i = 0;
while (freq > f[i + 1]) i++;

float t = (freq - f[i]) / (f[i + 1] - f[i]);
return g[i] + t * (g[i + 1] - g[i]);
}
}

NAudioSpectrumAnalyzer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
public partial class NaudioSpectrumAnalyzer : ISpectrumAnalyzer
{
private WasapiLoopbackCapture _capture;
public event Action<float[]> SpectrumDataUpdated;
private int _sampleRate = 48000;
private readonly int _fftLength = 2048;
private readonly float[] _fftLeftBuffer;
private readonly float[] _fftRightBuffer;
private readonly NAudio.Dsp.Complex[] _fftLeftData;
private readonly NAudio.Dsp.Complex[] _fftRightData;
private float[] _spectrumLeftData;
private float[] _spectrumRightData;
private float[] _spectrumData;
private bool _disposed = false;
private double[] _hammingWindow;

public NaudioSpectrumAnalyzer()
{
_fftLeftBuffer = new float[_fftLength];
_fftLeftData = new NAudio.Dsp.Complex[_fftLength];
_fftRightBuffer = new float[_fftLength];
_fftRightData = new NAudio.Dsp.Complex[_fftLength];
_hammingWindow = new double[_fftLength];

for (int i = 0; i < _fftLength; i++)
{
_hammingWindow[i] = 0.54 - 0.46 * Math.Cos((2 * Math.PI * i) / (_fftLength - 1));
}
}
public void Start()
{
try
{
_capture = new WasapiLoopbackCapture();
_sampleRate = _capture.WaveFormat.SampleRate;
_spectrumLeftData = new float[(int)(24000.0f / _sampleRate * _fftLength) / 2];
_spectrumRightData = new float[(int)(24000.0f / _sampleRate * _fftLength) / 2];
_spectrumData = new float[(int)(24000.0f / _sampleRate * _fftLength)];
_capture.DataAvailable -= OnDataAvailable;
_capture.DataAvailable += OnDataAvailable;
_capture.RecordingStopped -= OnRecordingStopped;
_capture.RecordingStopped += OnRecordingStopped;
_capture.StartRecording();
}
catch (Exception)
{
}
}
private void OnRecordingStopped(object sender, StoppedEventArgs e)
{

}
public void Stop()
{
_capture?.StopRecording();
}

public void Dispose()
{
if (!_disposed)
{
_capture?.Dispose();
_disposed = true;
}
}

private void OnDataAvailable(object sender, WaveInEventArgs e)
{
if (_disposed || e.BytesRecorded == 0) return;

int samples = e.BytesRecorded / 8;
if (samples < _fftLength) return;
for (int i = 0; i < _fftLength; i++)
{
_fftLeftBuffer[i] = BitConverter.ToSingle(e.Buffer, i * 8);
_fftRightBuffer[i] = BitConverter.ToSingle(e.Buffer, i * 8 + 4);
}
for (int i = 0; i < _fftLength; i++)
{
_fftLeftData[i].X = _fftLeftBuffer[i] * (float)_hammingWindow[i]; // Real part
_fftLeftData[i].Y = 0; // Imaginary part
_fftRightData[i].X = _fftRightBuffer[i] * (float)_hammingWindow[i];
_fftRightData[i].Y = 0;
}

// FFT
FastFourierTransform.FFT(true, (int)Math.Log(_fftLength, 2), _fftLeftData);
FastFourierTransform.FFT(true, (int)Math.Log(_fftLength, 2), _fftRightData);
for (int i = 0; i < _spectrumLeftData.Length; i++)
{
float real = (float)_fftLeftData[i].X;
float imaginary = (float)_fftLeftData[i].Y;
float magnitude = (float)Math.Sqrt(real * real + imaginary * imaginary);
float frequency = i * _sampleRate / _fftLength;
float compensationFactor = GetCompensationFactor(frequency);
_spectrumLeftData[i] = magnitude * compensationFactor;
_spectrumRightData[i] = (float)Math.Sqrt((float)_fftRightData[i].X * (float)_fftRightData[i].X + (float)_fftRightData[i].Y * (float)_fftRightData[i].Y) * compensationFactor;
for (int j = 0; j < _spectrumLeftData.Length; j++)
{
_spectrumData[j] = _spectrumLeftData[_spectrumLeftData.Length - 1 - j];
}
Array.Copy(_spectrumRightData, 0, _spectrumData, _spectrumLeftData.Length, _spectrumRightData.Length);
}
SpectrumDataUpdated?.Invoke(_spectrumData);
}

private float GetCompensationFactor(float freq)
{
float[] frequencies = { 20, 50, 100, 200, 500, 1000, 2000, 4000, 8000, 16000, 20000 };
float[] gains = { 0.5f, 0.3f, 0.4f, 0.6f, 0.8f, 1.0f, 1.2f, 1.3f, 1.1f, 0.9f, 0.8f };
if (freq <= frequencies[0])
{
return gains[0];
}
if (freq >= frequencies[frequencies.Length - 1])
{
return gains[gains.Length - 1];
}
int i = 0;
while (freq > frequencies[i + 1])
{
i++;
}
float x1 = frequencies[i];
float y1 = gains[i];
float x2 = frequencies[i + 1];
float y2 = gains[i + 1];
return y1 + (freq - x1) * ((y2 - y1) / (x2 - x1));
}
}

now:

1
<dev:SpectrumVisualizer x:Name="SpectrumVisualizerSample" />
1
SpectrumVisualizerSample.Analyzer = new NaudioSpectrumAnalyzer();

Custom color

you can use custom defined color if you want, first set a ColorType property to Custom, then you can define your colors like this:

1
2
3
4
SpectrumVisualizerSample.CustomColorProvider = (intensity, i) =>
{
return i % 2 == 0 ? Colors.HotPink : Colors.Cyan;
};

SpectrumVisualizer

Demo

you can run demo and see this feature.

0%