Skip to content

Commit bc4ad21

Browse files
committed
GetBitmap: add rotate argument
resolves #47
1 parent 1880e71 commit bc4ad21

File tree

5 files changed

+139
-48
lines changed

5 files changed

+139
-48
lines changed

src/Spectrogram.Tests/ImageTests.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using NUnit.Framework;
2+
3+
namespace Spectrogram.Tests;
4+
5+
internal class ImageTests
6+
{
7+
[Test]
8+
public void Test_Image_Rotations()
9+
{
10+
string filePath = $"../../../../../data/cant-do-that-44100.wav";
11+
(double[] audio, int sampleRate) = AudioFile.ReadWAV(filePath);
12+
SpectrogramGenerator sg = new(sampleRate, 4096, 500, maxFreq: 3000);
13+
sg.Add(audio);
14+
15+
System.Drawing.Bitmap bmp1 = sg.GetBitmap(rotate: false);
16+
bmp1.Save("test-image-original.png");
17+
18+
System.Drawing.Bitmap bmp2 = sg.GetBitmap(rotate: true);
19+
bmp2.Save("test-image-rotated.png");
20+
}
21+
}

src/Spectrogram/Image.cs

Lines changed: 13 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -10,55 +10,22 @@ namespace Spectrogram
1010
{
1111
public static class Image
1212
{
13-
public static Bitmap GetBitmap(
14-
List<double[]> ffts,
15-
Colormap cmap,
16-
double intensity = 1,
17-
bool dB = false,
18-
double dBScale = 1,
19-
bool roll = false,
20-
int rollOffset = 0)
13+
public static Bitmap GetBitmap(List<double[]> ffts, Colormap cmap, double intensity = 1,
14+
bool dB = false, double dBScale = 1, bool roll = false, int rollOffset = 0, bool rotate = false)
2115
{
22-
if (ffts.Count == 0)
23-
throw new ArgumentException("Not enough data in FFTs to generate an image yet.");
2416

25-
int Width = ffts.Count;
26-
int Height = ffts[0].Length;
27-
28-
Bitmap bmp = new Bitmap(Width, Height, PixelFormat.Format8bppIndexed);
29-
cmap.Apply(bmp);
30-
31-
var lockRect = new Rectangle(0, 0, Width, Height);
32-
BitmapData bitmapData = bmp.LockBits(lockRect, ImageLockMode.ReadOnly, bmp.PixelFormat);
33-
int stride = bitmapData.Stride;
34-
35-
byte[] bytes = new byte[bitmapData.Stride * bmp.Height];
36-
Parallel.For(0, Width, col =>
17+
ImageMaker maker = new()
3718
{
38-
int sourceCol = col;
39-
if (roll)
40-
{
41-
sourceCol += Width - rollOffset % Width;
42-
if (sourceCol >= Width)
43-
sourceCol -= Width;
44-
}
45-
46-
for (int row = 0; row < Height; row++)
47-
{
48-
double value = ffts[sourceCol][row];
49-
if (dB)
50-
value = 20 * Math.Log10(value * dBScale + 1);
51-
value *= intensity;
52-
value = Math.Min(value, 255);
53-
int bytePosition = (Height - 1 - row) * stride + col;
54-
bytes[bytePosition] = (byte)value;
55-
}
56-
});
57-
58-
Marshal.Copy(bytes, 0, bitmapData.Scan0, bytes.Length);
59-
bmp.UnlockBits(bitmapData);
60-
61-
return bmp;
19+
Colormap = cmap,
20+
Intensity = intensity,
21+
IsDecibel = dB,
22+
DecibelScaleFactor = dBScale,
23+
IsRoll = roll,
24+
RollOffset = rollOffset,
25+
IsRotated = rotate,
26+
};
27+
28+
return maker.GetBitmap(ffts);
6229
}
6330
}
6431
}

