@@ -9,31 +9,104 @@ namespace Spectrogram
9
9
{
10
10
public class SpectrogramGenerator
11
11
{
12
+ /// <summary>
13
+ /// Number of pixel columns (FFT samples) in the spectrogram image
14
+ /// </summary>
12
15
public int Width { get { return ffts . Count ; } }
16
+
17
+ /// <summary>
18
+ /// Number of pixel rows (frequency bins) in the spectrogram image
19
+ /// </summary>
13
20
public int Height { get { return settings . Height ; } }
21
+
22
+ /// <summary>
23
+ /// Number of samples to use for each FFT (must be a power of 2)
24
+ /// </summary>
14
25
public int FftSize { get { return settings . FftSize ; } }
26
+
27
+ /// <summary>
28
+ /// Vertical resolution (frequency bin size depends on FftSize and SampleRate)
29
+ /// </summary>
15
30
public double HzPerPx { get { return settings . HzPerPixel ; } }
31
+
32
+ /// <summary>
33
+ /// Horizontal resolution (seconds per pixel depends on StepSize)
34
+ /// </summary>
16
35
public double SecPerPx { get { return settings . StepLengthSec ; } }
36
+
37
+ /// <summary>
38
+ /// Number of FFTs that remain to be processed for data which has been added but not yet analuyzed
39
+ /// </summary>
17
40
public int FftsToProcess { get { return ( newAudio . Count - settings . FftSize ) / settings . StepSize ; } }
41
+
42
+ /// <summary>
43
+ /// Total number of FFT steps processed
44
+ /// </summary>
18
45
public int FftsProcessed { get ; private set ; }
46
+
47
+ /// <summary>
48
+ /// Index of the pixel column which will be populated next. Location of vertical line for wrap-around displays.
49
+ /// </summary>
19
50
public int NextColumnIndex { get { return ( FftsProcessed + rollOffset ) % Width ; } }
51
+
52
+ /// <summary>
53
+ /// This value is added to displayed frequency axis tick labels
54
+ /// </summary>
20
55
public int OffsetHz { get { return settings . OffsetHz ; } set { settings . OffsetHz = value ; } }
56
+
57
+ /// <summary>
58
+ /// Number of samples per second
59
+ /// </summary>
21
60
public int SampleRate { get { return settings . SampleRate ; } }
61
+
62
+ /// <summary>
63
+ /// Number of samples to step forward after each FFT is processed.
64
+ /// This value controls the horizontal resolution of the spectrogram.
65
+ /// </summary>
22
66
public int StepSize { get { return settings . StepSize ; } }
67
+
68
+ /// <summary>
69
+ /// The spectrogram is trimmed to cut-off frequencies below this value.
70
+ /// </summary>
23
71
public double FreqMax { get { return settings . FreqMax ; } }
72
+
73
+ /// <summary>
74
+ /// The spectrogram is trimmed to cut-off frequencies above this value.
75
+ /// </summary>
24
76
public double FreqMin { get { return settings . FreqMin ; } }
25
77
26
78
private readonly Settings settings ;
27
79
private readonly List < double [ ] > ffts = new List < double [ ] > ( ) ;
28
- private readonly List < double > newAudio = new List < double > ( ) ;
80
+ private readonly List < double > newAudio ;
29
81
private Colormap cmap = Colormap . Viridis ;
30
82
31
- public SpectrogramGenerator ( int sampleRate , int fftSize , int stepSize ,
32
- double minFreq = 0 , double maxFreq = double . PositiveInfinity ,
33
- int ? fixedWidth = null , int offsetHz = 0 )
83
+ /// <summary>
84
+ /// Instantiate a spectrogram generator.
85
+ /// This module calculates the FFT over a moving window as data comes in.
86
+ /// Using the Add() method to load new data and process it as it arrives.
87
+ /// </summary>
88
+ /// <param name="sampleRate">Number of samples per second (Hz)</param>
89
+ /// <param name="fftSize">Number of samples to use for each FFT operation. This value must be a power of 2.</param>
90
+ /// <param name="stepSize">Number of samples to step forward</param>
91
+ /// <param name="minFreq">Frequency data lower than this value (Hz) will not be stored</param>
92
+ /// <param name="maxFreq">Frequency data higher than this value (Hz) will not be stored</param>
93
+ /// <param name="fixedWidth">Spectrogram output will always be sized to this width (column count)</param>
94
+ /// <param name="offsetHz">This value will be added to displayed frequency axis tick labels</param>
95
+ /// <param name="initialAudioList">Analyze this data immediately (alternative to calling Add() later)</param>
96
+ public SpectrogramGenerator (
97
+ int sampleRate ,
98
+ int fftSize ,
99
+ int stepSize ,
100
+ double minFreq = 0 ,
101
+ double maxFreq = double . PositiveInfinity ,
102
+ int ? fixedWidth = null ,
103
+ int offsetHz = 0 ,
104
+ List < double > initialAudioList = null )
34
105
{
35
106
settings = new Settings ( sampleRate , fftSize , stepSize , minFreq , maxFreq , offsetHz ) ;
36
107
108
+ newAudio = initialAudioList ?? new List < double > ( ) ;
109
+
37
110
if ( fixedWidth . HasValue )
38
111
SetFixedWidth ( fixedWidth . Value ) ;
39
112
}
@@ -56,11 +129,18 @@ public override string ToString()
56
129
$ "overlap: { settings . StepOverlapFrac * 100 : N0} %";
57
130
}
58
131
132
+ /// <summary>
133
+ /// Set the colormap to use for future renders
134
+ /// </summary>
59
135
public void SetColormap ( Colormap cmap )
60
136
{
61
137
this . cmap = cmap ?? this . cmap ;
62
138
}
63
139
140
+ /// <summary>
141
+ /// Load a custom window kernel to multiply against each FFT sample prior to processing.
142
+ /// Windows must be at least the length of FftSize and typically have a sum of 1.0.
143
+ /// </summary>
64
144
public void SetWindow ( double [ ] newWindow )
65
145
{
66
146
if ( newWindow . Length > settings . FftSize )
@@ -82,19 +162,36 @@ public void AddCircular(float[] values) { }
82
162
[ Obsolete ( "use the Add() method" , true ) ]
83
163
public void AddScroll ( float [ ] values ) { }
84
164
85
- public void Add ( double [ ] audio , bool process = true )
165
+ /// <summary>
166
+ /// Load new data into the spectrogram generator
167
+ /// </summary>
168
+ public void Add ( IEnumerable < double > audio , bool process = true )
86
169
{
87
170
newAudio . AddRange ( audio ) ;
88
171
if ( process )
89
172
Process ( ) ;
90
173
}
91
174
175
+ /// <summary>
176
+ /// The roll offset is used to calculate NextColumnIndex and can be set to a positive number
177
+ /// to begin adding new columns to the center of the spectrogram.
178
+ /// This can also be used to artificially move the next column index to zero even though some
179
+ /// data has already been accumulated.
180
+ /// </summary>
92
181
private int rollOffset = 0 ;
182
+
183
+ /// <summary>
184
+ /// Reset the next column index such that the next processed FFT will appear at the far left of the spectrogram.
185
+ /// </summary>
186
+ /// <param name="offset"></param>
93
187
public void RollReset ( int offset = 0 )
94
188
{
95
189
rollOffset = - FftsProcessed + offset ;
96
190
}
97
191
192
+ /// <summary>
193
+ /// Perform FFT analysis on all unprocessed data
194
+ /// </summary>
98
195
public double [ ] [ ] Process ( )
99
196
{
100
197
if ( FftsToProcess < 1 )
@@ -127,6 +224,10 @@ public double[][] Process()
127
224
return newFfts ;
128
225
}
129
226
227
+ /// <summary>
228
+ /// Return a list of the mel-scaled FFTs contained in this spectrogram
229
+ /// </summary>
230
+ /// <param name="melBinCount">Total number of output bins to use. Choose a value significantly smaller than Height.</param>
130
231
public List < double [ ] > GetMelFFTs ( int melBinCount )
131
232
{
132
233
if ( settings . FreqMin != 0 )
@@ -139,15 +240,44 @@ public List<double[]> GetMelFFTs(int melBinCount)
139
240
return fftsMel ;
140
241
}
141
242
243
+ /// <summary>
244
+ /// Create and return a spectrogram bitmap from the FFTs stored in memory.
245
+ /// </summary>
246
+ /// <param name="intensity">Multiply the output by a fixed value to change its brightness.</param>
247
+ /// <param name="dB">If true, output will be log-transformed.</param>
248
+ /// <param name="dBScale">If dB scaling is in use, this multiplier will be applied before log transformation.</param>
249
+ /// <param name="roll">Behavior of the spectrogram when it is full of data.
250
+ /// Roll (true) adds new columns on the left overwriting the oldest ones.
251
+ /// Scroll (false) slides the whole image to the left and adds new columns to the right.</param>
142
252
public Bitmap GetBitmap ( double intensity = 1 , bool dB = false , double dBScale = 1 , bool roll = false ) =>
143
253
Image . GetBitmap ( ffts , cmap , intensity , dB , dBScale , roll , NextColumnIndex ) ;
144
254
255
+ /// <summary>
256
+ /// Create a Mel-scaled spectrogram.
257
+ /// </summary>
258
+ /// <param name="melBinCount">Total number of output bins to use. Choose a value significantly smaller than Height.</param>
259
+ /// <param name="intensity">Multiply the output by a fixed value to change its brightness.</param>
260
+ /// <param name="dB">If true, output will be log-transformed.</param>
261
+ /// <param name="dBScale">If dB scaling is in use, this multiplier will be applied before log transformation.</param>
262
+ /// <param name="roll">Behavior of the spectrogram when it is full of data.
263
+ /// Roll (true) adds new columns on the left overwriting the oldest ones.
264
+ /// Scroll (false) slides the whole image to the left and adds new columns to the right.</param>
145
265
public Bitmap GetBitmapMel ( int melBinCount = 25 , double intensity = 1 , bool dB = false , double dBScale = 1 , bool roll = false ) =>
146
266
Image . GetBitmap ( GetMelFFTs ( melBinCount ) , cmap , intensity , dB , dBScale , roll , NextColumnIndex ) ;
147
267
148
268
[ Obsolete ( "use SaveImage()" , true ) ]
149
269
public void SaveBitmap ( Bitmap bmp , string fileName ) { }
150
270
271
+ /// <summary>
272
+ /// Generate the spectrogram and save it as an image file.
273
+ /// </summary>
274
+ /// <param name="fileName">Path of the file to save.</param>
275
+ /// <param name="intensity">Multiply the output by a fixed value to change its brightness.</param>
276
+ /// <param name="dB">If true, output will be log-transformed.</param>
277
+ /// <param name="dBScale">If dB scaling is in use, this multiplier will be applied before log transformation.</param>
278
+ /// <param name="roll">Behavior of the spectrogram when it is full of data.
279
+ /// Roll (true) adds new columns on the left overwriting the oldest ones.
280
+ /// Scroll (false) slides the whole image to the left and adds new columns to the right.</param>
151
281
public void SaveImage ( string fileName , double intensity = 1 , bool dB = false , double dBScale = 1 , bool roll = false )
152
282
{
153
283
if ( ffts . Count == 0 )
@@ -170,6 +300,15 @@ public void SaveImage(string fileName, double intensity = 1, bool dB = false, do
170
300
Image . GetBitmap ( ffts , cmap , intensity , dB , dBScale , roll , NextColumnIndex ) . Save ( fileName , fmt ) ;
171
301
}
172
302
303
+ /// <summary>
304
+ /// Create and return a spectrogram bitmap from the FFTs stored in memory.
305
+ /// The output will be scaled-down vertically by binning according to a reduction factor and keeping the brightest pixel value in each bin.
306
+ /// </summary>
307
+ /// <param name="intensity">Multiply the output by a fixed value to change its brightness.</param>
308
+ /// <param name="dB">If true, output will be log-transformed.</param>
309
+ /// <param name="dBScale">If dB scaling is in use, this multiplier will be applied before log transformation.</param>
310
+ /// <param name="roll">Behavior of the spectrogram when it is full of data.
311
+ /// <param name="reduction"></param>
173
312
public Bitmap GetBitmapMax ( double intensity = 1 , bool dB = false , double dBScale = 1 , bool roll = false , int reduction = 4 )
174
313
{
175
314
List < double [ ] > ffts2 = new List < double [ ] > ( ) ;
@@ -185,14 +324,25 @@ public Bitmap GetBitmapMax(double intensity = 1, bool dB = false, double dBScale
185
324
return Image . GetBitmap ( ffts2 , cmap , intensity , dB , dBScale , roll , NextColumnIndex ) ;
186
325
}
187
326
327
+ /// <summary>
328
+ /// Export spectrogram data using the Spectrogram File Format (SFF)
329
+ /// </summary>
188
330
public void SaveData ( string filePath , int melBinCount = 0 )
189
331
{
190
332
if ( ! filePath . EndsWith ( ".sff" , StringComparison . OrdinalIgnoreCase ) )
191
333
filePath += ".sff" ;
192
334
new SFF ( this , melBinCount ) . Save ( filePath ) ;
193
335
}
194
336
337
+ /// <summary>
338
+ /// Defines the total number of FFTs (spectrogram columns) to store in memory. Determines Width.
339
+ /// </summary>
195
340
private int fixedWidth = 0 ;
341
+
342
+ /// <summary>
343
+ /// Configure the Spectrogram to maintain a fixed number of pixel columns.
344
+ /// Zeros will be added to padd existing data to achieve this width, and extra columns will be deleted.
345
+ /// </summary>
196
346
public void SetFixedWidth ( int width )
197
347
{
198
348
fixedWidth = width ;
@@ -212,11 +362,21 @@ private void PadOrTrimForFixedWidth()
212
362
}
213
363
}
214
364
365
+ /// <summary>
366
+ /// Get a vertical image containing ticks and tick labels for the frequency axis.
367
+ /// </summary>
368
+ /// <param name="width">size (pixels)</param>
369
+ /// <param name="offsetHz">number to add to each tick label</param>
370
+ /// <param name="tickSize">length of each tick mark (pixels)</param>
371
+ /// <param name="reduction">bin size for vertical data reduction</param>
215
372
public Bitmap GetVerticalScale ( int width , int offsetHz = 0 , int tickSize = 3 , int reduction = 1 )
216
373
{
217
374
return Scale . Vertical ( width , settings , offsetHz , tickSize , reduction ) ;
218
375
}
219
376
377
+ /// <summary>
378
+ /// Return the vertical position (pixel units) for the given frequency
379
+ /// </summary>
220
380
public int PixelY ( double frequency , int reduction = 1 )
221
381
{
222
382
int pixelsFromZeroHz = ( int ) ( settings . PxPerHz * frequency / reduction ) ;
@@ -225,11 +385,18 @@ public int PixelY(double frequency, int reduction = 1)
225
385
return pixelRow - 1 ;
226
386
}
227
387
388
+ /// <summary>
389
+ /// Return a list of the FFTs in memory underlying the spectrogram
390
+ /// </summary>
228
391
public List < double [ ] > GetFFTs ( )
229
392
{
230
393
return ffts ;
231
394
}
232
395
396
+ /// <summary>
397
+ /// Return frequency and magnitude of the dominant frequency.
398
+ /// </summary>
399
+ /// <param name="latestFft">If true, only the latest FFT will be assessed.</param>
233
400
public ( double freqHz , double magRms ) GetPeak ( bool latestFft = true )
234
401
{
235
402
if ( ffts . Count == 0 )
0 commit comments