@@ -73,12 +73,27 @@ public class SpectrogramGenerator
73
73
/// <summary>
74
74
/// The spectrogram is trimmed to cut-off frequencies above this value.
75
75
/// </summary>
76
- public double FreqMin { get { return settings . FreqMin ; } }
76
+ public double FreqMin { get => Settings . FreqMin ; }
77
+
78
+ /// <summary>
79
+ /// This module contains detailed FFT/Spectrogram settings
80
+ /// </summary>
81
+ private readonly Settings Settings ;
82
+
83
+ /// <summary>
84
+ /// This is the list of FFTs which is translated to the spectrogram image when it is requested.
85
+ /// The length of this list is the spectrogram width.
86
+ /// The length of the arrays in this list is the spectrogram height.
87
+ /// </summary>
88
+ private readonly List < double [ ] > FFTs = new List < double [ ] > ( ) ;
89
+
90
+ /// <summary>
91
+ /// This list contains data values which have not yet been processed.
92
+ /// Process() processes all unprocessed data.
93
+ /// This list may not be empty after processing if there aren't enough values to fill a full FFT (FftSize).
94
+ /// </summary>
95
+ private readonly List < double > UnprocessedData ;
77
96
78
- private readonly Settings settings ;
79
- private readonly List < double [ ] > ffts = new List < double [ ] > ( ) ;
80
- private readonly List < double > newAudio ;
81
- private Colormap cmap = Colormap . Viridis ;
82
97
/// <summary>
83
98
/// Colormap to use when generating future FFTs.
84
99
/// </summary>
@@ -107,30 +122,30 @@ public SpectrogramGenerator(
107
122
int offsetHz = 0 ,
108
123
List < double > initialAudioList = null )
109
124
{
110
- settings = new Settings ( sampleRate , fftSize , stepSize , minFreq , maxFreq , offsetHz ) ;
125
+ Settings = new Settings ( sampleRate , fftSize , stepSize , minFreq , maxFreq , offsetHz ) ;
111
126
112
- newAudio = initialAudioList ?? new List < double > ( ) ;
127
+ UnprocessedData = initialAudioList ?? new List < double > ( ) ;
113
128
114
129
if ( fixedWidth . HasValue )
115
130
SetFixedWidth ( fixedWidth . Value ) ;
116
131
}
117
132
118
133
public override string ToString ( )
119
134
{
120
- double processedSamples = ffts . Count * settings . StepSize + settings . FftSize ;
121
- double processedSec = processedSamples / settings . SampleRate ;
135
+ double processedSamples = FFTs . Count * Settings . StepSize + Settings . FftSize ;
136
+ double processedSec = processedSamples / Settings . SampleRate ;
122
137
string processedTime = ( processedSec < 60 ) ? $ "{ processedSec : N2} sec" : $ "{ processedSec / 60.0 : N2} min";
123
138
124
139
return $ "Spectrogram ({ Width } , { Height } )" +
125
140
$ "\n Vertical ({ Height } px): " +
126
- $ "{ settings . FreqMin : N0} - { settings . FreqMax : N0} Hz, " +
127
- $ "FFT size: { settings . FftSize : N0} samples, " +
128
- $ "{ settings . HzPerPixel : N2} Hz/px" +
141
+ $ "{ Settings . FreqMin : N0} - { Settings . FreqMax : N0} Hz, " +
142
+ $ "FFT size: { Settings . FftSize : N0} samples, " +
143
+ $ "{ Settings . HzPerPixel : N2} Hz/px" +
129
144
$ "\n Horizontal ({ Width } px): " +
130
145
$ "{ processedTime } , " +
131
- $ "window: { settings . FftLengthSec : N2} sec, " +
132
- $ "step: { settings . StepLengthSec : N2} sec, " +
133
- $ "overlap: { settings . StepOverlapFrac * 100 : N0} %";
146
+ $ "window: { Settings . FftLengthSec : N2} sec, " +
147
+ $ "step: { Settings . StepLengthSec : N2} sec, " +
148
+ $ "overlap: { Settings . StepOverlapFrac * 100 : N0} %";
134
149
}
135
150
136
151
[ Obsolete ( "Assign to the Colormap field" ) ]
@@ -148,14 +163,14 @@ public void SetColormap(Colormap cmap)
148
163
/// </summary>
149
164
public void SetWindow ( double [ ] newWindow )
150
165
{
151
- if ( newWindow . Length > settings . FftSize )
166
+ if ( newWindow . Length > Settings . FftSize )
152
167
throw new ArgumentException ( "window length cannot exceed FFT size" ) ;
153
168
154
- for ( int i = 0 ; i < settings . FftSize ; i ++ )
155
- settings . Window [ i ] = 0 ;
169
+ for ( int i = 0 ; i < Settings . FftSize ; i ++ )
170
+ Settings . Window [ i ] = 0 ;
156
171
157
- int offset = ( settings . FftSize - newWindow . Length ) / 2 ;
158
- Array . Copy ( newWindow , 0 , settings . Window , offset , newWindow . Length ) ;
172
+ int offset = ( Settings . FftSize - newWindow . Length ) / 2 ;
173
+ Array . Copy ( newWindow , 0 , Settings . Window , offset , newWindow . Length ) ;
159
174
}
160
175
161
176
[ Obsolete ( "use the Add() method" , true ) ]
@@ -172,7 +187,7 @@ public void AddScroll(float[] values) { }
172
187
/// </summary>
173
188
public void Add ( IEnumerable < double > audio , bool process = true )
174
189
{
175
- newAudio . AddRange ( audio ) ;
190
+ UnprocessedData . AddRange ( audio ) ;
176
191
if ( process )
177
192
Process ( ) ;
178
193
}
@@ -207,23 +222,23 @@ public double[][] Process()
207
222
208
223
Parallel . For ( 0 , newFftCount , newFftIndex =>
209
224
{
210
- FftSharp . Complex [ ] buffer = new FftSharp . Complex [ settings . FftSize ] ;
211
- int sourceIndex = newFftIndex * settings . StepSize ;
212
- for ( int i = 0 ; i < settings . FftSize ; i ++ )
213
- buffer [ i ] . Real = newAudio [ sourceIndex + i ] * settings . Window [ i ] ;
225
+ FftSharp . Complex [ ] buffer = new FftSharp . Complex [ Settings . FftSize ] ;
226
+ int sourceIndex = newFftIndex * Settings . StepSize ;
227
+ for ( int i = 0 ; i < Settings . FftSize ; i ++ )
228
+ buffer [ i ] . Real = UnprocessedData [ sourceIndex + i ] * Settings . Window [ i ] ;
214
229
215
230
FftSharp . Transform . FFT ( buffer ) ;
216
231
217
- newFfts [ newFftIndex ] = new double [ settings . Height ] ;
218
- for ( int i = 0 ; i < settings . Height ; i ++ )
219
- newFfts [ newFftIndex ] [ i ] = buffer [ settings . FftIndex1 + i ] . Magnitude / settings . FftSize ;
232
+ newFfts [ newFftIndex ] = new double [ Settings . Height ] ;
233
+ for ( int i = 0 ; i < Settings . Height ; i ++ )
234
+ newFfts [ newFftIndex ] [ i ] = buffer [ Settings . FftIndex1 + i ] . Magnitude / Settings . FftSize ;
220
235
} ) ;
221
236
222
237
foreach ( var newFft in newFfts )
223
- ffts . Add ( newFft ) ;
238
+ FFTs . Add ( newFft ) ;
224
239
FftsProcessed += newFfts . Length ;
225
240
226
- newAudio . RemoveRange ( 0 , newFftCount * settings . StepSize ) ;
241
+ UnprocessedData . RemoveRange ( 0 , newFftCount * Settings . StepSize ) ;
227
242
PadOrTrimForFixedWidth ( ) ;
228
243
229
244
return newFfts ;
@@ -235,11 +250,11 @@ public double[][] Process()
235
250
/// <param name="melBinCount">Total number of output bins to use. Choose a value significantly smaller than Height.</param>
236
251
public List < double [ ] > GetMelFFTs ( int melBinCount )
237
252
{
238
- if ( settings . FreqMin != 0 )
253
+ if ( Settings . FreqMin != 0 )
239
254
throw new InvalidOperationException ( "cannot get Mel spectrogram unless minimum frequency is 0Hz" ) ;
240
255
241
256
var fftsMel = new List < double [ ] > ( ) ;
242
- foreach ( var fft in ffts )
257
+ foreach ( var fft in FFTs )
243
258
fftsMel . Add ( FftSharp . Transform . MelScale ( fft , SampleRate , melBinCount ) ) ;
244
259
245
260
return fftsMel ;
@@ -255,7 +270,7 @@ public List<double[]> GetMelFFTs(int melBinCount)
255
270
/// Roll (true) adds new columns on the left overwriting the oldest ones.
256
271
/// Scroll (false) slides the whole image to the left and adds new columns to the right.</param>
257
272
public Bitmap GetBitmap ( double intensity = 1 , bool dB = false , double dBScale = 1 , bool roll = false ) =>
258
- Image . GetBitmap ( ffts , cmap , intensity , dB , dBScale , roll , NextColumnIndex ) ;
273
+ Image . GetBitmap ( FFTs , Colormap , intensity , dB , dBScale , roll , NextColumnIndex ) ;
259
274
260
275
/// <summary>
261
276
/// Create a Mel-scaled spectrogram.
@@ -268,7 +283,7 @@ public Bitmap GetBitmap(double intensity = 1, bool dB = false, double dBScale =
268
283
/// Roll (true) adds new columns on the left overwriting the oldest ones.
269
284
/// Scroll (false) slides the whole image to the left and adds new columns to the right.</param>
270
285
public Bitmap GetBitmapMel ( int melBinCount = 25 , double intensity = 1 , bool dB = false , double dBScale = 1 , bool roll = false ) =>
271
- Image . GetBitmap ( GetMelFFTs ( melBinCount ) , cmap , intensity , dB , dBScale , roll , NextColumnIndex ) ;
286
+ Image . GetBitmap ( GetMelFFTs ( melBinCount ) , Colormap , intensity , dB , dBScale , roll , NextColumnIndex ) ;
272
287
273
288
[ Obsolete ( "use SaveImage()" , true ) ]
274
289
public void SaveBitmap ( Bitmap bmp , string fileName ) { }
@@ -285,7 +300,7 @@ public void SaveBitmap(Bitmap bmp, string fileName) { }
285
300
/// Scroll (false) slides the whole image to the left and adds new columns to the right.</param>
286
301
public void SaveImage ( string fileName , double intensity = 1 , bool dB = false , double dBScale = 1 , bool roll = false )
287
302
{
288
- if ( ffts . Count == 0 )
303
+ if ( FFTs . Count == 0 )
289
304
throw new InvalidOperationException ( "Spectrogram contains no data. Use Add() to add signal data." ) ;
290
305
291
306
string extension = Path . GetExtension ( fileName ) . ToLower ( ) ;
@@ -302,7 +317,7 @@ public void SaveImage(string fileName, double intensity = 1, bool dB = false, do
302
317
else
303
318
throw new ArgumentException ( "unknown file extension" ) ;
304
319
305
- Image . GetBitmap ( ffts , cmap , intensity , dB , dBScale , roll , NextColumnIndex ) . Save ( fileName , fmt ) ;
320
+ Image . GetBitmap ( FFTs , Colormap , intensity , dB , dBScale , roll , NextColumnIndex ) . Save ( fileName , fmt ) ;
306
321
}
307
322
308
323
/// <summary>
@@ -317,16 +332,16 @@ public void SaveImage(string fileName, double intensity = 1, bool dB = false, do
317
332
public Bitmap GetBitmapMax ( double intensity = 1 , bool dB = false , double dBScale = 1 , bool roll = false , int reduction = 4 )
318
333
{
319
334
List < double [ ] > ffts2 = new List < double [ ] > ( ) ;
320
- for ( int i = 0 ; i < ffts . Count ; i ++ )
335
+ for ( int i = 0 ; i < FFTs . Count ; i ++ )
321
336
{
322
- double [ ] d1 = ffts [ i ] ;
337
+ double [ ] d1 = FFTs [ i ] ;
323
338
double [ ] d2 = new double [ d1 . Length / reduction ] ;
324
339
for ( int j = 0 ; j < d2 . Length ; j ++ )
325
340
for ( int k = 0 ; k < reduction ; k ++ )
326
341
d2 [ j ] = Math . Max ( d2 [ j ] , d1 [ j * reduction + k ] ) ;
327
342
ffts2 . Add ( d2 ) ;
328
343
}
329
- return Image . GetBitmap ( ffts2 , cmap , intensity , dB , dBScale , roll , NextColumnIndex ) ;
344
+ return Image . GetBitmap ( ffts2 , Colormap , intensity , dB , dBScale , roll , NextColumnIndex ) ;
330
345
}
331
346
332
347
/// <summary>
@@ -360,10 +375,10 @@ private void PadOrTrimForFixedWidth()
360
375
{
361
376
int overhang = Width - fixedWidth ;
362
377
if ( overhang > 0 )
363
- ffts . RemoveRange ( 0 , overhang ) ;
378
+ FFTs . RemoveRange ( 0 , overhang ) ;
364
379
365
- while ( ffts . Count < fixedWidth )
366
- ffts . Insert ( 0 , new double [ Height ] ) ;
380
+ while ( FFTs . Count < fixedWidth )
381
+ FFTs . Insert ( 0 , new double [ Height ] ) ;
367
382
}
368
383
}
369
384
@@ -376,26 +391,27 @@ private void PadOrTrimForFixedWidth()
376
391
/// <param name="reduction">bin size for vertical data reduction</param>
377
392
public Bitmap GetVerticalScale ( int width , int offsetHz = 0 , int tickSize = 3 , int reduction = 1 )
378
393
{
379
- return Scale . Vertical ( width , settings , offsetHz , tickSize , reduction ) ;
394
+ return Scale . Vertical ( width , Settings , offsetHz , tickSize , reduction ) ;
380
395
}
381
396
382
397
/// <summary>
383
398
/// Return the vertical position (pixel units) for the given frequency
384
399
/// </summary>
385
400
public int PixelY ( double frequency , int reduction = 1 )
386
401
{
387
- int pixelsFromZeroHz = ( int ) ( settings . PxPerHz * frequency / reduction ) ;
388
- int pixelsFromMinFreq = pixelsFromZeroHz - settings . FftIndex1 / reduction + 1 ;
389
- int pixelRow = settings . Height / reduction - 1 - pixelsFromMinFreq ;
402
+ int pixelsFromZeroHz = ( int ) ( Settings . PxPerHz * frequency / reduction ) ;
403
+ int pixelsFromMinFreq = pixelsFromZeroHz - Settings . FftIndex1 / reduction + 1 ;
404
+ int pixelRow = Settings . Height / reduction - 1 - pixelsFromMinFreq ;
390
405
return pixelRow - 1 ;
391
406
}
392
407
393
408
/// <summary>
394
- /// Return a list of the FFTs in memory underlying the spectrogram
409
+ /// Return the list of FFTs in memory underlying the spectrogram.
410
+ /// This list may continue to evolve after it is returned.
395
411
/// </summary>
396
412
public List < double [ ] > GetFFTs ( )
397
413
{
398
- return ffts ;
414
+ return FFTs ;
399
415
}
400
416
401
417
/// <summary>
@@ -404,13 +420,13 @@ public List<double[]> GetFFTs()
404
420
/// <param name="latestFft">If true, only the latest FFT will be assessed.</param>
405
421
public ( double freqHz , double magRms ) GetPeak ( bool latestFft = true )
406
422
{
407
- if ( ffts . Count == 0 )
423
+ if ( FFTs . Count == 0 )
408
424
return ( double . NaN , double . NaN ) ;
409
425
410
426
if ( latestFft == false )
411
427
throw new NotImplementedException ( "peak of mean of all FFTs not yet supported" ) ;
412
428
413
- double [ ] freqs = ffts [ ffts . Count - 1 ] ;
429
+ double [ ] freqs = FFTs [ FFTs . Count - 1 ] ;
414
430
415
431
int peakIndex = 0 ;
416
432
double peakMagnitude = 0 ;
0 commit comments