Skip to content

Commit

Permalink
Create c# gui prototype
Browse files Browse the repository at this point in the history
  • Loading branch information
jurihock committed Apr 27, 2024
1 parent 52e51a1 commit 4638e1b
Show file tree
Hide file tree
Showing 18 changed files with 654 additions and 2 deletions.
4 changes: 2 additions & 2 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ charset = utf-8
insert_final_newline = true
trim_trailing_whitespace = true

[*.{h,c,cpp}]
[*.{h,c,cpp,cs}]
indent_style = space
indent_size = 2

[*.sh]
[*.{sh,bat}]
indent_style = space
indent_size = 2

Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,11 @@
build*/
cmake-build*/

bin/
obj/

.DS_Store
Thumbs.db

*.mp3
*.wav
16 changes: 16 additions & 0 deletions src/spectrality/App.axaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Spectrality.App"
xmlns:local="using:Spectrality"
RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->

<Application.DataTemplates>
<local:ViewLocator/>
</Application.DataTemplates>

<Application.Styles>
<FluentTheme />
<StyleInclude Source="avares://OxyPlot.Avalonia/Themes/Default.axaml"/>
</Application.Styles>
</Application>
28 changes: 28 additions & 0 deletions src/spectrality/App.axaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Spectrality.ViewModels;
using Spectrality.Views;

namespace Spectrality;

public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}

public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow
{
DataContext = new MainWindowViewModel(),
};
}

base.OnFrameworkInitializationCompleted();
}
}
Binary file added src/spectrality/Assets/avalonia-logo.ico
Binary file not shown.
171 changes: 171 additions & 0 deletions src/spectrality/DSP/QDFT.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
using System;
using System.Linq;
using System.Numerics;
using Nito.Collections;

class QDFT
{
public double Samplerate { get; private set; }
public (double, double) Bandwidth { get; private set; }
public double Resolution { get; private set; }
public double Quality { get; private set; }
public double Latency { get; private set; }
public (double, double) Window { get; private set; }

public int Length { get; private set; }

public double[] Frequencies { get; private set; }
public double[] Qualities { get; private set; }
public double[] Latencies { get; private set; }
public int[] Periods { get; private set; }
public int[] Offsets { get; private set; }
public double[] Weights { get; private set; }

public Complex[] Fiddles { get; private set; }
public Complex[] Twiddles { get; private set; }

private Deque<double> Inputs { get; set; }
private Complex[] Outputs { get; set; }

public QDFT(double samplerate,
(double, double) bandwidth,
double resolution = 24,
double quality = 0,
double latency = 0)
{
Samplerate = samplerate;
Bandwidth = bandwidth;
Resolution = resolution;
Quality = quality;
Latency = latency;
Window = (+0.5, -0.5);

Length = (int)Math.Ceiling(Resolution * Math.Log2(Bandwidth.Item2 / Bandwidth.Item1));

Frequencies = new double[Length];
Qualities = new double[Length];
Latencies = new double[Length];
Periods = new int[Length];
Offsets = new int[Length];
Weights = new double[Length];

Fiddles = new Complex[Length * 3];
Twiddles = new Complex[Length * 3];

Bootstrap();

Inputs = new Deque<double>(new double[Periods.First() + 1]);
Outputs = new Complex[Length * 3];
}

private void Bootstrap()
{
var alpha = Math.Pow(2.0, 1.0 / Resolution) - 1.0;
var beta = (Quality < 0) ? (alpha * 24.7 / 0.108) : Quality;

for (int i = 0; i < Length; i++)
{
var frequency = Bandwidth.Item1 * Math.Pow(2.0, i / Resolution);

Frequencies[i] = frequency;

var quality = frequency / (alpha * frequency + beta);

Qualities[i] = quality;

var period = Math.Ceiling(quality * Samplerate / frequency);

Periods[i] = (int)period;

var offset = Math.Ceiling((Periods.First() - period)
* Math.Clamp(Latency * 0.5 + 0.5, 0.0, 1.0));

Offsets[i] = (int)offset;

var latency = (Periods.First() - offset) / Samplerate;

Latencies[i] = latency;

var weight = 1.0 / period;

Weights[i] = weight;
}

foreach (int k in new[] { -1, 0, +1 })
{
for (int i = 0, j = 1; i < Length; ++i, j+=3)
{
var fiddle = Complex.FromPolarCoordinates(
1.0, -2.0 * Math.PI * (Qualities[i] + k));

Fiddles[j + k] = fiddle;

var twiddle = Complex.FromPolarCoordinates(
1.0, +2.0 * Math.PI * (Qualities[i] + k) / Periods[i]);

Twiddles[j + k] = twiddle;
}
}
}

public void Reset()
{
for (int i = 0; i < Inputs.Count; i++)
{
Inputs[i] = 0;
}

for (int i = 0; i < Outputs.Length; i++)
{
Outputs[i] = 0;
}
}

public void Analyze(double sample, Span<Complex> dft)
{
if (dft.Length != Length)
{
throw new ArgumentException();
}

var periods = Periods;
var offsets = Offsets;
var weights = Weights;

var fiddles = Fiddles;
var twiddles = Twiddles;

var inputs = Inputs;
var outputs = Outputs;

var a = Window.Item1;
var b = Window.Item2 / 2;

inputs.RemoveFromFront();
inputs.AddToBack(sample);

for (int i = 0, j = 1; i < Length; ++i, j+=3)
{
var period = periods[i];
var offset = offsets[i];
var weight = weights[i];

var left = inputs[offset + period];
var right = inputs[offset];

var k1 = j - 1;
var k2 = j;
var k3 = j + 1;

var delta1 = (fiddles[k1] * left - right) * weight;
var delta2 = (fiddles[k2] * left - right) * weight;
var delta3 = (fiddles[k3] * left - right) * weight;

outputs[k1] = twiddles[k1] * (outputs[k1] + delta1);
outputs[k2] = twiddles[k2] * (outputs[k2] + delta2);
outputs[k3] = twiddles[k3] * (outputs[k3] + delta3);

dft[i] = outputs[k2] * a + (outputs[k1] + outputs[k3]) * b;
}
}
}
7 changes: 7 additions & 0 deletions src/spectrality/DSP/Spectrogram.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
struct Spectrogram
{
public double Samplerate;
public double[] Timestamps;
public double[] Frequencies;
public float[,] Magnitudes;
}
68 changes: 68 additions & 0 deletions src/spectrality/DSP/SpectrumAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Numerics;

