Hi @mc ,
Thanks for reaching out.
I’ve created a sample .NET MAUI project that demonstrates a music spectrum visualizer with mirrored bars, peak markers, and play/stop functionality. The spectrum responds to audio playback on Windows (tested in VS 2022).
Currently, this project works with Windows only, but you can use it as a base to extend for Android or iOS later. The spectrum is generated from audio samples and provides a nice visualization of frequency amplitudes.
Make sure to install the following via NuGet:
-
Plugin.Maui.Audio– for audio playback -
SkiaSharp.Views.Maui– for drawing the spectrum -
MathNet.Numerics– for FFT calculation -
NAudio– for decoding audio files on Windows
MainPage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:skia="clr-namespace:SkiaSharp.Views.Maui.Controls;assembly=SkiaSharp.Views.Maui.Controls"
x:Class="MauiMusicSpectrum.MainPage">
<VerticalStackLayout Padding="20" Spacing="20">
<HorizontalStackLayout Spacing="10">
<Button Text="Pick Music File" Clicked="OnPickMusic" />
<Button Text="Play" Clicked="OnPlayMusic" />
<Button Text="Stop" Clicked="OnStopMusic" />
</HorizontalStackLayout>
<skia:SKCanvasView x:Name="SpectrumCanvas"
HeightRequest="200"
PaintSurface="OnPaintSurface" />
</VerticalStackLayout>
</ContentPage>
MainPage.xaml.cs
using Plugin.Maui.Audio;
using SkiaSharp;
using SkiaSharp.Views.Maui;
using MathNet.Numerics.IntegralTransforms;
using System.Numerics;
namespace MauiMusicSpectrum;
public partial class MainPage : ContentPage
{
IAudioPlayer player;
float[] spectrum = new float[32];
float[] spectrumPeak = new float[32]; // for peak decay
float[] samples;
bool isPlaying = false;
bool spectrumLoopRunning = false;
public MainPage()
{
InitializeComponent();
}
// -------------------------
// File Picker
// -------------------------
async void OnPickMusic(object sender, EventArgs e)
{
var file = await FilePicker.PickAsync();
if (file == null) return;
player?.Stop();
player?.Dispose();
await LoadAudioSamples(file);
var stream = await file.OpenReadAsync();
player = AudioManager.Current.CreatePlayer(stream);
player.Volume = 1.0;
// Start playing
player.Play();
isPlaying = true;
if (samples != null && !spectrumLoopRunning)
StartAudioSpectrum();
}
void OnPlayMusic(object sender, EventArgs e)
{
if (player != null && !isPlaying)
{
player.Play();
isPlaying = true;
}
}
void OnStopMusic(object sender, EventArgs e)
{
if (player != null && isPlaying)
{
player.Pause();
isPlaying = false;
}
}
// -------------------------
// Load audio samples (Windows only)
// -------------------------
async Task LoadAudioSamples(FileResult file)
{
#if WINDOWS
var tempPath = Path.Combine(Path.GetTempPath(), file.FileName);
using (var fs = File.Create(tempPath))
using (var src = await file.OpenReadAsync())
await src.CopyToAsync(fs);
samples = DecodePCMWindows(tempPath);
#else
samples = null;
#endif
}
void StartAudioSpectrum()
{
spectrumLoopRunning = true;
int fftSize = 1024;
Task.Run(async () =>
{
int pos = 0;
while (true)
{
if (!isPlaying || samples == null || pos + fftSize >= samples.Length)
{
await Task.Delay(30);
continue;
}
var buffer = new Complex[fftSize];
for (int i = 0; i < fftSize; i++)
buffer[i] = new Complex(samples[pos + i], 0);
Fourier.Forward(buffer, FourierOptions.Matlab);
int binsPerBar = fftSize / 2 / spectrum.Length;
for (int i = 0; i < spectrum.Length; i++)
{
double sum = 0;
for (int j = 0; j < binsPerBar; j++)
sum += buffer[i * binsPerBar + j].Magnitude;
float value = (float)(Math.Log10(sum + 1) * 20);
spectrum[i] = value;
if (value > spectrumPeak[i])
spectrumPeak[i] = value;
else
spectrumPeak[i] *= 0.95f;
}
pos += fftSize / 4;
MainThread.BeginInvokeOnMainThread(() => SpectrumCanvas.InvalidateSurface());
await Task.Delay(30);
}
});
}
void OnPaintSurface(object sender, SKPaintSurfaceEventArgs e)
{
var canvas = e.Surface.Canvas;
canvas.Clear(SKColors.Black);
int barCount = spectrum.Length;
float barWidth = e.Info.Width / (barCount * 3f);
float centerX = e.Info.Width / 2f;
float centerY = e.Info.Height / 2f;
for (int i = 0; i < barCount; i++)
{
float hue = (i * 360f / barCount + DateTime.Now.Millisecond / 10f) % 360f;
var baseColor = SKColor.FromHsl(hue, 100, 30);
float barHeight = spectrum[i] * 2.5f;
if (barHeight > centerY) barHeight = centerY;
var paint = new SKPaint { Color = baseColor, IsAntialias = true };
canvas.DrawRect(centerX + i * barWidth, centerY - barHeight, barWidth - 1, barHeight, paint);
canvas.DrawRect(centerX - (i + 1) * barWidth, centerY - barHeight, barWidth - 1, barHeight, paint);
canvas.DrawRect(centerX + i * barWidth, centerY, barWidth - 1, barHeight, paint);
canvas.DrawRect(centerX - (i + 1) * barWidth, centerY, barWidth - 1, barHeight, paint);
var peakPaint = new SKPaint { Color = SKColors.White.WithAlpha(180), IsAntialias = true };
float peakYTop = centerY - spectrumPeak[i] * 2.5f - 2;
float peakYBottom = centerY + spectrumPeak[i] * 2.5f;
if (peakYTop < 0) peakYTop = 0;
if (peakYBottom > e.Info.Height) peakYBottom = e.Info.Height;
canvas.DrawRect(centerX + i * barWidth, peakYTop, barWidth - 1, 2, peakPaint);
canvas.DrawRect(centerX - (i + 1) * barWidth, peakYTop, barWidth - 1, 2, peakPaint);
canvas.DrawRect(centerX + i * barWidth, peakYBottom, barWidth - 1, 2, peakPaint);
canvas.DrawRect(centerX - (i + 1) * barWidth, peakYBottom, barWidth - 1, 2, peakPaint);
}
}
#if WINDOWS
float[] DecodePCMWindows(string path)
{
using var reader = new NAudio.Wave.AudioFileReader(path);
var list = new List<float>();
float[] buffer = new float[1024];
int read;
while ((read = reader.Read(buffer, 0, buffer.Length)) > 0)
list.AddRange(buffer.Take(read));
return list.ToArray();
}
#endif
}
Limitations / Notes
- Platform Support:
- This sample is fully tested only on Windows.
- Android / iOS decoding is not included.
- Spectrum Behavior:
- The spectrum reflects audio samples, but in Windows it may appear somewhat “random” due to FFT simplification.
- Extensibility:
- You can expand this project with Android/iOS decoding, smoother animations, color themes, volume control, etc.
Hope this helps! If my answer was helpful - kindly follow the instructions here so others with the same problem can benefit as well.