Skip to content

Commit 3f105f1

Browse files
committed
support for ticks inside the spectrogram
1 parent 2348a44 commit 3f105f1

8 files changed

Lines changed: 154 additions & 56 deletions

File tree

src/AudioMonitor/Form1.Designer.cs

Lines changed: 31 additions & 3 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: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ private void OnDataAvailable(object sender, NAudio.Wave.WaveInEventArgs args)
6262
try
6363
{
6464
if (waterfall)
65-
spec.AddScroll(buffer, fixedSize: pictureBox1.Width);
65+
spec.AddScroll(buffer, fixedSize: pictureBox1.Height);
6666
else
6767
spec.AddCircular(buffer, fixedSize: pictureBox1.Width);
6868
renderNeeded = true;
@@ -114,7 +114,8 @@ private void Timer1_Tick(object sender, EventArgs e)
114114
frequencyMin: 0,
115115
frequencyMax: 4000,
116116
vertical: waterfall,
117-
colormap: colormap
117+
colormap: colormap,
118+
showTicks: cbTicks.Checked
118119
);
119120
lblStatus.Text = $"spectrogram contains {spec.fftList.Count} FFT samples | last render: {spec.GetLastRenderTime()} ms";
120121
renderNeeded = false;

src/ConsoleDemo/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ static void DemoMozart()
2424
float[] values = Spectrogram.WavFile.Read("mozart.wav");
2525
spec.AddExtend(values);
2626

27-
Bitmap bmp = spec.GetBitmap();
27+
Bitmap bmp = spec.GetBitmap(frequencyMax: 2500, intensity: 5);
2828
spec.SaveBitmap(bmp, "mozart.jpg");
2929
}
3030
}

src/Spectrogram/Annotations.cs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
using Spectrogram.Settings;
2+
using System.Drawing;
3+
4+
namespace Spectrogram
5+
{
6+
public class Annotations
7+
{
8+
public static void drawTicks(Bitmap bmp, FftSettings fftSettings, DisplaySettings displaySettings)
9+
{
10+
Graphics gfx = Graphics.FromImage(bmp);
11+
12+
double frequency = fftSettings.FrequencyFromIndex(displaySettings.pixelLower);
13+
double deltaFreq = 500;
14+
frequency += deltaFreq;
15+
while (frequency < fftSettings.FrequencyFromIndex(displaySettings.pixelUpper))
16+
{
17+
int yPosition = bmp.Height - (fftSettings.IndexFromFrequency(frequency) - displaySettings.pixelLower);
18+
Point p1 = new Point(bmp.Width - displaySettings.tickSize, yPosition);
19+
Point p2 = new Point(bmp.Width, yPosition);
20+
DrawLineWithShadow(gfx, p1, p2);
21+
DrawTextWithShadow(gfx, frequency.ToString(), p1, displaySettings.tickFont, displaySettings.sfTicksRight);
22+
frequency += deltaFreq;
23+
}
24+
25+
double xPx = 0;
26+
while (xPx < bmp.Width)
27+
{
28+
xPx += fftSettings.segmentsPerSecond;
29+
Point p1 = new Point((int)xPx, bmp.Height);
30+
Point p2 = new Point((int)xPx, bmp.Height - displaySettings.tickSize);
31+
DrawLineWithShadow(gfx, p1, p2);
32+
//DrawTextWithShadow(gfx, xPx.ToString(), p2, displaySettings.tickFont, displaySettings.sfTicksLower);
33+
}
34+
}
35+
36+
static void DrawTextWithShadow(Graphics gfx, string s, Point pt, Font fnt, StringFormat sf)
37+
{
38+
for (int dX = -1; dX < 2; dX++)
39+
for (int dY = -1; dY < 2; dY++)
40+
gfx.DrawString(s, fnt, Brushes.Black, pt.X + dX, pt.Y + dY, sf);
41+
42+
gfx.DrawString(s, fnt, Brushes.White, pt, sf);
43+
}
44+
45+
static void DrawLineWithShadow(Graphics gfx, Point p1, Point p2)
46+
{
47+
Pen penShadow = new Pen(Brushes.Black, 3);
48+
Pen penTick = new Pen(Brushes.White, 1);
49+
penShadow.EndCap = System.Drawing.Drawing2D.LineCap.Round;
50+
penTick.EndCap = System.Drawing.Drawing2D.LineCap.Round;
51+
gfx.DrawLine(penShadow, p1, p2);
52+
gfx.DrawLine(penTick, p1, p2);
53+
}
54+
}
55+
}

