Skip to content

Spectrogram 2.0 using SkiaSharp #49

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/Spectrogram.Tests/StaticSpectrogramTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Spectrogram.Tests;

internal class StaticSpectrogramTests
{
[Test]
public void Test_StaticSpectrogram_Process()
{
(double[] audio, int sampleRate) = AudioFile.ReadWAV("../../../../../data/cant-do-that-44100.wav");

StaticSpectrogram sg = new(audio);
sg.SaveImage("test.png", .2);
}
}
66 changes: 66 additions & 0 deletions src/Spectrogram/StaticSpectrogram.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace Spectrogram;

/// <summary>
/// This class creates spectrogram images from fixed-length array containing signal data.
/// </summary>
public class StaticSpectrogram
{
private double[,] Ffts; // [columns, frequencies]
private readonly double[] Signal;

public int StepSize = 200;
public int FftSize = 1 << 12;

public FftSharp.Window Window = new FftSharp.Windows.Hanning();
public IColormap Colormap = new Colormaps.Viridis();

public StaticSpectrogram(double[] signal)
{
Signal = signal;
Recalculate();
}

public void Recalculate()
{
int columns = (Signal.Length - FftSize) / StepSize;
Ffts = new double[columns, FftSize / 2];

double[] buffer = new double[FftSize];
for (int i = 0; i < columns; i++)
{
Array.Copy(Signal, i * StepSize, buffer, 0, FftSize);

Window.ApplyInPlace(buffer);
double[] fft = FftSharp.Transform.FFTmagnitude(buffer);

for (int j = 0; j < FftSize / 2; j++)
{
Ffts[i, j] = fft[j];
}
}
}

public void SaveImage(string filePath, double mult = 1)
{
filePath = System.IO.Path.GetFullPath(filePath);

int newBottomIndex = 0;
int newTopIndex = 500;
int newHeightCount = newTopIndex - newBottomIndex;
double[,] ffts2 = new double[Ffts.GetLength(0), newHeightCount];
for (int i = 0; i < Ffts.GetLength(0); i++)
{
for (int j = 0; j < newHeightCount; j++)
{
ffts2[i, j] = Ffts[i, j];
}
}

Tools.FftsToImage(ffts2, mult, Colormap).Save(filePath);
Console.WriteLine($"Saved: {filePath}");
}
}
56 changes: 56 additions & 0 deletions src/Spectrogram/Tools.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;

namespace Spectrogram
Expand Down Expand Up @@ -100,5 +103,58 @@ public static int GetMidiNote(double frequencyHz)
{
return GetPianoKey(frequencyHz) + 20;
}

public static Bitmap FftsToImage(double[,] ffts, double mult, IColormap cmap)
{
byte[,,] pixelArray = new byte[ffts.GetLength(1), ffts.GetLength(0), 3];
for (int x = 0; x < pixelArray.GetLength(1); x++)
{
for (int y = 0; y < pixelArray.GetLength(0); y++)
{
int y2 = pixelArray.GetLength(0) - y - 1;
double value = ffts[x, y] * mult;
byte clampedValue = (byte)Math.Min(255, Math.Max(0, value));
(byte r, byte g, byte b) = cmap.GetRGB(clampedValue);
pixelArray[y2, x, 0] = r;
pixelArray[y2, x, 1] = g;
pixelArray[y2, x, 2] = b;
}
}

return ArrayToImage(pixelArray);
}

public static Bitmap ArrayToImage(byte[,,] pixelArray)
{
int width = pixelArray.GetLength(1);
int height = pixelArray.GetLength(0);
int stride = (width % 4 == 0) ? width : width + 4 - width % 4;
int bytesPerPixel = 3;

byte[] bytes = new byte[stride * height * bytesPerPixel];
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
int offset = (y * stride + x) * bytesPerPixel;
bytes[offset + 0] = pixelArray[y, x, 2]; // blue
bytes[offset + 1] = pixelArray[y, x, 1]; // green
bytes[offset + 2] = pixelArray[y, x, 0]; // red
}
}

PixelFormat formatOutput = PixelFormat.Format24bppRgb;
Rectangle rect = new(0, 0, width, height);
Bitmap bmp = new(stride, height, formatOutput);
BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadOnly, formatOutput);
Marshal.Copy(bytes, 0, bmpData.Scan0, bytes.Length);
bmp.UnlockBits(bmpData);

Bitmap bmp2 = new(width, height, PixelFormat.Format32bppPArgb);
Graphics gfx2 = Graphics.FromImage(bmp2);
gfx2.DrawImage(bmp, 0, 0);

return bmp2;
}
}
}