src/Spectrogram/ImageMaker.cs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Drawing;
4+
using System.Drawing.Imaging;
5+
using System.Runtime.InteropServices;
6+
using System.Text;
7+
using System.Threading.Tasks;
8+
9+
namespace Spectrogram
10+
{
11+
/// <summary>
12+
/// This class converts a collection of FFTs to a colormapped spectrogram image
13+
/// </summary>
14+
public class ImageMaker
15+
{
16+
/// <summary>
17+
/// Colormap used to translate intensity to pixel color
18+
/// </summary>
19+
public Colormap Colormap;
20+
21+
/// <summary>
22+
/// Intensity is multiplied by this number before converting it to the pixel color according to the colormap
23+
/// </summary>
24+
public double Intensity = 1;
25+
26+
/// <summary>
27+
/// If True, intensity will be log-scaled to represent Decibels
28+
/// </summary>
29+
public bool IsDecibel = false;
30+
31+
/// <summary>
32+
/// If <see cref="IsDecibel"/> is enabled, intensity will be scaled by this value prior to log transformation
33+
/// </summary>
34+
public double DecibelScaleFactor = 1;
35+
36+
/// <summary>
37+
/// If False, the spectrogram will proceed in time from left to right across the whole image.
38+
/// If True, the image will be broken and the newest FFTs will appear on the left and oldest on the right.
39+
/// </summary>
40+
public bool IsRoll = false;
41+
42+
/// <summary>
43+
/// If <see cref="IsRoll"/> is enabled, this value indicates the pixel position of the break point.
44+
/// </summary>
45+
public int RollOffset = 0;
46+
47+
/// <summary>
48+
/// If True, the spectrogram will flow top-down (oldest to newest) rather than left-right.
49+
/// </summary>
50+
public bool IsRotated = false;
51+
52+
public ImageMaker()
53+
{
54+
55+
}
56+
57+
public Bitmap GetBitmap(List<double[]> ffts)
58+
{
59+
if (ffts.Count == 0)
60+
throw new ArgumentException("Not enough data in FFTs to generate an image yet.");
61+
62+
int Width = IsRotated ? ffts[0].Length : ffts.Count;
63+
int Height = IsRotated ? ffts.Count : ffts[0].Length;
64+
65+
Bitmap bmp = new(Width, Height, PixelFormat.Format8bppIndexed);
66+
Colormap.Apply(bmp);
67+
68+
Rectangle lockRect = new(0, 0, Width, Height);
69+
BitmapData bitmapData = bmp.LockBits(lockRect, ImageLockMode.ReadOnly, bmp.PixelFormat);
70+
int stride = bitmapData.Stride;
71+
72+
byte[] bytes = new byte[bitmapData.Stride * bmp.Height];
73+
Parallel.For(0, Width, col =>
74+
{
75+
int sourceCol = col;
76+
if (IsRoll)
77+
{
78+
sourceCol += Width - RollOffset % Width;
79+
if (sourceCol >= Width)
80+
sourceCol -= Width;
81+
}
82+
83+
for (int row = 0; row < Height; row++)
84+
{
85+
double value = IsRotated ? ffts[row][sourceCol] : ffts[sourceCol][row];
86+
if (IsDecibel)
87+
value = 20 * Math.Log10(value * DecibelScaleFactor + 1);
88+
value *= Intensity;
89+
value = Math.Min(value, 255);
90+
int bytePosition = (Height - 1 - row) * stride + col;
91+
bytes[bytePosition] = (byte)value;
92+
}
93+
});
94+
95+
Marshal.Copy(bytes, 0, bitmapData.Scan0, bytes.Length);
96+
bmp.UnlockBits(bitmapData);
97+
98+
return bmp;
99+
}
100+
}
101+
}

src/Spectrogram/Spectrogram.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
<EmbedUntrackedSources>true</EmbedUntrackedSources>
2222
<Deterministic>true</Deterministic>
2323
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
24+
<LangVersion>latest</LangVersion>
2425
</PropertyGroup>
2526

2627
<ItemGroup>

src/Spectrogram/SpectrogramGenerator.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -267,15 +267,16 @@ public List<double[]> GetMelFFTs(int melBinCount)
267267
/// <param name="dB">If true, output will be log-transformed.</param>
268268
/// <param name="dBScale">If dB scaling is in use, this multiplier will be applied before log transformation.</param>
269269
/// <param name="roll">Behavior of the spectrogram when it is full of data.
270+
/// <param name="rotate">If True, the image will be rotated so time flows from top to bottom (rather than left to right).
270271
/// Roll (true) adds new columns on the left overwriting the oldest ones.
271272
/// Scroll (false) slides the whole image to the left and adds new columns to the right.</param>
272-
public Bitmap GetBitmap(double intensity = 1, bool dB = false, double dBScale = 1, bool roll = false)
273+
public Bitmap GetBitmap(double intensity = 1, bool dB = false, double dBScale = 1, bool roll = false, bool rotate = false)
273274
{
274275
if (FFTs.Count == 0)
275276
throw new InvalidOperationException("Not enough data to create an image. " +
276277
$"Ensure {nameof(Width)} is >0 before calling {nameof(GetBitmap)}().");
277278

278-
return Image.GetBitmap(FFTs, Colormap, intensity, dB, dBScale, roll, NextColumnIndex);
279+
return Image.GetBitmap(FFTs, Colormap, intensity, dB, dBScale, roll, NextColumnIndex, rotate);
279280
}
280281

281282
/// <summary>

0 commit comments

Comments
 (0)