class SpectrumAnalyzer
{
public double Samplerate { get; private set; }
public double Hopsize { get; private set; }

private readonly QDFT QDFT;

public SpectrumAnalyzer(double samplerate, double hopsize)
{
Samplerate = samplerate;
Hopsize = hopsize;

QDFT = new QDFT(samplerate, (100, 10000), 12 * 4);
}

public Spectrogram GetSpectrogram(Span<float> samples)
{
var watch = Stopwatch.GetTimestamp();

var qdft = QDFT;

var hop = (int)Math.Ceiling(1.0 * Hopsize * Samplerate);
var hops = (int)Math.Ceiling(1.0 * samples.Length / hop);
var bins = qdft.Length;

var timestamps = Enumerable.Range(0, hops).Select(_ => _ * Hopsize).ToArray();
var frequencies = qdft.Frequencies;
var magnitudes = new float[hops, bins];
var dft = new Complex[bins];

qdft.Reset();

for (var i = 0; i < samples.Length; i++)
{
qdft.Analyze(samples[i], dft);

if (i % hop != 0)
continue;

var t = i / hop;

for (var j = 0; j < bins; j++)
{
var magnitude = dft[j].Magnitude;

magnitude = 20.0 * Math.Log10(
magnitude + double.Epsilon);

magnitudes[t, j] = (float)magnitude;
}
}

System.Console.WriteLine($"{Stopwatch.GetElapsedTime(watch).Milliseconds}ms");

return new Spectrogram
{
Samplerate = Samplerate,
Timestamps = timestamps,
Frequencies = frequencies,
Magnitudes = magnitudes
};
}
}
58 changes: 58 additions & 0 deletions src/spectrality/Plot/SpectrogramImage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using OxyPlot;

class SpectrogramImage
{
public (double, double) Limit { get; private set; }
public double Gamma { get; private set; }
public double Saturation { get; private set; }

public SpectrogramImage((double, double) limit, double gamma = 1, double saturation = 1)
{
Limit = limit;
Gamma = gamma;
Saturation = saturation;
}

public OxyImage GetImage(Spectrogram spectrogram)
{
var data = spectrogram.Magnitudes;

var m = data.GetLength(0);
var n = data.GetLength(1);

var min = Limit.Item1;
var max = Limit.Item2;

var slope = 1.0 / (max - min);
var intercept = min / (max - min);

var colors = Enumerable.Range(0, n).Select(_ => 1.0 - (double)_ / n).ToArray();
var gamma = Math.Clamp(Gamma, 0.1, 10);
var saturation = (Saturation >= 0) ? Math.Clamp(Saturation, 0.0, 1.0) : 1.0;
var highlighting = (Saturation < 0) ? Math.Clamp(Saturation, -1.0, 0.0) : 0.0;

var pixels = new OxyColor[m, n];

for (var i = 0; i < m; i++)
{
for (var j = 0; j < n; j++)
{
var h = colors[j];
var s = saturation;
var v = (double)data[i, j];

v = Math.Clamp(v * slope - intercept, 0.0, 1.0);
v = Math.Pow(v, gamma);

s += v * highlighting;

pixels[i, j] = OxyColor.FromHsv(h, s, v);
}
}

return OxyImage.Create(pixels, ImageFormat.Bmp);
}
}
Loading

0 comments on commit 4638e1b

Please sign in to comment.