Skip to content
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;
}
}
}