Skip to content

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 or AudioGraph to analyze audio files.

AudioGraphSpectrumAnalyzer ​

cs
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 ​

cs
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:

xml
<dev:SpectrumVisualizer x:Name="SpectrumVisualizerSample" />
cs
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:

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

SpectrumVisualizer

Demo ​

you can run demo and see this feature.

Released under the MIT License.