Added first working visualization

This commit is contained in:
Darth Affe 2017-08-03 23:08:53 +02:00
parent d738e8fc5d
commit 89330368fa
17 changed files with 764 additions and 2 deletions

View File

@ -2,6 +2,7 @@
using System.IO;
using System.Windows;
using Hardcodet.Wpf.TaskbarNotification;
using KeyboardAudioVisualizer.AudioProcessing;
using KeyboardAudioVisualizer.Helper;
namespace KeyboardAudioVisualizer
@ -42,6 +43,9 @@ namespace KeyboardAudioVisualizer
_taskbarIcon.ShowBalloonTip("Keyboard Audio-Visualizer is starting in the tray!", "Click on the icon to open the configuration.", BalloonIcon.Info);
}
ApplicationManager.Instance.Settings = settings;
AudioProcessor.Initialize();
ApplicationManager.Instance.InitializeDevices();
}
catch (Exception ex)
{

View File

@ -1,6 +1,13 @@
using System.Windows;
using KeyboardAudioVisualizer.AudioProcessing;
using KeyboardAudioVisualizer.Brushes;
using KeyboardAudioVisualizer.Helper;
using KeyboardAudioVisualizer.UI;
using RGB.NET.Brushes;
using RGB.NET.Brushes.Gradients;
using RGB.NET.Core;
using RGB.NET.Devices.Corsair;
using RGB.NET.Groups;
namespace KeyboardAudioVisualizer
{
@ -28,13 +35,37 @@ namespace KeyboardAudioVisualizer
#region Constructors
private ApplicationManager()
{ }
private ApplicationManager() { }
#endregion
#region Methods
public void InitializeDevices()
{
RGBSurface surface = RGBSurface.Instance;
//surface.Exception += args =>;
surface.UpdateFrequency = 1 / 30.0; //TODO DarthAffe 03.08.2017: Settings
surface.UpdateMode = UpdateMode.Continuous;
surface.LoadDevices(CorsairDeviceProvider.Instance);
//surface.LoadDevices(LogitechDeviceProvider.Instance);
//surface.LoadDevices(CoolerMasterDeviceProvider.Instance);
ILedGroup background = new ListLedGroup(surface.Leds);
background.Brush = new SolidColorBrush(new Color(0, 0, 0));
//TODO DarthAffe 03.08.2017: Changeable, Settings etc.
foreach (IRGBDevice device in surface.Devices)
{
if (device.DeviceInfo.DeviceType == RGBDeviceType.Keyboard)
new ListLedGroup(device).Brush = new FrequencyBarsBrush(AudioProcessor.Instance.PrimaryVisualizationProvider, new RainbowGradient(300, -14));
}
surface.Updating += args => AudioProcessor.Instance.Update();
}
private void OpenConfiguration()
{
if (_configurationWindow == null) _configurationWindow = new ConfigurationWindow();

View File

@ -0,0 +1,79 @@
namespace KeyboardAudioVisualizer.AudioCapture
{
public class AudioBuffer
{
#region Properties & Fields
private readonly int _capacity;
private readonly float[] _bufferLeft;
private readonly float[] _bufferRight;
private int _currentIndex;
public int Size => _capacity;
#endregion
#region Constructors
public AudioBuffer(int capacity)
{
this._capacity = capacity;
_bufferLeft = new float[capacity];
_bufferRight = new float[capacity];
}
#endregion
#region Methods
public void Put(float[] src, int offset, int count)
{
lock (_bufferLeft)
{
if ((count & 1) != 0) return; // we expect stereo-data to be an even amount of values
if (count > _capacity)
{
offset += count - _capacity;
count = _capacity;
}
for (int i = 0; i < count; i += 2)
{
_currentIndex++;
if (_currentIndex >= _capacity) _currentIndex = 0;
_bufferLeft[_currentIndex] = src[offset + i];
_bufferRight[_currentIndex] = src[offset + i + 1];
}
}
}
public void CopyLeftInto(ref float[] data, int offset)
{
lock (_bufferLeft)
for (int i = 0; i < _capacity; i++)
data[offset + i] = _bufferLeft[(_currentIndex + i) % _capacity];
}
public void CopyRightInto(ref float[] data, int offset)
{
lock (_bufferLeft)
for (int i = 0; i < _capacity; i++)
data[offset + i] = _bufferRight[(_currentIndex + i) % _capacity];
}
public void CopyMixInto(ref float[] data, int offset)
{
lock (_bufferLeft)
for (int i = 0; i < _capacity; i++)
{
int index = (_currentIndex + i) % _capacity;
data[offset + i] = (_bufferLeft[index] + _bufferRight[index]) / 2f;
}
}
#endregion
}
}

View File

@ -0,0 +1,50 @@
using CSCore;
using CSCore.SoundIn;
using CSCore.Streams;
namespace KeyboardAudioVisualizer.AudioCapture
{
public class CSCoreAudioInput : IAudioInput
{
#region Properties & Fields
private WasapiCapture _capture;
private SoundInSource _soundInSource;
private SingleBlockNotificationStream _stream;
private readonly float[] _readBuffer = new float[2048];
public int SampleRate => _soundInSource?.WaveFormat?.SampleRate ?? -1;
#endregion
#region Event
public event AudioData DataAvailable;
#endregion
#region Methods
public void Initialize()
{
_capture = new WasapiLoopbackCapture();
_capture.Initialize();
_soundInSource = new SoundInSource(_capture) { FillWithZeros = false };
_stream = new SingleBlockNotificationStream(_soundInSource.ToStereo().ToSampleSource());
_soundInSource.DataAvailable += OnSoundDataAvailable;
_capture.Start();
}
private void OnSoundDataAvailable(object sender, DataAvailableEventArgs dataAvailableEventArgs)
{
int readCount;
while ((readCount = _stream.Read(_readBuffer, 0, _readBuffer.Length)) > 0)
DataAvailable?.Invoke(_readBuffer, 0, readCount);
}
#endregion
}
}

View File

@ -0,0 +1,13 @@
namespace KeyboardAudioVisualizer.AudioCapture
{
public delegate void AudioData(float[] data, int offset, int count);
public interface IAudioInput
{
int SampleRate { get; }
event AudioData DataAvailable;
void Initialize();
}
}

View File

@ -0,0 +1,77 @@
using KeyboardAudioVisualizer.AudioCapture;
using KeyboardAudioVisualizer.AudioProcessing.Equalizer;
using KeyboardAudioVisualizer.AudioProcessing.Spectrum;
using KeyboardAudioVisualizer.AudioProcessing.VisualizationPRovider;
namespace KeyboardAudioVisualizer.AudioProcessing
{
public class AudioProcessor
{
#region Constants
private const int MAXIMUM_UPDATE_RATE = 40; // We won't allow to change the FPS beyond this
#endregion
#region Properties & Fields
public static AudioProcessor Instance { get; private set; }
private AudioBuffer _audioBuffer;
private IAudioInput _audioInput;
private ISpectrumProvider _spectrumProvider;
public IVisualizationProvider PrimaryVisualizationProvider { get; private set; }
#endregion
#region Constructors
private AudioProcessor() { }
#endregion
#region Methods
public void Update()
{
_spectrumProvider.Update();
PrimaryVisualizationProvider.Update();
}
public static void Initialize()
{
if (Instance != null) return;
Instance = new AudioProcessor();
Instance.InitializeInstance();
}
private void InitializeInstance()
{
_audioInput = new CSCoreAudioInput();
_audioInput.Initialize();
_audioBuffer = new AudioBuffer(CalculateSampleSize(_audioInput.SampleRate, MAXIMUM_UPDATE_RATE));
_audioInput.DataAvailable += (data, offset, count) => _audioBuffer.Put(data, offset, count);
_spectrumProvider = new FourierSpectrumProvider(_audioBuffer, _audioInput.SampleRate);
_spectrumProvider.Initialize();
//TODO DarthAffe 03.08.2017: Initialize correctly; Settings
MultiBandEqualizer equalizer = new MultiBandEqualizer { [0] = -5, [1] = -1, [2] = 0, [3] = 2, [4] = 2 };
PrimaryVisualizationProvider = new FrequencyBarsVisualizationProvider(new FrequencyBarsVisualizationProviderConfiguration { Scale = 38 }, _spectrumProvider) { Equalizer = equalizer };
PrimaryVisualizationProvider.Initialize();
}
private int CalculateSampleSize(int sampleRate, int maximumUpdateRate)
{
int sampleSize = 2;
while ((sampleSize * maximumUpdateRate) < sampleRate)
sampleSize <<= 1;
return sampleSize;
}
#endregion
}
}

View File

@ -0,0 +1,8 @@
namespace KeyboardAudioVisualizer.AudioProcessing.Equalizer
{
public interface IEqualizer
{
bool IsEnabled { get; set; }
float[] CalculateValues(int values);
}
}

View File

@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace KeyboardAudioVisualizer.AudioProcessing.Equalizer
{
public class MultiBandEqualizer : IEqualizer
{
#region Properties & Fields
private float[] _values;
private readonly List<Band> _bands = new List<Band>();
public int Bands => _bands.Count;
public float this[int band]
{
get => _bands[band].Value;
set
{
_bands[band].Value = value;
RecalculateValues();
}
}
public bool IsEnabled { get; set; } = true;
#endregion
#region Constructors
public MultiBandEqualizer(int bands = 5)
{
if (bands < 2) throw new ArgumentOutOfRangeException(nameof(bands), "There must be at least two bands for an working equalizer!");
float reference = (float)Math.Log(bands);
for (int i = bands - 1; i >= 0; i--)
{
Band band = new Band((reference - (float)Math.Log(i + 1)) / reference);
_bands.Add(band);
}
CalculateValues(1);
}
#endregion
#region Methods
public float[] CalculateValues(int values)
{
if ((_values == null) || (_values.Length != values))
{
_values = new float[values];
RecalculateValues();
}
return _values;
}
private void RecalculateValues()
{
float width = _values.Length;
for (int i = 0; i < _values.Length; i++)
{
float offset = (i / width);
Band bandBefore = _bands.Last(n => n.Offset <= offset);
Band bandAfter = _bands.First(n => n.Offset >= offset);
offset = bandAfter.Offset <= 0 ? 0 : (offset - bandBefore.Offset) / (bandAfter.Offset - bandBefore.Offset);
float value = (float)((3.0 * (offset * offset)) - (2.0 * (offset * offset * offset)));
_values[i] = bandBefore.Value + (value * (bandAfter.Value - bandBefore.Value));
}
}
#endregion
#region Data
private class Band
{
#region Properties & Fields
public float Offset { get; set; }
public float Value { get; set; }
#endregion
#region Constructors
public Band(float offset, float value = 0)
{
this.Offset = offset;
this.Value = value;
}
#endregion
}
#endregion
}
}

View File

@ -0,0 +1,8 @@
namespace KeyboardAudioVisualizer.AudioProcessing
{
public interface IAudioProcessor
{
void Initialize();
void Update();
}
}

View File

@ -0,0 +1,79 @@
using KeyboardAudioVisualizer.AudioCapture;
using MathNet.Numerics;
using MathNet.Numerics.IntegralTransforms;
namespace KeyboardAudioVisualizer.AudioProcessing.Spectrum
{
public class FourierSpectrumProvider : ISpectrumProvider
{
#region Properties & Fields
private readonly AudioBuffer _audioBuffer;
private float[] _sampleData;
private double[] _hamming;
private float[] _spectrum;
public float[] Spectrum => _spectrum;
public int SampleRate { get; private set; }
public float Resolution { get; private set; }
#endregion
#region Constructors
public FourierSpectrumProvider(AudioBuffer audioBuffer, int sampleRate)
{
this._audioBuffer = audioBuffer;
this.SampleRate = sampleRate;
}
#endregion
#region Methods
public void Initialize()
{
_hamming = Window.Hamming(_audioBuffer.Size);
_sampleData = new float[_audioBuffer.Size];
_spectrum = new float[_audioBuffer.Size / 2];
Resolution = (float)SampleRate / (float)_audioBuffer.Size;
}
public void Update()
{
_audioBuffer.CopyMixInto(ref _sampleData, 0);
ApplyHamming(ref _sampleData);
CreateSpectrum(ref _sampleData);
}
private void ApplyHamming(ref float[] data)
{
for (int i = 0; i < data.Length; i++)
data[i] = (float)(data[i] * _hamming[i]);
}
private void CreateSpectrum(ref float[] data)
{
Complex32[] complexData = CreateComplexData(ref data);
Fourier.Forward(complexData, FourierOptions.NoScaling);
for (int i = 0; i < _spectrum.Length; i++)
{
Complex32 fourierData = complexData[i];
_spectrum[i] = (fourierData.Real * fourierData.Real) + (fourierData.Imaginary * fourierData.Imaginary);
}
}
private static Complex32[] CreateComplexData(ref float[] data)
{
Complex32[] complexData = new Complex32[data.Length];
for (int i = 0; i < data.Length; i++)
complexData[i] = new Complex32(data[i], 0);
return complexData;
}
#endregion
}
}

View File

@ -0,0 +1,9 @@
namespace KeyboardAudioVisualizer.AudioProcessing.Spectrum
{
public interface ISpectrumProvider : IAudioProcessor
{
float[] Spectrum { get; }
int SampleRate { get; }
float Resolution { get; }
}
}

View File

@ -0,0 +1,183 @@
using System;
using System.Linq;
using KeyboardAudioVisualizer.AudioProcessing.Equalizer;
using KeyboardAudioVisualizer.AudioProcessing.Spectrum;
using KeyboardAudioVisualizer.Configuration;
using KeyboardAudioVisualizer.Helper;
namespace KeyboardAudioVisualizer.AudioProcessing.VisualizationPRovider
{
#region Configuration
public class FrequencyBarsVisualizationProviderConfiguration : AbstractConfiguration
{
private int _bars = 48;
public int Bars
{
get => _bars;
set => SetProperty(ref _bars, value);
}
private double _smoothing = 8;
public double Smoothing
{
get => _smoothing;
set => SetProperty(ref _smoothing, value);
}
private double _minFrequency = 100;
public double MinFrequency
{
get => _minFrequency;
set => SetProperty(ref _minFrequency, value);
}
private double _maxFrequency = 15000;
public double MaxFrequency
{
get => _maxFrequency;
set => SetProperty(ref _maxFrequency, value);
}
private double _scale = 20;
public double Scale
{
get => _scale;
set => SetProperty(ref _scale, value);
}
private int _emphasisePeaks = 1;
public int EmphasisePeaks
{
get => _emphasisePeaks;
set => SetProperty(ref _emphasisePeaks, value);
}
private double _gamma = 3;
public double Gamma
{
get => _gamma;
set => SetProperty(ref _gamma, value);
}
}
#endregion
public class FrequencyBarsVisualizationProvider : IVisualizationProvider
{
#region Properties & Fields
private readonly FrequencyBarsVisualizationProviderConfiguration _configuration;
private readonly ISpectrumProvider _spectrumProvider;
private int _frequencySkipCount;
private int _frequencyCount;
private double _smoothingFactor;
private double _scalingValue;
public IEqualizer Equalizer { get; set; }
public float[] VisualizationData { get; private set; }
#endregion
#region Constructors
public FrequencyBarsVisualizationProvider(FrequencyBarsVisualizationProviderConfiguration configuration, ISpectrumProvider spectrumProvider)
{
this._configuration = configuration;
this._spectrumProvider = spectrumProvider;
configuration.PropertyChanged += (sender, args) => RecalculateConfigValues(args.PropertyName);
}
#endregion
#region Methods
public void Initialize() => RecalculateConfigValues(null);
private void RecalculateConfigValues(string changedPropertyName)
{
if ((changedPropertyName == null) || (changedPropertyName == nameof(FrequencyBarsVisualizationProviderConfiguration.Bars)))
VisualizationData = new float[_configuration.Bars];
if ((changedPropertyName == null) || (changedPropertyName == nameof(FrequencyBarsVisualizationProviderConfiguration.Smoothing)))
_smoothingFactor = Math.Pow((0.0000025 * Math.Pow(2, _configuration.Smoothing)), ((double)_spectrumProvider.Spectrum.Length * 2) / (double)_spectrumProvider.SampleRate);
if ((changedPropertyName == null)
|| (changedPropertyName == nameof(FrequencyBarsVisualizationProviderConfiguration.MinFrequency))
|| (changedPropertyName == nameof(FrequencyBarsVisualizationProviderConfiguration.MaxFrequency)))
CalculateFrequencyCount(MathHelper.Clamp(_configuration.MinFrequency, 0, _spectrumProvider.SampleRate / 2.0),
MathHelper.Clamp(_configuration.MaxFrequency, 0, _spectrumProvider.SampleRate / 2.0),
_spectrumProvider.SampleRate, _spectrumProvider.Spectrum.Length * 2);
if ((changedPropertyName == null) || (changedPropertyName == nameof(FrequencyBarsVisualizationProviderConfiguration.Scale)))
_scalingValue = _configuration.Scale * 0.0001;
}
private void CalculateFrequencyCount(double minFrequency, double maxFrequency, int sampleRate, int sampleSize)
{
int firstFrequency = Math.Max(0, (int)Math.Ceiling((minFrequency / sampleRate) * sampleSize) - 1);
int lastFrequency = Math.Max(firstFrequency, (int)Math.Ceiling((maxFrequency / sampleRate) * sampleSize));
if (firstFrequency == lastFrequency)
{
if (firstFrequency == 0) lastFrequency++;
else firstFrequency--;
}
_frequencyCount = lastFrequency - firstFrequency;
_frequencySkipCount = firstFrequency;
}
public void Update()
{
float[] spectrum = _spectrumProvider.Spectrum.Skip(_frequencySkipCount).Take(_frequencyCount).ToArray();
int startFrequency = 0;
for (int i = 0; i < VisualizationData.Length; i++)
{
int endFrequency = Math.Max(startFrequency + 1, Math.Min(_frequencyCount, (int)Math.Round(Math.Pow((i + 1f) / VisualizationData.Length, _configuration.Gamma) * _frequencyCount)));
int bandWidth = endFrequency - startFrequency;
double binPower = 0;
for (int j = 0; j < bandWidth; j++)
{
float power = spectrum[Math.Min(spectrum.Length - 1, startFrequency + j)];
binPower = Math.Max(power, binPower);
}
if (Equalizer?.IsEnabled == true)
{
float value = Equalizer.CalculateValues(VisualizationData.Length)[i];
if (Math.Abs(value) > 0.000001)
{
bool lower = value < 0;
value = 1 + (value * value);
binPower *= lower ? 1f / value : value;
}
}
binPower = Math.Log(binPower);
binPower = Math.Max(0, binPower);
if (_configuration.EmphasisePeaks == 1)
{
binPower *= binPower;
binPower *= 0.15;
}
if (_configuration.EmphasisePeaks == 2)
binPower *= binPower * binPower;
else
binPower *= 40;
VisualizationData[i] = (float)((VisualizationData[i] * _smoothingFactor) + (binPower * _scalingValue * (1.0 - _smoothingFactor)));
if (double.IsNaN(VisualizationData[i])) VisualizationData[i] = 0;
startFrequency = endFrequency;
}
}
#endregion
}
}

View File

@ -0,0 +1,7 @@
namespace KeyboardAudioVisualizer.AudioProcessing.VisualizationPRovider
{
public interface IVisualizationProvider : IAudioProcessor
{
float[] VisualizationData { get; }
}
}

View File

@ -0,0 +1,42 @@
using System;
using KeyboardAudioVisualizer.AudioProcessing.VisualizationPRovider;
using RGB.NET.Brushes;
using RGB.NET.Brushes.Gradients;
using RGB.NET.Core;
using Color = RGB.NET.Core.Color;
using Rectangle = RGB.NET.Core.Rectangle;
namespace KeyboardAudioVisualizer.Brushes
{
public class FrequencyBarsBrush : LinearGradientBrush
{
#region Properties & Fields
private readonly IVisualizationProvider _visualizationProvider;
#endregion
#region Constructors
public FrequencyBarsBrush(IVisualizationProvider visualizationProvider, IGradient gradient)
: base(gradient)
{
this._visualizationProvider = visualizationProvider;
}
#endregion
#region Methods
protected override Color GetColorAtPoint(Rectangle rectangle, BrushRenderTarget renderTarget)
{
int barSampleIndex = (int)Math.Floor(_visualizationProvider.VisualizationData.Length * (renderTarget.Point.X / (rectangle.Location.X + rectangle.Size.Width)));
double curBarHeight = 1.0 - Math.Max(0f, _visualizationProvider.VisualizationData[barSampleIndex]);
double verticalPos = (renderTarget.Point.Y / rectangle.Size.Height);
return curBarHeight <= verticalPos ? base.GetColorAtPoint(rectangle, renderTarget) : Color.Transparent;
}
#endregion
}
}

View File

@ -0,0 +1,38 @@
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace KeyboardAudioVisualizer.Configuration
{
public class AbstractConfiguration : INotifyPropertyChanged
{
#region Events
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region Methods
protected virtual bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if ((typeof(T) == typeof(double)) || (typeof(T) == typeof(float)))
{
if (Math.Abs((double)(object)storage - (double)(object)value) < 0.000001) return false;
}
else
{
if (Equals(storage, value)) return false;
}
storage = value;
// ReSharper disable once ExplicitCallerInfoArgument
OnPropertyChanged(propertyName);
return true;
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
#endregion
}
}

View File

@ -0,0 +1,14 @@
using System;
namespace KeyboardAudioVisualizer.Helper
{
public static class MathHelper
{
#region Methods
public static double Clamp(double value, double min, double max) => Math.Max(min, Math.Min(max / 2.0, value));
public static float Clamp(float value, float min, float max) => (float)Clamp((double)value, min, max);
#endregion
}
}

View File

@ -63,6 +63,7 @@
<Reference Include="System" />
<Reference Include="System.Data" />
<Reference Include="System.Drawing" />
<Reference Include="System.Numerics" />
<Reference Include="System.Xml" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Core" />
@ -86,9 +87,23 @@
<SubType>Code</SubType>
</Compile>
<Compile Include="ApplicationManager.cs" />
<Compile Include="AudioCapture\AudioBuffer.cs" />
<Compile Include="AudioCapture\CSCoreAudioInput.cs" />
<Compile Include="AudioCapture\IAudioInput.cs" />
<Compile Include="AudioProcessing\AudioProcessor.cs" />
<Compile Include="AudioProcessing\Equalizer\IEqualizer.cs" />
<Compile Include="AudioProcessing\Equalizer\MultiBandEqualizer.cs" />
<Compile Include="AudioProcessing\IAudioProcessor.cs" />
<Compile Include="AudioProcessing\Spectrum\ISpectrumProvider.cs" />
<Compile Include="AudioProcessing\Spectrum\FourierSpectrumProvider.cs" />
<Compile Include="AudioProcessing\VisualizationPRovider\FrequencyBarsVisualizationProvider.cs" />
<Compile Include="AudioProcessing\VisualizationPRovider\IVisualizationProvider.cs" />
<Compile Include="Brushes\FrequencyBarsBrush.cs" />
<Compile Include="Configuration\AbstractConfiguration.cs" />
<Compile Include="Controls\ImageButton.cs" />
<Compile Include="Helper\ActionCommand.cs" />
<Compile Include="Helper\ExceptionExtension.cs" />
<Compile Include="Helper\MathHelper.cs" />
<Compile Include="Settings.cs" />
<Compile Include="Controls\BlurredDecorationWindow.cs" />
<Compile Include="Styles\CachedResourceDictionary.cs" />