Skip to content

Commit 54e076e

Browse files
committed
numerous new spectrogram features
1 parent dcfeaac commit 54e076e

7 files changed

Lines changed: 203 additions & 56 deletions

File tree

README.md

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,30 @@
33

44
![](data/mozart.jpg)
55

6-
**Quickstart:** This example program converts [/data/mozart.wav](/data/mozart.wav) (Mozart's Piano Sonata No. 11 in A major) to a spectrograph (above) and saves it as an image file.
6+
**Quickstart:** The image above was created by this program which which converts [/data/mozart.wav](/data/mozart.wav) (Mozart's Piano Sonata No. 11 in A major) to a spectrograph and saves it as an image.
77

88
```cs
99
var spec = new Spectrogram.Spectrogram();
1010
float[] values = Spectrogram.WavFile.Read("mozart.wav");
1111
spec.Add(values);
12-
spec.SaveBitmap("mozart.png");
12+
spec.SaveBitmap("mozart.jpg");
1313
```
1414

1515
## Realtime Audio Monitor
1616

17-
A demo program is included which monitors the sound card and continuously creates spectrograms from microphone input. Because of how WinForms are displayed this looks slightly jerky as it scrolls ascross the screen. It renders very fast though (just a few milliseconds), and the entire bitmap is created from scratch on each render.
17+
A demo program is included which monitors the sound card and continuously creates spectrograms from microphone input. It runs fast enough that the entire bitmap can be recreated on each render. This means brightness and color adjustments can be applied to the whole image, not just new parts.
1818

19-
![](data/screenshot3.gif)
19+
![](data/screenshot4.gif)
2020

21-
Unforunately NAudio's input device is Windows Only, so this demo is restricted to .NET Framework (can't use .NET Core 3.0)
22-
23-
## TODO:
24-
* render horizontally or vertically
21+
### TODO:
22+
* ~~render horizontally or vertically~~
2523
* optional display of axis labels (scales)
26-
* create bitmaps in real time from audio input
27-
* advanced color (LUT) options
24+
* ~~create bitmaps in real time from audio input~~
25+
* ~~advanced color (LUT) options~~
2826
* advanced intensity options (nonlinear scaling)
2927
* create a user control to display a spectrogram
3028
* create a user control to adjust spectrogram settings
31-
* options for bitmap to scroll or to staticly repeat
29+
* ~~options for bitmap to scroll or to statically repeat~~
3230

3331
## Resources
3432
* [microphone spectrograph in C#](https://github.com/swharden/Csharp-Data-Visualization/tree/master/projects/18-01-11_microphone_spectrograph)
@@ -45,9 +43,9 @@ Unforunately NAudio's input device is Windows Only, so this demo is restricted t
4543
#### Software
4644
* Argo ([website](http://digilander.libero.it/i2phd/argo/)) - closed-source QRSS viewer for Windows
4745
* SpectrumLab ([website](http://www.qsl.net/dl4yhf/spectra1.html)) - closed-source spectrum analyzer for Windows
48-
* QrssPIG ([gitlab](https://gitlab.com/hb9fxx/qrsspig)) - open-source spectrograph for Raspberry Pi (C++)
46+
* QrssPIG ([GitLab](https://gitlab.com/hb9fxx/qrsspig)) - open-source spectrograph for Raspberry Pi (C++)
4947
* Lopora ([website](http://www.qsl.net/pa2ohh/11lop.htm)) - open-source spectrograph (Python 3)
50-
* QRSS VD ([github](https://github.com/swharden/QRSS-VD)) - open source spectrograph (Python 2)
48+
* QRSS VD ([GitHub](https://github.com/swharden/QRSS-VD)) - open source spectrograph (Python 2)
5149

5250
### Spectrogram vs ~~Spectrograph~~
5351
* A spectrogram is an image

data/screenshot4.gif

811 KB
Loading

src/AudioMonitor/Form1.Designer.cs

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/AudioMonitor/Form1.cs

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ public Form1()
2222
cbMicrophones.Items.AddRange(Listener.GetInputDevices());
2323
if (cbMicrophones.Items.Count > 0)
2424
cbMicrophones.SelectedItem = cbMicrophones.Items[0];
25+
26+
cbDisplay.Items.Add("waterfall");
27+
cbDisplay.Items.Add("horizontal repeat");
28+
cbDisplay.SelectedItem = cbDisplay.Items[0];
2529
}
2630

2731
private void Form1_Load(object sender, EventArgs e)
@@ -31,7 +35,17 @@ private void Form1_Load(object sender, EventArgs e)
3135

3236
private void BtnSetMicrophone_Click(object sender, EventArgs e)
3337
{
34-
AudioMonitorInitialize(DeviceIndex: cbMicrophones.SelectedIndex);
38+
if (btnSetMicrophone.Text == "open")
39+
{
40+
AudioMonitorInitialize(DeviceIndex: cbMicrophones.SelectedIndex);
41+
btnSetMicrophone.Text = "close";
42+
}
43+
else
44+
{
45+
btnSetMicrophone.Text = "open";
46+
wvin.StopRecording();
47+
wvin = null;
48+
}
3549
}
3650

3751
private void OnDataAvailable(object sender, NAudio.Wave.WaveInEventArgs args)
@@ -52,7 +66,19 @@ private void AudioMonitorInitialize(
5266
int bufferMilliseconds = 10
5367
)
5468
{
55-
spec = new Spectrogram.Spectrogram(sampleRate);
69+
switch (cbDisplay.Text)
70+
{
71+
case "waterfall":
72+
spec = new Spectrogram.Spectrogram(sampleRate, fixedSize: pictureBox1.Height, scroll: true, vertical: true, pixelUpper: 200);
73+
break;
74+
75+
case "horizontal repeat":
76+
spec = new Spectrogram.Spectrogram(sampleRate, fixedSize: pictureBox1.Width, scroll: false, pixelUpper: 200);
77+
break;
78+
79+
default:
80+
throw new NotImplementedException("unknown display type");
81+
}
5682

5783
wvin = new NAudio.Wave.WaveInEvent();
5884
wvin.DeviceNumber = DeviceIndex;
@@ -65,11 +91,15 @@ private void AudioMonitorInitialize(
6591
bool renderNeeded = false;
6692
private void Timer1_Tick(object sender, EventArgs e)
6793
{
68-
if ((renderNeeded) && (spec != null) && (spec.ffts.Count > 0))
69-
{
70-
pictureBox1.BackgroundImage = spec.GetBitmap();
71-
lblStatus.Text = $"spectrogram has {spec.ffts.Count} FFT columns | last render: {spec.lastRenderMsec} ms";
72-
}
94+
if (!renderNeeded)
95+
return;
96+
97+
if ((spec == null) || (spec.ffts.Count == 0))
98+
return;
99+
100+
pictureBox1.BackgroundImage = spec.GetBitmap();
101+
lblStatus.Text = $"spectrogram has {spec.ffts.Count} FFT columns | last render: {spec.lastRenderMsec} ms";
102+
renderNeeded = false;
73103
}
74104
}
75105
}

src/ConsoleDemo/Program.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ class Program
1010
{
1111
static void Main(string[] args)
1212
{
13-
var spec = new Spectrogram.Spectrogram(fftSize: 2048, stepSize: 500);
14-
float[] values = Spectrogram.WavFile.Read("mozart.wav");
13+
var spec = new Spectrogram.Spectrogram(fftSize: 1024, stepSize: 100_000);
14+
float[] values = Spectrogram.WavFile.Read(@"C:\Users\scott\Documents\temp\megaDrive.Wav");
1515
spec.SignalExtend(values);
16-
spec.SaveBitmap("mozart.jpg");
16+
spec.SaveBitmap("megaDrive.jpg");
1717
}
1818
}
1919
}

src/Spectrogram/Image.cs

Lines changed: 60 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,35 @@ namespace Spectrogram
99
{
1010
class Image
1111
{
12-
public static Bitmap BitmapFromFFTs(List<float[]> ffts, int? fixedWidth = null)
12+
public static Bitmap BitmapFromFFTs(List<float[]> ffts, int? fixedWidth = null, int? verticalLine = null, int? pixelLow = null, int? pixelHigh = null)
1313
{
1414

1515
if (ffts == null || ffts.Count == 0)
1616
throw new ArgumentException("ffts must contain float arrays");
1717

18-
// use indexed colors to make it easy to convert from value to color
1918
int width = (fixedWidth == null) ? ffts.Count : (int)fixedWidth;
20-
Bitmap bmp = new Bitmap(width, ffts[0].Length, PixelFormat.Format8bppIndexed);
19+
20+
int fftHeight;
21+
if (ffts[0] != null)
22+
fftHeight = ffts[0].Length;
23+
else if (ffts[ffts.Count - 1] != null)
24+
fftHeight = ffts[ffts.Count - 1].Length;
25+
else
26+
return null;
27+
28+
if (pixelLow == null)
29+
pixelLow = 0;
30+
else
31+
pixelLow = Math.Max((int)pixelLow, 0);
32+
33+
if (pixelHigh == null)
34+
pixelHigh = fftHeight;
35+
else
36+
pixelHigh = Math.Min((int)pixelHigh, fftHeight);
37+
38+
int height = (int)pixelHigh - (int)pixelLow;
39+
40+
Bitmap bmp = new Bitmap(width, height, PixelFormat.Format8bppIndexed);
2141
Palette.ApplyLUT(bmp, Palette.LUT.viridis);
2242

2343
var rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
@@ -26,25 +46,32 @@ public static Bitmap BitmapFromFFTs(List<float[]> ffts, int? fixedWidth = null)
2646

2747
// TODO: smarter intensity scaling (adjustable gain?)
2848
float scaleMax = 100;
29-
/*
30-
float scaleMax = 0;
31-
foreach (float value in ffts[ffts.Count / 2])
32-
scaleMax = Math.Max(scaleMax, value);
33-
scaleMax *= (float).2;
34-
*/
35-
36-
for (int col = 0; col < ffts.Count; col++)
49+
50+
for (int col = 0; col < bmp.Width; col++)
3751
{
38-
if (col < width)
52+
if (col >= width)
53+
continue;
54+
55+
if (ffts[col] == null)
56+
continue;
57+
58+
for (int row = 0; row < bmp.Height; row++)
3959
{
40-
for (int row = 0; row < ffts[col].Length; row++)
60+
int bytePosition = (bmp.Height - 1 - row) * bitmapData.Stride + col;
61+
float pixelValue;
62+
63+
if ((verticalLine != null) && (col == verticalLine))
4164
{
42-
int bytePosition = (ffts[col].Length - 1 - row) * bitmapData.Stride + col;
43-
float pixelValue = ffts[col][row] / scaleMax * 255;
65+
pixelValue = byte.MaxValue;
66+
}
67+
else
68+
{
69+
pixelValue = ffts[col][row + (int)pixelLow];
70+
pixelValue = pixelValue / scaleMax * 255;
4471
pixelValue = Math.Max(0, pixelValue);
4572
pixelValue = Math.Min(255, pixelValue);
46-
pixels[bytePosition] = (byte)(pixelValue);
4773
}
74+
pixels[bytePosition] = (byte)(pixelValue);
4875
}
4976
}
5077

@@ -53,5 +80,22 @@ public static Bitmap BitmapFromFFTs(List<float[]> ffts, int? fixedWidth = null)
5380

5481
return bmp;
5582
}
83+
84+
public static Bitmap Rotate(Bitmap bmpIn, float angle = 90)
85+
{
86+
// TODO: this could be faster with byte manipulation since it's 90 degrees
87+
88+
if (bmpIn == null)
89+
return null;
90+
91+
Bitmap bmp = new Bitmap(bmpIn);
92+
Bitmap bmpRotated = new Bitmap(bmp.Height, bmp.Width);
93+
94+
Graphics gfx = Graphics.FromImage(bmpRotated);
95+
gfx.RotateTransform(angle);
96+
gfx.DrawImage(bmp, new Point(0, -bmp.Height));
97+
98+
return bmpRotated;
99+
}
56100
}
57101
}

0 commit comments

Comments
 (0)