src/Spectrogram/Image.cs

Lines changed: 7 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -9,53 +9,22 @@ namespace Spectrogram
99
{
1010
class Image
1111
{
12-
public static Bitmap BitmapFromFFTs(
13-
float[][] ffts,
14-
int? pixelLow,
15-
int? pixelHigh,
16-
float intensity,
17-
bool decibels,
18-
Colormap colormap
19-
)
12+
public static Bitmap BitmapFromFFTs(float[][] ffts, Settings.DisplaySettings displaySettings)
2013
{
2114

2215
if (ffts == null || ffts.Length == 0)
2316
throw new ArgumentException("ffts must contain float arrays");
2417

25-
int fftHeight;
26-
if (ffts[0] != null)
27-
fftHeight = ffts[0].Length;
28-
else if (ffts[ffts.Length - 1] != null)
29-
fftHeight = ffts[ffts.Length - 1].Length;
30-
else
31-
return null;
32-
33-
if (pixelLow == null)
34-
pixelLow = 0;
35-
else
36-
pixelLow = Math.Max((int)pixelLow, 0);
37-
38-
if (pixelHigh == null)
39-
pixelHigh = fftHeight;
40-
else
41-
pixelHigh = Math.Min((int)pixelHigh, fftHeight);
42-
43-
if ((int)pixelHigh <= (int)pixelLow)
44-
throw new ArgumentException("pixelHigh must be greater than pixelLow");
45-
46-
int height = (int)pixelHigh - (int)pixelLow;
47-
int width = ffts.Length;
48-
49-
Bitmap bmp = new Bitmap(width, height, PixelFormat.Format8bppIndexed);
50-
ApplyColormap(bmp, colormap);
18+
Bitmap bmp = new Bitmap(ffts.Length, displaySettings.height, PixelFormat.Format8bppIndexed);
19+
ApplyColormap(bmp, displaySettings.colormap);
5120

5221
var rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
5322
BitmapData bitmapData = bmp.LockBits(rect, ImageLockMode.ReadOnly, bmp.PixelFormat);
5423
byte[] pixels = new byte[bitmapData.Stride * bmp.Height];
5524

5625
for (int col = 0; col < bmp.Width; col++)
5726
{
58-
if (col >= width)
27+
if (col >= bmp.Width)
5928
continue;
6029

6130
if (ffts[col] == null)
@@ -65,10 +34,10 @@ Colormap colormap
6534
{
6635
int bytePosition = (bmp.Height - 1 - row) * bitmapData.Stride + col;
6736
float pixelValue;
68-
pixelValue = ffts[col][row + (int)pixelLow];
69-
if (decibels)
37+
pixelValue = ffts[col][row + displaySettings.pixelLower];
38+
if (displaySettings.decibels)
7039
pixelValue = (float)(Math.Log10(pixelValue) * 20);
71-
pixelValue = (pixelValue * intensity);
40+
pixelValue = (pixelValue * displaySettings.intensity);
7241
pixelValue = Math.Max(0, pixelValue);
7342
pixelValue = Math.Min(255, pixelValue);
7443
pixels[bytePosition] = (byte)(pixelValue);

src/Spectrogram/Settings/DisplaySettings.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Drawing;
34
using System.Text;
45

56
namespace Spectrogram.Settings
@@ -12,6 +13,28 @@ public class DisplaySettings
1213

1314
// This class stores settings that control how the Bitmap looks (#2)
1415

16+
public int pixelLower;
17+
public int pixelUpper;
18+
public int height { get { return pixelUpper - pixelLower; } }
19+
20+
public float intensity;
21+
public bool decibels;
22+
public Colormap colormap;
23+
1524
public double lastRenderMsec;
25+
26+
public int tickSize = 5;
27+
public Font tickFont = new Font(FontFamily.GenericMonospace, (float)8);
28+
public StringFormat sfTicksRight = new StringFormat()
29+
{
30+
LineAlignment = StringAlignment.Center,
31+
Alignment = StringAlignment.Far
32+
};
33+
public StringFormat sfTicksLower = new StringFormat()
34+
{
35+
LineAlignment = StringAlignment.Far,
36+
Alignment = StringAlignment.Center
37+
};
38+
1639
}
1740
}

src/Spectrogram/Settings/FftSettings.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public FftSettings(int sampleRate, int fftSize, int segmentSize)
3232
public double maxFreq { get { return sampleRate / 2; } }
3333
public int fftOutputPointCount { get { return fftSize / 2; } }
3434
public double fftResolution { get { return maxFreq / fftOutputPointCount; } }
35+
public double segmentsPerSecond { get { return sampleRate / segmentSize; } }
3536

3637
public override string ToString()
3738
{
@@ -45,10 +46,12 @@ public override string ToString()
4546

4647
public int IndexFromFrequency(double frequency)
4748
{
48-
double maxFreq = sampleRate / 2;
49-
int fftOutputPoints = fftSize / 2;
50-
double fftResolution = maxFreq / fftOutputPoints;
5149
return (int)(frequency / fftResolution);
5250
}
51+
52+
public double FrequencyFromIndex(int index)
53+
{
54+
return index * fftResolution;
55+
}
5356
}
5457
}

src/Spectrogram/Spectrogram.cs

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Drawing;
4+
using System.Drawing.Imaging;
45

56
namespace Spectrogram
67
{
@@ -107,36 +108,54 @@ private void AddNewFftFixed(float[] fft, int fixedSize, bool scroll)
107108
public Bitmap GetBitmap(
108109
float intensity = 10,
109110
bool decibels = false,
110-
double frequencyMin = 0,
111-
double frequencyMax = double.MaxValue,
111+
double? frequencyMin = null,
112+
double? frequencyMax = null,
112113
bool vertical = false,
113-
Colormap colormap = Colormap.viridis
114+
Colormap colormap = Colormap.viridis,
115+
bool showTicks = false
114116
)
115117
{
116118
if (fftList.Count == 0)
117119
return null;
120+
if (frequencyMin == null)
121+
frequencyMin = 0;
122+
if (frequencyMax == null)
123+
frequencyMax = fftSettings.maxFreq;
124+
118125
if (frequencyMin < 0)
119126
throw new ArgumentException("frequencyMin must be greater than 0");
120127
if (frequencyMax < frequencyMin)
121128
throw new ArgumentException("frequencyMin must be less than frequencyMax");
122129

123-
int pixelLower = fftSettings.IndexFromFrequency(frequencyMin);
124-
int pixelUpper = fftSettings.IndexFromFrequency(frequencyMax);
130+
int pixelLower = fftSettings.IndexFromFrequency((double)frequencyMin);
131+
int pixelUpper = fftSettings.IndexFromFrequency((double)frequencyMax);
125132
if (pixelUpper > fftSettings.fftOutputPointCount)
126133
pixelUpper = fftSettings.fftOutputPointCount;
127134
if (pixelUpper - pixelLower < 1)
128135
throw new ArgumentException("FFT frequency range is too small");
129136

130-
Bitmap bmp;
137+
displaySettings.pixelLower = pixelLower;
138+
displaySettings.pixelUpper = pixelUpper;
139+
displaySettings.intensity = intensity;
140+
displaySettings.decibels = decibels;
141+
displaySettings.colormap = colormap;
142+
143+
Bitmap bmpIndexed;
144+
Bitmap bmpRgb;
145+
131146
using (var benchmark = new Benchmark())
132147
{
133-
bmp = Image.BitmapFromFFTs(fftList.ToArray(), pixelLower, pixelUpper, intensity, decibels, colormap);
148+
bmpIndexed = Image.BitmapFromFFTs(fftList.ToArray(), displaySettings);
134149
if (vertical)
135-
bmp = Image.Rotate(bmp);
150+
bmpIndexed = Image.Rotate(bmpIndexed);
151+
bmpRgb = bmpIndexed.Clone(new Rectangle(0, 0, bmpIndexed.Width, bmpIndexed.Height), PixelFormat.Format32bppPArgb);
136152
displaySettings.lastRenderMsec = benchmark.elapsedMilliseconds;
137153
}
138154

139-
return bmp;
155+
if (showTicks)
156+
Annotations.drawTicks(bmpRgb, fftSettings, displaySettings);
157+
158+
return bmpRgb;
140159
}
141160

142161
public double GetLastRenderTime()

0 commit comments

Comments
 (0)