diff --git a/src/Zen.Common/Constants.cs b/src/Zen.Common/Constants.cs index a70b6885..906fc244 100644 --- a/src/Zen.Common/Constants.cs +++ b/src/Zen.Common/Constants.cs @@ -47,6 +47,9 @@ public static class Constants /* Wave Visualiser */ public const int WaveVisualisationPanelWidth = 200; + /* Spectrum Analyser Visualiser 8*/ + public const int SpectrumVisualisationPanelWidth = 200; + /* Counters Visualiser */ public const int CountersPanelHeight = 24; diff --git a/src/Zen.Desktop.Host/Features/SpectrumAnalyser.cs b/src/Zen.Desktop.Host/Features/SpectrumAnalyser.cs new file mode 100644 index 00000000..faf4dff4 --- /dev/null +++ b/src/Zen.Desktop.Host/Features/SpectrumAnalyser.cs @@ -0,0 +1,228 @@ +using System; +using System.Numerics; +using MathNet.Numerics.IntegralTransforms; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Zen.Common; + +namespace Zen.Desktop.Host.Features; + +public class SpectrumAnalyser +{ + private const int BufferSize = System.Modules.Audio.Constants.SampleRate / Constants.SpectrumFramesPerSecond; + + private const int MagnitudeDivisor = 250; + + private const int BarWidth = 8; + + private const int BarSpacing = 4; + + private const int SegmentHeight = 5; + + // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable + private readonly GraphicsDeviceManager _graphicsDeviceManager; + + private readonly Color[] _data; + + private readonly Complex[][] _buffers; + + private readonly Texture2D _spectrum; + + private int _bufferPosition; + + private bool _rendering; + + private readonly Color[] _palette; + + private readonly FrequencyRange[] _frequencyRanges = + [ + new FrequencyRange(30, 40), + new FrequencyRange(40, 60), // Bass + new FrequencyRange(60, 80), + new FrequencyRange(80, 120), // Lower midrange + new FrequencyRange(120, 160), + new FrequencyRange(160, 240), // Midrange + new FrequencyRange(240, 320), + new FrequencyRange(320, 480), // Upper midrange + new FrequencyRange(480, 640), + new FrequencyRange(640, 960), // Presence + new FrequencyRange(960, 1280), + new FrequencyRange(1280, 1920), // Brilliance + new FrequencyRange(1920, 2560) + ]; + + public Texture2D Spectrum => _spectrum; + + public SpectrumAnalyser(GraphicsDeviceManager graphicsDeviceManager) + { + _graphicsDeviceManager = graphicsDeviceManager; + + _data = new Color[Constants.WaveVisualisationPanelWidth * Constants.ScreenHeightPixels]; + + _spectrum = new Texture2D(_graphicsDeviceManager.GraphicsDevice, Constants.WaveVisualisationPanelWidth, Constants.ScreenHeightPixels); + + _buffers = new Complex[4][]; + + for (var i = 0; i < 4; i++) + { + _buffers[i] = new Complex[BufferSize]; + } + + _palette = PaletteGenerator.GetPalette(46, + [ + new Color(119, 35, 172), + new Color(176, 83, 203), + new Color(255, 168, 76), + new Color(254, 211, 56), + new Color(254, 253, 0) + ]); + } + + public void ReceiveSignals(float[] signals) + { + if (_rendering) + { + return; + } + + _buffers[0][_bufferPosition] = new Complex(signals[0], 0); + _buffers[1][_bufferPosition] = new Complex(signals[1], 0); + _buffers[2][_bufferPosition] = new Complex(signals[2], 0); + + _bufferPosition++; + + if (_bufferPosition >= BufferSize) + { + _bufferPosition = 0; + + if (! _rendering) + { + RenderSpectrum(); + } + } + } + + private void RenderSpectrum() + { + _rendering = true; + + Array.Fill(_data, Color.Black); + + RenderSpectrumChannel(0); + RenderSpectrumChannel(1); + RenderSpectrumChannel(2); + + _spectrum.SetData(_data); + + _rendering = false; + } + + private void RenderSpectrumChannel(int channel) + { + Fourier.Forward(_buffers[channel], FourierOptions.Matlab); + + var height = Constants.ScreenHeightPixels / 4; + + var magnitudes = new float[_buffers[channel].Length]; + + var axis = height * Constants.SpectrumVisualisationPanelWidth * (channel + 1) - Constants.SpectrumVisualisationPanelWidth; + + for (var i = 0; i < magnitudes.Length; i++) + { + magnitudes[i] = (float) Math.Sqrt(_buffers[channel][i].Real * _buffers[channel][i].Real + _buffers[channel][i].Imaginary * _buffers[channel][i].Imaginary); + } + + var groupedMagnitudes = new float[_frequencyRanges.Length]; + + for (var i = 0; i < _frequencyRanges.Length; i++) + { + var lowBin = (int)Math.Round(_frequencyRanges[i].Low * BufferSize / System.Modules.Audio.Constants.SampleRate); + var highBin = (int)Math.Round(_frequencyRanges[i].High * BufferSize / System.Modules.Audio.Constants.SampleRate); + + float sum = 0; + + for (var j = lowBin; j <= highBin; j++) + { + sum += magnitudes[j]; + } + + groupedMagnitudes[i] = sum / (highBin - lowBin + 1); + } + + for (var i = 0; i < _frequencyRanges.Length; i++) + { + var dataPoint = groupedMagnitudes[i] / MagnitudeDivisor; + + for (var x = 0; x < BarWidth; x++) + { + var offset = -(int) (dataPoint * height * (channel == 3 ? 1 : 4)); + + while (offset <= 0) + { + if (Math.Abs(offset) % SegmentHeight == 4) + { + offset++; + + continue; + } + + _data[22 + axis + i * (BarWidth + BarSpacing) + x + offset * Constants.SpectrumVisualisationPanelWidth] = _palette[-offset]; + + offset++; + } + } + } + } + + private class FrequencyRange + { + public float Low { get; } + public float High { get; } + + public FrequencyRange(float low, float high) + { + Low = low; + High = high; + } + } + + private static class PaletteGenerator + { + public static Color[] GetPalette(int steps, Color[] markers) + { + var palette = new Color[steps]; + + var markerPeriod = steps / (markers.Length - 2); + + var current = markers[0]; + + var next = markers[1]; + + var counter = markerPeriod; + + var markerIndex = 1; + + for (var i = 0; i < steps; i++) + { + palette[i] = new Color(current.R, current.G, current.B); + + current.R += (byte) ((next.R - current.R) / markerPeriod); + current.G += (byte) ((next.G - current.G) / markerPeriod); + current.B += (byte) ((next.B - current.B) / markerPeriod); + + counter--; + + if (counter == 0) + { + counter = markerPeriod; + + markerIndex++; + + next = markers[markerIndex]; + } + } + + return palette; + } + } +} \ No newline at end of file diff --git a/src/Zen.Desktop.Host/Infrastructure/Host.cs b/src/Zen.Desktop.Host/Infrastructure/Host.cs index 5c811908..2070a33f 100644 --- a/src/Zen.Desktop.Host/Infrastructure/Host.cs +++ b/src/Zen.Desktop.Host/Infrastructure/Host.cs @@ -46,6 +46,8 @@ public class Host : Game private WaveVisualiser _waveVisualiser; + private SpectrumAnalyser _spectrumAnalyser; + private CountersVisualiser _countersVisualiser; private VideoRamVisualiser _videoRamVisualiser; @@ -60,6 +62,11 @@ public Host() width += Constants.WaveVisualisationPanelWidth * _scaleFactor; } + if (AppSettings.Instance.Visualisation == Visualisation.SpectrumAnalyser) + { + width += Constants.SpectrumVisualisationPanelWidth * _scaleFactor; + } + if (AppSettings.Instance.Visualisation == Visualisation.VideoRam) { width += Constants.VideoRamVisualisationPanelWidth * _scaleFactor; @@ -125,6 +132,11 @@ private void SetMotherboard(Model model) { _videoRamVisualiser.Ram = _motherboard.Ram; } + + if (_spectrumAnalyser != null) + { + _motherboard.AyAudio.AySignalHook = _spectrumAnalyser.ReceiveSignals; + } } protected override void OnActivated(object sender, EventArgs args) @@ -170,6 +182,13 @@ protected override void LoadContent() _motherboard.AyAudio.BeeperSignalHook = _waveVisualiser.ReceiveSignal; } + if (AppSettings.Instance.Visualisation == Visualisation.SpectrumAnalyser) + { + _spectrumAnalyser = new SpectrumAnalyser(_graphicsDeviceManager); + + _motherboard.AyAudio.AySignalHook = _spectrumAnalyser.ReceiveSignals; + } + if (AppSettings.Instance.Visualisation == Visualisation.VideoRam) { _videoRamVisualiser = new VideoRamVisualiser(_graphicsDeviceManager, _motherboard.Ram, false, _videoRenderer); @@ -316,6 +335,7 @@ private void MenuFinished(MenuResult result, object arguments) AppSettings.Instance.Save(); _waveVisualiser = null; + _videoRamVisualiser = null; _motherboard.AyAudio.AySignalHook = null; _motherboard.AyAudio.BeeperSignalHook = null; @@ -336,6 +356,19 @@ private void MenuFinished(MenuResult result, object arguments) break; + case MenuResult.VisualisationSpectrumAnalyser: + AppSettings.Instance.Visualisation = Visualisation.SpectrumAnalyser; + AppSettings.Instance.Save(); + + _spectrumAnalyser = new SpectrumAnalyser(_graphicsDeviceManager); + _motherboard.AyAudio.AySignalHook = _spectrumAnalyser.ReceiveSignals; + _videoRamVisualiser = null; + _waveVisualiser = null; + + ChangeScale(_scaleFactor); + + break; + case MenuResult.VisualisationVideoRam: AppSettings.Instance.Visualisation = Visualisation.VideoRam; AppSettings.Instance.Save(); @@ -415,6 +448,11 @@ private void ChangeScale(int scale) width += Constants.WaveVisualisationPanelWidth * _scaleFactor; } + if (AppSettings.Instance.Visualisation == Visualisation.SpectrumAnalyser) + { + width += Constants.SpectrumVisualisationPanelWidth * _scaleFactor; + } + if (AppSettings.Instance.Visualisation == Visualisation.VideoRam) { width += Constants.VideoRamVisualisationPanelWidth * _scaleFactor; @@ -544,6 +582,18 @@ protected override void Draw(GameTime gameTime) } } + if (_spectrumAnalyser != null) + { + var spectrum = _spectrumAnalyser.Spectrum; + + if (spectrum != null) + { + _spriteBatch.Draw(spectrum, + new Rectangle(Constants.ScreenWidthPixels * _scaleFactor, 0, Constants.SpectrumVisualisationPanelWidth * _scaleFactor, Constants.ScreenHeightPixels * _scaleFactor), + new Rectangle(0, 0, Constants.SpectrumVisualisationPanelWidth, Constants.ScreenHeightPixels), Color.White); + } + } + if (_videoRamVisualiser != null) { if (_videoRamVisualiser.BanksView) diff --git a/src/Zen.Desktop.Host/Infrastructure/Menu/MenuResult.cs b/src/Zen.Desktop.Host/Infrastructure/Menu/MenuResult.cs index c53cbe16..53b8c95f 100644 --- a/src/Zen.Desktop.Host/Infrastructure/Menu/MenuResult.cs +++ b/src/Zen.Desktop.Host/Infrastructure/Menu/MenuResult.cs @@ -19,6 +19,7 @@ public enum MenuResult SoundOff, VisualisationOff, VisualisationWaveform, + VisualisationSpectrumAnalyser, VisualisationVideoRam, VisualisationVideoBanks, CountersOn, diff --git a/src/Zen.Desktop.Host/Infrastructure/Menu/VisualisationMenu.cs b/src/Zen.Desktop.Host/Infrastructure/Menu/VisualisationMenu.cs index d52a1f35..578e956c 100644 --- a/src/Zen.Desktop.Host/Infrastructure/Menu/VisualisationMenu.cs +++ b/src/Zen.Desktop.Host/Infrastructure/Menu/VisualisationMenu.cs @@ -14,8 +14,9 @@ public override List