Skip to content

Commit 9ee7b22

Browse files
authored
Merge pull request #48 from swharden/47
GetBitmap: add Rotate argument
2 parents 577d9d2 + 99b0c71 commit 9ee7b22

File tree

9 files changed

+151
-76
lines changed

9 files changed

+151
-76
lines changed

.github/workflows/ci.yaml

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,10 @@ jobs:
1919
- name: 🛒 Checkout
2020
uses: actions/checkout@v2
2121

22-
- name: ✨ Setup .NET 5
23-
uses: actions/setup-dotnet@v1
24-
with:
25-
dotnet-version: "5.0.x"
26-
2722
- name: ✨ Setup .NET 6
2823
uses: actions/setup-dotnet@v1
2924
with:
3025
dotnet-version: "6.0.x"
31-
include-prerelease: true
3226

3327
- name: 🚚 Restore
3428
run: dotnet restore src
@@ -42,22 +36,12 @@ jobs:
4236
- name: 📦 Pack
4337
run: dotnet pack src --configuration Release --no-build
4438

45-
- name: 💾 Store Release Package
46-
if: github.event_name == 'release'
47-
uses: actions/upload-artifact@v2
48-
with:
49-
name: Packages
50-
retention-days: 1
51-
path: |
52-
src/Spectrogram/bin/Release/*.nupkg
53-
src/Spectrogram/bin/Release/*.snupkg
54-
55-
- name: 🔑 Configure NuGet Secrets
39+
- name: 🔑 Configure Secrets
5640
if: github.event_name == 'release'
5741
uses: nuget/setup-nuget@v1
5842
with:
5943
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
6044

61-
- name: 🚀 Deploy Release Package
45+
- name: 🚀 Deploy Package
6246
if: github.event_name == 'release'
6347
run: nuget push "src\Spectrogram\bin\Release\*.nupkg" -SkipDuplicate -Source https://api.nuget.org/v3/index.json

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ _"I'm sorry Dave... I'm afraid I can't do that"_
2121
* Source code for the WAV reading method is at the bottom of this page.
2222

2323
```cs
24-
(double[] audio, int sampleRate) = ReadWavMono("hal.wav");
24+
(double[] audio, int sampleRate) = ReadMono("hal.wav");
2525
var sg = new SpectrogramGenerator(sampleRate, fftSize: 4096, stepSize: 500, maxFreq: 3000);
2626
sg.Add(audio);
2727
sg.SaveImage("hal.png");
@@ -81,7 +81,7 @@ Review the source code of the demo application for additional details and consid
8181
This example demonstrates how to convert a MP3 file to a spectrogram image. A sample MP3 audio file in the [data folder](data) contains the audio track from Ken Barker's excellent piano performance of George Frideric Handel's Suite No. 5 in E major for harpsichord ([_The Harmonious Blacksmith_](https://en.wikipedia.org/wiki/The_Harmonious_Blacksmith)). This audio file is included [with permission](dev/Handel%20-%20Air%20and%20Variations.txt), and the [original video can be viewed on YouTube](https://www.youtube.com/watch?v=Mza-xqk770k).
8282

8383
```cs
84-
(double[] audio, int sampleRate) = ReadWavMono("song.wav");
84+
(double[] audio, int sampleRate) = ReadMono("song.wav");
8585

8686
int fftSize = 16384;
8787
int targetWidthPx = 3000;
@@ -117,7 +117,7 @@ Spectrogram (2993, 817)
117117
These examples demonstrate the identical spectrogram analyzed with a variety of different colormaps. Spectrogram colormaps can be changed by calling the `SetColormap()` method:
118118

119119
```cs
120-
(double[] audio, int sampleRate) = ReadWavMono("hal.wav");
120+
(double[] audio, int sampleRate) = ReadMono("hal.wav");
121121
var sg = new SpectrogramGenerator(sampleRate, fftSize: 8192, stepSize: 200, maxFreq: 3000);
122122
sg.Add(audio);
123123
sg.SetColormap(Colormap.Jet);
@@ -141,7 +141,7 @@ Cropped Linear Scale (0-3kHz) | Mel Scale (0-22 kHz)
141141
Amplitude perception in humans, like frequency perception, is logarithmic. Therefore, Mel spectrograms typically display log-transformed spectral power and are presented using Decibel units.
142142

143143
```cs
144-
(double[] audio, int sampleRate) = ReadWavMono("hal.wav");
144+
(double[] audio, int sampleRate) = ReadMono("hal.wav");
145145
var sg = new SpectrogramGenerator(sampleRate, fftSize: 4096, stepSize: 500, maxFreq: 3000);
146146
sg.Add(audio);
147147

@@ -153,12 +153,12 @@ Bitmap bmp = sg.GetBitmapMel(melSizePoints: 250);
153153
bmp.Save("halMel.png", ImageFormat.Png);
154154
```
155155

156-
## Read data from a WAV File
156+
## Read Data from an Audio File
157157

158158
You should customize your file-reading method to suit your specific application. I frequently use the NAudio package to read data from WAV and MP3 files. This function reads audio data from a mono WAV file and will be used for the examples on this page.
159159

160160
```cs
161-
(double[] audio, int sampleRate) ReadWavMono(string filePath, double multiplier = 16_000)
161+
(double[] audio, int sampleRate) ReadMono(string filePath, double multiplier = 16_000)
162162
{
163163
using var afr = new NAudio.Wave.AudioFileReader(filePath);
164164
int sampleRate = afr.WaveFormat.SampleRate;

src/Spectrogram.MicrophoneDemo/Spectrogram.Demo.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<TargetFramework>net5.0-windows</TargetFramework>
3+
<TargetFramework>net6.0-windows</TargetFramework>
44
<OutputType>WinExe</OutputType>
55
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
66
<UseWindowsForms>true</UseWindowsForms>

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.Tests/Spectrogram.Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>net5.0</TargetFramework>
4+
<TargetFramework>net6.0</TargetFramework>
55
<IsPackable>false</IsPackable>
66
</PropertyGroup>
77

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: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFramework>netstandard2.0</TargetFramework>
5-
<Version>1.5.0</Version>
5+
<Version>1.6.0</Version>
66
<Description>A .NET Standard library for creating spectrograms</Description>
77
<Authors>Scott Harden</Authors>
88
<Company>Harden Technologies, LLC</Company>
@@ -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)