From f1e9fe59b59c2c48b1dbf5c870da877291e4ef2a Mon Sep 17 00:00:00 2001 From: shirok1 Date: Mon, 30 Aug 2021 04:02:05 +0800 Subject: [PATCH 01/62] change the `sampleCount` expression in sample `ReadWAV` in readme.md to match the eventual size of `audio` --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 83f9206..a4d45ea 100644 --- a/README.md +++ b/README.md @@ -214,7 +214,7 @@ You should customize your file-reading method to suit your specific application. { using var afr = new NAudio.Wave.AudioFileReader(filePath); int sampleRate = afr.WaveFormat.SampleRate; - int sampleCount = (int)(afr.Length / afr.WaveFormat.BitsPerSample / 8); + int sampleCount = (int)(afr.Length / afr.WaveFormat.BitsPerSample) * 8; int channelCount = afr.WaveFormat.Channels; var audio = new List(sampleCount); var buffer = new float[sampleRate * channelCount]; From f313dc113e232d0a8a4699808d4f9f7f094f91f9 Mon Sep 17 00:00:00 2001 From: shirok1 Date: Mon, 30 Aug 2021 04:06:03 +0800 Subject: [PATCH 02/62] change the type of the 1st para of `Add` from `double[]` to `IEnumerable` --- README.md | 16 ++++++++-------- src/Spectrogram/SpectrogramGenerator.cs | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index a4d45ea..63cfd98 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ _"I'm sorry Dave... I'm afraid I can't do that"_ * Source code for the WAV reading method is at the bottom of this page. ```cs -(double[] audio, int sampleRate) = ReadWAV("hal.wav"); +(IEnumerable audio, int sampleRate) = ReadWAV("hal.wav"); var sg = new SpectrogramGenerator(sampleRate, fftSize: 4096, stepSize: 500, maxFreq: 3000); sg.Add(audio); sg.SaveImage("hal.png"); @@ -59,7 +59,7 @@ public Form1() Whenever an audio buffer gets filled, add the data to your Spectrogram: ```cs -private void GotNewBuffer(double[] audio) +private void GotNewBuffer(IEnumerable audio) { sg.Add(audio); } @@ -81,7 +81,7 @@ Review the source code of the demo application for additional details and consid 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). ```cs -(double[] audio, int sampleRate) = ReadWAV("song.wav"); +(IEnumerable audio, int sampleRate) = ReadWAV("song.wav"); int fftSize = 16384; int targetWidthPx = 3000; @@ -117,7 +117,7 @@ Spectrogram (2993, 817) These examples demonstrate the identical spectrogram analyzed with a variety of different colormaps. Spectrogram colormaps can be changed by calling the `SetColormap()` method: ```cs -(double[] audio, int sampleRate) = ReadWAV("hal.wav"); +(IEnumerable audio, int sampleRate) = ReadWAV("hal.wav"); var sg = new SpectrogramGenerator(sampleRate, fftSize: 8192, stepSize: 200, maxFreq: 3000); sg.Add(audio); sg.SetColormap(Colormap.Jet); @@ -141,7 +141,7 @@ Cropped Linear Scale (0-3kHz) | Mel Scale (0-22 kHz) Amplitude perception in humans, like frequency perception, is logarithmic. Therefore, Mel spectrograms typically display log-transformed spectral power and are presented using Decibel units. ```cs -(double[] audio, int sampleRate) = ReadWAV("hal.wav"); +(IEnumerable audio, int sampleRate) = ReadWAV("hal.wav"); var sg = new SpectrogramGenerator(sampleRate, fftSize: 4096, stepSize: 500, maxFreq: 3000); sg.Add(audio); @@ -166,7 +166,7 @@ SFF files be saved using `Complex` data format (with real and imaginary values f This example creates a spectrogram but saves it using the SFF file format instead of saving it as an image. The SFF file can then be read in any language. ```cs -(double[] audio, int sampleRate) = ReadWAV("hal.wav"); +(IEnumerable audio, int sampleRate) = ReadWAV("hal.wav"); var sg = new SpectrogramGenerator(sampleRate, fftSize: 4096, stepSize: 700, maxFreq: 2000); sg.Add(audio); sg.SaveData("hal.sff"); @@ -210,7 +210,7 @@ plt.show() 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. ```cs -(double[] audio, int sampleRate) ReadWAV(string filePath, double multiplier = 16_000) +(IEnumerable audio, int sampleRate) ReadWAV(string filePath, double multiplier = 16_000) { using var afr = new NAudio.Wave.AudioFileReader(filePath); int sampleRate = afr.WaveFormat.SampleRate; @@ -221,6 +221,6 @@ You should customize your file-reading method to suit your specific application. int samplesRead = 0; while ((samplesRead = afr.Read(buffer, 0, buffer.Length)) > 0) audio.AddRange(buffer.Take(samplesRead).Select(x => x * multiplier)); - return (audio.ToArray(), sampleRate); + return (audio, sampleRate); } ``` \ No newline at end of file diff --git a/src/Spectrogram/SpectrogramGenerator.cs b/src/Spectrogram/SpectrogramGenerator.cs index 89af261..ebc9c56 100644 --- a/src/Spectrogram/SpectrogramGenerator.cs +++ b/src/Spectrogram/SpectrogramGenerator.cs @@ -82,7 +82,7 @@ public void AddCircular(float[] values) { } [Obsolete("use the Add() method", true)] public void AddScroll(float[] values) { } - public void Add(double[] audio, bool process = true) + public void Add(IEnumerable audio, bool process = true) { newAudio.AddRange(audio); if (process) From 164b3dbe40f1d3f1a977e948b2d3a50f456b00f9 Mon Sep 17 00:00:00 2001 From: shirok1 Date: Mon, 30 Aug 2021 04:07:40 +0800 Subject: [PATCH 03/62] add an additional optional argument to manually init `newAudio` with an external `List` --- src/Spectrogram/SpectrogramGenerator.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Spectrogram/SpectrogramGenerator.cs b/src/Spectrogram/SpectrogramGenerator.cs index ebc9c56..4d96233 100644 --- a/src/Spectrogram/SpectrogramGenerator.cs +++ b/src/Spectrogram/SpectrogramGenerator.cs @@ -25,15 +25,17 @@ public class SpectrogramGenerator private readonly Settings settings; private readonly List ffts = new List(); - private readonly List newAudio = new List(); + private readonly List newAudio; private Colormap cmap = Colormap.Viridis; public SpectrogramGenerator(int sampleRate, int fftSize, int stepSize, double minFreq = 0, double maxFreq = double.PositiveInfinity, - int? fixedWidth = null, int offsetHz = 0) + int? fixedWidth = null, int offsetHz = 0, List initialAudioList = null) { settings = new Settings(sampleRate, fftSize, stepSize, minFreq, maxFreq, offsetHz); + newAudio = initialAudioList ?? new List(); + if (fixedWidth.HasValue) SetFixedWidth(fixedWidth.Value); } From 6640625b5bbf908d354011d75e3314fa18e8e233 Mon Sep 17 00:00:00 2001 From: shirok1 Date: Mon, 30 Aug 2021 04:40:49 +0800 Subject: [PATCH 04/62] change the type of tuple in readme.md to clarify --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 63cfd98..f81aa6d 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ _"I'm sorry Dave... I'm afraid I can't do that"_ * Source code for the WAV reading method is at the bottom of this page. ```cs -(IEnumerable audio, int sampleRate) = ReadWAV("hal.wav"); +(List audio, int sampleRate) = ReadWAV("hal.wav"); var sg = new SpectrogramGenerator(sampleRate, fftSize: 4096, stepSize: 500, maxFreq: 3000); sg.Add(audio); sg.SaveImage("hal.png"); @@ -81,7 +81,7 @@ Review the source code of the demo application for additional details and consid 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). ```cs -(IEnumerable audio, int sampleRate) = ReadWAV("song.wav"); +(List audio, int sampleRate) = ReadWAV("song.wav"); int fftSize = 16384; int targetWidthPx = 3000; @@ -117,7 +117,7 @@ Spectrogram (2993, 817) These examples demonstrate the identical spectrogram analyzed with a variety of different colormaps. Spectrogram colormaps can be changed by calling the `SetColormap()` method: ```cs -(IEnumerable audio, int sampleRate) = ReadWAV("hal.wav"); +(List audio, int sampleRate) = ReadWAV("hal.wav"); var sg = new SpectrogramGenerator(sampleRate, fftSize: 8192, stepSize: 200, maxFreq: 3000); sg.Add(audio); sg.SetColormap(Colormap.Jet); @@ -141,7 +141,7 @@ Cropped Linear Scale (0-3kHz) | Mel Scale (0-22 kHz) Amplitude perception in humans, like frequency perception, is logarithmic. Therefore, Mel spectrograms typically display log-transformed spectral power and are presented using Decibel units. ```cs -(IEnumerable audio, int sampleRate) = ReadWAV("hal.wav"); +(List audio, int sampleRate) = ReadWAV("hal.wav"); var sg = new SpectrogramGenerator(sampleRate, fftSize: 4096, stepSize: 500, maxFreq: 3000); sg.Add(audio); @@ -166,7 +166,7 @@ SFF files be saved using `Complex` data format (with real and imaginary values f This example creates a spectrogram but saves it using the SFF file format instead of saving it as an image. The SFF file can then be read in any language. ```cs -(IEnumerable audio, int sampleRate) = ReadWAV("hal.wav"); +(List audio, int sampleRate) = ReadWAV("hal.wav"); var sg = new SpectrogramGenerator(sampleRate, fftSize: 4096, stepSize: 700, maxFreq: 2000); sg.Add(audio); sg.SaveData("hal.sff"); @@ -210,7 +210,7 @@ plt.show() 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. ```cs -(IEnumerable audio, int sampleRate) ReadWAV(string filePath, double multiplier = 16_000) +(List audio, int sampleRate) ReadWAV(string filePath, double multiplier = 16_000) { using var afr = new NAudio.Wave.AudioFileReader(filePath); int sampleRate = afr.WaveFormat.SampleRate; From ab2890efde0dd3e2659809fb02e503601da5ca47 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Sat, 4 Sep 2021 19:54:53 -0400 Subject: [PATCH 05/62] Revert "change the type of tuple in readme.md to clarify" This reverts commit 6640625b5bbf908d354011d75e3314fa18e8e233. --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f81aa6d..63cfd98 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ _"I'm sorry Dave... I'm afraid I can't do that"_ * Source code for the WAV reading method is at the bottom of this page. ```cs -(List audio, int sampleRate) = ReadWAV("hal.wav"); +(IEnumerable audio, int sampleRate) = ReadWAV("hal.wav"); var sg = new SpectrogramGenerator(sampleRate, fftSize: 4096, stepSize: 500, maxFreq: 3000); sg.Add(audio); sg.SaveImage("hal.png"); @@ -81,7 +81,7 @@ Review the source code of the demo application for additional details and consid 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). ```cs -(List audio, int sampleRate) = ReadWAV("song.wav"); +(IEnumerable audio, int sampleRate) = ReadWAV("song.wav"); int fftSize = 16384; int targetWidthPx = 3000; @@ -117,7 +117,7 @@ Spectrogram (2993, 817) These examples demonstrate the identical spectrogram analyzed with a variety of different colormaps. Spectrogram colormaps can be changed by calling the `SetColormap()` method: ```cs -(List audio, int sampleRate) = ReadWAV("hal.wav"); +(IEnumerable audio, int sampleRate) = ReadWAV("hal.wav"); var sg = new SpectrogramGenerator(sampleRate, fftSize: 8192, stepSize: 200, maxFreq: 3000); sg.Add(audio); sg.SetColormap(Colormap.Jet); @@ -141,7 +141,7 @@ Cropped Linear Scale (0-3kHz) | Mel Scale (0-22 kHz) Amplitude perception in humans, like frequency perception, is logarithmic. Therefore, Mel spectrograms typically display log-transformed spectral power and are presented using Decibel units. ```cs -(List audio, int sampleRate) = ReadWAV("hal.wav"); +(IEnumerable audio, int sampleRate) = ReadWAV("hal.wav"); var sg = new SpectrogramGenerator(sampleRate, fftSize: 4096, stepSize: 500, maxFreq: 3000); sg.Add(audio); @@ -166,7 +166,7 @@ SFF files be saved using `Complex` data format (with real and imaginary values f This example creates a spectrogram but saves it using the SFF file format instead of saving it as an image. The SFF file can then be read in any language. ```cs -(List audio, int sampleRate) = ReadWAV("hal.wav"); +(IEnumerable audio, int sampleRate) = ReadWAV("hal.wav"); var sg = new SpectrogramGenerator(sampleRate, fftSize: 4096, stepSize: 700, maxFreq: 2000); sg.Add(audio); sg.SaveData("hal.sff"); @@ -210,7 +210,7 @@ plt.show() 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. ```cs -(List audio, int sampleRate) ReadWAV(string filePath, double multiplier = 16_000) +(IEnumerable audio, int sampleRate) ReadWAV(string filePath, double multiplier = 16_000) { using var afr = new NAudio.Wave.AudioFileReader(filePath); int sampleRate = afr.WaveFormat.SampleRate; From 08f42544de23f920823273e819569495db845c0a Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Sat, 4 Sep 2021 20:34:36 -0400 Subject: [PATCH 06/62] SpectrogramGenerator: add XML docs --- src/Spectrogram/SpectrogramGenerator.cs | 171 +++++++++++++++++++++++- 1 file changed, 168 insertions(+), 3 deletions(-) diff --git a/src/Spectrogram/SpectrogramGenerator.cs b/src/Spectrogram/SpectrogramGenerator.cs index 4d96233..3dad42d 100644 --- a/src/Spectrogram/SpectrogramGenerator.cs +++ b/src/Spectrogram/SpectrogramGenerator.cs @@ -9,18 +9,70 @@ namespace Spectrogram { public class SpectrogramGenerator { + /// + /// Number of pixel columns (FFT samples) in the spectrogram image + /// public int Width { get { return ffts.Count; } } + + /// + /// Number of pixel rows (frequency bins) in the spectrogram image + /// public int Height { get { return settings.Height; } } + + /// + /// Number of samples to use for each FFT (must be a power of 2) + /// public int FftSize { get { return settings.FftSize; } } + + /// + /// Vertical resolution (frequency bin size depends on FftSize and SampleRate) + /// public double HzPerPx { get { return settings.HzPerPixel; } } + + /// + /// Horizontal resolution (seconds per pixel depends on StepSize) + /// public double SecPerPx { get { return settings.StepLengthSec; } } + + /// + /// Number of FFTs that remain to be processed for data which has been added but not yet analuyzed + /// public int FftsToProcess { get { return (newAudio.Count - settings.FftSize) / settings.StepSize; } } + + /// + /// Total number of FFT steps processed + /// public int FftsProcessed { get; private set; } + + /// + /// Index of the pixel column which will be populated next. Location of vertical line for wrap-around displays. + /// public int NextColumnIndex { get { return (FftsProcessed + rollOffset) % Width; } } + + /// + /// This value is added to displayed frequency axis tick labels + /// public int OffsetHz { get { return settings.OffsetHz; } set { settings.OffsetHz = value; } } + + /// + /// Number of samples per second + /// public int SampleRate { get { return settings.SampleRate; } } + + /// + /// Number of samples to step forward after each FFT is processed. + /// This value controls the horizontal resolution of the spectrogram. + /// public int StepSize { get { return settings.StepSize; } } + + /// + /// The spectrogram is trimmed to cut-off frequencies below this value. + /// public double FreqMax { get { return settings.FreqMax; } } + + /// + /// The spectrogram is trimmed to cut-off frequencies above this value. + /// public double FreqMin { get { return settings.FreqMin; } } private readonly Settings settings; @@ -28,9 +80,28 @@ public class SpectrogramGenerator private readonly List newAudio; private Colormap cmap = Colormap.Viridis; - public SpectrogramGenerator(int sampleRate, int fftSize, int stepSize, - double minFreq = 0, double maxFreq = double.PositiveInfinity, - int? fixedWidth = null, int offsetHz = 0, List initialAudioList = null) + /// + /// Instantiate a spectrogram generator. + /// This module calculates the FFT over a moving window as data comes in. + /// Using the Add() method to load new data and process it as it arrives. + /// + /// Number of samples per second (Hz) + /// Number of samples to use for each FFT operation. This value must be a power of 2. + /// Number of samples to step forward + /// Frequency data lower than this value (Hz) will not be stored + /// Frequency data higher than this value (Hz) will not be stored + /// Spectrogram output will always be sized to this width (column count) + /// This value will be added to displayed frequency axis tick labels + /// Analyze this data immediately (alternative to calling Add() later) + public SpectrogramGenerator( + int sampleRate, + int fftSize, + int stepSize, + double minFreq = 0, + double maxFreq = double.PositiveInfinity, + int? fixedWidth = null, + int offsetHz = 0, + List initialAudioList = null) { settings = new Settings(sampleRate, fftSize, stepSize, minFreq, maxFreq, offsetHz); @@ -58,11 +129,18 @@ public override string ToString() $"overlap: {settings.StepOverlapFrac * 100:N0}%"; } + /// + /// Set the colormap to use for future renders + /// public void SetColormap(Colormap cmap) { this.cmap = cmap ?? this.cmap; } + /// + /// Load a custom window kernel to multiply against each FFT sample prior to processing. + /// Windows must be at least the length of FftSize and typically have a sum of 1.0. + /// public void SetWindow(double[] newWindow) { if (newWindow.Length > settings.FftSize) @@ -84,6 +162,9 @@ public void AddCircular(float[] values) { } [Obsolete("use the Add() method", true)] public void AddScroll(float[] values) { } + /// + /// Load new data into the spectrogram generator + /// public void Add(IEnumerable audio, bool process = true) { newAudio.AddRange(audio); @@ -91,12 +172,26 @@ public void Add(IEnumerable audio, bool process = true) Process(); } + /// + /// The roll offset is used to calculate NextColumnIndex and can be set to a positive number + /// to begin adding new columns to the center of the spectrogram. + /// This can also be used to artificially move the next column index to zero even though some + /// data has already been accumulated. + /// private int rollOffset = 0; + + /// + /// Reset the next column index such that the next processed FFT will appear at the far left of the spectrogram. + /// + /// public void RollReset(int offset = 0) { rollOffset = -FftsProcessed + offset; } + /// + /// Perform FFT analysis on all unprocessed data + /// public double[][] Process() { if (FftsToProcess < 1) @@ -129,6 +224,10 @@ public double[][] Process() return newFfts; } + /// + /// Return a list of the mel-scaled FFTs contained in this spectrogram + /// + /// Total number of output bins to use. Choose a value significantly smaller than Height. public List GetMelFFTs(int melBinCount) { if (settings.FreqMin != 0) @@ -141,15 +240,44 @@ public List GetMelFFTs(int melBinCount) return fftsMel; } + /// + /// Create and return a spectrogram bitmap from the FFTs stored in memory. + /// + /// Multiply the output by a fixed value to change its brightness. + /// If true, output will be log-transformed. + /// If dB scaling is in use, this multiplier will be applied before log transformation. + /// Behavior of the spectrogram when it is full of data. + /// Roll (true) adds new columns on the left overwriting the oldest ones. + /// Scroll (false) slides the whole image to the left and adds new columns to the right. public Bitmap GetBitmap(double intensity = 1, bool dB = false, double dBScale = 1, bool roll = false) => Image.GetBitmap(ffts, cmap, intensity, dB, dBScale, roll, NextColumnIndex); + /// + /// Create a Mel-scaled spectrogram. + /// + /// Total number of output bins to use. Choose a value significantly smaller than Height. + /// Multiply the output by a fixed value to change its brightness. + /// If true, output will be log-transformed. + /// If dB scaling is in use, this multiplier will be applied before log transformation. + /// Behavior of the spectrogram when it is full of data. + /// Roll (true) adds new columns on the left overwriting the oldest ones. + /// Scroll (false) slides the whole image to the left and adds new columns to the right. public Bitmap GetBitmapMel(int melBinCount = 25, double intensity = 1, bool dB = false, double dBScale = 1, bool roll = false) => Image.GetBitmap(GetMelFFTs(melBinCount), cmap, intensity, dB, dBScale, roll, NextColumnIndex); [Obsolete("use SaveImage()", true)] public void SaveBitmap(Bitmap bmp, string fileName) { } + /// + /// Generate the spectrogram and save it as an image file. + /// + /// Path of the file to save. + /// Multiply the output by a fixed value to change its brightness. + /// If true, output will be log-transformed. + /// If dB scaling is in use, this multiplier will be applied before log transformation. + /// Behavior of the spectrogram when it is full of data. + /// Roll (true) adds new columns on the left overwriting the oldest ones. + /// Scroll (false) slides the whole image to the left and adds new columns to the right. public void SaveImage(string fileName, double intensity = 1, bool dB = false, double dBScale = 1, bool roll = false) { if (ffts.Count == 0) @@ -172,6 +300,15 @@ public void SaveImage(string fileName, double intensity = 1, bool dB = false, do Image.GetBitmap(ffts, cmap, intensity, dB, dBScale, roll, NextColumnIndex).Save(fileName, fmt); } + /// + /// Create and return a spectrogram bitmap from the FFTs stored in memory. + /// The output will be scaled-down vertically by binning according to a reduction factor and keeping the brightest pixel value in each bin. + /// + /// Multiply the output by a fixed value to change its brightness. + /// If true, output will be log-transformed. + /// If dB scaling is in use, this multiplier will be applied before log transformation. + /// Behavior of the spectrogram when it is full of data. + /// public Bitmap GetBitmapMax(double intensity = 1, bool dB = false, double dBScale = 1, bool roll = false, int reduction = 4) { List ffts2 = new List(); @@ -187,6 +324,9 @@ public Bitmap GetBitmapMax(double intensity = 1, bool dB = false, double dBScale return Image.GetBitmap(ffts2, cmap, intensity, dB, dBScale, roll, NextColumnIndex); } + /// + /// Export spectrogram data using the Spectrogram File Format (SFF) + /// public void SaveData(string filePath, int melBinCount = 0) { if (!filePath.EndsWith(".sff", StringComparison.OrdinalIgnoreCase)) @@ -194,7 +334,15 @@ public void SaveData(string filePath, int melBinCount = 0) new SFF(this, melBinCount).Save(filePath); } + /// + /// Defines the total number of FFTs (spectrogram columns) to store in memory. Determines Width. + /// private int fixedWidth = 0; + + /// + /// Configure the Spectrogram to maintain a fixed number of pixel columns. + /// Zeros will be added to padd existing data to achieve this width, and extra columns will be deleted. + /// public void SetFixedWidth(int width) { fixedWidth = width; @@ -214,11 +362,21 @@ private void PadOrTrimForFixedWidth() } } + /// + /// Get a vertical image containing ticks and tick labels for the frequency axis. + /// + /// size (pixels) + /// number to add to each tick label + /// length of each tick mark (pixels) + /// bin size for vertical data reduction public Bitmap GetVerticalScale(int width, int offsetHz = 0, int tickSize = 3, int reduction = 1) { return Scale.Vertical(width, settings, offsetHz, tickSize, reduction); } + /// + /// Return the vertical position (pixel units) for the given frequency + /// public int PixelY(double frequency, int reduction = 1) { int pixelsFromZeroHz = (int)(settings.PxPerHz * frequency / reduction); @@ -227,11 +385,18 @@ public int PixelY(double frequency, int reduction = 1) return pixelRow - 1; } + /// + /// Return a list of the FFTs in memory underlying the spectrogram + /// public List GetFFTs() { return ffts; } + /// + /// Return frequency and magnitude of the dominant frequency. + /// + /// If true, only the latest FFT will be assessed. public (double freqHz, double magRms) GetPeak(bool latestFft = true) { if (ffts.Count == 0) From dd9fb572e5a62993cae58a21b3bd74f06ec1fa44 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Sat, 4 Sep 2021 20:56:04 -0400 Subject: [PATCH 07/62] Tests: assert ReadWAV() length is accurate Uses Python's scipy.io.wavfile as a source of truth. #33 #34 --- dev/python/readwav.py | 16 ++++++++++++++++ src/Spectrogram.Tests/AudioFileTests.cs | 23 +++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 dev/python/readwav.py create mode 100644 src/Spectrogram.Tests/AudioFileTests.cs diff --git a/dev/python/readwav.py b/dev/python/readwav.py new file mode 100644 index 0000000..c53ba5a --- /dev/null +++ b/dev/python/readwav.py @@ -0,0 +1,16 @@ +""" +sample rate: 44100 +values: 166671 +value 12345: 4435 +""" +from scipy.io import wavfile +import pathlib +PATH_HERE = pathlib.Path(__file__).parent +PATH_DATA = PATH_HERE.joinpath("../../data") + +if __name__ == "__main__": + wavFilePath = PATH_DATA.joinpath("cant-do-that-44100.wav") + samplerate, data = wavfile.read(wavFilePath) + print(f"sample rate: {samplerate}") + print(f"values: {len(data)}") + print(f"value 12345: {data[12345]}") diff --git a/src/Spectrogram.Tests/AudioFileTests.cs b/src/Spectrogram.Tests/AudioFileTests.cs new file mode 100644 index 0000000..f4f4c22 --- /dev/null +++ b/src/Spectrogram.Tests/AudioFileTests.cs @@ -0,0 +1,23 @@ +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Spectrogram.Tests +{ + class AudioFileTests + { + /// + /// Compare values read from the WAV reader against those read by Python's SciPy module (see script in /dev folder) + /// + [Test] + public void Test_AudioFile_KnownValues() + { + (double[] audio, int sampleRate) = AudioFile.ReadWAV("../../../../../data/cant-do-that-44100.wav", multiplier: 32_000); + + Assert.AreEqual(44100, sampleRate); + Assert.AreEqual(166671, audio.Length); + Assert.AreEqual(4435, audio[12345], 1000); + } + } +} From a17a8c09f8af3c1f0361feb8e53e98c12da1bcc9 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Sat, 4 Sep 2021 20:56:26 -0400 Subject: [PATCH 08/62] Tests: AudioFile XML docs --- src/Spectrogram.Tests/AudioFile.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Spectrogram.Tests/AudioFile.cs b/src/Spectrogram.Tests/AudioFile.cs index ab6b197..99875c6 100644 --- a/src/Spectrogram.Tests/AudioFile.cs +++ b/src/Spectrogram.Tests/AudioFile.cs @@ -7,11 +7,15 @@ namespace Spectrogram.Tests { public static class AudioFile { + /// + /// Use NAudio to read the contents of a WAV file. + /// public static (double[] audio, int sampleRate) ReadWAV(string filePath, double multiplier = 16_000) { using var afr = new NAudio.Wave.AudioFileReader(filePath); int sampleRate = afr.WaveFormat.SampleRate; - int sampleCount = (int)(afr.Length / afr.WaveFormat.BitsPerSample / 8); + int bytesPerSample = afr.WaveFormat.BitsPerSample / 8; + int sampleCount = (int)afr.Length / bytesPerSample; int channelCount = afr.WaveFormat.Channels; var audio = new List(sampleCount); var buffer = new float[sampleRate * channelCount]; @@ -21,6 +25,9 @@ public static (double[] audio, int sampleRate) ReadWAV(string filePath, double m return (audio.ToArray(), sampleRate); } + /// + /// Use MP3Sharp to read the contents of an MP3 file. + /// public static double[] ReadMP3(string filePath, int bufferSize = 4096) { List audio = new List(); From 448282f716083072ecb3b5b6a1fdc853716774a8 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Sat, 4 Sep 2021 21:15:46 -0400 Subject: [PATCH 09/62] Tests: AudioFile test all WAV files #33 #34 --- dev/python/readwav.py | 9 ++++----- src/Spectrogram.Tests/AudioFileTests.cs | 16 ++++++++++------ 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/dev/python/readwav.py b/dev/python/readwav.py index c53ba5a..7515c8d 100644 --- a/dev/python/readwav.py +++ b/dev/python/readwav.py @@ -9,8 +9,7 @@ PATH_DATA = PATH_HERE.joinpath("../../data") if __name__ == "__main__": - wavFilePath = PATH_DATA.joinpath("cant-do-that-44100.wav") - samplerate, data = wavfile.read(wavFilePath) - print(f"sample rate: {samplerate}") - print(f"values: {len(data)}") - print(f"value 12345: {data[12345]}") + for wavFilePath in PATH_DATA.glob("*.wav"): + wavFilePath = PATH_DATA.joinpath(wavFilePath) + samplerate, data = wavfile.read(wavFilePath) + print(f"{wavFilePath.name}, {samplerate}, {len(data)}") diff --git a/src/Spectrogram.Tests/AudioFileTests.cs b/src/Spectrogram.Tests/AudioFileTests.cs index f4f4c22..60248fc 100644 --- a/src/Spectrogram.Tests/AudioFileTests.cs +++ b/src/Spectrogram.Tests/AudioFileTests.cs @@ -10,14 +10,18 @@ class AudioFileTests /// /// Compare values read from the WAV reader against those read by Python's SciPy module (see script in /dev folder) /// - [Test] - public void Test_AudioFile_KnownValues() + [TestCase("cant-do-that-44100.wav", 44_100, 166_671, 1)] + [TestCase("03-02-03-01-02-01-19.wav", 48_000, 214_615, 1)] + [TestCase("qrss-10min.wav", 6_000, 3_600_000, 1)] + [TestCase("cant-do-that-11025-stereo.wav", 11_025, 41668, 2)] + [TestCase("asehgal-original.wav", 40_000, 1_600_000, 1)] + public void Test_AudioFile_LengthAndSampleRate(string filename, int knownRate, int knownLength, int channels) { - (double[] audio, int sampleRate) = AudioFile.ReadWAV("../../../../../data/cant-do-that-44100.wav", multiplier: 32_000); + string filePath = $"../../../../../data/{filename}"; + (double[] audio, int sampleRate) = AudioFile.ReadWAV(filePath); - Assert.AreEqual(44100, sampleRate); - Assert.AreEqual(166671, audio.Length); - Assert.AreEqual(4435, audio[12345], 1000); + Assert.AreEqual(knownRate, sampleRate); + Assert.AreEqual(knownLength, audio.Length / channels); } } } From 817ae4692edfa7929a9b140a3866c3f938d2d861 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Sat, 4 Sep 2021 21:27:57 -0400 Subject: [PATCH 10/62] use original readme --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 63cfd98..83f9206 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ _"I'm sorry Dave... I'm afraid I can't do that"_ * Source code for the WAV reading method is at the bottom of this page. ```cs -(IEnumerable audio, int sampleRate) = ReadWAV("hal.wav"); +(double[] audio, int sampleRate) = ReadWAV("hal.wav"); var sg = new SpectrogramGenerator(sampleRate, fftSize: 4096, stepSize: 500, maxFreq: 3000); sg.Add(audio); sg.SaveImage("hal.png"); @@ -59,7 +59,7 @@ public Form1() Whenever an audio buffer gets filled, add the data to your Spectrogram: ```cs -private void GotNewBuffer(IEnumerable audio) +private void GotNewBuffer(double[] audio) { sg.Add(audio); } @@ -81,7 +81,7 @@ Review the source code of the demo application for additional details and consid 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). ```cs -(IEnumerable audio, int sampleRate) = ReadWAV("song.wav"); +(double[] audio, int sampleRate) = ReadWAV("song.wav"); int fftSize = 16384; int targetWidthPx = 3000; @@ -117,7 +117,7 @@ Spectrogram (2993, 817) These examples demonstrate the identical spectrogram analyzed with a variety of different colormaps. Spectrogram colormaps can be changed by calling the `SetColormap()` method: ```cs -(IEnumerable audio, int sampleRate) = ReadWAV("hal.wav"); +(double[] audio, int sampleRate) = ReadWAV("hal.wav"); var sg = new SpectrogramGenerator(sampleRate, fftSize: 8192, stepSize: 200, maxFreq: 3000); sg.Add(audio); sg.SetColormap(Colormap.Jet); @@ -141,7 +141,7 @@ Cropped Linear Scale (0-3kHz) | Mel Scale (0-22 kHz) Amplitude perception in humans, like frequency perception, is logarithmic. Therefore, Mel spectrograms typically display log-transformed spectral power and are presented using Decibel units. ```cs -(IEnumerable audio, int sampleRate) = ReadWAV("hal.wav"); +(double[] audio, int sampleRate) = ReadWAV("hal.wav"); var sg = new SpectrogramGenerator(sampleRate, fftSize: 4096, stepSize: 500, maxFreq: 3000); sg.Add(audio); @@ -166,7 +166,7 @@ SFF files be saved using `Complex` data format (with real and imaginary values f This example creates a spectrogram but saves it using the SFF file format instead of saving it as an image. The SFF file can then be read in any language. ```cs -(IEnumerable audio, int sampleRate) = ReadWAV("hal.wav"); +(double[] audio, int sampleRate) = ReadWAV("hal.wav"); var sg = new SpectrogramGenerator(sampleRate, fftSize: 4096, stepSize: 700, maxFreq: 2000); sg.Add(audio); sg.SaveData("hal.sff"); @@ -210,17 +210,17 @@ plt.show() 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. ```cs -(IEnumerable audio, int sampleRate) ReadWAV(string filePath, double multiplier = 16_000) +(double[] audio, int sampleRate) ReadWAV(string filePath, double multiplier = 16_000) { using var afr = new NAudio.Wave.AudioFileReader(filePath); int sampleRate = afr.WaveFormat.SampleRate; - int sampleCount = (int)(afr.Length / afr.WaveFormat.BitsPerSample) * 8; + int sampleCount = (int)(afr.Length / afr.WaveFormat.BitsPerSample / 8); int channelCount = afr.WaveFormat.Channels; var audio = new List(sampleCount); var buffer = new float[sampleRate * channelCount]; int samplesRead = 0; while ((samplesRead = afr.Read(buffer, 0, buffer.Length)) > 0) audio.AddRange(buffer.Take(samplesRead).Select(x => x * multiplier)); - return (audio, sampleRate); + return (audio.ToArray(), sampleRate); } ``` \ No newline at end of file From 59a466291493fabcfb2714f879dbe8221f1449a1 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Sat, 4 Sep 2021 21:29:16 -0400 Subject: [PATCH 11/62] readme: increase verbosity of ReadWAV() --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 83f9206..ccc1c1d 100644 --- a/README.md +++ b/README.md @@ -214,7 +214,8 @@ You should customize your file-reading method to suit your specific application. { using var afr = new NAudio.Wave.AudioFileReader(filePath); int sampleRate = afr.WaveFormat.SampleRate; - int sampleCount = (int)(afr.Length / afr.WaveFormat.BitsPerSample / 8); + int bytesPerSample = afr.WaveFormat.BitsPerSample / 8; + int sampleCount = (int)(afr.Length / bytesPerSample); int channelCount = afr.WaveFormat.Channels; var audio = new List(sampleCount); var buffer = new float[sampleRate * channelCount]; From 61908eba232c26500262dbc8831bc33590b3c430 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Sat, 4 Sep 2021 21:39:36 -0400 Subject: [PATCH 12/62] Readme: ReadWAV() -> ReadWavMono() Makes it more explicit that this sample function only works with mono WAV files. #33 --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ccc1c1d..94e2d79 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ _"I'm sorry Dave... I'm afraid I can't do that"_ * Source code for the WAV reading method is at the bottom of this page. ```cs -(double[] audio, int sampleRate) = ReadWAV("hal.wav"); +(double[] audio, int sampleRate) = ReadWavMono("hal.wav"); var sg = new SpectrogramGenerator(sampleRate, fftSize: 4096, stepSize: 500, maxFreq: 3000); sg.Add(audio); sg.SaveImage("hal.png"); @@ -81,7 +81,7 @@ Review the source code of the demo application for additional details and consid 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). ```cs -(double[] audio, int sampleRate) = ReadWAV("song.wav"); +(double[] audio, int sampleRate) = ReadWavMono("song.wav"); int fftSize = 16384; int targetWidthPx = 3000; @@ -117,7 +117,7 @@ Spectrogram (2993, 817) These examples demonstrate the identical spectrogram analyzed with a variety of different colormaps. Spectrogram colormaps can be changed by calling the `SetColormap()` method: ```cs -(double[] audio, int sampleRate) = ReadWAV("hal.wav"); +(double[] audio, int sampleRate) = ReadWavMono("hal.wav"); var sg = new SpectrogramGenerator(sampleRate, fftSize: 8192, stepSize: 200, maxFreq: 3000); sg.Add(audio); sg.SetColormap(Colormap.Jet); @@ -141,7 +141,7 @@ Cropped Linear Scale (0-3kHz) | Mel Scale (0-22 kHz) Amplitude perception in humans, like frequency perception, is logarithmic. Therefore, Mel spectrograms typically display log-transformed spectral power and are presented using Decibel units. ```cs -(double[] audio, int sampleRate) = ReadWAV("hal.wav"); +(double[] audio, int sampleRate) = ReadWavMono("hal.wav"); var sg = new SpectrogramGenerator(sampleRate, fftSize: 4096, stepSize: 500, maxFreq: 3000); sg.Add(audio); @@ -166,7 +166,7 @@ SFF files be saved using `Complex` data format (with real and imaginary values f This example creates a spectrogram but saves it using the SFF file format instead of saving it as an image. The SFF file can then be read in any language. ```cs -(double[] audio, int sampleRate) = ReadWAV("hal.wav"); +(double[] audio, int sampleRate) = ReadWavMono("hal.wav"); var sg = new SpectrogramGenerator(sampleRate, fftSize: 4096, stepSize: 700, maxFreq: 2000); sg.Add(audio); sg.SaveData("hal.sff"); @@ -210,7 +210,7 @@ plt.show() 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. ```cs -(double[] audio, int sampleRate) ReadWAV(string filePath, double multiplier = 16_000) +(double[] audio, int sampleRate) ReadWavMono(string filePath, double multiplier = 16_000) { using var afr = new NAudio.Wave.AudioFileReader(filePath); int sampleRate = afr.WaveFormat.SampleRate; From d8eb877a818add5f99ab2d7f08926975d29e8dbc Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Sat, 4 Sep 2021 21:48:34 -0400 Subject: [PATCH 13/62] NuGet: use new readme format --- src/Spectrogram/README.md | 36 ++++++++++++++++++++++++++++++ src/Spectrogram/Spectrogram.csproj | 6 ++--- 2 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 src/Spectrogram/README.md diff --git a/src/Spectrogram/README.md b/src/Spectrogram/README.md new file mode 100644 index 0000000..959faa7 --- /dev/null +++ b/src/Spectrogram/README.md @@ -0,0 +1,36 @@ +**Spectrogram is a .NET library for creating frequency spectrograms from pre-recorded signals, streaming data, or microphone audio from the sound card.** Spectrogram uses FFT algorithms and window functions provided by the [FftSharp](https://github.com/swharden/FftSharp) project, and it targets .NET Standard so it can be used in .NET Framework and .NET Core projects. + +[![](https://raw.githubusercontent.com/swharden/Spectrogram/master/dev/graphics/hal-spectrogram.png)](https://github.com/swharden/Spectrogram) + +## Quickstart + +```cs +(double[] audio, int sampleRate) = ReadWavMono("hal.wav"); +var sg = new SpectrogramGenerator(sampleRate, fftSize: 4096, stepSize: 500, maxFreq: 3000); +sg.Add(audio); +sg.SaveImage("hal.png"); +``` + +This example generates the image at the top of the page. + + +## How to Read a WAV File + +There are many excellent libraries that read audio files. Consult the documentation _for those libraries_ to learn how to do this well. Here's an example method I use to read audio values from mono WAV files using the NAudio package: + +```cs +(double[] audio, int sampleRate) ReadWavMono(string filePath, double multiplier = 16_000) +{ + using var afr = new NAudio.Wave.AudioFileReader(filePath); + int sampleRate = afr.WaveFormat.SampleRate; + int bytesPerSample = afr.WaveFormat.BitsPerSample / 8; + int sampleCount = (int)(afr.Length / bytesPerSample); + int channelCount = afr.WaveFormat.Channels; + var audio = new List(sampleCount); + var buffer = new float[sampleRate * channelCount]; + int samplesRead = 0; + while ((samplesRead = afr.Read(buffer, 0, buffer.Length)) > 0) + audio.AddRange(buffer.Take(samplesRead).Select(x => x * multiplier)); + return (audio.ToArray(), sampleRate); +} +``` \ No newline at end of file diff --git a/src/Spectrogram/Spectrogram.csproj b/src/Spectrogram/Spectrogram.csproj index c02aa12..bdd9792 100644 --- a/src/Spectrogram/Spectrogram.csproj +++ b/src/Spectrogram/Spectrogram.csproj @@ -22,10 +22,8 @@ Release Notes: https://github.com/swharden/Spectrogram/releases - - True - - + + From cbf31412a18b4f13e54610af0f3ac4fc2c5a1e99 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Sat, 4 Sep 2021 21:49:21 -0400 Subject: [PATCH 14/62] NuGet: System.Drawing.Common 5.0.0->4.6.1 --- src/Spectrogram/Spectrogram.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Spectrogram/Spectrogram.csproj b/src/Spectrogram/Spectrogram.csproj index bdd9792..29c0a9a 100644 --- a/src/Spectrogram/Spectrogram.csproj +++ b/src/Spectrogram/Spectrogram.csproj @@ -20,7 +20,7 @@ Release Notes: https://github.com/swharden/Spectrogram/releases - + From 69a68cabf553d9a817ed74d7cbdedd7bce3a458f Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Sat, 4 Sep 2021 21:49:32 -0400 Subject: [PATCH 15/62] NuGet: FftSharp 1.0.8->1.0.9 --- src/Spectrogram/Spectrogram.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Spectrogram/Spectrogram.csproj b/src/Spectrogram/Spectrogram.csproj index 29c0a9a..d22dea9 100644 --- a/src/Spectrogram/Spectrogram.csproj +++ b/src/Spectrogram/Spectrogram.csproj @@ -19,7 +19,7 @@ Release Notes: https://github.com/swharden/Spectrogram/releases - + From 562f76ea4d2b57b2d6be587a7e902c1aeb6d3b8f Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Sat, 4 Sep 2021 21:50:04 -0400 Subject: [PATCH 16/62] NuGet: modernize package information --- src/Spectrogram/Spectrogram.csproj | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Spectrogram/Spectrogram.csproj b/src/Spectrogram/Spectrogram.csproj index d22dea9..e917399 100644 --- a/src/Spectrogram/Spectrogram.csproj +++ b/src/Spectrogram/Spectrogram.csproj @@ -11,11 +11,9 @@ icon.png https://github.com/swharden/Spectrogram spectrogram spectrum fft frequency audio microphone signal - Quickstart: https://github.com/swharden/Spectrogram -Release Notes: https://github.com/swharden/Spectrogram/releases + https://github.com/swharden/Spectrogram/releases true - true - snupkg + true From c7cb5af03c27fdff7e927f3a44d0d60847907d39 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Sat, 4 Sep 2021 22:01:17 -0400 Subject: [PATCH 17/62] replace SetColormap() with a public property --- src/Spectrogram.MicrophoneDemo/FormMicrophone.cs | 2 +- src/Spectrogram.Tests/ColormapExamples.cs | 2 +- src/Spectrogram/SpectrogramGenerator.cs | 7 ++++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Spectrogram.MicrophoneDemo/FormMicrophone.cs b/src/Spectrogram.MicrophoneDemo/FormMicrophone.cs index 1ff9a7a..e6b4964 100644 --- a/src/Spectrogram.MicrophoneDemo/FormMicrophone.cs +++ b/src/Spectrogram.MicrophoneDemo/FormMicrophone.cs @@ -105,7 +105,7 @@ private void timer1_Tick(object sender, EventArgs e) private void cbColormap_SelectedIndexChanged(object sender, EventArgs e) { - spec.SetColormap(cmaps[cbColormap.SelectedIndex]); + spec.Colormap = cmaps[cbColormap.SelectedIndex]; } private void btnResetRoll_Click(object sender, EventArgs e) diff --git a/src/Spectrogram.Tests/ColormapExamples.cs b/src/Spectrogram.Tests/ColormapExamples.cs index b484119..650ca3c 100644 --- a/src/Spectrogram.Tests/ColormapExamples.cs +++ b/src/Spectrogram.Tests/ColormapExamples.cs @@ -21,7 +21,7 @@ public void Test_Make_CommonColormaps() foreach (var cmap in Colormap.GetColormaps()) { - spec.SetColormap(cmap); + spec.Colormap = cmap; spec.SaveImage($"../../../../../dev/graphics/hal-{cmap.Name}.png"); Debug.WriteLine($"![](dev/graphics/hal-{cmap.Name}.png)"); } diff --git a/src/Spectrogram/SpectrogramGenerator.cs b/src/Spectrogram/SpectrogramGenerator.cs index 3dad42d..98f9fb9 100644 --- a/src/Spectrogram/SpectrogramGenerator.cs +++ b/src/Spectrogram/SpectrogramGenerator.cs @@ -79,6 +79,10 @@ public class SpectrogramGenerator private readonly List ffts = new List(); private readonly List newAudio; private Colormap cmap = Colormap.Viridis; + /// + /// Colormap to use when generating future FFTs. + /// + public Colormap Colormap = Colormap.Viridis; /// /// Instantiate a spectrogram generator. @@ -129,12 +133,13 @@ public override string ToString() $"overlap: {settings.StepOverlapFrac * 100:N0}%"; } + [Obsolete("Assign to the Colormap field")] /// /// Set the colormap to use for future renders /// public void SetColormap(Colormap cmap) { - this.cmap = cmap ?? this.cmap; + Colormap = cmap ?? this.Colormap; } /// From 2f7fdec5ce32c5360c0c636a353c6355682f22d8 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Sat, 4 Sep 2021 22:01:26 -0400 Subject: [PATCH 18/62] improve obsolete message --- src/Spectrogram/Spectrogram.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Spectrogram/Spectrogram.cs b/src/Spectrogram/Spectrogram.cs index a28d026..9844130 100644 --- a/src/Spectrogram/Spectrogram.cs +++ b/src/Spectrogram/Spectrogram.cs @@ -2,7 +2,7 @@ namespace Spectrogram { - [Obsolete("it has been renamed to SpectrogramGenerator")] + [Obsolete("This class has been replaced by SpectrogramGenerator")] public class Spectrogram : SpectrogramGenerator { public Spectrogram(int sampleRate, int fftSize, int stepSize, double minFreq = 0, double maxFreq = double.PositiveInfinity, int? fixedWidth = null, int offsetHz = 0) : From c6152b6aa7e7620237c31625182e724f1d8e6022 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Sat, 4 Sep 2021 22:02:37 -0400 Subject: [PATCH 19/62] improve property access --- src/Spectrogram/SpectrogramGenerator.cs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Spectrogram/SpectrogramGenerator.cs b/src/Spectrogram/SpectrogramGenerator.cs index 98f9fb9..46df823 100644 --- a/src/Spectrogram/SpectrogramGenerator.cs +++ b/src/Spectrogram/SpectrogramGenerator.cs @@ -12,32 +12,32 @@ public class SpectrogramGenerator /// /// Number of pixel columns (FFT samples) in the spectrogram image /// - public int Width { get { return ffts.Count; } } + public int Width { get => FFTs.Count; } /// /// Number of pixel rows (frequency bins) in the spectrogram image /// - public int Height { get { return settings.Height; } } + public int Height { get => Settings.Height; } /// /// Number of samples to use for each FFT (must be a power of 2) /// - public int FftSize { get { return settings.FftSize; } } + public int FftSize { get => Settings.FftSize; } /// /// Vertical resolution (frequency bin size depends on FftSize and SampleRate) /// - public double HzPerPx { get { return settings.HzPerPixel; } } + public double HzPerPx { get => Settings.HzPerPixel; } /// /// Horizontal resolution (seconds per pixel depends on StepSize) /// - public double SecPerPx { get { return settings.StepLengthSec; } } + public double SecPerPx { get => Settings.StepLengthSec; } /// /// Number of FFTs that remain to be processed for data which has been added but not yet analuyzed /// - public int FftsToProcess { get { return (newAudio.Count - settings.FftSize) / settings.StepSize; } } + public int FftsToProcess { get => (UnprocessedData.Count - Settings.FftSize) / Settings.StepSize; } /// /// Total number of FFT steps processed @@ -47,28 +47,28 @@ public class SpectrogramGenerator /// /// Index of the pixel column which will be populated next. Location of vertical line for wrap-around displays. /// - public int NextColumnIndex { get { return (FftsProcessed + rollOffset) % Width; } } + public int NextColumnIndex { get => (FftsProcessed + rollOffset) % Width; } /// /// This value is added to displayed frequency axis tick labels /// - public int OffsetHz { get { return settings.OffsetHz; } set { settings.OffsetHz = value; } } + public int OffsetHz { get => Settings.OffsetHz; set { Settings.OffsetHz = value; } } /// /// Number of samples per second /// - public int SampleRate { get { return settings.SampleRate; } } + public int SampleRate { get => Settings.SampleRate; } /// /// Number of samples to step forward after each FFT is processed. /// This value controls the horizontal resolution of the spectrogram. /// - public int StepSize { get { return settings.StepSize; } } + public int StepSize { get => Settings.StepSize; } /// /// The spectrogram is trimmed to cut-off frequencies below this value. /// - public double FreqMax { get { return settings.FreqMax; } } + public double FreqMax { get => Settings.FreqMax; } /// /// The spectrogram is trimmed to cut-off frequencies above this value. From 7798cd544a372b938c5c3b5d3a118dc6d1fe5e77 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Sat, 4 Sep 2021 22:02:57 -0400 Subject: [PATCH 20/62] capitalize all field names --- src/Spectrogram/SpectrogramGenerator.cs | 116 ++++++++++++++---------- 1 file changed, 66 insertions(+), 50 deletions(-) diff --git a/src/Spectrogram/SpectrogramGenerator.cs b/src/Spectrogram/SpectrogramGenerator.cs index 46df823..b86106b 100644 --- a/src/Spectrogram/SpectrogramGenerator.cs +++ b/src/Spectrogram/SpectrogramGenerator.cs @@ -73,12 +73,27 @@ public class SpectrogramGenerator /// /// The spectrogram is trimmed to cut-off frequencies above this value. /// - public double FreqMin { get { return settings.FreqMin; } } + public double FreqMin { get => Settings.FreqMin; } + + /// + /// This module contains detailed FFT/Spectrogram settings + /// + private readonly Settings Settings; + + /// + /// This is the list of FFTs which is translated to the spectrogram image when it is requested. + /// The length of this list is the spectrogram width. + /// The length of the arrays in this list is the spectrogram height. + /// + private readonly List FFTs = new List(); + + /// + /// This list contains data values which have not yet been processed. + /// Process() processes all unprocessed data. + /// This list may not be empty after processing if there aren't enough values to fill a full FFT (FftSize). + /// + private readonly List UnprocessedData; - private readonly Settings settings; - private readonly List ffts = new List(); - private readonly List newAudio; - private Colormap cmap = Colormap.Viridis; /// /// Colormap to use when generating future FFTs. /// @@ -107,9 +122,9 @@ public SpectrogramGenerator( int offsetHz = 0, List initialAudioList = null) { - settings = new Settings(sampleRate, fftSize, stepSize, minFreq, maxFreq, offsetHz); + Settings = new Settings(sampleRate, fftSize, stepSize, minFreq, maxFreq, offsetHz); - newAudio = initialAudioList ?? new List(); + UnprocessedData = initialAudioList ?? new List(); if (fixedWidth.HasValue) SetFixedWidth(fixedWidth.Value); @@ -117,20 +132,20 @@ public SpectrogramGenerator( public override string ToString() { - double processedSamples = ffts.Count * settings.StepSize + settings.FftSize; - double processedSec = processedSamples / settings.SampleRate; + double processedSamples = FFTs.Count * Settings.StepSize + Settings.FftSize; + double processedSec = processedSamples / Settings.SampleRate; string processedTime = (processedSec < 60) ? $"{processedSec:N2} sec" : $"{processedSec / 60.0:N2} min"; return $"Spectrogram ({Width}, {Height})" + $"\n Vertical ({Height} px): " + - $"{settings.FreqMin:N0} - {settings.FreqMax:N0} Hz, " + - $"FFT size: {settings.FftSize:N0} samples, " + - $"{settings.HzPerPixel:N2} Hz/px" + + $"{Settings.FreqMin:N0} - {Settings.FreqMax:N0} Hz, " + + $"FFT size: {Settings.FftSize:N0} samples, " + + $"{Settings.HzPerPixel:N2} Hz/px" + $"\n Horizontal ({Width} px): " + $"{processedTime}, " + - $"window: {settings.FftLengthSec:N2} sec, " + - $"step: {settings.StepLengthSec:N2} sec, " + - $"overlap: {settings.StepOverlapFrac * 100:N0}%"; + $"window: {Settings.FftLengthSec:N2} sec, " + + $"step: {Settings.StepLengthSec:N2} sec, " + + $"overlap: {Settings.StepOverlapFrac * 100:N0}%"; } [Obsolete("Assign to the Colormap field")] @@ -148,14 +163,14 @@ public void SetColormap(Colormap cmap) /// public void SetWindow(double[] newWindow) { - if (newWindow.Length > settings.FftSize) + if (newWindow.Length > Settings.FftSize) throw new ArgumentException("window length cannot exceed FFT size"); - for (int i = 0; i < settings.FftSize; i++) - settings.Window[i] = 0; + for (int i = 0; i < Settings.FftSize; i++) + Settings.Window[i] = 0; - int offset = (settings.FftSize - newWindow.Length) / 2; - Array.Copy(newWindow, 0, settings.Window, offset, newWindow.Length); + int offset = (Settings.FftSize - newWindow.Length) / 2; + Array.Copy(newWindow, 0, Settings.Window, offset, newWindow.Length); } [Obsolete("use the Add() method", true)] @@ -172,7 +187,7 @@ public void AddScroll(float[] values) { } /// public void Add(IEnumerable audio, bool process = true) { - newAudio.AddRange(audio); + UnprocessedData.AddRange(audio); if (process) Process(); } @@ -207,23 +222,23 @@ public double[][] Process() Parallel.For(0, newFftCount, newFftIndex => { - FftSharp.Complex[] buffer = new FftSharp.Complex[settings.FftSize]; - int sourceIndex = newFftIndex * settings.StepSize; - for (int i = 0; i < settings.FftSize; i++) - buffer[i].Real = newAudio[sourceIndex + i] * settings.Window[i]; + FftSharp.Complex[] buffer = new FftSharp.Complex[Settings.FftSize]; + int sourceIndex = newFftIndex * Settings.StepSize; + for (int i = 0; i < Settings.FftSize; i++) + buffer[i].Real = UnprocessedData[sourceIndex + i] * Settings.Window[i]; FftSharp.Transform.FFT(buffer); - newFfts[newFftIndex] = new double[settings.Height]; - for (int i = 0; i < settings.Height; i++) - newFfts[newFftIndex][i] = buffer[settings.FftIndex1 + i].Magnitude / settings.FftSize; + newFfts[newFftIndex] = new double[Settings.Height]; + for (int i = 0; i < Settings.Height; i++) + newFfts[newFftIndex][i] = buffer[Settings.FftIndex1 + i].Magnitude / Settings.FftSize; }); foreach (var newFft in newFfts) - ffts.Add(newFft); + FFTs.Add(newFft); FftsProcessed += newFfts.Length; - newAudio.RemoveRange(0, newFftCount * settings.StepSize); + UnprocessedData.RemoveRange(0, newFftCount * Settings.StepSize); PadOrTrimForFixedWidth(); return newFfts; @@ -235,11 +250,11 @@ public double[][] Process() /// Total number of output bins to use. Choose a value significantly smaller than Height. public List GetMelFFTs(int melBinCount) { - if (settings.FreqMin != 0) + if (Settings.FreqMin != 0) throw new InvalidOperationException("cannot get Mel spectrogram unless minimum frequency is 0Hz"); var fftsMel = new List(); - foreach (var fft in ffts) + foreach (var fft in FFTs) fftsMel.Add(FftSharp.Transform.MelScale(fft, SampleRate, melBinCount)); return fftsMel; @@ -255,7 +270,7 @@ public List GetMelFFTs(int melBinCount) /// Roll (true) adds new columns on the left overwriting the oldest ones. /// Scroll (false) slides the whole image to the left and adds new columns to the right. public Bitmap GetBitmap(double intensity = 1, bool dB = false, double dBScale = 1, bool roll = false) => - Image.GetBitmap(ffts, cmap, intensity, dB, dBScale, roll, NextColumnIndex); + Image.GetBitmap(FFTs, Colormap, intensity, dB, dBScale, roll, NextColumnIndex); /// /// Create a Mel-scaled spectrogram. @@ -268,7 +283,7 @@ public Bitmap GetBitmap(double intensity = 1, bool dB = false, double dBScale = /// Roll (true) adds new columns on the left overwriting the oldest ones. /// Scroll (false) slides the whole image to the left and adds new columns to the right. public Bitmap GetBitmapMel(int melBinCount = 25, double intensity = 1, bool dB = false, double dBScale = 1, bool roll = false) => - Image.GetBitmap(GetMelFFTs(melBinCount), cmap, intensity, dB, dBScale, roll, NextColumnIndex); + Image.GetBitmap(GetMelFFTs(melBinCount), Colormap, intensity, dB, dBScale, roll, NextColumnIndex); [Obsolete("use SaveImage()", true)] public void SaveBitmap(Bitmap bmp, string fileName) { } @@ -285,7 +300,7 @@ public void SaveBitmap(Bitmap bmp, string fileName) { } /// Scroll (false) slides the whole image to the left and adds new columns to the right. public void SaveImage(string fileName, double intensity = 1, bool dB = false, double dBScale = 1, bool roll = false) { - if (ffts.Count == 0) + if (FFTs.Count == 0) throw new InvalidOperationException("Spectrogram contains no data. Use Add() to add signal data."); string extension = Path.GetExtension(fileName).ToLower(); @@ -302,7 +317,7 @@ public void SaveImage(string fileName, double intensity = 1, bool dB = false, do else throw new ArgumentException("unknown file extension"); - Image.GetBitmap(ffts, cmap, intensity, dB, dBScale, roll, NextColumnIndex).Save(fileName, fmt); + Image.GetBitmap(FFTs, Colormap, intensity, dB, dBScale, roll, NextColumnIndex).Save(fileName, fmt); } /// @@ -317,16 +332,16 @@ public void SaveImage(string fileName, double intensity = 1, bool dB = false, do public Bitmap GetBitmapMax(double intensity = 1, bool dB = false, double dBScale = 1, bool roll = false, int reduction = 4) { List ffts2 = new List(); - for (int i = 0; i < ffts.Count; i++) + for (int i = 0; i < FFTs.Count; i++) { - double[] d1 = ffts[i]; + double[] d1 = FFTs[i]; double[] d2 = new double[d1.Length / reduction]; for (int j = 0; j < d2.Length; j++) for (int k = 0; k < reduction; k++) d2[j] = Math.Max(d2[j], d1[j * reduction + k]); ffts2.Add(d2); } - return Image.GetBitmap(ffts2, cmap, intensity, dB, dBScale, roll, NextColumnIndex); + return Image.GetBitmap(ffts2, Colormap, intensity, dB, dBScale, roll, NextColumnIndex); } /// @@ -360,10 +375,10 @@ private void PadOrTrimForFixedWidth() { int overhang = Width - fixedWidth; if (overhang > 0) - ffts.RemoveRange(0, overhang); + FFTs.RemoveRange(0, overhang); - while (ffts.Count < fixedWidth) - ffts.Insert(0, new double[Height]); + while (FFTs.Count < fixedWidth) + FFTs.Insert(0, new double[Height]); } } @@ -376,7 +391,7 @@ private void PadOrTrimForFixedWidth() /// bin size for vertical data reduction public Bitmap GetVerticalScale(int width, int offsetHz = 0, int tickSize = 3, int reduction = 1) { - return Scale.Vertical(width, settings, offsetHz, tickSize, reduction); + return Scale.Vertical(width, Settings, offsetHz, tickSize, reduction); } /// @@ -384,18 +399,19 @@ public Bitmap GetVerticalScale(int width, int offsetHz = 0, int tickSize = 3, in /// public int PixelY(double frequency, int reduction = 1) { - int pixelsFromZeroHz = (int)(settings.PxPerHz * frequency / reduction); - int pixelsFromMinFreq = pixelsFromZeroHz - settings.FftIndex1 / reduction + 1; - int pixelRow = settings.Height / reduction - 1 - pixelsFromMinFreq; + int pixelsFromZeroHz = (int)(Settings.PxPerHz * frequency / reduction); + int pixelsFromMinFreq = pixelsFromZeroHz - Settings.FftIndex1 / reduction + 1; + int pixelRow = Settings.Height / reduction - 1 - pixelsFromMinFreq; return pixelRow - 1; } /// - /// Return a list of the FFTs in memory underlying the spectrogram + /// Return the list of FFTs in memory underlying the spectrogram. + /// This list may continue to evolve after it is returned. /// public List GetFFTs() { - return ffts; + return FFTs; } /// @@ -404,13 +420,13 @@ public List GetFFTs() /// If true, only the latest FFT will be assessed. public (double freqHz, double magRms) GetPeak(bool latestFft = true) { - if (ffts.Count == 0) + if (FFTs.Count == 0) return (double.NaN, double.NaN); if (latestFft == false) throw new NotImplementedException("peak of mean of all FFTs not yet supported"); - double[] freqs = ffts[ffts.Count - 1]; + double[] freqs = FFTs[FFTs.Count - 1]; int peakIndex = 0; double peakMagnitude = 0; From cc798c6c2b73761f5d0597893f49b3cb73e256e8 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Sat, 4 Sep 2021 22:03:06 -0400 Subject: [PATCH 21/62] Spectrogram 1.4.0 --- src/Spectrogram/Spectrogram.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Spectrogram/Spectrogram.csproj b/src/Spectrogram/Spectrogram.csproj index e917399..9548df7 100644 --- a/src/Spectrogram/Spectrogram.csproj +++ b/src/Spectrogram/Spectrogram.csproj @@ -2,7 +2,7 @@ netstandard2.0 - 1.3.0 + 1.4.0 A .NET Standard library for creating spectrograms Scott Harden Harden Technologies, LLC From e2eb322846306162a589f2649304369d9a913644 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Sat, 4 Sep 2021 22:14:24 -0400 Subject: [PATCH 22/62] Spectrogram 1.4.1 use new readme format in NuGet description --- src/Spectrogram/Spectrogram.csproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Spectrogram/Spectrogram.csproj b/src/Spectrogram/Spectrogram.csproj index 9548df7..a2897c4 100644 --- a/src/Spectrogram/Spectrogram.csproj +++ b/src/Spectrogram/Spectrogram.csproj @@ -2,7 +2,7 @@ netstandard2.0 - 1.4.0 + 1.4.1 A .NET Standard library for creating spectrograms Scott Harden Harden Technologies, LLC @@ -14,6 +14,7 @@ https://github.com/swharden/Spectrogram/releases true true + README.md From f79903c24a29126fdc7d92f352e6b9d85218c424 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Thu, 7 Oct 2021 21:25:03 -0400 Subject: [PATCH 23/62] Update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 3898b62..a2796a9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.vscode + ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## From c3a6657849ba497c9b98beba395751abad2ca5a1 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Thu, 7 Oct 2021 21:25:50 -0400 Subject: [PATCH 24/62] Package: deterministic build with SourceLink --- src/Spectrogram/Spectrogram.csproj | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/Spectrogram/Spectrogram.csproj b/src/Spectrogram/Spectrogram.csproj index a2897c4..c245610 100644 --- a/src/Spectrogram/Spectrogram.csproj +++ b/src/Spectrogram/Spectrogram.csproj @@ -15,14 +15,27 @@ true true README.md + portable + true + + true + true + true - - - + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + From 966dc62c970cb70ff333b6e94500729f28c924bd Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Thu, 7 Oct 2021 21:28:24 -0400 Subject: [PATCH 25/62] CI: Azure Pipelines -> GitHub Actions --- .github/workflows/ci.yaml | 72 +++++++++++++++++++++++++++++++++++ dev/build/azure-pipelines.yml | 68 --------------------------------- 2 files changed, 72 insertions(+), 68 deletions(-) create mode 100644 .github/workflows/ci.yaml delete mode 100644 dev/build/azure-pipelines.yml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..458df3c --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,72 @@ +name: CI + +on: + workflow_dispatch: + push: + branches: + - master + pull_request: + types: [opened, synchronize, reopened] + release: + types: + - created + +jobs: + package: + # build, test, and deploy the core package on Linux + name: Package + runs-on: ubuntu-latest + env: + CSPROJ_CORE: src/Spectrogram/Spectrogram.csproj + CSPROJ_TESTS: src/Spectrogram.Tests/Spectrogram.Tests.csproj + steps: + - name: 🛒 Checkout + uses: actions/checkout@v2 + - name: ✨ Setup .NET 6 + uses: actions/setup-dotnet@v1 + with: + dotnet-version: "6.0.x" + include-prerelease: true + - name: 🚚 Restore + run: | + dotnet restore ${{ env.CSPROJ_CORE }} + dotnet restore ${{ env.CSPROJ_TESTS }} + - name: 🛠️ Build + run: | + dotnet build ${{ env.CSPROJ_CORE }} --configuration Release + dotnet build ${{ env.CSPROJ_TESTS }} + - name: 🧪 Test + run: dotnet test ${{ env.CSPROJ_TESTS }} + - name: 📦 Pack + run: dotnet pack ${{ env.CSPROJ_CORE }} --configuration Release + - name: 💾 Store + uses: actions/upload-artifact@v2 + with: + name: Packages + retention-days: 1 + path: src/Spectrogram/bin/Release/*.nupkg + - name: 🛠️ Setup NuGet + if: github.event_name == 'release' + uses: nuget/setup-nuget@v1 + with: + nuget-api-key: ${{ secrets.NUGET_API_KEY }} + - name: 🚀 Publish + if: github.event_name == 'release' + run: nuget push "src/Spectrogram/bin/Release/*.nupkg" -SkipDuplicate -Source https://api.nuget.org/v3/index.json + + solution: + # build the whole solution (.NET Framework demos require Windows/MSBuild) + name: Build Solution + runs-on: windows-latest + steps: + - name: 🛒 Checkout + uses: actions/checkout@v1 + - name: ✨ Setup NuGet + uses: nuget/setup-nuget@v1 + - name: ✨ Setup MSBuild + uses: microsoft/setup-msbuild@v1.0.3 + - name: 🚚 Restore + working-directory: src + run: nuget restore + - name: 🛠️ Build Release + run: msbuild src -verbosity:minimal \ No newline at end of file diff --git a/dev/build/azure-pipelines.yml b/dev/build/azure-pipelines.yml deleted file mode 100644 index 9477383..0000000 --- a/dev/build/azure-pipelines.yml +++ /dev/null @@ -1,68 +0,0 @@ -trigger: -- master - -strategy: - matrix: - - 'Build and Test on MacOS': - purpose: 'library' - imageName: 'macOS-latest' - - 'Build and Test on Linux': - purpose: 'library' - imageName: 'ubuntu-latest' - - 'Build and Test on Windows': - purpose: 'library' - imageName: 'windows-latest' - - 'Rebuild Solution on Windows': - purpose: 'solution' - imageName: 'windows-latest' - -pool: - vmImage: $(imageName) - -steps: - -### INSTALL NUGET AND RESTORE PACKAGES - -- task: NuGetToolInstaller@1 - displayName: 'Install NuGet' - -- task: NuGetCommand@2 - displayName: 'Restore packages' - inputs: - restoreSolution: 'src/Spectrogram.sln' - -### BUILD THE CORE LIBRARY AND RUN TESTS - -- task: DotNetCoreCLI@2 - displayName: 'Build Spectrogram' - condition: eq(variables['purpose'], 'library') - inputs: - command: 'build' - projects: 'src/Spectrogram/Spectrogram.csproj' - -- task: DotNetCoreCLI@2 - displayName: 'Build Tests' - condition: eq(variables['purpose'], 'library') - inputs: - command: 'build' - projects: 'src/Spectrogram.Tests/Spectrogram.Tests.csproj' - -- task: DotNetCoreCLI@2 - displayName: 'Run Tests' - condition: eq(variables['purpose'], 'library') - inputs: - command: test - projects: 'src/Spectrogram.Tests/Spectrogram.Tests.csproj' - -### REBUILD FULL SOLUTION - -- task: VSBuild@1 - displayName: 'Build Release (Windows)' - condition: eq(variables['purpose'], 'solution') - inputs: - solution: 'src/Spectrogram.sln' - configuration: 'release' \ No newline at end of file From bc73f137a3c83bc51af37c8f002baaeecb4fded4 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Thu, 7 Oct 2021 21:32:25 -0400 Subject: [PATCH 26/62] Tests: run tests on .NET 5 --- src/Spectrogram.Tests/Spectrogram.Tests.csproj | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Spectrogram.Tests/Spectrogram.Tests.csproj b/src/Spectrogram.Tests/Spectrogram.Tests.csproj index 5f8193f..c3a8b6a 100644 --- a/src/Spectrogram.Tests/Spectrogram.Tests.csproj +++ b/src/Spectrogram.Tests/Spectrogram.Tests.csproj @@ -1,8 +1,7 @@ - netcoreapp3.1 - + net5.0 false From 01e833320cb094f65a94c5cada9924d58c9990b0 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Thu, 7 Oct 2021 21:39:21 -0400 Subject: [PATCH 27/62] Tests: test on .NET Core 3.1 --- src/Spectrogram.Tests/Spectrogram.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Spectrogram.Tests/Spectrogram.Tests.csproj b/src/Spectrogram.Tests/Spectrogram.Tests.csproj index c3a8b6a..2777423 100644 --- a/src/Spectrogram.Tests/Spectrogram.Tests.csproj +++ b/src/Spectrogram.Tests/Spectrogram.Tests.csproj @@ -1,7 +1,7 @@ - net5.0 + netcoreapp3.1 false From c79ded70e5dd6c96fbe1fdce31d983efa9e1e955 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Thu, 7 Oct 2021 21:39:28 -0400 Subject: [PATCH 28/62] CI: test on Windows --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 458df3c..19521eb 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -15,7 +15,7 @@ jobs: package: # build, test, and deploy the core package on Linux name: Package - runs-on: ubuntu-latest + runs-on: windows-latest env: CSPROJ_CORE: src/Spectrogram/Spectrogram.csproj CSPROJ_TESTS: src/Spectrogram.Tests/Spectrogram.Tests.csproj From e2ca8d90df7c9887f14b78a6401423992d1d7673 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Thu, 7 Oct 2021 21:42:37 -0400 Subject: [PATCH 29/62] CI: setup .NET Core 3.1 --- .github/workflows/ci.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 19521eb..4c069fa 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -15,13 +15,17 @@ jobs: package: # build, test, and deploy the core package on Linux name: Package - runs-on: windows-latest + runs-on: ubuntu-latest env: CSPROJ_CORE: src/Spectrogram/Spectrogram.csproj CSPROJ_TESTS: src/Spectrogram.Tests/Spectrogram.Tests.csproj steps: - name: 🛒 Checkout uses: actions/checkout@v2 + - name: ✨ Setup .NET Core 3.1 + uses: actions/setup-dotnet@v1 + with: + dotnet-version: "3.1.x" - name: ✨ Setup .NET 6 uses: actions/setup-dotnet@v1 with: From 178bf43e90559140fab0703462f6440e95e4856d Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Thu, 7 Oct 2021 21:47:56 -0400 Subject: [PATCH 30/62] Spectrogram 1.4.2 --- src/Spectrogram/Spectrogram.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Spectrogram/Spectrogram.csproj b/src/Spectrogram/Spectrogram.csproj index c245610..f36c533 100644 --- a/src/Spectrogram/Spectrogram.csproj +++ b/src/Spectrogram/Spectrogram.csproj @@ -2,7 +2,7 @@ netstandard2.0 - 1.4.1 + 1.4.2 A .NET Standard library for creating spectrograms Scott Harden Harden Technologies, LLC From 520ab0eba82c16582b4a8cf598576de12b8619e0 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Thu, 7 Oct 2021 21:51:22 -0400 Subject: [PATCH 31/62] Readme: update status badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 94e2d79..417ebd8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Spectrogram -[![](https://img.shields.io/azure-devops/build/swharden/swharden/9?label=Build&logo=azure%20pipelines)](https://dev.azure.com/swharden/swharden/_build/latest?definitionId=9&branchName=master) +[![CI](https://github.com/swharden/Spectrogram/actions/workflows/ci.yaml/badge.svg)](https://github.com/swharden/Spectrogram/actions/workflows/ci.yaml) [![Nuget](https://img.shields.io/nuget/v/Spectrogram?label=NuGet&logo=nuget)](https://www.nuget.org/packages/Spectrogram/) **Spectrogram** is a .NET library for creating spectrograms from pre-recorded signals or live audio from the sound card. Spectrogram uses FFT algorithms and window functions provided by the [FftSharp](https://github.com/swharden/FftSharp) project, and it targets .NET Standard so it can be used in .NET Framework and .NET Core projects. From fe9f19b2cf072fa6435169e7fafd9a7f249e9ab0 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Wed, 27 Oct 2021 00:17:48 -0400 Subject: [PATCH 32/62] try-convert --- dev/sff/SffViewer/SffViewer.csproj | 97 ++--------------- .../Spectrogram.MicrophoneDemo.csproj | 103 ++---------------- 2 files changed, 21 insertions(+), 179 deletions(-) diff --git a/dev/sff/SffViewer/SffViewer.csproj b/dev/sff/SffViewer/SffViewer.csproj index a8e54c0..58946c2 100644 --- a/dev/sff/SffViewer/SffViewer.csproj +++ b/dev/sff/SffViewer/SffViewer.csproj @@ -1,93 +1,16 @@ - - - + - Debug - AnyCPU - {9478208D-60C7-4F6A-B2E4-6325D38139DA} + net5.0-windows WinExe - SffViewer - SffViewer - v4.7.2 - 512 - true - true + false + true + true - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - ..\..\..\src\packages\FftSharp.1.0.8\lib\netstandard2.0\FftSharp.dll - - - ..\..\..\src\packages\Spectrogram.1.2.6\lib\netstandard2.0\Spectrogram.dll - - - - - ..\..\..\src\packages\System.Drawing.Common.5.0.0\lib\net461\System.Drawing.Common.dll - - - - - - - - - - - - - - Form - - - Form1.cs - - - - - Form1.cs - - - ResXFileCodeGenerator - Resources.Designer.cs - Designer - - - True - Resources.resx - - - - SettingsSingleFileGenerator - Settings.Designer.cs - - - True - Settings.settings - True - - - + + + + + - \ No newline at end of file diff --git a/src/Spectrogram.MicrophoneDemo/Spectrogram.MicrophoneDemo.csproj b/src/Spectrogram.MicrophoneDemo/Spectrogram.MicrophoneDemo.csproj index 0738e8e..12e82b8 100644 --- a/src/Spectrogram.MicrophoneDemo/Spectrogram.MicrophoneDemo.csproj +++ b/src/Spectrogram.MicrophoneDemo/Spectrogram.MicrophoneDemo.csproj @@ -1,100 +1,19 @@ - - - + - Debug - AnyCPU - {D51ABC6A-53F4-4620-88A1-14EA1D779538} + net5.0-windows WinExe - Spectrogram.MicrophoneDemo - Spectrogram.MicrophoneDemo - v4.7.2 - 512 - true - true + false + true + true - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - ..\packages\FftSharp.1.0.8\lib\netstandard2.0\FftSharp.dll - - - ..\packages\NAudio.1.10.0\lib\net35\NAudio.dll - - - - - ..\packages\System.Drawing.Common.5.0.0\lib\net461\System.Drawing.Common.dll - - - - - - - - - - - - - - Form - - - FormMicrophone.cs - - - - - - FormMicrophone.cs - - - ResXFileCodeGenerator - Resources.Designer.cs - Designer - - - True - Resources.resx - - - - SettingsSingleFileGenerator - Settings.Designer.cs - - - True - Settings.settings - True - - - + - - {6ff83edd-e18a-4edd-8d53-d2281515ac47} - Spectrogram - + + + + + - \ No newline at end of file From 81b30d6746b204cc70847bae5ca0b14efd6fe1f3 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Wed, 27 Oct 2021 00:24:39 -0400 Subject: [PATCH 33/62] Upgrade FftSharp and ScottPlot --- ...MicrophoneDemo.csproj => Spectrogram.Demo.csproj} | 2 +- src/Spectrogram.Tests/ColormapExamples.cs | 3 ++- src/Spectrogram.Tests/FileFormat.cs | 6 ++++-- src/Spectrogram.Tests/Mel.cs | 6 +++--- src/Spectrogram.Tests/Spectrogram.Tests.csproj | 2 +- src/Spectrogram.Tests/TestAGC.cs | 3 ++- src/Spectrogram.sln | 12 +++--------- src/Spectrogram/Settings.cs | 3 ++- src/Spectrogram/Spectrogram.csproj | 2 +- 9 files changed, 19 insertions(+), 20 deletions(-) rename src/Spectrogram.MicrophoneDemo/{Spectrogram.MicrophoneDemo.csproj => Spectrogram.Demo.csproj} (92%) diff --git a/src/Spectrogram.MicrophoneDemo/Spectrogram.MicrophoneDemo.csproj b/src/Spectrogram.MicrophoneDemo/Spectrogram.Demo.csproj similarity index 92% rename from src/Spectrogram.MicrophoneDemo/Spectrogram.MicrophoneDemo.csproj rename to src/Spectrogram.MicrophoneDemo/Spectrogram.Demo.csproj index 12e82b8..bb31c5c 100644 --- a/src/Spectrogram.MicrophoneDemo/Spectrogram.MicrophoneDemo.csproj +++ b/src/Spectrogram.MicrophoneDemo/Spectrogram.Demo.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/Spectrogram.Tests/ColormapExamples.cs b/src/Spectrogram.Tests/ColormapExamples.cs index 650ca3c..276b3f1 100644 --- a/src/Spectrogram.Tests/ColormapExamples.cs +++ b/src/Spectrogram.Tests/ColormapExamples.cs @@ -12,7 +12,8 @@ public void Test_Make_CommonColormaps() (double[] audio, int sampleRate) = AudioFile.ReadWAV("../../../../../data/cant-do-that-44100.wav"); int fftSize = 1 << 12; var spec = new SpectrogramGenerator(sampleRate, fftSize, stepSize: 700, maxFreq: 2000); - spec.SetWindow(FftSharp.Window.Hanning(fftSize / 3)); // sharper window than typical + var window = new FftSharp.Windows.Hanning(); + spec.SetWindow(window.Create(fftSize / 3)); // sharper window than typical spec.Add(audio); // delete old colormap files diff --git a/src/Spectrogram.Tests/FileFormat.cs b/src/Spectrogram.Tests/FileFormat.cs index 46f4708..7a3b8d9 100644 --- a/src/Spectrogram.Tests/FileFormat.cs +++ b/src/Spectrogram.Tests/FileFormat.cs @@ -14,7 +14,8 @@ public void Test_SFF_Linear() (double[] audio, int sampleRate) = AudioFile.ReadWAV("../../../../../data/cant-do-that-44100.wav"); int fftSize = 1 << 12; var spec = new SpectrogramGenerator(sampleRate, fftSize, stepSize: 700, maxFreq: 2000); - spec.SetWindow(FftSharp.Window.Hanning(fftSize / 3)); // sharper window than typical + var window = new FftSharp.Windows.Hanning(); + spec.SetWindow(window.Create(fftSize / 3)); // sharper window than typical spec.Add(audio); spec.SaveData("../../../../../dev/sff/hal.sff"); @@ -34,7 +35,8 @@ public void Test_SFF_Mel() (double[] audio, int sampleRate) = AudioFile.ReadWAV("../../../../../data/cant-do-that-44100.wav"); int fftSize = 1 << 12; var spec = new SpectrogramGenerator(sampleRate, fftSize, stepSize: 700); - spec.SetWindow(FftSharp.Window.Hanning(fftSize / 3)); // sharper window than typical + var window = new FftSharp.Windows.Hanning(); + spec.SetWindow(window.Create(fftSize / 3)); // sharper window than typical spec.Add(audio); Bitmap bmp = spec.GetBitmapMel(250); diff --git a/src/Spectrogram.Tests/Mel.cs b/src/Spectrogram.Tests/Mel.cs index e4a1ee7..43f0d56 100644 --- a/src/Spectrogram.Tests/Mel.cs +++ b/src/Spectrogram.Tests/Mel.cs @@ -41,7 +41,7 @@ public void Test_Mel_Graph() double[] power = ScottPlot.DataGen.RandomWalk(rand, specPoints, .02, .5); var plt1 = new ScottPlot.Plot(800, 300); - plt1.PlotScatter(freq, power, markerSize: 0); + plt1.AddScatter(freq, power, markerSize: 0); int filterSize = 25; @@ -64,7 +64,7 @@ public void Test_Mel_Graph() double freqCenter = binStartFreqs[binIndex + 1]; double freqHigh = binStartFreqs[binIndex + 2]; - var sctr = plt1.PlotScatter( + var sctr = plt1.AddScatter( xs: new double[] { freqLow, freqCenter, freqHigh }, ys: new double[] { 0, 1, 0 }, markerSize: 0, lineWidth: 2); @@ -84,7 +84,7 @@ public void Test_Mel_Graph() binValue += power[indexLow + i] * frac; } binValue /= binScaleSum; - plt1.PlotPoint(freqCenter, binValue, sctr.color, 10); + plt1.AddPoint(freqCenter, binValue, sctr.Color, 10); } plt1.SaveFig("mel1.png"); diff --git a/src/Spectrogram.Tests/Spectrogram.Tests.csproj b/src/Spectrogram.Tests/Spectrogram.Tests.csproj index 2777423..0fd8f93 100644 --- a/src/Spectrogram.Tests/Spectrogram.Tests.csproj +++ b/src/Spectrogram.Tests/Spectrogram.Tests.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/Spectrogram.Tests/TestAGC.cs b/src/Spectrogram.Tests/TestAGC.cs index a0011e2..70cb9e8 100644 --- a/src/Spectrogram.Tests/TestAGC.cs +++ b/src/Spectrogram.Tests/TestAGC.cs @@ -78,7 +78,8 @@ private double[] SubtractMovingWindow(double[] input, int windowSizePx = 100) { // return a copy of the input array with the moving window subtracted - double[] window = FftSharp.Window.Hanning(windowSizePx); + var hanningWindow = new FftSharp.Windows.Hanning(); + double[] window = hanningWindow.Create(windowSizePx); double windowSum = window.Sum(); double[] windowed = new double[input.Length]; diff --git a/src/Spectrogram.sln b/src/Spectrogram.sln index 2c92801..215c96f 100644 --- a/src/Spectrogram.sln +++ b/src/Spectrogram.sln @@ -1,16 +1,14 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30128.74 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31815.197 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectrogram", "Spectrogram\Spectrogram.csproj", "{6FF83EDD-E18A-4EDD-8D53-D2281515AC47}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spectrogram.MicrophoneDemo", "Spectrogram.MicrophoneDemo\Spectrogram.MicrophoneDemo.csproj", "{D51ABC6A-53F4-4620-88A1-14EA1D779538}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectrogram.Demo", "Spectrogram.MicrophoneDemo\Spectrogram.Demo.csproj", "{D51ABC6A-53F4-4620-88A1-14EA1D779538}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectrogram.Tests", "Spectrogram.Tests\Spectrogram.Tests.csproj", "{E7482801-78C7-41FD-88D1-72A7ED3EFC9D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SffViewer", "..\dev\sff\SffViewer\SffViewer.csproj", "{9478208D-60C7-4F6A-B2E4-6325D38139DA}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -29,10 +27,6 @@ Global {E7482801-78C7-41FD-88D1-72A7ED3EFC9D}.Debug|Any CPU.Build.0 = Debug|Any CPU {E7482801-78C7-41FD-88D1-72A7ED3EFC9D}.Release|Any CPU.ActiveCfg = Release|Any CPU {E7482801-78C7-41FD-88D1-72A7ED3EFC9D}.Release|Any CPU.Build.0 = Release|Any CPU - {9478208D-60C7-4F6A-B2E4-6325D38139DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9478208D-60C7-4F6A-B2E4-6325D38139DA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9478208D-60C7-4F6A-B2E4-6325D38139DA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9478208D-60C7-4F6A-B2E4-6325D38139DA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Spectrogram/Settings.cs b/src/Spectrogram/Settings.cs index b765051..a799260 100644 --- a/src/Spectrogram/Settings.cs +++ b/src/Spectrogram/Settings.cs @@ -55,7 +55,8 @@ public Settings(int sampleRate, int fftSize, int stepSize, double minFreq, doubl // horizontal StepLengthSec = (double)StepSize / sampleRate; - Window = FftSharp.Window.Hanning(fftSize); + var window = new FftSharp.Windows.Hanning(); + Window = window.Create(fftSize); StepOverlapSec = FftLengthSec - StepLengthSec; StepOverlapFrac = StepOverlapSec / FftLengthSec; } diff --git a/src/Spectrogram/Spectrogram.csproj b/src/Spectrogram/Spectrogram.csproj index f36c533..e78ca17 100644 --- a/src/Spectrogram/Spectrogram.csproj +++ b/src/Spectrogram/Spectrogram.csproj @@ -29,7 +29,7 @@ - + From fa1891cb9c888cdea22d1e6270a4cc062892bb65 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Wed, 27 Oct 2021 00:27:24 -0400 Subject: [PATCH 34/62] CI: test on .NET 5 --- .github/workflows/ci.yaml | 58 ++++++------------- .../Spectrogram.Tests.csproj | 2 +- 2 files changed, 18 insertions(+), 42 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4c069fa..07d59c7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,65 +12,41 @@ on: - created jobs: - package: - # build, test, and deploy the core package on Linux - name: Package - runs-on: ubuntu-latest - env: - CSPROJ_CORE: src/Spectrogram/Spectrogram.csproj - CSPROJ_TESTS: src/Spectrogram.Tests/Spectrogram.Tests.csproj + test: + name: Test + runs-on: windows-latest steps: - name: 🛒 Checkout uses: actions/checkout@v2 - - name: ✨ Setup .NET Core 3.1 + - name: ✨ Setup .NET 5 uses: actions/setup-dotnet@v1 with: - dotnet-version: "3.1.x" + dotnet-version: "5.0.x" - name: ✨ Setup .NET 6 uses: actions/setup-dotnet@v1 with: dotnet-version: "6.0.x" include-prerelease: true + - name: 🛠️ Setup NuGet + uses: nuget/setup-nuget@v1 + with: + nuget-api-key: ${{ secrets.NUGET_API_KEY }} - name: 🚚 Restore - run: | - dotnet restore ${{ env.CSPROJ_CORE }} - dotnet restore ${{ env.CSPROJ_TESTS }} + run: dotnet restore src - name: 🛠️ Build - run: | - dotnet build ${{ env.CSPROJ_CORE }} --configuration Release - dotnet build ${{ env.CSPROJ_TESTS }} + run: dotnet build src --configuration Release - name: 🧪 Test - run: dotnet test ${{ env.CSPROJ_TESTS }} + run: dotnet test src - name: 📦 Pack - run: dotnet pack ${{ env.CSPROJ_CORE }} --configuration Release + run: dotnet pack src --configuration Release - name: 💾 Store uses: actions/upload-artifact@v2 with: name: Packages retention-days: 1 - path: src/Spectrogram/bin/Release/*.nupkg - - name: 🛠️ Setup NuGet - if: github.event_name == 'release' - uses: nuget/setup-nuget@v1 - with: - nuget-api-key: ${{ secrets.NUGET_API_KEY }} + path: | + src/Spectrogram/bin/Release/*.nupkg + src/Spectrogram/bin/Release/*.snupkg - name: 🚀 Publish if: github.event_name == 'release' - run: nuget push "src/Spectrogram/bin/Release/*.nupkg" -SkipDuplicate -Source https://api.nuget.org/v3/index.json - - solution: - # build the whole solution (.NET Framework demos require Windows/MSBuild) - name: Build Solution - runs-on: windows-latest - steps: - - name: 🛒 Checkout - uses: actions/checkout@v1 - - name: ✨ Setup NuGet - uses: nuget/setup-nuget@v1 - - name: ✨ Setup MSBuild - uses: microsoft/setup-msbuild@v1.0.3 - - name: 🚚 Restore - working-directory: src - run: nuget restore - - name: 🛠️ Build Release - run: msbuild src -verbosity:minimal \ No newline at end of file + run: nuget push "src\Spectrogram\bin\Release\*.nupkg" -SkipDuplicate -Source https://api.nuget.org/v3/index.json \ No newline at end of file diff --git a/src/Spectrogram.Tests/Spectrogram.Tests.csproj b/src/Spectrogram.Tests/Spectrogram.Tests.csproj index 0fd8f93..073a9df 100644 --- a/src/Spectrogram.Tests/Spectrogram.Tests.csproj +++ b/src/Spectrogram.Tests/Spectrogram.Tests.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net5.0 false From 4e5b0329db02cc3e0f00c9490de8369dc42f6b84 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Wed, 27 Oct 2021 00:32:01 -0400 Subject: [PATCH 35/62] Spectrogram 1.4.3 --- src/Spectrogram/Spectrogram.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Spectrogram/Spectrogram.csproj b/src/Spectrogram/Spectrogram.csproj index e78ca17..d1fed7d 100644 --- a/src/Spectrogram/Spectrogram.csproj +++ b/src/Spectrogram/Spectrogram.csproj @@ -2,7 +2,7 @@ netstandard2.0 - 1.4.2 + 1.4.3 A .NET Standard library for creating spectrograms Scott Harden Harden Technologies, LLC From b510ae7d3a6983c20b444a0baf989dcac423b9f9 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Wed, 27 Oct 2021 00:32:12 -0400 Subject: [PATCH 36/62] Package: use snupkg debug symbols now that NuGet bug has been resolved https://github.com/NuGet/Home/issues/10791 --- src/Spectrogram/Spectrogram.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Spectrogram/Spectrogram.csproj b/src/Spectrogram/Spectrogram.csproj index d1fed7d..bd9cb66 100644 --- a/src/Spectrogram/Spectrogram.csproj +++ b/src/Spectrogram/Spectrogram.csproj @@ -17,7 +17,7 @@ README.md portable true - + snupkg true true true From bf06d54cb2429b9137988aa58cfffaf07978e5f3 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Tue, 22 Mar 2022 08:03:13 -0400 Subject: [PATCH 37/62] obsolete Spectrogram with error --- src/Spectrogram/Spectrogram.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Spectrogram/Spectrogram.cs b/src/Spectrogram/Spectrogram.cs index 9844130..1b09e1a 100644 --- a/src/Spectrogram/Spectrogram.cs +++ b/src/Spectrogram/Spectrogram.cs @@ -2,7 +2,7 @@ namespace Spectrogram { - [Obsolete("This class has been replaced by SpectrogramGenerator")] + [Obsolete("This class has been replaced by SpectrogramGenerator", true)] public class Spectrogram : SpectrogramGenerator { public Spectrogram(int sampleRate, int fftSize, int stepSize, double minFreq = 0, double maxFreq = double.PositiveInfinity, int? fixedWidth = null, int offsetHz = 0) : From 6ed42b4e706bd68a87aabeb0cbe2ff5076b94a75 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Tue, 22 Mar 2022 08:11:42 -0400 Subject: [PATCH 38/62] tests: demonstrate divide-by-zero error #42 --- src/Spectrogram.Tests/AddTests.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/Spectrogram.Tests/AddTests.cs diff --git a/src/Spectrogram.Tests/AddTests.cs b/src/Spectrogram.Tests/AddTests.cs new file mode 100644 index 0000000..e45845b --- /dev/null +++ b/src/Spectrogram.Tests/AddTests.cs @@ -0,0 +1,19 @@ +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Spectrogram.Tests +{ + internal class AddTests + { + [Test] + public void Test_No_Data() + { + SpectrogramGenerator sg = new(44100, 2048, 1000); + var bmp = sg.GetBitmap(); + } + } +} From 17d35028650d650fd1b622630762007bf4accb8d Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Tue, 22 Mar 2022 08:12:08 -0400 Subject: [PATCH 39/62] Generator: improve exception message resolves #42 --- src/Spectrogram/SpectrogramGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Spectrogram/SpectrogramGenerator.cs b/src/Spectrogram/SpectrogramGenerator.cs index b86106b..711df86 100644 --- a/src/Spectrogram/SpectrogramGenerator.cs +++ b/src/Spectrogram/SpectrogramGenerator.cs @@ -47,7 +47,7 @@ public class SpectrogramGenerator /// /// Index of the pixel column which will be populated next. Location of vertical line for wrap-around displays. /// - public int NextColumnIndex { get => (FftsProcessed + rollOffset) % Width; } + public int NextColumnIndex { get => Width > 0 ? (FftsProcessed + rollOffset) % Width : 0; } /// /// This value is added to displayed frequency axis tick labels From e3b6e3f1603c4a2c331cee67fb438202edb47051 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Tue, 22 Mar 2022 08:14:56 -0400 Subject: [PATCH 40/62] GetBitmap: improve no-data exception message #42 --- src/Spectrogram.Tests/AddTests.cs | 2 +- src/Spectrogram/Image.cs | 2 +- src/Spectrogram/SpectrogramGenerator.cs | 10 ++++++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Spectrogram.Tests/AddTests.cs b/src/Spectrogram.Tests/AddTests.cs index e45845b..396e879 100644 --- a/src/Spectrogram.Tests/AddTests.cs +++ b/src/Spectrogram.Tests/AddTests.cs @@ -13,7 +13,7 @@ internal class AddTests public void Test_No_Data() { SpectrogramGenerator sg = new(44100, 2048, 1000); - var bmp = sg.GetBitmap(); + Assert.Throws(() => sg.GetBitmap()); } } } diff --git a/src/Spectrogram/Image.cs b/src/Spectrogram/Image.cs index 18df297..7b4866a 100644 --- a/src/Spectrogram/Image.cs +++ b/src/Spectrogram/Image.cs @@ -20,7 +20,7 @@ public static Bitmap GetBitmap( int rollOffset = 0) { if (ffts.Count == 0) - throw new ArgumentException("This Spectrogram contains no FFTs (likely because no signal was added)"); + throw new ArgumentException("Not enough data in FFTs to generate an image yet."); int Width = ffts.Count; int Height = ffts[0].Length; diff --git a/src/Spectrogram/SpectrogramGenerator.cs b/src/Spectrogram/SpectrogramGenerator.cs index 711df86..76afc8e 100644 --- a/src/Spectrogram/SpectrogramGenerator.cs +++ b/src/Spectrogram/SpectrogramGenerator.cs @@ -269,8 +269,14 @@ public List GetMelFFTs(int melBinCount) /// Behavior of the spectrogram when it is full of data. /// Roll (true) adds new columns on the left overwriting the oldest ones. /// Scroll (false) slides the whole image to the left and adds new columns to the right. - public Bitmap GetBitmap(double intensity = 1, bool dB = false, double dBScale = 1, bool roll = false) => - Image.GetBitmap(FFTs, Colormap, intensity, dB, dBScale, roll, NextColumnIndex); + public Bitmap GetBitmap(double intensity = 1, bool dB = false, double dBScale = 1, bool roll = false) + { + if (FFTs.Count == 0) + throw new InvalidOperationException("Not enough data to create an image. " + + $"Ensure {nameof(Width)} is >0 before calling {nameof(GetBitmap)}()."); + + return Image.GetBitmap(FFTs, Colormap, intensity, dB, dBScale, roll, NextColumnIndex); + } /// /// Create a Mel-scaled spectrogram. From e0fc25a9f96916cdc94ae56d9274093f47523cae Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Tue, 22 Mar 2022 08:16:43 -0400 Subject: [PATCH 41/62] Spectrogram 1.4.4 --- src/Spectrogram/Spectrogram.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Spectrogram/Spectrogram.csproj b/src/Spectrogram/Spectrogram.csproj index bd9cb66..8d6100a 100644 --- a/src/Spectrogram/Spectrogram.csproj +++ b/src/Spectrogram/Spectrogram.csproj @@ -2,7 +2,7 @@ netstandard2.0 - 1.4.3 + 1.4.4 A .NET Standard library for creating spectrograms Scott Harden Harden Technologies, LLC From 1684eb67216203b0d79a32c3b573b5fd6371865e Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Tue, 22 Mar 2022 08:31:26 -0400 Subject: [PATCH 42/62] dev: delete local build script --- dev/build/build-and-publish.bat | 49 --------------------------------- 1 file changed, 49 deletions(-) delete mode 100644 dev/build/build-and-publish.bat diff --git a/dev/build/build-and-publish.bat b/dev/build/build-and-publish.bat deleted file mode 100644 index f807a86..0000000 --- a/dev/build/build-and-publish.bat +++ /dev/null @@ -1,49 +0,0 @@ -@echo off - -:: this script requires nuget.exe to be in this folder -:: https://www.nuget.org/downloads - -echo. -echo ### DELETING OLD PACKAGES ### -DEL *.nupkg -DEL *.snupkg - -echo. -echo ### DELETING RELEASE FOLDERS ### -RMDIR ..\..\src\Spectrogram\bin\Release /S /Q - -echo. -echo ### CLEANING SOLUTION ### -dotnet clean ..\..\src\Spectrogram.sln --verbosity quiet --configuration Release - -echo. -echo ### REBUILDING SOLUTION ### -dotnet build ..\..\src\Spectrogram\Spectrogram.csproj --verbosity quiet --configuration Release - -echo. -echo ### COPYING PACKAGE HERE ### -copy ..\..\src\Spectrogram\bin\Release\*.nupkg .\ -copy ..\..\src\Spectrogram\bin\Release\*.snupkg .\ - -echo ### RUNNING TESTS ### -dotnet test ..\..\src\Spectrogram.sln --configuration Release - -echo. -echo WARNING! This script will UPLOAD packages to nuget.org -echo. -echo press ENTER 3 times to proceed... -pause -pause -pause - -echo. -echo ### UPDATING NUGET ### -nuget update -self - -echo. -echo ### UPLOADING TO NUGET ### -nuget push *.nupkg -Source https://api.nuget.org/v3/index.json -::nuget push *.snupkg -Source https://api.nuget.org/v3/index.json - -echo. -pause \ No newline at end of file From a987308aa9b4d73618072fdb2642fbff36564818 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Tue, 22 Mar 2022 08:31:43 -0400 Subject: [PATCH 43/62] CI: limit deployment jobs to releases --- .github/workflows/ci.yaml | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 07d59c7..f30f7a6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,4 +1,4 @@ -name: CI +name: CI/CD on: workflow_dispatch: @@ -12,34 +12,38 @@ on: - created jobs: - test: - name: Test + build: + name: Build, Test, and Deploy runs-on: windows-latest steps: - name: 🛒 Checkout uses: actions/checkout@v2 + - name: ✨ Setup .NET 5 uses: actions/setup-dotnet@v1 with: dotnet-version: "5.0.x" + - name: ✨ Setup .NET 6 uses: actions/setup-dotnet@v1 with: dotnet-version: "6.0.x" include-prerelease: true - - name: 🛠️ Setup NuGet - uses: nuget/setup-nuget@v1 - with: - nuget-api-key: ${{ secrets.NUGET_API_KEY }} + - name: 🚚 Restore run: dotnet restore src + - name: 🛠️ Build - run: dotnet build src --configuration Release + run: dotnet build src --configuration Release --no-restore + - name: 🧪 Test - run: dotnet test src + run: dotnet test src --configuration Release --no-build + - name: 📦 Pack - run: dotnet pack src --configuration Release - - name: 💾 Store + run: dotnet pack src --configuration Release --no-build + + - name: 💾 Store Release Package + if: github.event_name == 'release' uses: actions/upload-artifact@v2 with: name: Packages @@ -47,6 +51,13 @@ jobs: path: | src/Spectrogram/bin/Release/*.nupkg src/Spectrogram/bin/Release/*.snupkg - - name: 🚀 Publish + + - name: 🔑 Configure NuGet Secrets + if: github.event_name == 'release' + uses: nuget/setup-nuget@v1 + with: + nuget-api-key: ${{ secrets.NUGET_API_KEY }} + + - name: 🚀 Deploy Release Package if: github.event_name == 'release' - run: nuget push "src\Spectrogram\bin\Release\*.nupkg" -SkipDuplicate -Source https://api.nuget.org/v3/index.json \ No newline at end of file + run: nuget push "src\Spectrogram\bin\Release\*.nupkg" -SkipDuplicate -Source https://api.nuget.org/v3/index.json From f647f09350809f7ef641cdd326c418e4c3b46eb5 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Tue, 22 Mar 2022 08:34:21 -0400 Subject: [PATCH 44/62] CI: target main branch --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f30f7a6..c1dd401 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -4,7 +4,7 @@ on: workflow_dispatch: push: branches: - - master + - main pull_request: types: [opened, synchronize, reopened] release: From 3828e2f6d36750590ed76318bd2c165fb799b216 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Thu, 16 Jun 2022 18:15:08 -0400 Subject: [PATCH 45/62] csproj: upgrade dependencies --- src/Spectrogram.MicrophoneDemo/Spectrogram.Demo.csproj | 2 +- src/Spectrogram/Spectrogram.csproj | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Spectrogram.MicrophoneDemo/Spectrogram.Demo.csproj b/src/Spectrogram.MicrophoneDemo/Spectrogram.Demo.csproj index bb31c5c..0aee820 100644 --- a/src/Spectrogram.MicrophoneDemo/Spectrogram.Demo.csproj +++ b/src/Spectrogram.MicrophoneDemo/Spectrogram.Demo.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/Spectrogram/Spectrogram.csproj b/src/Spectrogram/Spectrogram.csproj index 8d6100a..6e28915 100644 --- a/src/Spectrogram/Spectrogram.csproj +++ b/src/Spectrogram/Spectrogram.csproj @@ -29,10 +29,10 @@ - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all From dd54c7eb5c484e61ea9f4e159a407f2fade20dd6 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Thu, 16 Jun 2022 18:37:01 -0400 Subject: [PATCH 46/62] SFF: deprecate resolves #44 --- README.md | 52 ------------- src/Spectrogram.Tests/FileFormat.cs | 97 ------------------------- src/Spectrogram/SFF.cs | 5 +- src/Spectrogram/SpectrogramGenerator.cs | 4 + src/Spectrogram/Tools.cs | 13 ++++ 5 files changed, 21 insertions(+), 150 deletions(-) delete mode 100644 src/Spectrogram.Tests/FileFormat.cs diff --git a/README.md b/README.md index 417ebd8..3c160f0 100644 --- a/README.md +++ b/README.md @@ -153,58 +153,6 @@ Bitmap bmp = sg.GetBitmapMel(melSizePoints: 250); bmp.Save("halMel.png", ImageFormat.Png); ``` -## Spectrogram File Format (SFF) - -The Spectrogram library has methods which can read and write SFF files, a file format specifically designed for storing spectrogram data. SFF files contain 2D spectrogram data (repeated FFTs) with a [small header](dev/sff) describing the audio and FFT settings suitable for deriving scale information. - -SFF files store `double` values (8-byte floating-point data) which is far superior to saving spectrograms as indexed color images (which represent intensity with a single `byte` per pixel). - -SFF files be saved using `Complex` data format (with real and imaginary values for each point) to faithfully represent the FFT output, or `double` format to represent magnitude (with an optional pre-conversion to Decibels to represent power). - -### Create SFF Files with C# - -This example creates a spectrogram but saves it using the SFF file format instead of saving it as an image. The SFF file can then be read in any language. - -```cs -(double[] audio, int sampleRate) = ReadWavMono("hal.wav"); -var sg = new SpectrogramGenerator(sampleRate, fftSize: 4096, stepSize: 700, maxFreq: 2000); -sg.Add(audio); -sg.SaveData("hal.sff"); -``` - -### Display SFF Files with C# -Spectrogram data can be loaded from SFF files to facilitate rapid recall of data which can otherwise be resource-intensive to calculate. Spectrogram's `SFF` module facilitates this operation and has methods which can directly convert spectrograms to Bitmaps with options to customize the colormap, intensity, and Decibel scaling. - -![](dev/sff/SffViewer/screenshot.png) - -A simple SFF file viewer has been added to [dev/sff](dev/sff) and serves as a demonstration of how the `SFF` module can be used to generate spectrogram images from SFF files. - -### Read SFF Files with Python -A Python module to read SFF files has been created (in [dev/sff/python](dev/sff/python)) which allows Spectrograms created by this library and stored in SFF format to be loaded as 2D numpy arrays in Python. - -This example demonstrates how the SFF file created in the previous C# example can be loaded into Python and displayed with matplotlib. This example has a few lines related to styling omitted for brevity, but the full Python demo can be found in [dev/sff/python](dev/sff/python). - -```python -import matplotlib.pyplot as plt -import sffLib - -# load spectrogram data as a 2D numpy array -sf = sffLib.SpectrogramFile("hal.sff") - -# display the spectrogram as a pseudocolor mesh -plt.pcolormesh(freqs, times, sf.values) -plt.colorbar() -plt.show() -``` - -![](dev/sff/python/hal.sff.png) - -## Resources -* [FftSharp](https://github.com/swharden/FftSharp) - the module which actually performs the FFT and related transformations -* [MP3Sharp](https://github.com/ZaneDubya/MP3Sharp) - a library I use to read MP3 files during testing -* [FSKview](https://github.com/swharden/FSKview) - a real-time spectrogram for viewing frequency-shift-keyed (FSK) signals from audio transmitted over radio frequency. -* [NAudio](https://github.com/naudio/NAudio) - an open source .NET library which makes it easy to get samples from the microphone or sound card in real time - ## Read data from a WAV File 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. diff --git a/src/Spectrogram.Tests/FileFormat.cs b/src/Spectrogram.Tests/FileFormat.cs deleted file mode 100644 index 7a3b8d9..0000000 --- a/src/Spectrogram.Tests/FileFormat.cs +++ /dev/null @@ -1,97 +0,0 @@ -using NUnit.Framework; -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Text; - -namespace Spectrogram.Tests -{ - class FileFormat - { - [Test] - public void Test_SFF_Linear() - { - (double[] audio, int sampleRate) = AudioFile.ReadWAV("../../../../../data/cant-do-that-44100.wav"); - int fftSize = 1 << 12; - var spec = new SpectrogramGenerator(sampleRate, fftSize, stepSize: 700, maxFreq: 2000); - var window = new FftSharp.Windows.Hanning(); - spec.SetWindow(window.Create(fftSize / 3)); // sharper window than typical - spec.Add(audio); - spec.SaveData("../../../../../dev/sff/hal.sff"); - - var spec2 = new SFF("../../../../../dev/sff/hal.sff"); - Assert.AreEqual(spec.SampleRate, spec2.SampleRate); - Assert.AreEqual(spec.StepSize, spec2.StepSize); - Assert.AreEqual(spec.Width, spec2.Width); - Assert.AreEqual(spec.FftSize, spec2.FftSize); - Assert.AreEqual(spec.NextColumnIndex, spec2.FftFirstIndex); - Assert.AreEqual(spec.Height, spec2.Height); - Assert.AreEqual(spec.OffsetHz, spec2.OffsetHz); - } - - [Test] - public void Test_SFF_Mel() - { - (double[] audio, int sampleRate) = AudioFile.ReadWAV("../../../../../data/cant-do-that-44100.wav"); - int fftSize = 1 << 12; - var spec = new SpectrogramGenerator(sampleRate, fftSize, stepSize: 700); - var window = new FftSharp.Windows.Hanning(); - spec.SetWindow(window.Create(fftSize / 3)); // sharper window than typical - spec.Add(audio); - - Bitmap bmp = spec.GetBitmapMel(250); - bmp.Save("../../../../../dev/sff/halMel.png", System.Drawing.Imaging.ImageFormat.Png); - spec.SaveData("../../../../../dev/sff/halMel.sff", melBinCount: 250); - - var spec2 = new SFF("../../../../../dev/sff/halMel.sff"); - Assert.AreEqual(spec.SampleRate, spec2.SampleRate); - Assert.AreEqual(spec.StepSize, spec2.StepSize); - Assert.AreEqual(spec.Width, spec2.Width); - Assert.AreEqual(spec.FftSize, spec2.FftSize); - Assert.AreEqual(spec.NextColumnIndex, spec2.FftFirstIndex); - Assert.AreEqual(spec.Height, spec2.Height); - Assert.AreEqual(spec.OffsetHz, spec2.OffsetHz); - } - - [Test] - public void Test_SFF_Linear2() - { - // test creating SFF file from 16-bit 48kHz mono WAV file - - // read the wav file - (double[] audio, int sampleRate) = AudioFile.ReadWAV("../../../../../data/03-02-03-01-02-01-19.wav"); - Assert.AreEqual(48000, sampleRate); - - // save the SFF - int fftSize = 1 << 12; - var spec = new SpectrogramGenerator(sampleRate, fftSize, stepSize: 300, maxFreq: 2000); - spec.Add(audio); - spec.SaveData("testDoor.sff"); - - // load the SFF and verify all the values are the same - var spec2 = new SFF("testDoor.sff"); - Assert.AreEqual(spec.SampleRate, spec2.SampleRate); - Assert.AreEqual(spec.StepSize, spec2.StepSize); - Assert.AreEqual(spec.Width, spec2.Width); - Assert.AreEqual(spec.FftSize, spec2.FftSize); - Assert.AreEqual(spec.NextColumnIndex, spec2.FftFirstIndex); - Assert.AreEqual(spec.Height, spec2.Height); - Assert.AreEqual(spec.OffsetHz, spec2.OffsetHz); - Assert.AreEqual("SFF 701x170", spec2.ToString()); - } - - [Test] - public void Test_SFF_LinearBigMaxFreq() - { - // test creating SFF file from 16-bit 48kHz mono WAV file - - (double[] audio, int sampleRate) = AudioFile.ReadWAV("../../../../../data/03-02-03-01-02-01-19.wav"); - Assert.AreEqual(48000, sampleRate); - - int fftSize = 1 << 12; - var spec = new SpectrogramGenerator(sampleRate, fftSize, stepSize: 300, maxFreq: 7999); - spec.Add(audio); - spec.SaveData("testDoorBig.sff"); - } - } -} diff --git a/src/Spectrogram/SFF.cs b/src/Spectrogram/SFF.cs index 8d68d9b..d64bb51 100644 --- a/src/Spectrogram/SFF.cs +++ b/src/Spectrogram/SFF.cs @@ -8,7 +8,10 @@ namespace Spectrogram { - // Spectrogram File Format reader/writer + [Obsolete("The SFF file format is obsolete. " + + "Users are encouraged to write their own IO routines specific to their application. "+ + "To get a copy of the original SFF reader/writer see https://github.com/swharden/Spectrogram/issues/44", + error: true)] public class SFF { public readonly byte VersionMajor = 1; diff --git a/src/Spectrogram/SpectrogramGenerator.cs b/src/Spectrogram/SpectrogramGenerator.cs index 76afc8e..367d91d 100644 --- a/src/Spectrogram/SpectrogramGenerator.cs +++ b/src/Spectrogram/SpectrogramGenerator.cs @@ -350,6 +350,10 @@ public Bitmap GetBitmapMax(double intensity = 1, bool dB = false, double dBScale return Image.GetBitmap(ffts2, Colormap, intensity, dB, dBScale, roll, NextColumnIndex); } + [Obsolete("The SFF file format is obsolete. " + + "Users are encouraged to write their own IO routines specific to their application. " + + "To get a copy of the original SFF reader/writer see https://github.com/swharden/Spectrogram/issues/44", + error: true)] /// /// Export spectrogram data using the Spectrogram File Format (SFF) /// diff --git a/src/Spectrogram/Tools.cs b/src/Spectrogram/Tools.cs index 2186644..6300194 100644 --- a/src/Spectrogram/Tools.cs +++ b/src/Spectrogram/Tools.cs @@ -7,6 +7,10 @@ namespace Spectrogram { public static class Tools { + [Obsolete("The SFF file format is obsolete. " + + "Users are encouraged to write their own IO routines specific to their application. " + + "To get a copy of the original SFF reader/writer see https://github.com/swharden/Spectrogram/issues/44", + error: true)] /// /// Collapse the 2D spectrogram into a 1D array (mean power of each frequency) /// @@ -31,6 +35,11 @@ public static double[] SffMeanFFT(SFF sff, bool dB = false) return mean; } + + [Obsolete("The SFF file format is obsolete. " + + "Users are encouraged to write their own IO routines specific to their application. " + + "To get a copy of the original SFF reader/writer see https://github.com/swharden/Spectrogram/issues/44", + error: true)] /// /// Collapse the 2D spectrogram into a 1D array (mean power of each time point) /// @@ -48,6 +57,10 @@ public static double[] SffMeanPower(SFF sff, bool dB = false) return power; } + [Obsolete("The SFF file format is obsolete. " + + "Users are encouraged to write their own IO routines specific to their application. " + + "To get a copy of the original SFF reader/writer see https://github.com/swharden/Spectrogram/issues/44", + error: true)] public static double GetPeakFrequency(SFF sff, bool firstFftOnly = false) { double[] freqs = firstFftOnly ? sff.Ffts[0] : SffMeanFFT(sff, false); From 1ee81a91234aad800e36d96c9798e945471502aa Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Thu, 16 Jun 2022 18:37:42 -0400 Subject: [PATCH 47/62] Spectrogram 1.5.0 --- src/Spectrogram/Spectrogram.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Spectrogram/Spectrogram.csproj b/src/Spectrogram/Spectrogram.csproj index 6e28915..028ff85 100644 --- a/src/Spectrogram/Spectrogram.csproj +++ b/src/Spectrogram/Spectrogram.csproj @@ -2,7 +2,7 @@ netstandard2.0 - 1.4.4 + 1.5.0 A .NET Standard library for creating spectrograms Scott Harden Harden Technologies, LLC From c8a43194cdbd86be873399fc09e4b87761b1dbc4 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Sun, 10 Jul 2022 14:06:52 -0400 Subject: [PATCH 48/62] Update README.md clarify that the audio file reader works for WAV and MP3 files --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3c160f0..aee7e88 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ _"I'm sorry Dave... I'm afraid I can't do that"_ * Source code for the WAV reading method is at the bottom of this page. ```cs -(double[] audio, int sampleRate) = ReadWavMono("hal.wav"); +(double[] audio, int sampleRate) = ReadMono("hal.wav"); var sg = new SpectrogramGenerator(sampleRate, fftSize: 4096, stepSize: 500, maxFreq: 3000); sg.Add(audio); sg.SaveImage("hal.png"); @@ -81,7 +81,7 @@ Review the source code of the demo application for additional details and consid 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). ```cs -(double[] audio, int sampleRate) = ReadWavMono("song.wav"); +(double[] audio, int sampleRate) = ReadMono("song.wav"); int fftSize = 16384; int targetWidthPx = 3000; @@ -117,7 +117,7 @@ Spectrogram (2993, 817) These examples demonstrate the identical spectrogram analyzed with a variety of different colormaps. Spectrogram colormaps can be changed by calling the `SetColormap()` method: ```cs -(double[] audio, int sampleRate) = ReadWavMono("hal.wav"); +(double[] audio, int sampleRate) = ReadMono("hal.wav"); var sg = new SpectrogramGenerator(sampleRate, fftSize: 8192, stepSize: 200, maxFreq: 3000); sg.Add(audio); sg.SetColormap(Colormap.Jet); @@ -141,7 +141,7 @@ Cropped Linear Scale (0-3kHz) | Mel Scale (0-22 kHz) Amplitude perception in humans, like frequency perception, is logarithmic. Therefore, Mel spectrograms typically display log-transformed spectral power and are presented using Decibel units. ```cs -(double[] audio, int sampleRate) = ReadWavMono("hal.wav"); +(double[] audio, int sampleRate) = ReadMono("hal.wav"); var sg = new SpectrogramGenerator(sampleRate, fftSize: 4096, stepSize: 500, maxFreq: 3000); sg.Add(audio); @@ -153,12 +153,12 @@ Bitmap bmp = sg.GetBitmapMel(melSizePoints: 250); bmp.Save("halMel.png", ImageFormat.Png); ``` -## Read data from a WAV File +## Read Data from an Audio File 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. ```cs -(double[] audio, int sampleRate) ReadWavMono(string filePath, double multiplier = 16_000) +(double[] audio, int sampleRate) ReadMono(string filePath, double multiplier = 16_000) { using var afr = new NAudio.Wave.AudioFileReader(filePath); int sampleRate = afr.WaveFormat.SampleRate; From caa99e12aba729c47580c706ea394a2a41965c3d Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Sun, 10 Jul 2022 14:09:07 -0400 Subject: [PATCH 49/62] Tests: target .NET 6 --- src/Spectrogram.Tests/Spectrogram.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Spectrogram.Tests/Spectrogram.Tests.csproj b/src/Spectrogram.Tests/Spectrogram.Tests.csproj index 073a9df..3ec3d29 100644 --- a/src/Spectrogram.Tests/Spectrogram.Tests.csproj +++ b/src/Spectrogram.Tests/Spectrogram.Tests.csproj @@ -1,7 +1,7 @@ - net5.0 + net6.0 false From 4130489618472a0464898cb002983d27fadd0b15 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Sun, 10 Jul 2022 14:09:14 -0400 Subject: [PATCH 50/62] Demo: target .NET 6 --- src/Spectrogram.MicrophoneDemo/Spectrogram.Demo.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Spectrogram.MicrophoneDemo/Spectrogram.Demo.csproj b/src/Spectrogram.MicrophoneDemo/Spectrogram.Demo.csproj index 0aee820..607dc15 100644 --- a/src/Spectrogram.MicrophoneDemo/Spectrogram.Demo.csproj +++ b/src/Spectrogram.MicrophoneDemo/Spectrogram.Demo.csproj @@ -1,6 +1,6 @@  - net5.0-windows + net6.0-windows WinExe false true From 1880e71a2ef547eadc2d65101c9bdbc9e7cbf187 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Sun, 10 Jul 2022 14:10:27 -0400 Subject: [PATCH 51/62] CI: target .NET 6 --- .github/workflows/ci.yaml | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c1dd401..2966d92 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -19,16 +19,10 @@ jobs: - name: 🛒 Checkout uses: actions/checkout@v2 - - name: ✨ Setup .NET 5 - uses: actions/setup-dotnet@v1 - with: - dotnet-version: "5.0.x" - - name: ✨ Setup .NET 6 uses: actions/setup-dotnet@v1 with: dotnet-version: "6.0.x" - include-prerelease: true - name: 🚚 Restore run: dotnet restore src @@ -42,22 +36,12 @@ jobs: - name: 📦 Pack run: dotnet pack src --configuration Release --no-build - - name: 💾 Store Release Package - if: github.event_name == 'release' - uses: actions/upload-artifact@v2 - with: - name: Packages - retention-days: 1 - path: | - src/Spectrogram/bin/Release/*.nupkg - src/Spectrogram/bin/Release/*.snupkg - - - name: 🔑 Configure NuGet Secrets + - name: 🔑 Configure Secrets if: github.event_name == 'release' uses: nuget/setup-nuget@v1 with: nuget-api-key: ${{ secrets.NUGET_API_KEY }} - - name: 🚀 Deploy Release Package + - name: 🚀 Deploy Package if: github.event_name == 'release' run: nuget push "src\Spectrogram\bin\Release\*.nupkg" -SkipDuplicate -Source https://api.nuget.org/v3/index.json From bc4ad217e80df9df665d78f13b89041a5dcfd2f0 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Sun, 10 Jul 2022 14:43:24 -0400 Subject: [PATCH 52/62] GetBitmap: add rotate argument resolves #47 --- src/Spectrogram.Tests/ImageTests.cs | 21 +++++ src/Spectrogram/Image.cs | 59 +++----------- src/Spectrogram/ImageMaker.cs | 101 ++++++++++++++++++++++++ src/Spectrogram/Spectrogram.csproj | 1 + src/Spectrogram/SpectrogramGenerator.cs | 5 +- 5 files changed, 139 insertions(+), 48 deletions(-) create mode 100644 src/Spectrogram.Tests/ImageTests.cs create mode 100644 src/Spectrogram/ImageMaker.cs diff --git a/src/Spectrogram.Tests/ImageTests.cs b/src/Spectrogram.Tests/ImageTests.cs new file mode 100644 index 0000000..d02a218 --- /dev/null +++ b/src/Spectrogram.Tests/ImageTests.cs @@ -0,0 +1,21 @@ +using NUnit.Framework; + +namespace Spectrogram.Tests; + +internal class ImageTests +{ + [Test] + public void Test_Image_Rotations() + { + string filePath = $"../../../../../data/cant-do-that-44100.wav"; + (double[] audio, int sampleRate) = AudioFile.ReadWAV(filePath); + SpectrogramGenerator sg = new(sampleRate, 4096, 500, maxFreq: 3000); + sg.Add(audio); + + System.Drawing.Bitmap bmp1 = sg.GetBitmap(rotate: false); + bmp1.Save("test-image-original.png"); + + System.Drawing.Bitmap bmp2 = sg.GetBitmap(rotate: true); + bmp2.Save("test-image-rotated.png"); + } +} diff --git a/src/Spectrogram/Image.cs b/src/Spectrogram/Image.cs index 7b4866a..283543b 100644 --- a/src/Spectrogram/Image.cs +++ b/src/Spectrogram/Image.cs @@ -10,55 +10,22 @@ namespace Spectrogram { public static class Image { - public static Bitmap GetBitmap( - List ffts, - Colormap cmap, - double intensity = 1, - bool dB = false, - double dBScale = 1, - bool roll = false, - int rollOffset = 0) + public static Bitmap GetBitmap(List ffts, Colormap cmap, double intensity = 1, + bool dB = false, double dBScale = 1, bool roll = false, int rollOffset = 0, bool rotate = false) { - if (ffts.Count == 0) - throw new ArgumentException("Not enough data in FFTs to generate an image yet."); - int Width = ffts.Count; - int Height = ffts[0].Length; - - Bitmap bmp = new Bitmap(Width, Height, PixelFormat.Format8bppIndexed); - cmap.Apply(bmp); - - var lockRect = new Rectangle(0, 0, Width, Height); - BitmapData bitmapData = bmp.LockBits(lockRect, ImageLockMode.ReadOnly, bmp.PixelFormat); - int stride = bitmapData.Stride; - - byte[] bytes = new byte[bitmapData.Stride * bmp.Height]; - Parallel.For(0, Width, col => + ImageMaker maker = new() { - int sourceCol = col; - if (roll) - { - sourceCol += Width - rollOffset % Width; - if (sourceCol >= Width) - sourceCol -= Width; - } - - for (int row = 0; row < Height; row++) - { - double value = ffts[sourceCol][row]; - if (dB) - value = 20 * Math.Log10(value * dBScale + 1); - value *= intensity; - value = Math.Min(value, 255); - int bytePosition = (Height - 1 - row) * stride + col; - bytes[bytePosition] = (byte)value; - } - }); - - Marshal.Copy(bytes, 0, bitmapData.Scan0, bytes.Length); - bmp.UnlockBits(bitmapData); - - return bmp; + Colormap = cmap, + Intensity = intensity, + IsDecibel = dB, + DecibelScaleFactor = dBScale, + IsRoll = roll, + RollOffset = rollOffset, + IsRotated = rotate, + }; + + return maker.GetBitmap(ffts); } } } diff --git a/src/Spectrogram/ImageMaker.cs b/src/Spectrogram/ImageMaker.cs new file mode 100644 index 0000000..5d70013 --- /dev/null +++ b/src/Spectrogram/ImageMaker.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Imaging; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Spectrogram +{ + /// + /// This class converts a collection of FFTs to a colormapped spectrogram image + /// + public class ImageMaker + { + /// + /// Colormap used to translate intensity to pixel color + /// + public Colormap Colormap; + + /// + /// Intensity is multiplied by this number before converting it to the pixel color according to the colormap + /// + public double Intensity = 1; + + /// + /// If True, intensity will be log-scaled to represent Decibels + /// + public bool IsDecibel = false; + + /// + /// If is enabled, intensity will be scaled by this value prior to log transformation + /// + public double DecibelScaleFactor = 1; + + /// + /// If False, the spectrogram will proceed in time from left to right across the whole image. + /// If True, the image will be broken and the newest FFTs will appear on the left and oldest on the right. + /// + public bool IsRoll = false; + + /// + /// If is enabled, this value indicates the pixel position of the break point. + /// + public int RollOffset = 0; + + /// + /// If True, the spectrogram will flow top-down (oldest to newest) rather than left-right. + /// + public bool IsRotated = false; + + public ImageMaker() + { + + } + + public Bitmap GetBitmap(List ffts) + { + if (ffts.Count == 0) + throw new ArgumentException("Not enough data in FFTs to generate an image yet."); + + int Width = IsRotated ? ffts[0].Length : ffts.Count; + int Height = IsRotated ? ffts.Count : ffts[0].Length; + + Bitmap bmp = new(Width, Height, PixelFormat.Format8bppIndexed); + Colormap.Apply(bmp); + + Rectangle lockRect = new(0, 0, Width, Height); + BitmapData bitmapData = bmp.LockBits(lockRect, ImageLockMode.ReadOnly, bmp.PixelFormat); + int stride = bitmapData.Stride; + + byte[] bytes = new byte[bitmapData.Stride * bmp.Height]; + Parallel.For(0, Width, col => + { + int sourceCol = col; + if (IsRoll) + { + sourceCol += Width - RollOffset % Width; + if (sourceCol >= Width) + sourceCol -= Width; + } + + for (int row = 0; row < Height; row++) + { + double value = IsRotated ? ffts[row][sourceCol] : ffts[sourceCol][row]; + if (IsDecibel) + value = 20 * Math.Log10(value * DecibelScaleFactor + 1); + value *= Intensity; + value = Math.Min(value, 255); + int bytePosition = (Height - 1 - row) * stride + col; + bytes[bytePosition] = (byte)value; + } + }); + + Marshal.Copy(bytes, 0, bitmapData.Scan0, bytes.Length); + bmp.UnlockBits(bitmapData); + + return bmp; + } + } +} diff --git a/src/Spectrogram/Spectrogram.csproj b/src/Spectrogram/Spectrogram.csproj index 028ff85..7d543ea 100644 --- a/src/Spectrogram/Spectrogram.csproj +++ b/src/Spectrogram/Spectrogram.csproj @@ -21,6 +21,7 @@ true true true + latest diff --git a/src/Spectrogram/SpectrogramGenerator.cs b/src/Spectrogram/SpectrogramGenerator.cs index 367d91d..bf4cac9 100644 --- a/src/Spectrogram/SpectrogramGenerator.cs +++ b/src/Spectrogram/SpectrogramGenerator.cs @@ -267,15 +267,16 @@ public List GetMelFFTs(int melBinCount) /// If true, output will be log-transformed. /// If dB scaling is in use, this multiplier will be applied before log transformation. /// Behavior of the spectrogram when it is full of data. + /// If True, the image will be rotated so time flows from top to bottom (rather than left to right). /// Roll (true) adds new columns on the left overwriting the oldest ones. /// Scroll (false) slides the whole image to the left and adds new columns to the right. - public Bitmap GetBitmap(double intensity = 1, bool dB = false, double dBScale = 1, bool roll = false) + public Bitmap GetBitmap(double intensity = 1, bool dB = false, double dBScale = 1, bool roll = false, bool rotate = false) { if (FFTs.Count == 0) throw new InvalidOperationException("Not enough data to create an image. " + $"Ensure {nameof(Width)} is >0 before calling {nameof(GetBitmap)}()."); - return Image.GetBitmap(FFTs, Colormap, intensity, dB, dBScale, roll, NextColumnIndex); + return Image.GetBitmap(FFTs, Colormap, intensity, dB, dBScale, roll, NextColumnIndex, rotate); } /// From 99b0c71ceb4b00ce07a1d9e669f6491ef48c450d Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Sun, 10 Jul 2022 14:44:56 -0400 Subject: [PATCH 53/62] Spectrogram 1.6 --- src/Spectrogram/Spectrogram.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Spectrogram/Spectrogram.csproj b/src/Spectrogram/Spectrogram.csproj index 7d543ea..bd74147 100644 --- a/src/Spectrogram/Spectrogram.csproj +++ b/src/Spectrogram/Spectrogram.csproj @@ -1,8 +1,8 @@ - + netstandard2.0 - 1.5.0 + 1.6.0 A .NET Standard library for creating spectrograms Scott Harden Harden Technologies, LLC From 5d9dc0fdd9088052978ee07e806d7304b4df794f Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Sun, 10 Jul 2022 15:00:07 -0400 Subject: [PATCH 54/62] GetBitmap: fix rotated image orientation resolves #47 --- src/Spectrogram/ImageMaker.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Spectrogram/ImageMaker.cs b/src/Spectrogram/ImageMaker.cs index 5d70013..dd4348f 100644 --- a/src/Spectrogram/ImageMaker.cs +++ b/src/Spectrogram/ImageMaker.cs @@ -82,9 +82,13 @@ public Bitmap GetBitmap(List ffts) for (int row = 0; row < Height; row++) { - double value = IsRotated ? ffts[row][sourceCol] : ffts[sourceCol][row]; + double value = IsRotated + ? ffts[Height - row - 1][sourceCol] + : ffts[sourceCol][row]; + if (IsDecibel) value = 20 * Math.Log10(value * DecibelScaleFactor + 1); + value *= Intensity; value = Math.Min(value, 255); int bytePosition = (Height - 1 - row) * stride + col; From d73ad895e78b90afd9550f79df76374b3beb2b33 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Sun, 10 Jul 2022 15:00:42 -0400 Subject: [PATCH 55/62] Spectrogram 1.6.1 --- src/Spectrogram/Spectrogram.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Spectrogram/Spectrogram.csproj b/src/Spectrogram/Spectrogram.csproj index bd74147..beb2689 100644 --- a/src/Spectrogram/Spectrogram.csproj +++ b/src/Spectrogram/Spectrogram.csproj @@ -2,7 +2,7 @@ netstandard2.0 - 1.6.0 + 1.6.1 A .NET Standard library for creating spectrograms Scott Harden Harden Technologies, LLC From 86a94f7058b88e5fbc0d87caa55f5077fe12efe2 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Tue, 16 May 2023 23:12:18 -0400 Subject: [PATCH 56/62] csproj: target .NET6 --- .../Spectrogram.Demo.csproj | 34 ++++----- .../Spectrogram.Tests.csproj | 30 ++++---- src/Spectrogram/Spectrogram.csproj | 72 +++++++++---------- 3 files changed, 68 insertions(+), 68 deletions(-) diff --git a/src/Spectrogram.MicrophoneDemo/Spectrogram.Demo.csproj b/src/Spectrogram.MicrophoneDemo/Spectrogram.Demo.csproj index 607dc15..65f7548 100644 --- a/src/Spectrogram.MicrophoneDemo/Spectrogram.Demo.csproj +++ b/src/Spectrogram.MicrophoneDemo/Spectrogram.Demo.csproj @@ -1,19 +1,19 @@  - - net6.0-windows - WinExe - false - true - true - - - - - - - - - - - + + net6.0-windows + WinExe + false + true + true + + + + + + + + + + + \ No newline at end of file diff --git a/src/Spectrogram.Tests/Spectrogram.Tests.csproj b/src/Spectrogram.Tests/Spectrogram.Tests.csproj index 3ec3d29..7754517 100644 --- a/src/Spectrogram.Tests/Spectrogram.Tests.csproj +++ b/src/Spectrogram.Tests/Spectrogram.Tests.csproj @@ -1,21 +1,21 @@ - - net6.0 - false - + + net6.0 + false + - - - - - - - - + + + + + + + + - - - + + + diff --git a/src/Spectrogram/Spectrogram.csproj b/src/Spectrogram/Spectrogram.csproj index beb2689..dcbb279 100644 --- a/src/Spectrogram/Spectrogram.csproj +++ b/src/Spectrogram/Spectrogram.csproj @@ -1,42 +1,42 @@  - - netstandard2.0 - 1.6.1 - A .NET Standard library for creating spectrograms - Scott Harden - Harden Technologies, LLC - MIT - https://github.com/swharden/Spectrogram - icon.png - https://github.com/swharden/Spectrogram - spectrogram spectrum fft frequency audio microphone signal - https://github.com/swharden/Spectrogram/releases - true - true - README.md - portable - true - snupkg - true - true - true - latest - + + netstandard2.0;net6.0 + 1.6.1 + A .NET Standard library for creating spectrograms + Scott Harden + Harden Technologies, LLC + MIT + https://github.com/swharden/Spectrogram + icon.png + https://github.com/swharden/Spectrogram + spectrogram spectrum fft frequency audio microphone signal + https://github.com/swharden/Spectrogram/releases + true + true + README.md + portable + true + snupkg + true + true + true + latest + - - - - + + + + - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + From 83b24f987ce02a30d21257adc2efe0649d50724f Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Tue, 16 May 2023 23:15:11 -0400 Subject: [PATCH 57/62] remove obsolete classes --- src/Spectrogram/SFF.cs | 289 --------------------------------- src/Spectrogram/Spectrogram.cs | 12 -- src/Spectrogram/Tools.cs | 104 ------------ src/Spectrogram/WavFile.cs | 121 -------------- 4 files changed, 526 deletions(-) delete mode 100644 src/Spectrogram/SFF.cs delete mode 100644 src/Spectrogram/Spectrogram.cs delete mode 100644 src/Spectrogram/Tools.cs delete mode 100644 src/Spectrogram/WavFile.cs diff --git a/src/Spectrogram/SFF.cs b/src/Spectrogram/SFF.cs deleted file mode 100644 index d64bb51..0000000 --- a/src/Spectrogram/SFF.cs +++ /dev/null @@ -1,289 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Drawing; -using System.IO; -using System.Linq; -using System.Text; - -namespace Spectrogram -{ - [Obsolete("The SFF file format is obsolete. " + - "Users are encouraged to write their own IO routines specific to their application. "+ - "To get a copy of the original SFF reader/writer see https://github.com/swharden/Spectrogram/issues/44", - error: true)] - public class SFF - { - public readonly byte VersionMajor = 1; - public readonly byte VersionMinor = 1; - public string FilePath { get; private set; } - - // time information - public int SampleRate { get; private set; } - public int StepSize { get; private set; } - public int Width { get; private set; } - - // frequency information - public int FftSize { get; private set; } - public int FftFirstIndex { get; private set; } - public int Height { get; private set; } - public int OffsetHz { get; private set; } - public int MelBinCount { get; private set; } - public bool Decibels { get; private set; } - public bool IsMel { get { return MelBinCount > 0; } } - - // image details - public List Ffts { get; private set; } - public int ImageHeight { get { return (Ffts is null) ? 0 : Ffts[0].Length; } } - public int ImageWidth { get { return (Ffts is null) ? 0 : Ffts.Count; } } - public double[] times { get; private set; } - public double[] freqs { get; private set; } - public double[] mels { get; private set; } - - [Obsolete("use ImageWidth", error: false)] - public int FftWidth { get { return ImageWidth; } } - - [Obsolete("use ImageHeight", error: false)] - public int FftHeight { get { return ImageHeight; } } - - public SFF() - { - - } - - public override string ToString() - { - return $"SFF {ImageWidth}x{ImageHeight}"; - } - - public SFF(string loadFilePath) - { - Load(loadFilePath); - CalculateTimes(); - CalculateFrequencies(); - } - - public SFF(SpectrogramGenerator spec, int melBinCount = 0) - { - SampleRate = spec.SampleRate; - StepSize = spec.StepSize; - Width = spec.Width; - FftSize = spec.FftSize; - FftFirstIndex = spec.NextColumnIndex; - Height = spec.Height; - OffsetHz = spec.OffsetHz; - MelBinCount = melBinCount; - Ffts = (melBinCount > 0) ? spec.GetMelFFTs(melBinCount) : spec.GetFFTs(); - CalculateTimes(); - CalculateFrequencies(); - } - - public Bitmap GetBitmap(Colormap cmap = null, double intensity = 1, bool dB = false) - { - cmap = cmap ?? Colormap.Viridis; - return Image.GetBitmap(Ffts, cmap, intensity, dB); - } - - public void Load(string filePath) - { - FilePath = Path.GetFullPath(filePath); - byte[] bytes = File.ReadAllBytes(filePath); - - // ensure the first 4 bytes match what we expect - int magicNumber = BitConverter.ToInt32(bytes, 0); - if (magicNumber != 1179014099) - throw new InvalidDataException("not a valid SFF file"); - - // read file version - byte versionMajor = bytes[40]; - byte versionMinor = bytes[41]; - - // read time information - SampleRate = BitConverter.ToInt32(bytes, 42); - StepSize = BitConverter.ToInt32(bytes, 46); - Width = BitConverter.ToInt32(bytes, 50); - - // read frequency information - FftSize = BitConverter.ToInt32(bytes, 54); - FftFirstIndex = BitConverter.ToInt32(bytes, 58); - Height = BitConverter.ToInt32(bytes, 62); - OffsetHz = BitConverter.ToInt32(bytes, 66); - MelBinCount = BitConverter.ToInt32(bytes, 84); - - // data format - byte valuesPerPoint = bytes[70]; - bool isComplex = valuesPerPoint == 2; - if (isComplex) - throw new NotImplementedException("complex data is not yet supported"); - byte bytesPerValue = bytes[71]; - Decibels = bytes[72] == 1; - - // recording start time - no longer stored in the SFF file - //DateTime dt = new DateTime(bytes[74] + 2000, bytes[75], bytes[76], bytes[77], bytes[78], bytes[79]); - - // data storage - int firstDataByte = (int)BitConverter.ToUInt32(bytes, 80); - - // FFT dimensions - MelBinCount = BitConverter.ToInt32(bytes, 84); - int FftHeight = BitConverter.ToInt32(bytes, 88); - int FftWidth = BitConverter.ToInt32(bytes, 92); - - // create the FFT by reading data from file - Ffts = new List(); - int bytesPerPoint = bytesPerValue * valuesPerPoint; - int bytesPerColumn = FftHeight * bytesPerPoint; - for (int x = 0; x < FftWidth; x++) - { - Ffts.Add(new double[FftHeight]); - int columnOffset = bytesPerColumn * x; - for (int y = 0; y < FftHeight; y++) - { - int rowOffset = y * bytesPerPoint; - int valueOffset = firstDataByte + columnOffset + rowOffset; - double value = BitConverter.ToDouble(bytes, valueOffset); - Ffts[x][y] = value; - } - } - } - - public void Save(string filePath) - { - FilePath = Path.GetFullPath(filePath); - byte[] header = new byte[256]; - - // file type designator - header[0] = 211; // intentionally non-ASCII - header[1] = (byte)'S'; - header[2] = (byte)'F'; - header[3] = (byte)'F'; - header[4] = (byte)'\r'; - header[5] = (byte)'\n'; - header[6] = (byte)' '; - header[7] = (byte)'\n'; - - int magicNumber = BitConverter.ToInt32(header, 0); - if (magicNumber != 1179014099) - throw new InvalidDataException("magic number for SFF files is 1179014099"); - - // plain text helpful for people who open this file in a text editor - string fileInfo = $"Spectrogram File Format {VersionMajor}.{VersionMinor}\r\n"; - byte[] fileInfoBytes = Encoding.UTF8.GetBytes(fileInfo); - if (fileInfoBytes.Length > 32) - throw new InvalidDataException("file info cannot exceed 32 bytes"); - Array.Copy(fileInfoBytes, 0, header, 8, fileInfoBytes.Length); - - // version - header[40] = VersionMajor; - header[41] = VersionMinor; - - // time information - Array.Copy(BitConverter.GetBytes(SampleRate), 0, header, 42, 4); - Array.Copy(BitConverter.GetBytes(StepSize), 0, header, 46, 4); - Array.Copy(BitConverter.GetBytes(Width), 0, header, 50, 4); - - // frequency information - Array.Copy(BitConverter.GetBytes(FftSize), 0, header, 54, 4); - Array.Copy(BitConverter.GetBytes(FftFirstIndex), 0, header, 58, 4); - Array.Copy(BitConverter.GetBytes(Height), 0, header, 62, 4); - Array.Copy(BitConverter.GetBytes(OffsetHz), 0, header, 66, 4); - - // data encoding details - byte valuesPerPoint = 1; // 1 for magnitude or power data, 2 for complex data - byte bytesPerValue = 8; // a double is 8 bytes - byte decibelUnits = 0; // 1 if units are in dB - byte dataExtraByte = 0; // unused - header[70] = valuesPerPoint; - header[71] = bytesPerValue; - header[72] = decibelUnits; - header[73] = dataExtraByte; - - // source file date and time - // dont store this because it makes SFF files different every time they are generated - //header[74] = (byte)(DateTime.UtcNow.Year - 2000); - //header[75] = (byte)DateTime.UtcNow.Month; - //header[76] = (byte)DateTime.UtcNow.Day; - //header[77] = (byte)DateTime.UtcNow.Hour; - //header[78] = (byte)DateTime.UtcNow.Minute; - //header[79] = (byte)DateTime.UtcNow.Second; - - // ADD NEW VALUES HERE (after byte 80) - Array.Copy(BitConverter.GetBytes(MelBinCount), 0, header, 84, 4); - Array.Copy(BitConverter.GetBytes(ImageHeight), 0, header, 88, 4); - Array.Copy(BitConverter.GetBytes(ImageWidth), 0, header, 92, 4); - - // binary data location (keep this at byte 80) - int firstDataByte = header.Length; - Array.Copy(BitConverter.GetBytes(firstDataByte), 0, header, 80, 4); - - // create bytes to write to file - int dataPointCount = ImageHeight * ImageWidth; - int bytesPerPoint = bytesPerValue * valuesPerPoint; - byte[] fileBytes = new byte[header.Length + dataPointCount * bytesPerPoint]; - Array.Copy(header, 0, fileBytes, 0, header.Length); - - // copy data into byte area - int bytesPerColumn = ImageHeight * bytesPerPoint; - for (int x = 0; x < ImageWidth; x++) - { - int columnOffset = bytesPerColumn * x; - for (int y = 0; y < ImageHeight; y++) - { - int rowOffset = y * bytesPerPoint; - int valueOffset = firstDataByte + columnOffset + rowOffset; - double value = Ffts[x][y]; - Array.Copy(BitConverter.GetBytes(value), 0, fileBytes, valueOffset, 8); - } - } - - // write file to disk - File.WriteAllBytes(filePath, fileBytes); - } - - public (double timeSec, double freqHz, double magRms) GetPixelInfo(int x, int y) - { - double timeSec = (double)x * StepSize / SampleRate; - - double maxFreq = SampleRate / 2; - double maxMel = FftSharp.Transform.MelFromFreq(maxFreq); - double frac = (ImageHeight - y) / (double)ImageHeight; - double freq = IsMel ? FftSharp.Transform.MelToFreq(frac * maxMel) : frac * maxFreq; - - double mag = double.NaN; - try { mag = Ffts[x][ImageHeight - y - 1]; } catch { } - - return (timeSec, freq, mag); - } - - private void CalculateTimes() - { - times = new double[ImageWidth]; - double stepSec = (double)StepSize / SampleRate; - for (int i = 0; i < ImageWidth; i++) - times[i] = i * stepSec; - } - - private void CalculateFrequencies() - { - freqs = new double[ImageHeight]; - mels = new double[ImageHeight]; - - double maxFreq = SampleRate / 2; - double maxMel = FftSharp.Transform.MelFromFreq(maxFreq); - for (int y = 0; y < ImageHeight; y++) - { - double frac = (ImageHeight - y) / (double)ImageHeight; - if (IsMel) - { - mels[y] = frac * maxMel; - freqs[y] = FftSharp.Transform.MelToFreq(mels[y]); - } - else - { - freqs[y] = frac * maxFreq; - mels[y] = FftSharp.Transform.MelFromFreq(freqs[y]); - } - } - } - } -} diff --git a/src/Spectrogram/Spectrogram.cs b/src/Spectrogram/Spectrogram.cs deleted file mode 100644 index 1b09e1a..0000000 --- a/src/Spectrogram/Spectrogram.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace Spectrogram -{ - [Obsolete("This class has been replaced by SpectrogramGenerator", true)] - public class Spectrogram : SpectrogramGenerator - { - public Spectrogram(int sampleRate, int fftSize, int stepSize, double minFreq = 0, double maxFreq = double.PositiveInfinity, int? fixedWidth = null, int offsetHz = 0) : - base(sampleRate, fftSize, stepSize, minFreq, maxFreq, fixedWidth, offsetHz) - { } - } -} diff --git a/src/Spectrogram/Tools.cs b/src/Spectrogram/Tools.cs deleted file mode 100644 index 6300194..0000000 --- a/src/Spectrogram/Tools.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace Spectrogram -{ - public static class Tools - { - [Obsolete("The SFF file format is obsolete. " + - "Users are encouraged to write their own IO routines specific to their application. " + - "To get a copy of the original SFF reader/writer see https://github.com/swharden/Spectrogram/issues/44", - error: true)] - /// - /// Collapse the 2D spectrogram into a 1D array (mean power of each frequency) - /// - public static double[] SffMeanFFT(SFF sff, bool dB = false) - { - double[] mean = new double[sff.Ffts[0].Length]; - - foreach (var fft in sff.Ffts) - for (int y = 0; y < fft.Length; y++) - mean[y] += fft[y]; - - for (int i = 0; i < mean.Length; i++) - mean[i] /= sff.Ffts.Count(); - - if (dB) - for (int i = 0; i < mean.Length; i++) - mean[i] = 20 * Math.Log10(mean[i]); - - if (mean[mean.Length - 1] <= 0) - mean[mean.Length - 1] = mean[mean.Length - 2]; - - return mean; - } - - - [Obsolete("The SFF file format is obsolete. " + - "Users are encouraged to write their own IO routines specific to their application. " + - "To get a copy of the original SFF reader/writer see https://github.com/swharden/Spectrogram/issues/44", - error: true)] - /// - /// Collapse the 2D spectrogram into a 1D array (mean power of each time point) - /// - public static double[] SffMeanPower(SFF sff, bool dB = false) - { - double[] power = new double[sff.Ffts.Count]; - - for (int i = 0; i < sff.Ffts.Count; i++) - power[i] = (double)sff.Ffts[i].Sum() / sff.Ffts[i].Length; - - if (dB) - for (int i = 0; i < power.Length; i++) - power[i] = 20 * Math.Log10(power[i]); - - return power; - } - - [Obsolete("The SFF file format is obsolete. " + - "Users are encouraged to write their own IO routines specific to their application. " + - "To get a copy of the original SFF reader/writer see https://github.com/swharden/Spectrogram/issues/44", - error: true)] - public static double GetPeakFrequency(SFF sff, bool firstFftOnly = false) - { - double[] freqs = firstFftOnly ? sff.Ffts[0] : SffMeanFFT(sff, false); - - int peakIndex = 0; - double peakPower = 0; - for (int i = 0; i < freqs.Length; i++) - { - if (freqs[i] > peakPower) - { - peakPower = freqs[i]; - peakIndex = i; - } - } - - double maxFreq = sff.SampleRate / 2; - double frac = peakIndex / (double)sff.ImageHeight; - - if (sff.MelBinCount > 0) - { - double maxMel = FftSharp.Transform.MelFromFreq(maxFreq); - return FftSharp.Transform.MelToFreq(frac * maxMel); - } - else - { - return frac * maxFreq; - } - } - - public static int GetPianoKey(double frequencyHz) - { - double pianoKey = (39.86 * Math.Log10(frequencyHz / 440)) + 49; - return (int)Math.Round(pianoKey); - } - - public static int GetMidiNote(double frequencyHz) - { - return GetPianoKey(frequencyHz) + 20; - } - } -} diff --git a/src/Spectrogram/WavFile.cs b/src/Spectrogram/WavFile.cs deleted file mode 100644 index dc8a182..0000000 --- a/src/Spectrogram/WavFile.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; - -namespace Spectrogram -{ - [Obsolete("Use a library like NAudio to extract data from WAV files (see spectrogram quickstart for examples)", true)] - public static class WavFile - { - private static (string id, uint length) ChunkInfo(BinaryReader br, long position) - { - br.BaseStream.Seek(position, SeekOrigin.Begin); - string chunkID = new string(br.ReadChars(4)); - uint chunkBytes = br.ReadUInt32(); - return (chunkID, chunkBytes); - } - - public static (int sampleRate, double[] L) ReadMono(string filePath) - { - (int sampleRate, double[] L, _) = ReadStereo(filePath); - return (sampleRate, L); - } - - public static (int sampleRate, double[] L, double[] R) ReadStereo(string filePath) - { - using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read)) - using (BinaryReader br = new BinaryReader(fs)) - { - // The first chunk is RIFF section - // Length should be the number of bytes in the file minus 4 - var (id, length) = ChunkInfo(br, 0); - Console.WriteLine($"First chunk '{id}' indicates {length:N0} bytes"); - if (id != "RIFF") - throw new InvalidOperationException($"Unsupported WAV format (first chunk ID was '{id}', not 'RIFF')"); - - // The second chunk is FORMAT section - var fmtChunk = ChunkInfo(br, 12); - Console.WriteLine($"Format chunk '{fmtChunk.id}' indicates {fmtChunk.length:N0} bytes"); - if (fmtChunk.id != "fmt ") - throw new InvalidOperationException($"Unsupported WAV format (first chunk ID was '{fmtChunk.id}', not 'fmt ')"); - if (fmtChunk.length != 16) - throw new InvalidOperationException($"Unsupported WAV format (expect 16 byte 'fmt' chunk, got {fmtChunk.length} bytes)"); - - // By now we verified this is probably a valid FORMAT section, so read its values. - int audioFormat = br.ReadUInt16(); - Console.WriteLine($"audio format: {audioFormat}"); - if (audioFormat != 1) - throw new NotImplementedException("Unsupported WAV format (audio format must be 1, indicating uncompressed PCM data)"); - - int channelCount = br.ReadUInt16(); - Console.WriteLine($"channel count: {channelCount}"); - if (channelCount < 0 || channelCount > 2) - throw new NotImplementedException($"Unsupported WAV format (must be 1 or 2 channel, file has {channelCount})"); - - int sampleRate = (int)br.ReadUInt32(); - Console.WriteLine($"sample rate: {sampleRate} Hz"); - - int byteRate = (int)br.ReadUInt32(); - Console.WriteLine($"byteRate: {byteRate}"); - - ushort blockSize = br.ReadUInt16(); - Console.WriteLine($"block size: {blockSize} bytes per sample"); - - ushort bitsPerSample = br.ReadUInt16(); - Console.WriteLine($"resolution: {bitsPerSample}-bit"); - if (bitsPerSample != 16) - throw new NotImplementedException("Only 16-bit WAV files are supported"); - - // Cycle custom chunks until we get to the DATA chunk - // Various chunks may exist until the data chunk appears - long nextChunkPosition = 36; - int maximumChunkNumber = 42; - long firstDataByte = 0; - long dataByteCount = 0; - for (int i = 0; i < maximumChunkNumber; i++) - { - var chunk = ChunkInfo(br, nextChunkPosition); - Console.WriteLine($"Chunk at {nextChunkPosition} ('{chunk.id}') indicates {chunk.length:N0} bytes"); - if (chunk.id == "data") - { - firstDataByte = nextChunkPosition + 8; - dataByteCount = chunk.length; - break; - } - nextChunkPosition += chunk.length + 8; - } - if (firstDataByte == 0 || dataByteCount == 0) - throw new InvalidOperationException("Unsupported WAV format (no 'data' chunk found)"); - Console.WriteLine($"PCM data starts at {firstDataByte} and contains {dataByteCount} bytes"); - - // Now read PCM data values into an array and return it - long sampleCount = dataByteCount / blockSize; - Debug.WriteLine($"Samples in file: {sampleCount}"); - - double[] L = null; - double[] R = null; - - if (channelCount == 1) - { - L = new double[sampleCount]; - for (int i = 0; i < sampleCount; i++) - { - L[i] = br.ReadInt16(); - } - } - else if (channelCount == 2) - { - L = new double[sampleCount]; - R = new double[sampleCount]; - for (int i = 0; i < sampleCount; i++) - { - L[i] = br.ReadInt16(); - R[i] = br.ReadInt16(); - } - } - - return (sampleRate, L, R); - } - } - } -} \ No newline at end of file From 1aa4c69cbcc0605961cf5ee31dd555d86600b3dc Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Tue, 16 May 2023 23:22:14 -0400 Subject: [PATCH 58/62] remove obsolete methods --- src/Spectrogram/SpectrogramGenerator.cs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/Spectrogram/SpectrogramGenerator.cs b/src/Spectrogram/SpectrogramGenerator.cs index bf4cac9..68a9c3b 100644 --- a/src/Spectrogram/SpectrogramGenerator.cs +++ b/src/Spectrogram/SpectrogramGenerator.cs @@ -351,20 +351,6 @@ public Bitmap GetBitmapMax(double intensity = 1, bool dB = false, double dBScale return Image.GetBitmap(ffts2, Colormap, intensity, dB, dBScale, roll, NextColumnIndex); } - [Obsolete("The SFF file format is obsolete. " + - "Users are encouraged to write their own IO routines specific to their application. " + - "To get a copy of the original SFF reader/writer see https://github.com/swharden/Spectrogram/issues/44", - error: true)] - /// - /// Export spectrogram data using the Spectrogram File Format (SFF) - /// - public void SaveData(string filePath, int melBinCount = 0) - { - if (!filePath.EndsWith(".sff", StringComparison.OrdinalIgnoreCase)) - filePath += ".sff"; - new SFF(this, melBinCount).Save(filePath); - } - /// /// Defines the total number of FFTs (spectrogram columns) to store in memory. Determines Width. /// From 5aeee49ca3b6556bed21471b345405f2982ec176 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Tue, 16 May 2023 23:26:27 -0400 Subject: [PATCH 59/62] csproj: FftSharp 2.0.0 --- src/Spectrogram.MicrophoneDemo/Spectrogram.Demo.csproj | 2 +- src/Spectrogram/Spectrogram.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Spectrogram.MicrophoneDemo/Spectrogram.Demo.csproj b/src/Spectrogram.MicrophoneDemo/Spectrogram.Demo.csproj index 65f7548..344f096 100644 --- a/src/Spectrogram.MicrophoneDemo/Spectrogram.Demo.csproj +++ b/src/Spectrogram.MicrophoneDemo/Spectrogram.Demo.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/Spectrogram/Spectrogram.csproj b/src/Spectrogram/Spectrogram.csproj index dcbb279..4804cff 100644 --- a/src/Spectrogram/Spectrogram.csproj +++ b/src/Spectrogram/Spectrogram.csproj @@ -30,7 +30,7 @@ - + From 44ef4fe256baee9991465de8c6df6dd47d6e31bd Mon Sep 17 00:00:00 2001 From: Vincenzo Addati <71467711+vadd98@users.noreply.github.com> Date: Fri, 1 Nov 2024 15:50:17 +0100 Subject: [PATCH 60/62] Replace `System.Drawing.Common` with `SkiaSharp` to improve cross platform support (#61) * SkiaSharp implementation * Target NET 8 * Improved ImageMaker performances * Target NET 8 * Removed System.Drawing references * Fix spectrogram orientation * Fix Colormap * modernize tests * modernize demo * Spectrogram: resolve warnings * demo: resolve warnings * nuget: version 2.0.0-alpha * Colormap: replace with a single class that uses ScottPlot * CICS: build and test with .NET 8 --------- Co-authored-by: Scott W Harden --- .github/workflows/ci.yaml | 23 +- src/Spectrogram.MicrophoneDemo/App.config | 6 - .../FormMicrophone.cs | 5 +- src/Spectrogram.MicrophoneDemo/Program.cs | 7 +- .../Properties/AssemblyInfo.cs | 36 - .../Properties/Resources.Designer.cs | 71 -- .../Properties/Resources.resx | 117 --- .../Properties/Settings.Designer.cs | 30 - .../Properties/Settings.settings | 7 - .../Spectrogram.Demo.csproj | 14 +- .../packages.config | 6 - src/Spectrogram.Tests/AudioFileTests.cs | 10 +- src/Spectrogram.Tests/ColormapExamples.cs | 41 - src/Spectrogram.Tests/ColormapValues.cs | 361 --------- src/Spectrogram.Tests/ImageTests.cs | 9 +- src/Spectrogram.Tests/Mel.cs | 60 +- src/Spectrogram.Tests/SkExtensions.cs | 13 + .../Spectrogram.Tests.csproj | 15 +- src/Spectrogram/Colormap.cs | 144 ++-- src/Spectrogram/Colormaps/Argo.cs | 55 -- src/Spectrogram/Colormaps/Blues.cs | 50 -- src/Spectrogram/Colormaps/Grayscale.cs | 12 - src/Spectrogram/Colormaps/GrayscaleR.cs | 13 - src/Spectrogram/Colormaps/Greens.cs | 51 -- src/Spectrogram/Colormaps/Inferno.cs | 57 -- src/Spectrogram/Colormaps/Lopora.cs | 55 -- src/Spectrogram/Colormaps/Magma.cs | 57 -- src/Spectrogram/Colormaps/Plasma.cs | 57 -- src/Spectrogram/Colormaps/Turbo.cs | 53 -- src/Spectrogram/Colormaps/Viridis.cs | 57 -- src/Spectrogram/IColormap.cs | 11 - src/Spectrogram/Image.cs | 11 +- src/Spectrogram/ImageMaker.cs | 49 +- src/Spectrogram/Scale.cs | 104 ++- src/Spectrogram/Settings.cs | 4 +- src/Spectrogram/Spectrogram.csproj | 20 +- src/Spectrogram/SpectrogramGenerator.cs | 752 +++++++++--------- 37 files changed, 600 insertions(+), 1843 deletions(-) delete mode 100644 src/Spectrogram.MicrophoneDemo/App.config delete mode 100644 src/Spectrogram.MicrophoneDemo/Properties/AssemblyInfo.cs delete mode 100644 src/Spectrogram.MicrophoneDemo/Properties/Resources.Designer.cs delete mode 100644 src/Spectrogram.MicrophoneDemo/Properties/Resources.resx delete mode 100644 src/Spectrogram.MicrophoneDemo/Properties/Settings.Designer.cs delete mode 100644 src/Spectrogram.MicrophoneDemo/Properties/Settings.settings delete mode 100644 src/Spectrogram.MicrophoneDemo/packages.config delete mode 100644 src/Spectrogram.Tests/ColormapExamples.cs delete mode 100644 src/Spectrogram.Tests/ColormapValues.cs create mode 100644 src/Spectrogram.Tests/SkExtensions.cs delete mode 100644 src/Spectrogram/Colormaps/Argo.cs delete mode 100644 src/Spectrogram/Colormaps/Blues.cs delete mode 100644 src/Spectrogram/Colormaps/Grayscale.cs delete mode 100644 src/Spectrogram/Colormaps/GrayscaleR.cs delete mode 100644 src/Spectrogram/Colormaps/Greens.cs delete mode 100644 src/Spectrogram/Colormaps/Inferno.cs delete mode 100644 src/Spectrogram/Colormaps/Lopora.cs delete mode 100644 src/Spectrogram/Colormaps/Magma.cs delete mode 100644 src/Spectrogram/Colormaps/Plasma.cs delete mode 100644 src/Spectrogram/Colormaps/Turbo.cs delete mode 100644 src/Spectrogram/Colormaps/Viridis.cs delete mode 100644 src/Spectrogram/IColormap.cs diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2966d92..8a2d2bd 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -17,31 +17,24 @@ jobs: runs-on: windows-latest steps: - name: 🛒 Checkout - uses: actions/checkout@v2 - - - name: ✨ Setup .NET 6 - uses: actions/setup-dotnet@v1 + uses: actions/checkout@v4 + - name: ✨ Setup .NET 8 + uses: actions/setup-dotnet@v4 with: - dotnet-version: "6.0.x" - + dotnet-version: "8.0.x" - name: 🚚 Restore run: dotnet restore src - - name: 🛠️ Build - run: dotnet build src --configuration Release --no-restore - + run: dotnet build src --configuration Release - name: 🧪 Test - run: dotnet test src --configuration Release --no-build - + run: dotnet test src --configuration Release - name: 📦 Pack - run: dotnet pack src --configuration Release --no-build - + run: dotnet pack src --configuration Release - name: 🔑 Configure Secrets if: github.event_name == 'release' uses: nuget/setup-nuget@v1 with: nuget-api-key: ${{ secrets.NUGET_API_KEY }} - - - name: 🚀 Deploy Package + - name: 🚀 Deploy NuGet Package if: github.event_name == 'release' run: nuget push "src\Spectrogram\bin\Release\*.nupkg" -SkipDuplicate -Source https://api.nuget.org/v3/index.json diff --git a/src/Spectrogram.MicrophoneDemo/App.config b/src/Spectrogram.MicrophoneDemo/App.config deleted file mode 100644 index 56efbc7..0000000 --- a/src/Spectrogram.MicrophoneDemo/App.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/Spectrogram.MicrophoneDemo/FormMicrophone.cs b/src/Spectrogram.MicrophoneDemo/FormMicrophone.cs index e6b4964..975c5b1 100644 --- a/src/Spectrogram.MicrophoneDemo/FormMicrophone.cs +++ b/src/Spectrogram.MicrophoneDemo/FormMicrophone.cs @@ -9,6 +9,7 @@ using System.Text; using System.Threading.Tasks; using System.Windows.Forms; +using SkiaSharp.Views.Desktop; namespace Spectrogram.MicrophoneDemo { @@ -64,7 +65,7 @@ private void StartListening() pbSpectrogram.Height = spec.Height; pbScaleVert.Image?.Dispose(); - pbScaleVert.Image = spec.GetVerticalScale(pbScaleVert.Width); + pbScaleVert.Image = spec.GetVerticalScale(pbScaleVert.Width).ToBitmap(); pbScaleVert.Height = spec.Height; } @@ -85,7 +86,7 @@ private void timer1_Tick(object sender, EventArgs e) using (var gfx = Graphics.FromImage(bmpSpec)) using (var pen = new Pen(Color.White)) { - gfx.DrawImage(bmpSpecIndexed, 0, 0); + gfx.DrawImage(bmpSpecIndexed.ToBitmap(), 0, 0); if (cbRoll.Checked) { gfx.DrawLine(pen, spec.NextColumnIndex, 0, spec.NextColumnIndex, pbSpectrogram.Height); diff --git a/src/Spectrogram.MicrophoneDemo/Program.cs b/src/Spectrogram.MicrophoneDemo/Program.cs index e1a34c4..821c47b 100644 --- a/src/Spectrogram.MicrophoneDemo/Program.cs +++ b/src/Spectrogram.MicrophoneDemo/Program.cs @@ -1,16 +1,11 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using System.Runtime.Versioning; using System.Windows.Forms; namespace Spectrogram.MicrophoneDemo { static class Program { - /// - /// The main entry point for the application. - /// [STAThread] static void Main() { diff --git a/src/Spectrogram.MicrophoneDemo/Properties/AssemblyInfo.cs b/src/Spectrogram.MicrophoneDemo/Properties/AssemblyInfo.cs deleted file mode 100644 index 5eaa0f3..0000000 --- a/src/Spectrogram.MicrophoneDemo/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Spectrogram.MicrophoneDemo")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Spectrogram.MicrophoneDemo")] -[assembly: AssemblyCopyright("Copyright © 2020")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("d51abc6a-53f4-4620-88a1-14ea1d779538")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Spectrogram.MicrophoneDemo/Properties/Resources.Designer.cs b/src/Spectrogram.MicrophoneDemo/Properties/Resources.Designer.cs deleted file mode 100644 index 1a71b5a..0000000 --- a/src/Spectrogram.MicrophoneDemo/Properties/Resources.Designer.cs +++ /dev/null @@ -1,71 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Spectrogram.MicrophoneDemo.Properties -{ - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources - { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() - { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager - { - get - { - if ((resourceMan == null)) - { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Spectrogram.MicrophoneDemo.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture - { - get - { - return resourceCulture; - } - set - { - resourceCulture = value; - } - } - } -} diff --git a/src/Spectrogram.MicrophoneDemo/Properties/Resources.resx b/src/Spectrogram.MicrophoneDemo/Properties/Resources.resx deleted file mode 100644 index af7dbeb..0000000 --- a/src/Spectrogram.MicrophoneDemo/Properties/Resources.resx +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/src/Spectrogram.MicrophoneDemo/Properties/Settings.Designer.cs b/src/Spectrogram.MicrophoneDemo/Properties/Settings.Designer.cs deleted file mode 100644 index 37d8130..0000000 --- a/src/Spectrogram.MicrophoneDemo/Properties/Settings.Designer.cs +++ /dev/null @@ -1,30 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Spectrogram.MicrophoneDemo.Properties -{ - - - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] - internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase - { - - private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); - - public static Settings Default - { - get - { - return defaultInstance; - } - } - } -} diff --git a/src/Spectrogram.MicrophoneDemo/Properties/Settings.settings b/src/Spectrogram.MicrophoneDemo/Properties/Settings.settings deleted file mode 100644 index 3964565..0000000 --- a/src/Spectrogram.MicrophoneDemo/Properties/Settings.settings +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/src/Spectrogram.MicrophoneDemo/Spectrogram.Demo.csproj b/src/Spectrogram.MicrophoneDemo/Spectrogram.Demo.csproj index 344f096..e58c01a 100644 --- a/src/Spectrogram.MicrophoneDemo/Spectrogram.Demo.csproj +++ b/src/Spectrogram.MicrophoneDemo/Spectrogram.Demo.csproj @@ -1,19 +1,15 @@  - net6.0-windows + net8.0-windows WinExe - false true true + NU1701 - - - - - - - + + + \ No newline at end of file diff --git a/src/Spectrogram.MicrophoneDemo/packages.config b/src/Spectrogram.MicrophoneDemo/packages.config deleted file mode 100644 index 64d4d6d..0000000 --- a/src/Spectrogram.MicrophoneDemo/packages.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/Spectrogram.Tests/AudioFileTests.cs b/src/Spectrogram.Tests/AudioFileTests.cs index 60248fc..ef99c25 100644 --- a/src/Spectrogram.Tests/AudioFileTests.cs +++ b/src/Spectrogram.Tests/AudioFileTests.cs @@ -1,7 +1,5 @@ -using NUnit.Framework; -using System; -using System.Collections.Generic; -using System.Text; +using FluentAssertions; +using NUnit.Framework; namespace Spectrogram.Tests { @@ -20,8 +18,8 @@ public void Test_AudioFile_LengthAndSampleRate(string filename, int knownRate, i string filePath = $"../../../../../data/{filename}"; (double[] audio, int sampleRate) = AudioFile.ReadWAV(filePath); - Assert.AreEqual(knownRate, sampleRate); - Assert.AreEqual(knownLength, audio.Length / channels); + sampleRate.Should().Be(knownRate); + (audio.Length / channels).Should().Be(knownLength); } } } diff --git a/src/Spectrogram.Tests/ColormapExamples.cs b/src/Spectrogram.Tests/ColormapExamples.cs deleted file mode 100644 index 276b3f1..0000000 --- a/src/Spectrogram.Tests/ColormapExamples.cs +++ /dev/null @@ -1,41 +0,0 @@ -using NUnit.Framework; -using System; -using System.Diagnostics; - -namespace Spectrogram.Tests -{ - class ColormapExamples - { - [Test] - public void Test_Make_CommonColormaps() - { - (double[] audio, int sampleRate) = AudioFile.ReadWAV("../../../../../data/cant-do-that-44100.wav"); - int fftSize = 1 << 12; - var spec = new SpectrogramGenerator(sampleRate, fftSize, stepSize: 700, maxFreq: 2000); - var window = new FftSharp.Windows.Hanning(); - spec.SetWindow(window.Create(fftSize / 3)); // sharper window than typical - spec.Add(audio); - - // delete old colormap files - foreach (var filePath in System.IO.Directory.GetFiles("../../../../../dev/graphics/", "hal-*.png")) - System.IO.File.Delete(filePath); - - foreach (var cmap in Colormap.GetColormaps()) - { - spec.Colormap = cmap; - spec.SaveImage($"../../../../../dev/graphics/hal-{cmap.Name}.png"); - Debug.WriteLine($"![](dev/graphics/hal-{cmap.Name}.png)"); - } - } - - [Test] - public void Test_Colormaps_ByName() - { - string[] names = Colormap.GetColormapNames(); - Console.WriteLine(string.Join(", ", names)); - - Colormap viridisCmap = Colormap.GetColormap("viridis"); - Assert.AreEqual("Viridis", viridisCmap.Name); - } - } -} diff --git a/src/Spectrogram.Tests/ColormapValues.cs b/src/Spectrogram.Tests/ColormapValues.cs deleted file mode 100644 index 9c5d9d1..0000000 --- a/src/Spectrogram.Tests/ColormapValues.cs +++ /dev/null @@ -1,361 +0,0 @@ -using Microsoft.VisualStudio.TestPlatform.Utilities; -using NUnit.Framework; -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Text; - -namespace Spectrogram.Tests -{ - class ColormapValues - { - [Test] - public void Test_Colormap_ExtendedFractionsReturnEdgeValues() - { - var cmap = Colormap.Viridis; - - Random rand = new Random(0); - for (double frac = -3; frac < 3; frac += rand.NextDouble() * .2) - { - Console.WriteLine($"{frac}: {cmap.GetRGB(frac)}"); - - if (frac <= 0) - Assert.AreEqual(cmap.GetRGB(0), cmap.GetRGB(frac)); - - if (frac >= 1) - Assert.AreEqual(cmap.GetRGB(1.0), cmap.GetRGB(frac)); - } - } - - [Test] - public void Test_Colormap_IntegerMatchesRGBColors() - { - var cmap = Colormap.Viridis; - - byte pixelIntensity = 123; - var (r, g, b) = cmap.GetRGB(pixelIntensity); - int int32 = cmap.GetInt32(pixelIntensity); - - Color color1 = Color.FromArgb(255, r, g, b); - Color color2 = Color.FromArgb(int32); - Color color3 = cmap.GetColor(pixelIntensity); - - Assert.AreEqual(color1, color2); - Assert.AreEqual(color1, color3); - } - - [Test] - public void Test_colorLookup_integerMatchesTriplet() - { - for (int i = 0; i < 256; i++) - { - byte[] bytes = BitConverter.GetBytes(ints[i]); - - Color color1 = Color.FromArgb(bytes[2], bytes[1], bytes[0]); - - Color color2 = Color.FromArgb(rgb[i, 0], rgb[i, 1], rgb[i, 2]); - - Assert.AreEqual(color2.R, color1.R, 1); - Assert.AreEqual(color2.G, color1.G, 1); - Assert.AreEqual(color2.B, color1.B, 1); - } - } - - private readonly int[] ints = - { - 04456788, 04457045, 04457303, 04523352, 04523610, 04524123, 04589916, 04590430, - 04590687, 04591201, 04656994, 04657507, 04657765, 04658278, 04658535, 04658793, - 04659306, 04725099, 04725356, 04725870, 04726127, 04726384, 04726897, 04727154, - 04727411, 04727668, 04662645, 04662902, 04663159, 04663416, 04663929, 04664186, - 04664443, 04599164, 04599676, 04599933, 04600190, 04534911, 04535423, 04535680, - 04535937, 04470657, 04471170, 04405891, 04406147, 04406404, 04341124, 04341381, - 04341893, 04276614, 04276870, 04211591, 04211847, 04146567, 04147080, 04081800, - 04082057, 04016777, 04017033, 04017289, 03952010, 03952266, 03887242, 03887498, - 03822219, 03822475, 03757195, 03757451, 03692171, 03692428, 03627148, 03627404, - 03562124, 03562380, 03497100, 03497356, 03432077, 03432333, 03367053, 03367309, - 03302029, 03302285, 03237005, 03237261, 03237517, 03172237, 03172493, 03107213, - 03107469, 03042190, 03042446, 03042702, 02977422, 02977678, 02912398, 02912654, - 02912910, 02847630, 02847886, 02782606, 02782862, 02783118, 02717838, 02718094, - 02652814, 02652814, 02653070, 02587790, 02588046, 02588302, 02523022, 02523278, - 02523534, 02458254, 02458509, 02393229, 02393485, 02393741, 02328461, 02328717, - 02328973, 02263437, 02263693, 02263949, 02198669, 02198924, 02199180, 02133900, - 02134156, 02134412, 02069132, 02069387, 02069643, 02069899, 02070155, 02004874, - 02005130, 02005386, 02005386, 02005641, 02005897, 02006153, 02006408, 02006664, - 02006920, 02007175, 02072967, 02073222, 02073478, 02139269, 02139525, 02205317, - 02205572, 02271108, 02336899, 02337154, 02402946, 02468737, 02534529, 02600320, - 02666111, 02731903, 02797694, 02863485, 02929021, 03060348, 03126139, 03191930, - 03323258, 03389049, 03520376, 03586167, 03717494, 03783030, 03914357, 04045684, - 04111475, 04242802, 04374129, 04505200, 04570991, 04702318, 04833645, 04964972, - 05096043, 05227369, 05358696, 05490023, 05621350, 05752421, 05883748, 06015074, - 06211937, 06343008, 06474335, 06605661, 06802524, 06933595, 07064921, 07196248, - 07392854, 07524181, 07655508, 07852114, 07983441, 08180303, 08311374, 08508236, - 08639307, 08836169, 08967495, 09164102, 09295428, 09492035, 09623361, 09819967, - 09951294, 10147900, 10344762, 10475832, 10672695, 10869301, 11000627, 11197234, - 11394096, 11525166, 11722028, 11918635, 12049705, 12246567, 12443174, 12574500, - 12771106, 12967713, 13099039, 13295646, 13492253, 13623580, 13820187, 13951258, - 14148121, 14344728, 14475800, 14672664, 14803736, 15000344, 15197209, 15328281, - 15524890, 15656219, 15852828, 15983902, 16180767, 16311841, 16442914, 16639780, - - }; - - private readonly byte[,] rgb = - { - {68, 1, 84}, - {68, 2, 86}, - {69, 4, 87}, - {69, 5, 89}, - {70, 7, 90}, - {70, 8, 92}, - {70, 10, 93}, - {70, 11, 94}, - {71, 13, 96}, - {71, 14, 97}, - {71, 16, 99}, - {71, 17, 100}, - {71, 19, 101}, - {72, 20, 103}, - {72, 22, 104}, - {72, 23, 105}, - {72, 24, 106}, - {72, 26, 108}, - {72, 27, 109}, - {72, 28, 110}, - {72, 29, 111}, - {72, 31, 112}, - {72, 32, 113}, - {72, 33, 115}, - {72, 35, 116}, - {72, 36, 117}, - {72, 37, 118}, - {72, 38, 119}, - {72, 40, 120}, - {72, 41, 121}, - {71, 42, 122}, - {71, 44, 122}, - {71, 45, 123}, - {71, 46, 124}, - {71, 47, 125}, - {70, 48, 126}, - {70, 50, 126}, - {70, 51, 127}, - {70, 52, 128}, - {69, 53, 129}, - {69, 55, 129}, - {69, 56, 130}, - {68, 57, 131}, - {68, 58, 131}, - {68, 59, 132}, - {67, 61, 132}, - {67, 62, 133}, - {66, 63, 133}, - {66, 64, 134}, - {66, 65, 134}, - {65, 66, 135}, - {65, 68, 135}, - {64, 69, 136}, - {64, 70, 136}, - {63, 71, 136}, - {63, 72, 137}, - {62, 73, 137}, - {62, 74, 137}, - {62, 76, 138}, - {61, 77, 138}, - {61, 78, 138}, - {60, 79, 138}, - {60, 80, 139}, - {59, 81, 139}, - {59, 82, 139}, - {58, 83, 139}, - {58, 84, 140}, - {57, 85, 140}, - {57, 86, 140}, - {56, 88, 140}, - {56, 89, 140}, - {55, 90, 140}, - {55, 91, 141}, - {54, 92, 141}, - {54, 93, 141}, - {53, 94, 141}, - {53, 95, 141}, - {52, 96, 141}, - {52, 97, 141}, - {51, 98, 141}, - {51, 99, 141}, - {50, 100, 142}, - {50, 101, 142}, - {49, 102, 142}, - {49, 103, 142}, - {49, 104, 142}, - {48, 105, 142}, - {48, 106, 142}, - {47, 107, 142}, - {47, 108, 142}, - {46, 109, 142}, - {46, 110, 142}, - {46, 111, 142}, - {45, 112, 142}, - {45, 113, 142}, - {44, 113, 142}, - {44, 114, 142}, - {44, 115, 142}, - {43, 116, 142}, - {43, 117, 142}, - {42, 118, 142}, - {42, 119, 142}, - {42, 120, 142}, - {41, 121, 142}, - {41, 122, 142}, - {41, 123, 142}, - {40, 124, 142}, - {40, 125, 142}, - {39, 126, 142}, - {39, 127, 142}, - {39, 128, 142}, - {38, 129, 142}, - {38, 130, 142}, - {38, 130, 142}, - {37, 131, 142}, - {37, 132, 142}, - {37, 133, 142}, - {36, 134, 142}, - {36, 135, 142}, - {35, 136, 142}, - {35, 137, 142}, - {35, 138, 141}, - {34, 139, 141}, - {34, 140, 141}, - {34, 141, 141}, - {33, 142, 141}, - {33, 143, 141}, - {33, 144, 141}, - {33, 145, 140}, - {32, 146, 140}, - {32, 146, 140}, - {32, 147, 140}, - {31, 148, 140}, - {31, 149, 139}, - {31, 150, 139}, - {31, 151, 139}, - {31, 152, 139}, - {31, 153, 138}, - {31, 154, 138}, - {30, 155, 138}, - {30, 156, 137}, - {30, 157, 137}, - {31, 158, 137}, - {31, 159, 136}, - {31, 160, 136}, - {31, 161, 136}, - {31, 161, 135}, - {31, 162, 135}, - {32, 163, 134}, - {32, 164, 134}, - {33, 165, 133}, - {33, 166, 133}, - {34, 167, 133}, - {34, 168, 132}, - {35, 169, 131}, - {36, 170, 131}, - {37, 171, 130}, - {37, 172, 130}, - {38, 173, 129}, - {39, 173, 129}, - {40, 174, 128}, - {41, 175, 127}, - {42, 176, 127}, - {44, 177, 126}, - {45, 178, 125}, - {46, 179, 124}, - {47, 180, 124}, - {49, 181, 123}, - {50, 182, 122}, - {52, 182, 121}, - {53, 183, 121}, - {55, 184, 120}, - {56, 185, 119}, - {58, 186, 118}, - {59, 187, 117}, - {61, 188, 116}, - {63, 188, 115}, - {64, 189, 114}, - {66, 190, 113}, - {68, 191, 112}, - {70, 192, 111}, - {72, 193, 110}, - {74, 193, 109}, - {76, 194, 108}, - {78, 195, 107}, - {80, 196, 106}, - {82, 197, 105}, - {84, 197, 104}, - {86, 198, 103}, - {88, 199, 101}, - {90, 200, 100}, - {92, 200, 99}, - {94, 201, 98}, - {96, 202, 96}, - {99, 203, 95}, - {101, 203, 94}, - {103, 204, 92}, - {105, 205, 91}, - {108, 205, 90}, - {110, 206, 88}, - {112, 207, 87}, - {115, 208, 86}, - {117, 208, 84}, - {119, 209, 83}, - {122, 209, 81}, - {124, 210, 80}, - {127, 211, 78}, - {129, 211, 77}, - {132, 212, 75}, - {134, 213, 73}, - {137, 213, 72}, - {139, 214, 70}, - {142, 214, 69}, - {144, 215, 67}, - {147, 215, 65}, - {149, 216, 64}, - {152, 216, 62}, - {155, 217, 60}, - {157, 217, 59}, - {160, 218, 57}, - {162, 218, 55}, - {165, 219, 54}, - {168, 219, 52}, - {170, 220, 50}, - {173, 220, 48}, - {176, 221, 47}, - {178, 221, 45}, - {181, 222, 43}, - {184, 222, 41}, - {186, 222, 40}, - {189, 223, 38}, - {192, 223, 37}, - {194, 223, 35}, - {197, 224, 33}, - {200, 224, 32}, - {202, 225, 31}, - {205, 225, 29}, - {208, 225, 28}, - {210, 226, 27}, - {213, 226, 26}, - {216, 226, 25}, - {218, 227, 25}, - {221, 227, 24}, - {223, 227, 24}, - {226, 228, 24}, - {229, 228, 25}, - {231, 228, 25}, - {234, 229, 26}, - {236, 229, 27}, - {239, 229, 28}, - {241, 229, 29}, - {244, 230, 30}, - {246, 230, 32}, - {248, 230, 33}, - {251, 231, 35}, - {253, 231, 37}, - }; - } -} diff --git a/src/Spectrogram.Tests/ImageTests.cs b/src/Spectrogram.Tests/ImageTests.cs index d02a218..c0fe5ab 100644 --- a/src/Spectrogram.Tests/ImageTests.cs +++ b/src/Spectrogram.Tests/ImageTests.cs @@ -1,4 +1,5 @@ using NUnit.Framework; +using SkiaSharp; namespace Spectrogram.Tests; @@ -12,10 +13,10 @@ public void Test_Image_Rotations() SpectrogramGenerator sg = new(sampleRate, 4096, 500, maxFreq: 3000); sg.Add(audio); - System.Drawing.Bitmap bmp1 = sg.GetBitmap(rotate: false); - bmp1.Save("test-image-original.png"); + SKBitmap bmp1 = sg.GetBitmap(rotate: false); + bmp1.SaveTo("test-image-original.png", SKEncodedImageFormat.Png); - System.Drawing.Bitmap bmp2 = sg.GetBitmap(rotate: true); - bmp2.Save("test-image-rotated.png"); + SKBitmap bmp2 = sg.GetBitmap(rotate: true); + bmp2.SaveTo("test-image-rotated.png", SKEncodedImageFormat.Png); } } diff --git a/src/Spectrogram.Tests/Mel.cs b/src/Spectrogram.Tests/Mel.cs index 43f0d56..be6b6f3 100644 --- a/src/Spectrogram.Tests/Mel.cs +++ b/src/Spectrogram.Tests/Mel.cs @@ -1,9 +1,6 @@ using NUnit.Framework; using System; -using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Imaging; -using System.Text; +using SkiaSharp; namespace Spectrogram.Tests { @@ -17,16 +14,38 @@ public void Test_MelSpectrogram_MelScale() var sg = new SpectrogramGenerator(sampleRate, fftSize, stepSize: 500); sg.Add(audio); - Bitmap bmpMel = sg.GetBitmapMel(250); - bmpMel.Save("../../../../../dev/graphics/halMel-MelScale.png", ImageFormat.Png); + // Ottieni l'immagine Mel-scaled come SKBitmap + SKBitmap bmpMel = sg.GetBitmapMel(250); // Presuppone che sg abbia un metodo GetSKBitmapMel + using (var image = SKImage.FromBitmap(bmpMel)) + using (var data = image.Encode(SKEncodedImageFormat.Png, 100)) + { + // Salva l'immagine Mel-scaled + using (var stream = System.IO.File.OpenWrite("../../../../../dev/graphics/halMel-MelScale.png")) + { + data.SaveTo(stream); + } + } + + // Ottieni l'immagine originale come SKBitmap + SKBitmap bmpRaw = sg.GetBitmap(); // Presuppone che sg abbia un metodo GetSKBitmap + SKBitmap bmpCropped = new SKBitmap(bmpRaw.Width, bmpMel.Height); - Bitmap bmpRaw = sg.GetBitmap(); - Bitmap bmpCropped = new Bitmap(bmpRaw.Width, bmpMel.Height); - using (Graphics gfx = Graphics.FromImage(bmpCropped)) + // Disegna bmpRaw su bmpCropped usando SKCanvas + using (var canvas = new SKCanvas(bmpCropped)) { - gfx.DrawImage(bmpRaw, 0, bmpMel.Height - bmpRaw.Height); + canvas.Clear(SKColors.Transparent); + canvas.DrawBitmap(bmpRaw, new SKRect(0, bmpMel.Height - bmpRaw.Height, bmpRaw.Width, bmpMel.Height)); + } + + using (var imageCropped = SKImage.FromBitmap(bmpCropped)) + using (var dataCropped = imageCropped.Encode(SKEncodedImageFormat.Png, 100)) + { + // Salva l'immagine croppata + using (var streamCropped = System.IO.File.OpenWrite("../../../../../dev/graphics/halMel-LinearCropped.png")) + { + dataCropped.SaveTo(streamCropped); + } } - bmpCropped.Save("../../../../../dev/graphics/halMel-LinearCropped.png", ImageFormat.Png); } [Test] @@ -37,11 +56,11 @@ public void Test_Mel_Graph() double maxMel = 2595 * Math.Log10(1 + maxFreq / 700); Random rand = new Random(1); - double[] freq = ScottPlot.DataGen.Consecutive(specPoints, maxFreq / specPoints); - double[] power = ScottPlot.DataGen.RandomWalk(rand, specPoints, .02, .5); + double[] freq = ScottPlot.Generate.Consecutive(specPoints, maxFreq / specPoints); + double[] power = ScottPlot.Generate.RandomWalk(specPoints, .02, .5); - var plt1 = new ScottPlot.Plot(800, 300); - plt1.AddScatter(freq, power, markerSize: 0); + var plt1 = new ScottPlot.Plot(); + plt1.Add.ScatterLine(freq, power); int filterSize = 25; @@ -64,10 +83,9 @@ public void Test_Mel_Graph() double freqCenter = binStartFreqs[binIndex + 1]; double freqHigh = binStartFreqs[binIndex + 2]; - var sctr = plt1.AddScatter( - xs: new double[] { freqLow, freqCenter, freqHigh }, - ys: new double[] { 0, 1, 0 }, - markerSize: 0, lineWidth: 2); + double[] xs = [freqLow, freqCenter, freqHigh]; + double[] ys = [0, 1, 0]; + var sctr = plt1.Add.ScatterLine(xs, ys); int indexLow = (int)(specPoints * freqLow / maxFreq); int indexHigh = (int)(specPoints * freqHigh / maxFreq); @@ -84,10 +102,10 @@ public void Test_Mel_Graph() binValue += power[indexLow + i] * frac; } binValue /= binScaleSum; - plt1.AddPoint(freqCenter, binValue, sctr.Color, 10); + plt1.Add.Marker(freqCenter, binValue, ScottPlot.MarkerShape.FilledCircle, 10, sctr.Color); } - plt1.SaveFig("mel1.png"); + plt1.SavePng("mel1.png", 800, 300); } [Test] diff --git a/src/Spectrogram.Tests/SkExtensions.cs b/src/Spectrogram.Tests/SkExtensions.cs new file mode 100644 index 0000000..f59a544 --- /dev/null +++ b/src/Spectrogram.Tests/SkExtensions.cs @@ -0,0 +1,13 @@ +using SkiaSharp; + +namespace Spectrogram.Tests; + +internal static class SkExtensions +{ + internal static void SaveTo(this SKBitmap bitmap, string fileName, SKEncodedImageFormat format, int quality = 100) + { + using var data = bitmap.Encode(format, quality); + using var stream = System.IO.File.OpenWrite(fileName); + data.SaveTo(stream); + } +} \ No newline at end of file diff --git a/src/Spectrogram.Tests/Spectrogram.Tests.csproj b/src/Spectrogram.Tests/Spectrogram.Tests.csproj index 7754517..83fa10d 100644 --- a/src/Spectrogram.Tests/Spectrogram.Tests.csproj +++ b/src/Spectrogram.Tests/Spectrogram.Tests.csproj @@ -1,17 +1,18 @@ - + - net6.0 + net8.0 false + - - - - - + + + + + diff --git a/src/Spectrogram/Colormap.cs b/src/Spectrogram/Colormap.cs index 1972fa6..1553712 100644 --- a/src/Spectrogram/Colormap.cs +++ b/src/Spectrogram/Colormap.cs @@ -1,95 +1,91 @@ using System; -using System.Drawing; using System.Linq; +using SkiaSharp; -namespace Spectrogram +namespace Spectrogram; + +public class Colormap(ScottPlot.IColormap colormap) { - public class Colormap + private ScottPlot.IColormap _Colormap { get; } = colormap; + + public string Name => _Colormap.Name; + + public override string ToString() => _Colormap.ToString(); + + public static Colormap[] GetColormaps() => ScottPlot.Colormap.GetColormaps() + .Select(x => new Colormap(x)) + .ToArray(); + + public static string[] GetColormapNames() => ScottPlot.Colormap.GetColormaps() + .Select(x => new Colormap(x).Name) + .ToArray(); + + public static Colormap GetColormap(string colormapName) { - public static Colormap Argo => new Colormap(new Colormaps.Argo()); - public static Colormap Blues => new Colormap(new Colormaps.Blues()); - public static Colormap Grayscale => new Colormap(new Colormaps.Grayscale()); - public static Colormap GrayscaleReversed => new Colormap(new Colormaps.GrayscaleR()); - public static Colormap Greens => new Colormap(new Colormaps.Greens()); - public static Colormap Inferno => new Colormap(new Colormaps.Inferno()); - public static Colormap Lopora => new Colormap(new Colormaps.Lopora()); - public static Colormap Magma => new Colormap(new Colormaps.Magma()); - public static Colormap Plasma => new Colormap(new Colormaps.Plasma()); - public static Colormap Turbo => new Colormap(new Colormaps.Turbo()); - public static Colormap Viridis => new Colormap(new Colormaps.Viridis()); - - private readonly IColormap cmap; - public readonly string Name; - public Colormap(IColormap colormap) - { - cmap = colormap ?? new Colormaps.Grayscale(); - Name = cmap.GetType().Name; - } + foreach (Colormap cmap in GetColormaps()) + if (string.Equals(cmap.Name, colormapName, StringComparison.InvariantCultureIgnoreCase)) + return cmap; - public override string ToString() - { - return $"Colormap {Name}"; - } + throw new ArgumentException($"Colormap does not exist: {colormapName}"); + } - public static Colormap[] GetColormaps() => - typeof(Colormap).GetProperties() - .Select(x => (Colormap)x.GetValue(x.Name)) - .ToArray(); + public (byte r, byte g, byte b) GetRGB(byte value) => GetRGB(value / 255.0); - public static string[] GetColormapNames() - { - return GetColormaps().Select(x => x.Name).ToArray(); - } + public (byte r, byte g, byte b) GetRGB(double fraction) + { + ScottPlot.Color color = _Colormap.GetColor(fraction); + return (color.R, color.G, color.B); + } - public static Colormap GetColormap(string colormapName) - { - foreach (Colormap cmap in GetColormaps()) - if (string.Equals(cmap.Name, colormapName, StringComparison.InvariantCultureIgnoreCase)) - return cmap; + public int GetInt32(byte value) + { + var (r, g, b) = GetRGB(value); + return 255 << 24 | r << 16 | g << 8 | b; + } - throw new ArgumentException($"Colormap does not exist: {colormapName}"); - } + public int GetInt32(double fraction) + { + var (r, g, b) = GetRGB(fraction); + return 255 << 24 | r << 16 | g << 8 | b; + } - public (byte r, byte g, byte b) GetRGB(byte value) - { - return cmap.GetRGB(value); - } + public SKColor GetColor(byte value) + { + var color = GetInt32(value); + return new SKColor((uint)color); + } - public (byte r, byte g, byte b) GetRGB(double fraction) - { - fraction = Math.Max(fraction, 0); - fraction = Math.Min(fraction, 1); - return cmap.GetRGB((byte)(fraction * 255)); - } + public SKColor GetColor(double fraction) + { + var color = GetInt32(fraction); + return new SKColor((uint)color); + } - public int GetInt32(byte value) - { - var (r, g, b) = GetRGB(value); - return 255 << 24 | r << 16 | g << 8 | b; - } + public SKBitmap ApplyFilter(SKBitmap bmp) + { + SKImageInfo info = new(bmp.Width, bmp.Height, SKColorType.Rgba8888); + SKBitmap newBitmap = new(info); + using SKCanvas canvas = new(newBitmap); + canvas.Clear(); - public int GetInt32(double fraction) - { - var (r, g, b) = GetRGB(fraction); - return 255 << 24 | r << 16 | g << 8 | b; - } + using SKPaint paint = new SKPaint(); - public Color GetColor(byte value) - { - return Color.FromArgb(GetInt32(value)); - } + byte[] A = new byte[256]; + byte[] R = new byte[256]; + byte[] G = new byte[256]; + byte[] B = new byte[256]; - public Color GetColor(double fraction) + for (int i = 0; i < 256; i++) { - return Color.FromArgb(GetInt32(fraction)); + var color = GetColor((byte)i); + A[i] = color.Alpha; + R[i] = color.Red; + G[i] = color.Green; + B[i] = color.Blue; } + paint.ColorFilter = SKColorFilter.CreateTable(A, R, G, B); - public void Apply(Bitmap bmp) - { - System.Drawing.Imaging.ColorPalette pal = bmp.Palette; - for (int i = 0; i < 256; i++) - pal.Entries[i] = GetColor((byte)i); - bmp.Palette = pal; - } + canvas.DrawBitmap(bmp, 0, 0, paint); + return newBitmap; } } diff --git a/src/Spectrogram/Colormaps/Argo.cs b/src/Spectrogram/Colormaps/Argo.cs deleted file mode 100644 index ac40f2d..0000000 --- a/src/Spectrogram/Colormaps/Argo.cs +++ /dev/null @@ -1,55 +0,0 @@ -/* Argo is a closed-source weak signal spectrogram. - * This colormap was created to mimic the colors used by Argo. - * https://www.i2phd.org/argo/index.html - * https://digilander.libero.it/i2phd/argo/ - */ - -using System; - -namespace Spectrogram.Colormaps -{ - class Argo : IColormap - { - public (byte r, byte g, byte b) GetRGB(byte value) - { - byte[] bytes = BitConverter.GetBytes(rgb[value]); - return (bytes[2], bytes[1], bytes[0]); - } - - private readonly int[] rgb = - { - 00000000, 00000004, 00000264, 00000267, 00000527, 00000530, 00000789, 00066328, - 00066588, 00066591, 00066849, 00132388, 00132647, 00132650, 00132908, 00198447, - 00198706, 00198708, 00264503, 00264505, 00330299, 00330557, 00330560, 00396354, - 00396612, 00462150, 00462408, 00527946, 00528204, 00593998, 00594000, 00659794, - 00660052, 00725590, 00791383, 00791641, 00857435, 00857437, 00923230, 00989024, - 00989026, 01054819, 01120613, 01120870, 01186408, 01252201, 01252459, 01318252, - 01384046, 01384047, 01449841, 01515634, 01515892, 01581429, 01647222, 01713016, - 01713273, 01779066, 01844860, 01910397, 01976190, 01976447, 02042241, 02108034, - 02173827, 02239364, 02239621, 02305415, 02371208, 02437001, 02502794, 02568587, - 02568844, 02634381, 02700174, 02765968, 02831761, 02897554, 02963347, 03029140, - 03029397, 03095190, 03160983, 03226520, 03292313, 03358106, 03423899, 03489692, - 03555485, 03621278, 03687071, 03752864, 03818656, 03884449, 03950242, 04016035, - 04081828, 04147621, 04147878, 04213671, 04279464, 04345256, 04411049, 04476842, - 04542635, 04608428, 04739757, 04805550, 04871342, 04937135, 05002928, 05068721, - 05134514, 05200306, 05266099, 05331892, 05397685, 05463477, 05529270, 05595063, - 05660856, 05726648, 05792441, 05858234, 05924027, 05989819, 06121148, 06186941, - 06252734, 06318526, 06384319, 06450112, 06515904, 06581953, 06647746, 06779074, - 06844867, 06910660, 06976452, 07042245, 07108038, 07173830, 07239623, 07370952, - 07436744, 07502793, 07568586, 07634378, 07700171, 07765964, 07897292, 07963085, - 08028877, 08094670, 08160719, 08226511, 08357840, 08423632, 08489425, 08555218, - 08621010, 08752339, 08818387, 08884180, 08949973, 09015765, 09147094, 09212886, - 09278679, 09344727, 09410520, 09541849, 09607641, 09673434, 09739226, 09870555, - 09936603, 10002396, 10068188, 10133981, 10265309, 10331102, 10397150, 10462943, - 10594272, 10660064, 10725857, 10791905, 10923234, 10989026, 11054819, 11120611, - 11251940, 11317988, 11383781, 11515109, 11580902, 11646694, 11712743, 11844071, - 11909864, 11975656, 12106985, 12173033, 12238826, 12304618, 12435947, 12501995, - 12567787, 12699116, 12764908, 12830701, 12962285, 13028078, 13093870, 13159663, - 13291247, 13357040, 13422832, 13554161, 13619953, 13686001, 13817330, 13883122, - 13948915, 14080499, 14146292, 14212084, 14343412, 14409461, 14475253, 14606582, - 14672374, 14738423, 14869751, 14935543, 15066872, 15132920, 15198713, 15330041, - 15396090, 15461882, 15593210, 15659003, 15725051, 15856380, 15922172, 16053500, - 16119549, 16185341, 16316670, 16382718, 16448510, 16579839, 16645631, 16777215, - }; - } -} diff --git a/src/Spectrogram/Colormaps/Blues.cs b/src/Spectrogram/Colormaps/Blues.cs deleted file mode 100644 index d917d9c..0000000 --- a/src/Spectrogram/Colormaps/Blues.cs +++ /dev/null @@ -1,50 +0,0 @@ -// This colormap was created by Scott Harden on 2020-06-16 and is released under a MIT license. -using System; - -namespace Spectrogram.Colormaps -{ - class Blues : IColormap - { - public (byte r, byte g, byte b) GetRGB(byte value) - { - byte[] bytes = BitConverter.GetBytes(argb[value]); - return (bytes[2], bytes[1], bytes[0]); - } - - private readonly int[] argb = - { - -16767403, -16767402, -16767144, -16766887, -16701093, -16700835, -16700578, -16634784, - -16634527, -16634269, -16568476, -16568218, -16568216, -16567959, -16502165, -16501908, - -16501650, -16435856, -16435599, -16435341, -16369548, -16369290, -16369033, -16303495, - -16303238, -16302980, -16237186, -16236929, -16236671, -16236414, -16170620, -16170363, - -16170105, -16104312, -16104054, -16103797, -16038259, -16038002, -16037745, -15971951, - -15971694, -15971436, -15905643, -15905385, -15905128, -15839335, -15839077, -15838820, - -15773027, -15772769, -15772512, -15706718, -15706717, -15706460, -15706203, -15640409, - -15640152, -15574359, -15574101, -15573844, -15508051, -15507794, -15507537, -15441743, - -15441486, -15441229, -15375436, -15375179, -15309386, -15309128, -15308871, -15243078, - -15242821, -15177284, -15177027, -15111234, -15110977, -15045184, -15044927, -14979134, - -14978877, -14913084, -14912827, -14847034, -14781241, -14780984, -14715191, -14715190, - -14649397, -14583605, -14517812, -14517555, -14451762, -14385969, -14320176, -14319920, - -14254383, -14188590, -14122797, -14057005, -13991212, -13925419, -13859626, -13859626, - -13793833, -13662505, -13596712, -13530919, -13465383, -13399590, -13333797, -13268005, - -13202212, -13136676, -13005347, -12939555, -12873762, -12808226, -12676897, -12611105, - -12545312, -12414240, -12348447, -12282655, -12151327, -12085790, -12019998, -11888669, - -11822877, -11757341, -11626012, -11560220, -11429148, -11363355, -11232027, -11166235, - -11035162, -10969370, -10903578, -10772505, -10706713, -10575385, -10509592, -10378520, - -10312728, -10181400, -10115863, -09984535, -09918743, -09787414, -09721878, -09590550, - -09524758, -09393429, -09327893, -09262101, -09130773, -09065237, -08933908, -08868116, - -08736788, -08671252, -08605459, -08474131, -08408339, -08277267, -08211475, -08145682, - -08014354, -07948818, -07817490, -07751698, -07685905, -07554833, -07489041, -07357713, - -07291921, -07226128, -07095056, -07029264, -06897936, -06832144, -06766351, -06635279, - -06569487, -06503695, -06372367, -06306575, -06175502, -06109710, -06043918, -05912590, - -05846798, -05781261, -05649933, -05584141, -05518349, -05387021, -05321485, -05190156, - -05124364, -05058572, -04927244, -04861452, -04730123, -04664587, -04598795, -04467467, - -04401675, -04335883, -04204554, -04139018, -04007690, -03941898, -03876106, -03744777, - -03678985, -03547657, -03481865, -03416329, -03285000, -03219208, -03087880, -03022088, - -02956296, -02824968, -02759175, -02628103, -02562311, -02430983, -02365191, -02299398, - -02168070, -02102278, -01970950, -01905158, -01839621, -01708293, -01642501, -01511173, - -01445381, -01314052, -01248260, -01182468, -01051140, -00985604, -00854275, -00788483, - -00657155, -00591363, -00525571, -00394242, -00328450, -00197122, -00131330, -00000001, - }; - } -} diff --git a/src/Spectrogram/Colormaps/Grayscale.cs b/src/Spectrogram/Colormaps/Grayscale.cs deleted file mode 100644 index f8d092a..0000000 --- a/src/Spectrogram/Colormaps/Grayscale.cs +++ /dev/null @@ -1,12 +0,0 @@ -// This colormap was created by Scott Harden on 2020-06-16 and is released under a MIT license. - -namespace Spectrogram.Colormaps -{ - class Grayscale : IColormap - { - public (byte r, byte g, byte b) GetRGB(byte value) - { - return (value, value, value); - } - } -} diff --git a/src/Spectrogram/Colormaps/GrayscaleR.cs b/src/Spectrogram/Colormaps/GrayscaleR.cs deleted file mode 100644 index 6c499e5..0000000 --- a/src/Spectrogram/Colormaps/GrayscaleR.cs +++ /dev/null @@ -1,13 +0,0 @@ -// This colormap was created by Scott Harden on 2020-06-16 and is released under a MIT license. - -namespace Spectrogram.Colormaps -{ - public class GrayscaleR : IColormap - { - public (byte r, byte g, byte b) GetRGB(byte value) - { - value = (byte)(255 - value); - return (value, value, value); - } - } -} \ No newline at end of file diff --git a/src/Spectrogram/Colormaps/Greens.cs b/src/Spectrogram/Colormaps/Greens.cs deleted file mode 100644 index 0ee2358..0000000 --- a/src/Spectrogram/Colormaps/Greens.cs +++ /dev/null @@ -1,51 +0,0 @@ -// This colormap was created by Scott Harden on 2020-06-16 and is released under a MIT license. -using System; - -namespace Spectrogram.Colormaps -{ - class Greens : IColormap - { - public (byte r, byte g, byte b) GetRGB(byte value) - { - byte[] bytes = BitConverter.GetBytes(argb[value]); - return (bytes[2], bytes[1], bytes[0]); - } - - private readonly int[] argb = - { - -16761088, -16760832, -16760575, -16760318, -16760061, -16759804, -16759547, -16759290, - -16759033, -16758776, -16758519, -16758006, -16757749, -16757492, -16757235, -16756979, - -16756722, -16756465, -16756208, -16755951, -16755694, -16755437, -16755180, -16754667, - -16754410, -16688617, -16688360, -16688104, -16687847, -16687590, -16687333, -16687076, - -16621283, -16621026, -16620769, -16620512, -16620256, -16554463, -16554206, -16553949, - -16553692, -16487899, -16487642, -16487385, -16421336, -16421080, -16420823, -16355030, - -16354773, -16288980, -16288723, -16222930, -16222930, -16222673, -16156880, -16156623, - -16090830, -16025038, -16024781, -15958988, -15958731, -15892938, -15827145, -15826889, - -15761096, -15695303, -15695046, -15629254, -15563461, -15497924, -15497667, -15431875, - -15366082, -15300289, -15234496, -15234240, -15168447, -15102654, -15037118, -14971325, - -14905532, -14839740, -14773947, -14708154, -14642618, -14576825, -14511033, -14445240, - -14379447, -14313655, -14248118, -14182326, -14116533, -14050741, -13985204, -13919412, - -13853619, -13722291, -13656498, -13590962, -13525169, -13459377, -13328048, -13262512, - -13196720, -13130927, -13065391, -12934063, -12868270, -12802478, -12671406, -12605613, - -12539821, -12408749, -12342957, -12277164, -12146092, -12080300, -12014508, -11883435, - -11817643, -11686315, -11620779, -11554986, -11423914, -11358122, -11226794, -11161258, - -11029929, -10964137, -10833065, -10767273, -10636201, -10570408, -10439080, -10373544, - -10242216, -10176680, -10045351, -09914023, -09848487, -09717159, -09651623, -09520294, - -09389222, -09323430, -09192102, -09061029, -08995237, -08864165, -08798372, -08667300, - -08535972, -08404643, -08339107, -08207779, -08076706, -08010914, -07879842, -07748513, - -07682977, -07551648, -07420576, -07289248, -07223711, -07092383, -06961054, -06895518, - -06764189, -06633116, -06501788, -06436251, -06304923, -06173850, -06108057, -05976984, - -05845656, -05714583, -05648790, -05517717, -05386389, -05320596, -05189523, -05123730, - -04992657, -04861328, -04795791, -04664462, -04598925, -04467596, -04336523, -04270730, - -04139400, -04073863, -03942534, -03876997, -03745667, -03680130, -03614337, -03483007, - -03417470, -03286140, -03220603, -03154809, -03023479, -02957942, -02892148, -02826610, - -02760817, -02629487, -02563949, -02498155, -02432617, -02366823, -02301029, -02235491, - -02169697, -02103903, -02038365, -01972571, -01906777, -01841239, -01775444, -01709650, - -01644112, -01578318, -01512523, -01446985, -01381191, -01315396, -01249858, -01249599, - -01183805, -01118266, -01052472, -00986678, -00986675, -00920880, -00855086, -00789547, - -00723753, -00723494, -00657956, -00592161, -00526367, -00526108, -00460569, -00394775, - -00394516, -00328977, -00263183, -00197388, -00197385, -00131591, -00065796, -00000001, - - }; - } -} diff --git a/src/Spectrogram/Colormaps/Inferno.cs b/src/Spectrogram/Colormaps/Inferno.cs deleted file mode 100644 index 0b45ed5..0000000 --- a/src/Spectrogram/Colormaps/Inferno.cs +++ /dev/null @@ -1,57 +0,0 @@ -/* Inferno is a colormap by Nathaniel J. Smith and Stefan van der Walt - * https://bids.github.io/colormap/ - * https://github.com/BIDS/colormap/blob/master/colormaps.py - * - * This colormap is provided under the CC0 license / public domain dedication - * http://creativecommons.org/publicdomain/zero/1.0/ - */ - -using System; - -namespace Spectrogram.Colormaps -{ - class Inferno : IColormap - { - public (byte r, byte g, byte b) GetRGB(byte value) - { - byte[] bytes = BitConverter.GetBytes(rgb[value]); - return (bytes[2], bytes[1], bytes[0]); - } - - private readonly int[] rgb = - { - 00000003, 00000004, 00000006, 00065543, 00065801, 00065803, 00131342, 00131600, - 00197138, 00262932, 00262934, 00328728, 00394267, 00460061, 00525855, 00591393, - 00657187, 00722726, 00854056, 00919594, 00985389, 01050927, 01182258, 01247796, - 01313590, 01444665, 01510203, 01641278, 01706816, 01838147, 01903685, 02034759, - 02100298, 02231116, 02362190, 02493264, 02558802, 02689876, 02820694, 02951768, - 03017306, 03148380, 03279197, 03410271, 03475808, 03606881, 03737954, 03869028, - 03934565, 04065638, 04196710, 04262247, 04393576, 04524649, 04590185, 04721514, - 04852586, 04918379, 05049451, 05180780, 05246316, 05377644, 05443181, 05574509, - 05705581, 05771373, 05902701, 05968238, 06099566, 06230638, 06296430, 06427758, - 06493294, 06624622, 06690158, 06821486, 06952814, 07018350, 07149678, 07215214, - 07346542, 07477613, 07543405, 07674733, 07740269, 07871597, 08002669, 08068460, - 08199532, 08265324, 08396651, 08462187, 08593515, 08724586, 08790378, 08921450, - 08987241, 09118313, 09249641, 09315432, 09446504, 09512295, 09643367, 09774694, - 09840230, 09971557, 10037348, 10168420, 10234211, 10365283, 10496610, 10562401, - 10693473, 10759264, 10890335, 10956127, 11087454, 11218525, 11284316, 11415643, - 11481435, 11612506, 11678297, 11809624, 11875159, 12006486, 12072278, 12203605, - 12269396, 12400467, 12466258, 12532049, 12663376, 12729167, 12860494, 12926285, - 13057612, 13123147, 13188938, 13320265, 13386056, 13451847, 13583430, 13649220, - 13715011, 13780802, 13912129, 13977920, 14043711, 14109502, 14241085, 14306875, - 14372666, 14438457, 14504504, 14570295, 14636086, 14702132, 14833459, 14899250, - 14965297, 15031088, 15096878, 15097389, 15163180, 15229227, 15295018, 15361064, - 15426855, 15492902, 15558693, 15559203, 15625250, 15691041, 15757087, 15757342, - 15823389, 15889436, 15889690, 15955737, 15956248, 16022038, 16088085, 16088596, - 16154642, 16154897, 16220944, 16221454, 16287501, 16287756, 16288267, 16354313, - 16354824, 16355336, 16421127, 16421638, 16422150, 16422662, 16488710, 16489222, - 16489734, 16489991, 16490503, 16491016, 16491530, 16492043, 16492557, 16493070, - 16493584, 16494098, 16494612, 16494870, 16495384, 16495898, 16496412, 16496926, - 16431905, 16432419, 16432933, 16433448, 16368426, 16368940, 16369455, 16304433, - 16304948, 16305463, 16240442, 16240956, 16175935, 16176450, 16111429, 16111944, - 16046923, 16047183, 15982162, 15982678, 15983193, 15918173, 15918688, 15853668, - 15853928, 15854444, 15854960, 15855220, 15855737, 15856253, 15922049, 15922309, - 15988361, 16054157, 16119953, 16186005, 16251801, 16383133, 16448928, 16580260, - }; - } -} diff --git a/src/Spectrogram/Colormaps/Lopora.cs b/src/Spectrogram/Colormaps/Lopora.cs deleted file mode 100644 index a250f7b..0000000 --- a/src/Spectrogram/Colormaps/Lopora.cs +++ /dev/null @@ -1,55 +0,0 @@ -/* Lopora is an open-source weak signal spectrogram by Onno Hoekstra (PA2OHH) - * This colormap was created to mimic the default colors used by Lopora. - * https://www.qsl.net/pa2ohh/11lop.htm - * https://github.com/swharden/Lopora/blob/20afe72416579f8b7d3c8861532c71a95b904066/src/LOPORA-v5a.py#L828-L872 - */ - -using System; - -namespace Spectrogram.Colormaps -{ - class Lopora : IColormap - { - public (byte r, byte g, byte b) GetRGB(byte value) - { - byte[] bytes = BitConverter.GetBytes(rgb[value]); - return (bytes[2], bytes[1], bytes[0]); - } - - private readonly int[] rgb = - { - 0000000000, 0000069696, 0000137036, 0000203860, 0000270426, 0000336991, 0000403300, 0000469608, - 0000535915, 0000602222, 0000668273, 0000734580, 0000800631, 0000866681, 0000932987, 0000999037, - 0001065088, 0001131137, 0001197187, 0001262981, 0001329031, 0001395080, 0001461130, 0001526924, - 0001592973, 0001659023, 0001724816, 0001790865, 0001856659, 0001922708, 0001988501, 0002054550, - 0002120344, 0002186393, 0002252186, 0002317979, 0002384028, 0002449821, 0002515614, 0002581663, - 0002647456, 0002713249, 0002779042, 0002845091, 0002910884, 0002976677, 0003042470, 0003108263, - 0003174056, 0003240105, 0003305898, 0003371690, 0003437483, 0003503276, 0003569069, 0003634862, - 0003700654, 0003766447, 0003832240, 0003898033, 0003963825, 0004029618, 0004095411, 0004161204, - 0004227252, 0004292789, 0004358582, 0004424374, 0004490167, 0004555960, 0004621752, 0004687545, - 0004753338, 0004819130, 0004884923, 0004950716, 0005016508, 0005082301, 0005148093, 0005213886, - 0005279679, 0005345215, 0005411008, 0005476800, 0005542593, 0005608386, 0005674178, 0005739971, - 0005805763, 0005871300, 0005937092, 0006002885, 0006068677, 0006134470, 0006200263, 0006265799, - 0006331592, 0006397384, 0006463177, 0006528969, 0006594506, 0006660298, 0006726091, 0006791883, - 0006857676, 0006923212, 0006989005, 0007054797, 0007120590, 0007186126, 0007251918, 0007317711, - 0007383503, 0007449040, 0007514832, 0007580625, 0007646417, 0007711954, 0007777746, 0007843539, - 0007909331, 0007974867, 0008040660, 0008106452, 0008171989, 0008237781, 0008303574, 0008369366, - 0008434902, 0008435159, 0008500951, 0008566488, 0008632280, 0008698072, 0008763609, 0008829401, - 0008895194, 0008960986, 0009026522, 0009092315, 0009158107, 0009223644, 0009289436, 0009355228, - 0009420765, 0009486557, 0009552350, 0009617886, 0009683678, 0009749471, 0009815007, 0009880799, - 0009946336, 0010012128, 0010077921, 0010143457, 0010209249, 0010275042, 0010340578, 0010406370, - 0010472163, 0010537699, 0010603491, 0010669028, 0010734820, 0010800612, 0010866149, 0010931941, - 0010997734, 0011063270, 0011129062, 0011194599, 0011260391, 0011326183, 0011391720, 0011457512, - 0011523048, 0011588841, 0011654633, 0011720169, 0011785962, 0011851498, 0011917290, 0011983082, - 0012048619, 0012114411, 0012179947, 0012245740, 0012311532, 0012377068, 0012442861, 0012508397, - 0012574189, 0012639726, 0012705518, 0012771310, 0012836847, 0012902639, 0012968175, 0013033967, - 0013099504, 0013165296, 0013231088, 0013296625, 0013362417, 0013427953, 0013493746, 0013559282, - 0013625074, 0013690610, 0013756403, 0013822195, 0013887731, 0013953524, 0014019060, 0014084852, - 0014150388, 0014216181, 0014281717, 0014347509, 0014413046, 0014478838, 0014544374, 0014610166, - 0014675959, 0014741495, 0014807287, 0014872823, 0014938616, 0015004152, 0015069944, 0015135481, - 0015201273, 0015266809, 0015332601, 0015398138, 0015463930, 0015529466, 0015595258, 0015660795, - 0015726587, 0015792123, 0015857915, 0015923452, 0015989244, 0016054780, 0016120572, 0016186109, - 0016251901, 0016317437, 0016383229, 0016448766, 0016514558, 0016580350, 0016645887, 0016711679, - }; - } -} diff --git a/src/Spectrogram/Colormaps/Magma.cs b/src/Spectrogram/Colormaps/Magma.cs deleted file mode 100644 index 105a177..0000000 --- a/src/Spectrogram/Colormaps/Magma.cs +++ /dev/null @@ -1,57 +0,0 @@ -/* Magma is a colormap by Nathaniel J. Smith and Stefan van der Walt - * https://bids.github.io/colormap/ - * https://github.com/BIDS/colormap/blob/master/colormaps.py - * - * This colormap is provided under the CC0 license / public domain dedication - * http://creativecommons.org/publicdomain/zero/1.0/ - */ - -using System; - -namespace Spectrogram.Colormaps -{ - class Magma : IColormap - { - public (byte r, byte g, byte b) GetRGB(byte value) - { - byte[] bytes = BitConverter.GetBytes(rgb[value]); - return (bytes[2], bytes[1], bytes[0]); - } - - private readonly int[] rgb = - { - 00000003, 00000004, 00000006, 00065543, 00065801, 00065803, 00131597, 00131599, - 00197393, 00262931, 00263189, 00328727, 00394521, 00460059, 00525853, 00591647, - 00657186, 00722980, 00788774, 00854568, 00920106, 00985900, 01051695, 01117233, - 01183027, 01314101, 01379896, 01445434, 01511228, 01576767, 01708097, 01773636, - 01839174, 01970249, 02036043, 02101581, 02232656, 02298194, 02429269, 02494807, - 02625881, 02756956, 02822494, 02953312, 03084386, 03149925, 03280999, 03412072, - 03477354, 03608428, 03739502, 03870575, 03936113, 04067186, 04198259, 04329332, - 04394869, 04525942, 04657015, 04722808, 04853881, 04919417, 05050746, 05181819, - 05247611, 05378684, 05444476, 05575549, 05706877, 05772670, 05903742, 05969534, - 06100862, 06166399, 06297727, 06363263, 06494591, 06625920, 06691456, 06822784, - 06888576, 07019648, 07085440, 07216769, 07282305, 07413633, 07544705, 07610497, - 07741825, 07807361, 07938689, 08004225, 08135553, 08266881, 08332417, 08463745, - 08529281, 08660609, 08726145, 08857473, 08988801, 09054337, 09185664, 09251200, - 09382528, 09513600, 09579392, 09710464, 09776256, 09907327, 10038655, 10104191, - 10235519, 10366590, 10432382, 10563454, 10694782, 10760317, 10891645, 10957181, - 11088508, 11219836, 11285371, 11416699, 11547771, 11613562, 11744634, 11875961, - 11941497, 12072824, 12138360, 12269687, 12401015, 12466550, 12597877, 12728949, - 12794740, 12926068, 12991603, 13122930, 13254258, 13319793, 13451120, 13516912, - 13648239, 13714030, 13845101, 13910893, 14042220, 14108011, 14239338, 14305129, - 14436457, 14502248, 14568039, 14699366, 14765158, 14830949, 14962276, 15028323, - 15094114, 15159906, 15225953, 15357280, 15423072, 15489119, 15554911, 15620958, - 15621469, 15687261, 15753309, 15819100, 15885148, 15951196, 15951707, 16017499, - 16083547, 16084059, 16150107, 16150619, 16216411, 16216924, 16282972, 16283484, - 16349532, 16350045, 16350557, 16416606, 16416862, 16417375, 16483424, 16483936, - 16484449, 16484962, 16551011, 16551523, 16552036, 16552549, 16552806, 16618855, - 16619368, 16619881, 16620394, 16620907, 16621420, 16621934, 16622191, 16622704, - 16688753, 16689267, 16689780, 16690293, 16690806, 16691064, 16691577, 16692091, - 16692604, 16693117, 16693631, 16694144, 16694402, 16694915, 16695429, 16695942, - 16696456, 16696969, 16697227, 16697741, 16698254, 16633232, 16633746, 16634259, - 16634517, 16635031, 16635544, 16636058, 16636572, 16637085, 16637343, 16637857, - 16638371, 16573349, 16573862, 16574120, 16574634, 16575148, 16575662, 16576176, - 16576689, 16576947, 16577461, 16577975, 16512953, 16513467, 16513725, 16514239, - }; - } -} diff --git a/src/Spectrogram/Colormaps/Plasma.cs b/src/Spectrogram/Colormaps/Plasma.cs deleted file mode 100644 index b03dc59..0000000 --- a/src/Spectrogram/Colormaps/Plasma.cs +++ /dev/null @@ -1,57 +0,0 @@ -/* Plasma is a colormap by Nathaniel J. Smith and Stefan van der Walt - * https://bids.github.io/colormap/ - * https://github.com/BIDS/colormap/blob/master/colormaps.py - * - * This colormap is provided under the CC0 license / public domain dedication - * http://creativecommons.org/publicdomain/zero/1.0/ - */ - -using System; - -namespace Spectrogram.Colormaps -{ - class Plasma : IColormap - { - public (byte r, byte g, byte b) GetRGB(byte value) - { - byte[] bytes = BitConverter.GetBytes(rgb[value]); - return (bytes[2], bytes[1], bytes[0]); - } - - private readonly int[] rgb = - { - 00788358, 01050503, 01246857, 01377930, 01574539, 01771148, 01902221, 02033038, - 02164111, 02295184, 02426257, 02557330, 02688403, 02819476, 02950292, 03081365, - 03212438, 03343511, 03409048, 03540120, 03671193, 03802266, 03867546, 03998619, - 04129692, 04195228, 04326301, 04457374, 04522910, 04653727, 04784799, 04850336, - 04981409, 05112481, 05178018, 05308834, 05374371, 05505443, 05636515, 05702052, - 05833124, 05898405, 06029477, 06160549, 06226086, 06357158, 06422694, 06553767, - 06619303, 06750375, 06815911, 06946983, 07078056, 07143592, 07274664, 07340200, - 07471272, 07536808, 07667880, 07733672, 07864744, 07930280, 08061608, 08127143, - 08258471, 08324007, 08455335, 08520871, 08652198, 08717990, 08783782, 08914853, - 08980645, 09111972, 09177764, 09309348, 09375139, 09440931, 09572258, 09638049, - 09769377, 09835168, 09900960, 10032287, 10098078, 10164126, 10295453, 10361244, - 10427035, 10492827, 10624154, 10689945, 10755736, 10821527, 10953111, 11018902, - 11084693, 11150484, 11281811, 11347602, 11413393, 11479184, 11545231, 11611023, - 11676814, 11808141, 11873932, 11939723, 12005514, 12071561, 12137352, 12203143, - 12268934, 12334725, 12400516, 12466307, 12532098, 12598145, 12663936, 12729728, - 12795519, 12861310, 12927101, 12992892, 13058683, 13124730, 13190521, 13256312, - 13322103, 13387894, 13453685, 13519477, 13585268, 13651315, 13717106, 13717361, - 13783152, 13848943, 13914734, 13980525, 14046573, 14112364, 14112619, 14178410, - 14244201, 14309992, 14375783, 14441830, 14442086, 14507877, 14573668, 14639459, - 14639714, 14705761, 14771552, 14837344, 14903135, 14903390, 14969437, 15035228, - 15035483, 15101274, 15167066, 15233113, 15233368, 15299159, 15364950, 15365205, - 15431252, 15497044, 15497299, 15563090, 15563601, 15629392, 15695183, 15695438, - 15761485, 15761741, 15827532, 15893579, 15893834, 15959625, 15959880, 16025927, - 16026183, 16091974, 16092485, 16158276, 16158531, 16159042, 16224833, 16225089, - 16291136, 16291391, 16291902, 16357693, 16357948, 16423995, 16424250, 16424762, - 16425017, 16491064, 16491319, 16491574, 16557621, 16557877, 16558388, 16558643, - 16559154, 16559409, 16625457, 16625712, 16626223, 16626478, 16626989, 16627245, - 16627756, 16628011, 16628523, 16628778, 16629289, 16629801, 16630056, 16630568, - 16630823, 16631334, 16566054, 16566566, 16567077, 16567333, 16567845, 16502820, - 16503076, 16503588, 16438564, 16438820, 16439332, 16374052, 16374564, 16309540, - 16310052, 16244772, 16245285, 16180261, 16180517, 16115494, 16116006, 16050726, - 15985702, 15986214, 15921190, 15921446, 15856422, 15791397, 15791651, 15726625, - }; - } -} diff --git a/src/Spectrogram/Colormaps/Turbo.cs b/src/Spectrogram/Colormaps/Turbo.cs deleted file mode 100644 index e935210..0000000 --- a/src/Spectrogram/Colormaps/Turbo.cs +++ /dev/null @@ -1,53 +0,0 @@ -// This colormap was created by Scott Harden on 2020-06-16 and is released under a MIT license. -// It was designed to mimic Turbo, but is not a copy of or derived from Turbo source code. -// https://ai.googleblog.com/2019/08/turbo-improved-rainbow-colormap-for.html - -using System; - -namespace Spectrogram.Colormaps -{ - class Turbo : IColormap - { - public (byte r, byte g, byte b) GetRGB(byte value) - { - byte[] bytes = BitConverter.GetBytes(argb[value]); - return (bytes[2], bytes[1], bytes[0]); - } - - private readonly int[] argb = - { - -13559489, -13493436, -13427382, -13361328, -13295018, -13228964, -13162911, -13096857, - -13030547, -12964493, -12898440, -12832130, -12766077, -12700023, -12633970, -12567660, - -12501607, -12435554, -12369245, -12303192, -12237139, -12171086, -12170313, -12104260, - -12038208, -12037436, -11971383, -11905331, -11904559, -11838507, -11837991, -11771940, - -11771168, -11770653, -11770138, -11703831, -11703316, -11702801, -11702287, -11701517, - -11701003, -11700489, -11765255, -11764742, -11764228, -11828995, -11828482, -11893506, - -11892737, -11957761, -12022785, -12022017, -12087042, -12152067, -12217092, -12347397, - -12412423, -12477448, -12542218, -12672780, -12737806, -12802577, -12933139, -12998166, - -13128729, -13193500, -13324063, -13389091, -13519654, -13584682, -13714989, -13780017, - -13910581, -13975609, -14106173, -14171201, -14301765, -14366793, -14431822, -14562386, - -14627414, -14692442, -14757471, -14822499, -14887527, -14952556, -14952048, -15017332, - -15082361, -15081853, -15147137, -15146629, -15146121, -15145869, -15145361, -15145109, - -15079065, -15078812, -15013024, -15012515, -14946726, -14880938, -14749356, -14683567, - -14617778, -14486453, -14355127, -14223801, -14092475, -13961405, -13764543, -13633217, - -13436355, -13239748, -13108422, -12911559, -12714952, -12518089, -12255946, -12059339, - -11862476, -11600333, -11403725, -11141582, -10879182, -10682575, -10420431, -10158287, - -09896143, -09633999, -09372111, -09175503, -08913359, -08651215, -08389327, -08127183, - -07865294, -07603150, -07341262, -07079117, -06817229, -06555341, -06293452, -06031308, - -05834955, -05573067, -05311178, -05114826, -04852937, -04656585, -04394952, -04198600, - -04002247, -03740358, -03544262, -03347910, -03217349, -03020997, -02824900, -02628804, - -02497987, -02301891, -02171331, -02040770, -01910210, -01779394, -01648834, -01518273, - -01387713, -01257153, -01126849, -01061824, -00931264, -00866240, -00801216, -00670656, - -00605888, -00540864, -00475840, -00411072, -00346048, -00346560, -00281792, -00282304, - -00217536, -00218048, -00153280, -00153793, -00154561, -00155073, -00155841, -00156354, - -00157122, -00157634, -00158403, -00224451, -00225219, -00291524, -00292036, -00358341, - -00424389, -00490694, -00491206, -00557511, -00623559, -00755400, -00821705, -00887754, - -00954058, -01085643, -01151948, -01283533, -01349837, -01481422, -01613263, -01679312, - -01811153, -01942738, -02074579, -02206164, -02337749, -02469590, -02601175, -02733016, - -02930137, -03061978, -03193563, -03390940, -03522526, -03654111, -03851488, -03983073, - -04180450, -04377572, -04509157, -04706534, -04838119, -05035497, -05232618, -05429739, - -05561580, -05758702, -05956079, -06153200, -06350322, -06482163, -06679284, -06876662, - -07073783, -07270904, -07468282, -07665403, -07862524, -08059902, -08257023, -08388608, - }; - } -} diff --git a/src/Spectrogram/Colormaps/Viridis.cs b/src/Spectrogram/Colormaps/Viridis.cs deleted file mode 100644 index a6e0bb1..0000000 --- a/src/Spectrogram/Colormaps/Viridis.cs +++ /dev/null @@ -1,57 +0,0 @@ -/* Viridis is a colormap by Nathaniel J. Smith, Stefan van der Walt, and Eric Firing - * https://bids.github.io/colormap/ - * https://github.com/BIDS/colormap/blob/master/colormaps.py - * - * This colormap is provided under the CC0 license / public domain dedication - * http://creativecommons.org/publicdomain/zero/1.0/ - */ - -using System; - -namespace Spectrogram.Colormaps -{ - class Viridis : IColormap - { - public (byte r, byte g, byte b) GetRGB(byte value) - { - byte[] bytes = BitConverter.GetBytes(rgb[value]); - return (bytes[2], bytes[1], bytes[0]); - } - - private readonly int[] rgb = - { - 04456788, 04457045, 04457303, 04523352, 04523610, 04524123, 04589916, 04590430, - 04590687, 04591201, 04656994, 04657507, 04657765, 04658278, 04658535, 04658793, - 04659306, 04725099, 04725356, 04725870, 04726127, 04726384, 04726897, 04727154, - 04727411, 04727668, 04662645, 04662902, 04663159, 04663416, 04663929, 04664186, - 04664443, 04599164, 04599676, 04599933, 04600190, 04534911, 04535423, 04535680, - 04535937, 04470657, 04471170, 04405891, 04406147, 04406404, 04341124, 04341381, - 04341893, 04276614, 04276870, 04211591, 04211847, 04146567, 04147080, 04081800, - 04082057, 04016777, 04017033, 04017289, 03952010, 03952266, 03887242, 03887498, - 03822219, 03822475, 03757195, 03757451, 03692171, 03692428, 03627148, 03627404, - 03562124, 03562380, 03497100, 03497356, 03432077, 03432333, 03367053, 03367309, - 03302029, 03302285, 03237005, 03237261, 03237517, 03172237, 03172493, 03107213, - 03107469, 03042190, 03042446, 03042702, 02977422, 02977678, 02912398, 02912654, - 02912910, 02847630, 02847886, 02782606, 02782862, 02783118, 02717838, 02718094, - 02652814, 02652814, 02653070, 02587790, 02588046, 02588302, 02523022, 02523278, - 02523534, 02458254, 02458509, 02393229, 02393485, 02393741, 02328461, 02328717, - 02328973, 02263437, 02263693, 02263949, 02198669, 02198924, 02199180, 02133900, - 02134156, 02134412, 02069132, 02069387, 02069643, 02069899, 02070155, 02004874, - 02005130, 02005386, 02005386, 02005641, 02005897, 02006153, 02006408, 02006664, - 02006920, 02007175, 02072967, 02073222, 02073478, 02139269, 02139525, 02205317, - 02205572, 02271108, 02336899, 02337154, 02402946, 02468737, 02534529, 02600320, - 02666111, 02731903, 02797694, 02863485, 02929021, 03060348, 03126139, 03191930, - 03323258, 03389049, 03520376, 03586167, 03717494, 03783030, 03914357, 04045684, - 04111475, 04242802, 04374129, 04505200, 04570991, 04702318, 04833645, 04964972, - 05096043, 05227369, 05358696, 05490023, 05621350, 05752421, 05883748, 06015074, - 06211937, 06343008, 06474335, 06605661, 06802524, 06933595, 07064921, 07196248, - 07392854, 07524181, 07655508, 07852114, 07983441, 08180303, 08311374, 08508236, - 08639307, 08836169, 08967495, 09164102, 09295428, 09492035, 09623361, 09819967, - 09951294, 10147900, 10344762, 10475832, 10672695, 10869301, 11000627, 11197234, - 11394096, 11525166, 11722028, 11918635, 12049705, 12246567, 12443174, 12574500, - 12771106, 12967713, 13099039, 13295646, 13492253, 13623580, 13820187, 13951258, - 14148121, 14344728, 14475800, 14672664, 14803736, 15000344, 15197209, 15328281, - 15524890, 15656219, 15852828, 15983902, 16180767, 16311841, 16442914, 16639780, - }; - } -} diff --git a/src/Spectrogram/IColormap.cs b/src/Spectrogram/IColormap.cs deleted file mode 100644 index 5c9c79e..0000000 --- a/src/Spectrogram/IColormap.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Spectrogram -{ - public interface IColormap - { - (byte r, byte g, byte b) GetRGB(byte value); - } -} diff --git a/src/Spectrogram/Image.cs b/src/Spectrogram/Image.cs index 283543b..7413b38 100644 --- a/src/Spectrogram/Image.cs +++ b/src/Spectrogram/Image.cs @@ -1,16 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Imaging; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; +using System.Collections.Generic; +using SkiaSharp; namespace Spectrogram { public static class Image { - public static Bitmap GetBitmap(List ffts, Colormap cmap, double intensity = 1, + public static SKBitmap GetBitmap(List ffts, Colormap cmap, double intensity = 1, bool dB = false, double dBScale = 1, bool roll = false, int rollOffset = 0, bool rotate = false) { diff --git a/src/Spectrogram/ImageMaker.cs b/src/Spectrogram/ImageMaker.cs index dd4348f..c55ef99 100644 --- a/src/Spectrogram/ImageMaker.cs +++ b/src/Spectrogram/ImageMaker.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Imaging; using System.Runtime.InteropServices; -using System.Text; using System.Threading.Tasks; +using SkiaSharp; namespace Spectrogram { @@ -53,37 +51,35 @@ public ImageMaker() { } - - public Bitmap GetBitmap(List ffts) + + public SKBitmap GetBitmap(List ffts) { if (ffts.Count == 0) throw new ArgumentException("Not enough data in FFTs to generate an image yet."); - int Width = IsRotated ? ffts[0].Length : ffts.Count; - int Height = IsRotated ? ffts.Count : ffts[0].Length; - - Bitmap bmp = new(Width, Height, PixelFormat.Format8bppIndexed); - Colormap.Apply(bmp); + int width = IsRotated ? ffts[0].Length : ffts.Count; + int height = IsRotated ? ffts.Count : ffts[0].Length; - Rectangle lockRect = new(0, 0, Width, Height); - BitmapData bitmapData = bmp.LockBits(lockRect, ImageLockMode.ReadOnly, bmp.PixelFormat); - int stride = bitmapData.Stride; + var imageInfo = new SKImageInfo(width, height, SKColorType.Gray8); + var bitmap = new SKBitmap(imageInfo); + + int pixelCount = width * height; + byte[] pixelBuffer = new byte[pixelCount]; - byte[] bytes = new byte[bitmapData.Stride * bmp.Height]; - Parallel.For(0, Width, col => + Parallel.For(0, width, col => { int sourceCol = col; if (IsRoll) { - sourceCol += Width - RollOffset % Width; - if (sourceCol >= Width) - sourceCol -= Width; + sourceCol += width - RollOffset % width; + if (sourceCol >= width) + sourceCol -= width; } - for (int row = 0; row < Height; row++) + for (int row = 0; row < height; row++) { double value = IsRotated - ? ffts[Height - row - 1][sourceCol] + ? ffts[height - row - 1][sourceCol] : ffts[sourceCol][row]; if (IsDecibel) @@ -91,15 +87,18 @@ public Bitmap GetBitmap(List ffts) value *= Intensity; value = Math.Min(value, 255); - int bytePosition = (Height - 1 - row) * stride + col; - bytes[bytePosition] = (byte)value; + + int bytePosition = (height - 1 - row) * width + col; + pixelBuffer[bytePosition] = (byte)value; } }); - Marshal.Copy(bytes, 0, bitmapData.Scan0, bytes.Length); - bmp.UnlockBits(bitmapData); + IntPtr pixelPtr = bitmap.GetPixels(); + Marshal.Copy(pixelBuffer, 0, pixelPtr, pixelBuffer.Length); - return bmp; + SKBitmap newBitmap = Colormap.ApplyFilter(bitmap); + bitmap.Dispose(); + return newBitmap; } } } diff --git a/src/Spectrogram/Scale.cs b/src/Spectrogram/Scale.cs index 767b21b..e4a7788 100644 --- a/src/Spectrogram/Scale.cs +++ b/src/Spectrogram/Scale.cs @@ -1,60 +1,58 @@ -using System; +using SkiaSharp; using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Imaging; -using System.Linq; -using System.Text; -namespace Spectrogram +namespace Spectrogram; + +static class Scale { - static class Scale + public static SKBitmap Vertical(int width, Settings settings, int offsetHz = 0, int tickSize = 3, int reduction = 1) { - public static Bitmap Vertical(int width, Settings settings, int offsetHz = 0, int tickSize = 3, int reduction = 1) + double tickHz = 1; + int minSpacingPx = 50; + double[] multipliers = { 2, 2.5, 2 }; + int multiplier = 0; + + while (true) + { + tickHz *= multipliers[multiplier++ % multipliers.Length]; + double tickCount = settings.FreqSpan / tickHz; + double pxBetweenTicks = settings.Height / tickCount; + if (pxBetweenTicks >= minSpacingPx * reduction) + break; + } + + var imageInfo = new SKImageInfo(width, settings.Height / reduction, SKColorType.Rgba8888); + var bitmap = new SKBitmap(imageInfo); + using var canvas = new SKCanvas(bitmap); + canvas.Clear(SKColors.White); + + var paint = new SKPaint + { + Color = SKColors.Black, + TextSize = 10, + IsAntialias = true, + Typeface = SKTypeface.FromFamilyName("Monospace") + }; + + List freqs = new List(); + for (double f = settings.FreqMin; f <= settings.FreqMax; f += tickHz) + freqs.Add(f); + + if (freqs.Count >= 2) { - double tickHz = 1; - int minSpacingPx = 50; - double[] multipliers = { 2, 2.5, 2 }; - int multiplier = 0; - while (true) - { - tickHz *= multipliers[multiplier++ % multipliers.Length]; - double tickCount = settings.FreqSpan / tickHz; - double pxBetweenTicks = settings.Height / tickCount; - if (pxBetweenTicks >= minSpacingPx * reduction) - break; - } - - Bitmap bmp = new Bitmap(width, settings.Height / reduction, PixelFormat.Format32bppPArgb); - - using (var gfx = Graphics.FromImage(bmp)) - using (var pen = new Pen(Color.Black)) - using (var brush = new SolidBrush(Color.Black)) - using (var font = new Font(FontFamily.GenericMonospace, 10)) - using (var sf = new StringFormat() { LineAlignment = StringAlignment.Center }) - { - gfx.Clear(Color.White); - - List freqs = new List(); - - for (double f = settings.FreqMin; f <= settings.FreqMax; f += tickHz) - freqs.Add(f); - - // don't show first or last tick - if (freqs.Count >= 2) - { - freqs.RemoveAt(0); - freqs.RemoveAt(freqs.Count - 1); - } - - foreach (var freq in freqs) - { - int y = settings.PixelY(freq) / reduction; - gfx.DrawLine(pen, 0, y, tickSize, y); - gfx.DrawString($"{freq + offsetHz:N0} Hz", font, brush, tickSize, y, sf); - } - } - - return bmp; + freqs.RemoveAt(0); + freqs.RemoveAt(freqs.Count - 1); } + + foreach (var freq in freqs) + { + int y = settings.PixelY(freq) / reduction; + canvas.DrawLine(0, y, tickSize, y, paint); + + var text = $"{freq + offsetHz:N0} Hz"; + canvas.DrawText(text, tickSize + 2, y + 5, paint); + } + + return bitmap; } -} \ No newline at end of file +} diff --git a/src/Spectrogram/Settings.cs b/src/Spectrogram/Settings.cs index a799260..3ec35f1 100644 --- a/src/Spectrogram/Settings.cs +++ b/src/Spectrogram/Settings.cs @@ -31,7 +31,9 @@ class Settings public Settings(int sampleRate, int fftSize, int stepSize, double minFreq, double maxFreq, int offsetHz) { - if (FftSharp.Transform.IsPowerOfTwo(fftSize) == false) + static bool IsPowerOfTwo(int x) => ((x & (x - 1)) == 0) && (x > 0); + + if (IsPowerOfTwo(fftSize) == false) throw new ArgumentException("FFT size must be a power of 2"); // FFT info diff --git a/src/Spectrogram/Spectrogram.csproj b/src/Spectrogram/Spectrogram.csproj index 4804cff..4bfc42f 100644 --- a/src/Spectrogram/Spectrogram.csproj +++ b/src/Spectrogram/Spectrogram.csproj @@ -1,8 +1,7 @@  - - netstandard2.0;net6.0 - 1.6.1 + netstandard2.0 + 2.0.0-alpha A .NET Standard library for creating spectrograms Scott Harden Harden Technologies, LLC @@ -23,20 +22,13 @@ true latest - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - + + + - - + \ No newline at end of file diff --git a/src/Spectrogram/SpectrogramGenerator.cs b/src/Spectrogram/SpectrogramGenerator.cs index 68a9c3b..edb85b4 100644 --- a/src/Spectrogram/SpectrogramGenerator.cs +++ b/src/Spectrogram/SpectrogramGenerator.cs @@ -1,446 +1,408 @@ using System; using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Imaging; using System.IO; using System.Threading.Tasks; +using SkiaSharp; -namespace Spectrogram +namespace Spectrogram; + +public class SpectrogramGenerator { - public class SpectrogramGenerator + /// + /// Number of pixel columns (FFT samples) in the spectrogram image + /// + public int Width { get => FFTs.Count; } + + /// + /// Number of pixel rows (frequency bins) in the spectrogram image + /// + public int Height { get => Settings.Height; } + + /// + /// Number of samples to use for each FFT (must be a power of 2) + /// + public int FftSize { get => Settings.FftSize; } + + /// + /// Vertical resolution (frequency bin size depends on FftSize and SampleRate) + /// + public double HzPerPx { get => Settings.HzPerPixel; } + + /// + /// Horizontal resolution (seconds per pixel depends on StepSize) + /// + public double SecPerPx { get => Settings.StepLengthSec; } + + /// + /// Number of FFTs that remain to be processed for data which has been added but not yet analyzed + /// + public int FftsToProcess { get => (UnprocessedData.Count - Settings.FftSize) / Settings.StepSize; } + + /// + /// Total number of FFT steps processed + /// + public int FftsProcessed { get; private set; } + + /// + /// Index of the pixel column which will be populated next. Location of vertical line for wrap-around displays. + /// + public int NextColumnIndex { get => Width > 0 ? (FftsProcessed + rollOffset) % Width : 0; } + + /// + /// This value is added to displayed frequency axis tick labels + /// + public int OffsetHz { get => Settings.OffsetHz; set { Settings.OffsetHz = value; } } + + /// + /// Number of samples per second + /// + public int SampleRate { get => Settings.SampleRate; } + + /// + /// Number of samples to step forward after each FFT is processed. + /// This value controls the horizontal resolution of the spectrogram. + /// + public int StepSize { get => Settings.StepSize; } + + /// + /// The spectrogram is trimmed to cut-off frequencies below this value. + /// + public double FreqMax { get => Settings.FreqMax; } + + /// + /// The spectrogram is trimmed to cut-off frequencies above this value. + /// + public double FreqMin { get => Settings.FreqMin; } + + /// + /// This module contains detailed FFT/Spectrogram settings + /// + private readonly Settings Settings; + + /// + /// This is the list of FFTs which is translated to the spectrogram image when it is requested. + /// The length of this list is the spectrogram width. + /// The length of the arrays in this list is the spectrogram height. + /// + private readonly List FFTs = []; + + /// + /// This list contains data values which have not yet been processed. + /// Process() processes all unprocessed data. + /// This list may not be empty after processing if there aren't enough values to fill a full FFT (FftSize). + /// + private readonly List UnprocessedData; + + /// + /// Colormap to use when generating future FFTs. + /// + public Colormap Colormap = new(new ScottPlot.Colormaps.Viridis()); + + /// + /// Instantiate a spectrogram generator. + /// This module calculates the FFT over a moving window as data comes in. + /// Using the Add() method to load new data and process it as it arrives. + /// + /// Number of samples per second (Hz) + /// Number of samples to use for each FFT operation. This value must be a power of 2. + /// Number of samples to step forward + /// Frequency data lower than this value (Hz) will not be stored + /// Frequency data higher than this value (Hz) will not be stored + /// Spectrogram output will always be sized to this width (column count) + /// This value will be added to displayed frequency axis tick labels + /// Analyze this data immediately (alternative to calling Add() later) + public SpectrogramGenerator( + int sampleRate, + int fftSize, + int stepSize, + double minFreq = 0, + double maxFreq = double.PositiveInfinity, + int? fixedWidth = null, + int offsetHz = 0, + List initialAudioList = null) { - /// - /// Number of pixel columns (FFT samples) in the spectrogram image - /// - public int Width { get => FFTs.Count; } - - /// - /// Number of pixel rows (frequency bins) in the spectrogram image - /// - public int Height { get => Settings.Height; } - - /// - /// Number of samples to use for each FFT (must be a power of 2) - /// - public int FftSize { get => Settings.FftSize; } - - /// - /// Vertical resolution (frequency bin size depends on FftSize and SampleRate) - /// - public double HzPerPx { get => Settings.HzPerPixel; } - - /// - /// Horizontal resolution (seconds per pixel depends on StepSize) - /// - public double SecPerPx { get => Settings.StepLengthSec; } - - /// - /// Number of FFTs that remain to be processed for data which has been added but not yet analuyzed - /// - public int FftsToProcess { get => (UnprocessedData.Count - Settings.FftSize) / Settings.StepSize; } - - /// - /// Total number of FFT steps processed - /// - public int FftsProcessed { get; private set; } - - /// - /// Index of the pixel column which will be populated next. Location of vertical line for wrap-around displays. - /// - public int NextColumnIndex { get => Width > 0 ? (FftsProcessed + rollOffset) % Width : 0; } - - /// - /// This value is added to displayed frequency axis tick labels - /// - public int OffsetHz { get => Settings.OffsetHz; set { Settings.OffsetHz = value; } } - - /// - /// Number of samples per second - /// - public int SampleRate { get => Settings.SampleRate; } - - /// - /// Number of samples to step forward after each FFT is processed. - /// This value controls the horizontal resolution of the spectrogram. - /// - public int StepSize { get => Settings.StepSize; } - - /// - /// The spectrogram is trimmed to cut-off frequencies below this value. - /// - public double FreqMax { get => Settings.FreqMax; } - - /// - /// The spectrogram is trimmed to cut-off frequencies above this value. - /// - public double FreqMin { get => Settings.FreqMin; } - - /// - /// This module contains detailed FFT/Spectrogram settings - /// - private readonly Settings Settings; - - /// - /// This is the list of FFTs which is translated to the spectrogram image when it is requested. - /// The length of this list is the spectrogram width. - /// The length of the arrays in this list is the spectrogram height. - /// - private readonly List FFTs = new List(); - - /// - /// This list contains data values which have not yet been processed. - /// Process() processes all unprocessed data. - /// This list may not be empty after processing if there aren't enough values to fill a full FFT (FftSize). - /// - private readonly List UnprocessedData; - - /// - /// Colormap to use when generating future FFTs. - /// - public Colormap Colormap = Colormap.Viridis; - - /// - /// Instantiate a spectrogram generator. - /// This module calculates the FFT over a moving window as data comes in. - /// Using the Add() method to load new data and process it as it arrives. - /// - /// Number of samples per second (Hz) - /// Number of samples to use for each FFT operation. This value must be a power of 2. - /// Number of samples to step forward - /// Frequency data lower than this value (Hz) will not be stored - /// Frequency data higher than this value (Hz) will not be stored - /// Spectrogram output will always be sized to this width (column count) - /// This value will be added to displayed frequency axis tick labels - /// Analyze this data immediately (alternative to calling Add() later) - public SpectrogramGenerator( - int sampleRate, - int fftSize, - int stepSize, - double minFreq = 0, - double maxFreq = double.PositiveInfinity, - int? fixedWidth = null, - int offsetHz = 0, - List initialAudioList = null) - { - Settings = new Settings(sampleRate, fftSize, stepSize, minFreq, maxFreq, offsetHz); - - UnprocessedData = initialAudioList ?? new List(); - - if (fixedWidth.HasValue) - SetFixedWidth(fixedWidth.Value); - } + Settings = new Settings(sampleRate, fftSize, stepSize, minFreq, maxFreq, offsetHz); - public override string ToString() - { - double processedSamples = FFTs.Count * Settings.StepSize + Settings.FftSize; - double processedSec = processedSamples / Settings.SampleRate; - string processedTime = (processedSec < 60) ? $"{processedSec:N2} sec" : $"{processedSec / 60.0:N2} min"; - - return $"Spectrogram ({Width}, {Height})" + - $"\n Vertical ({Height} px): " + - $"{Settings.FreqMin:N0} - {Settings.FreqMax:N0} Hz, " + - $"FFT size: {Settings.FftSize:N0} samples, " + - $"{Settings.HzPerPixel:N2} Hz/px" + - $"\n Horizontal ({Width} px): " + - $"{processedTime}, " + - $"window: {Settings.FftLengthSec:N2} sec, " + - $"step: {Settings.StepLengthSec:N2} sec, " + - $"overlap: {Settings.StepOverlapFrac * 100:N0}%"; - } - - [Obsolete("Assign to the Colormap field")] - /// - /// Set the colormap to use for future renders - /// - public void SetColormap(Colormap cmap) - { - Colormap = cmap ?? this.Colormap; - } + UnprocessedData = initialAudioList ?? new List(); - /// - /// Load a custom window kernel to multiply against each FFT sample prior to processing. - /// Windows must be at least the length of FftSize and typically have a sum of 1.0. - /// - public void SetWindow(double[] newWindow) - { - if (newWindow.Length > Settings.FftSize) - throw new ArgumentException("window length cannot exceed FFT size"); + if (fixedWidth.HasValue) + SetFixedWidth(fixedWidth.Value); + } - for (int i = 0; i < Settings.FftSize; i++) - Settings.Window[i] = 0; + /// + /// Load a custom window kernel to multiply against each FFT sample prior to processing. + /// Windows must be at least the length of FftSize and typically have a sum of 1.0. + /// + public void SetWindow(double[] newWindow) + { + if (newWindow.Length > Settings.FftSize) + throw new ArgumentException("window length cannot exceed FFT size"); - int offset = (Settings.FftSize - newWindow.Length) / 2; - Array.Copy(newWindow, 0, Settings.Window, offset, newWindow.Length); - } + for (int i = 0; i < Settings.FftSize; i++) + Settings.Window[i] = 0; - [Obsolete("use the Add() method", true)] - public void AddExtend(float[] values) { } + int offset = (Settings.FftSize - newWindow.Length) / 2; + Array.Copy(newWindow, 0, Settings.Window, offset, newWindow.Length); + } - [Obsolete("use the Add() method", true)] - public void AddCircular(float[] values) { } + /// + /// Load new data into the spectrogram generator + /// + public void Add(IEnumerable audio, bool process = true) + { + UnprocessedData.AddRange(audio); + if (process) + Process(); + } - [Obsolete("use the Add() method", true)] - public void AddScroll(float[] values) { } + /// + /// The roll offset is used to calculate NextColumnIndex and can be set to a positive number + /// to begin adding new columns to the center of the spectrogram. + /// This can also be used to artificially move the next column index to zero even though some + /// data has already been accumulated. + /// + private int rollOffset = 0; + + /// + /// Reset the next column index such that the next processed FFT will appear at the far left of the spectrogram. + /// + /// + public void RollReset(int offset = 0) + { + rollOffset = -FftsProcessed + offset; + } - /// - /// Load new data into the spectrogram generator - /// - public void Add(IEnumerable audio, bool process = true) - { - UnprocessedData.AddRange(audio); - if (process) - Process(); - } + /// + /// Perform FFT analysis on all unprocessed data + /// + public double[][] Process() + { + if (FftsToProcess < 1) + return null; - /// - /// The roll offset is used to calculate NextColumnIndex and can be set to a positive number - /// to begin adding new columns to the center of the spectrogram. - /// This can also be used to artificially move the next column index to zero even though some - /// data has already been accumulated. - /// - private int rollOffset = 0; - - /// - /// Reset the next column index such that the next processed FFT will appear at the far left of the spectrogram. - /// - /// - public void RollReset(int offset = 0) - { - rollOffset = -FftsProcessed + offset; - } + int newFftCount = FftsToProcess; + double[][] newFfts = new double[newFftCount][]; - /// - /// Perform FFT analysis on all unprocessed data - /// - public double[][] Process() + Parallel.For(0, newFftCount, newFftIndex => { - if (FftsToProcess < 1) - return null; - - int newFftCount = FftsToProcess; - double[][] newFfts = new double[newFftCount][]; - - Parallel.For(0, newFftCount, newFftIndex => - { - FftSharp.Complex[] buffer = new FftSharp.Complex[Settings.FftSize]; - int sourceIndex = newFftIndex * Settings.StepSize; - for (int i = 0; i < Settings.FftSize; i++) - buffer[i].Real = UnprocessedData[sourceIndex + i] * Settings.Window[i]; + var buffer = new System.Numerics.Complex[Settings.FftSize]; + int sourceIndex = newFftIndex * Settings.StepSize; + for (int i = 0; i < Settings.FftSize; i++) + buffer[i] = new(UnprocessedData[sourceIndex + i] * Settings.Window[i], 0); - FftSharp.Transform.FFT(buffer); + FftSharp.FFT.Forward(buffer); - newFfts[newFftIndex] = new double[Settings.Height]; - for (int i = 0; i < Settings.Height; i++) - newFfts[newFftIndex][i] = buffer[Settings.FftIndex1 + i].Magnitude / Settings.FftSize; - }); + newFfts[newFftIndex] = new double[Settings.Height]; + for (int i = 0; i < Settings.Height; i++) + newFfts[newFftIndex][i] = buffer[Settings.FftIndex1 + i].Magnitude / Settings.FftSize; + }); - foreach (var newFft in newFfts) - FFTs.Add(newFft); - FftsProcessed += newFfts.Length; + foreach (var newFft in newFfts) + FFTs.Add(newFft); + FftsProcessed += newFfts.Length; - UnprocessedData.RemoveRange(0, newFftCount * Settings.StepSize); - PadOrTrimForFixedWidth(); + UnprocessedData.RemoveRange(0, newFftCount * Settings.StepSize); + PadOrTrimForFixedWidth(); - return newFfts; - } + return newFfts; + } - /// - /// Return a list of the mel-scaled FFTs contained in this spectrogram - /// - /// Total number of output bins to use. Choose a value significantly smaller than Height. - public List GetMelFFTs(int melBinCount) - { - if (Settings.FreqMin != 0) - throw new InvalidOperationException("cannot get Mel spectrogram unless minimum frequency is 0Hz"); + /// + /// Return a list of the mel-scaled FFTs contained in this spectrogram + /// + /// Total number of output bins to use. Choose a value significantly smaller than Height. + public List GetMelFFTs(int melBinCount) + { + if (Settings.FreqMin != 0) + throw new InvalidOperationException("cannot get Mel spectrogram unless minimum frequency is 0Hz"); - var fftsMel = new List(); - foreach (var fft in FFTs) - fftsMel.Add(FftSharp.Transform.MelScale(fft, SampleRate, melBinCount)); + var fftsMel = new List(); + foreach (var fft in FFTs) + fftsMel.Add(FftSharp.Mel.Scale(fft, SampleRate, melBinCount)); - return fftsMel; - } + return fftsMel; + } - /// - /// Create and return a spectrogram bitmap from the FFTs stored in memory. - /// - /// Multiply the output by a fixed value to change its brightness. - /// If true, output will be log-transformed. - /// If dB scaling is in use, this multiplier will be applied before log transformation. - /// Behavior of the spectrogram when it is full of data. - /// If True, the image will be rotated so time flows from top to bottom (rather than left to right). - /// Roll (true) adds new columns on the left overwriting the oldest ones. - /// Scroll (false) slides the whole image to the left and adds new columns to the right. - public Bitmap GetBitmap(double intensity = 1, bool dB = false, double dBScale = 1, bool roll = false, bool rotate = false) - { - if (FFTs.Count == 0) - throw new InvalidOperationException("Not enough data to create an image. " + - $"Ensure {nameof(Width)} is >0 before calling {nameof(GetBitmap)}()."); + /// + /// Create and return a spectrogram bitmap from the FFTs stored in memory. + /// + /// Multiply the output by a fixed value to change its brightness. + /// If true, output will be log-transformed. + /// If dB scaling is in use, this multiplier will be applied before log transformation. + /// Behavior of the spectrogram when it is full of data. + /// If True, the image will be rotated so time flows from top to bottom (rather than left to right). + /// Roll (true) adds new columns on the left overwriting the oldest ones. + /// Scroll (false) slides the whole image to the left and adds new columns to the right. + public SKBitmap GetBitmap(double intensity = 1, bool dB = false, double dBScale = 1, bool roll = false, bool rotate = false) + { + if (FFTs.Count == 0) + throw new InvalidOperationException("Not enough data to create an image. " + + $"Ensure {nameof(Width)} is >0 before calling {nameof(GetBitmap)}()."); - return Image.GetBitmap(FFTs, Colormap, intensity, dB, dBScale, roll, NextColumnIndex, rotate); - } + return Image.GetBitmap(FFTs, Colormap, intensity, dB, dBScale, roll, NextColumnIndex, rotate); + } - /// - /// Create a Mel-scaled spectrogram. - /// - /// Total number of output bins to use. Choose a value significantly smaller than Height. - /// Multiply the output by a fixed value to change its brightness. - /// If true, output will be log-transformed. - /// If dB scaling is in use, this multiplier will be applied before log transformation. - /// Behavior of the spectrogram when it is full of data. - /// Roll (true) adds new columns on the left overwriting the oldest ones. - /// Scroll (false) slides the whole image to the left and adds new columns to the right. - public Bitmap GetBitmapMel(int melBinCount = 25, double intensity = 1, bool dB = false, double dBScale = 1, bool roll = false) => - Image.GetBitmap(GetMelFFTs(melBinCount), Colormap, intensity, dB, dBScale, roll, NextColumnIndex); - - [Obsolete("use SaveImage()", true)] - public void SaveBitmap(Bitmap bmp, string fileName) { } - - /// - /// Generate the spectrogram and save it as an image file. - /// - /// Path of the file to save. - /// Multiply the output by a fixed value to change its brightness. - /// If true, output will be log-transformed. - /// If dB scaling is in use, this multiplier will be applied before log transformation. - /// Behavior of the spectrogram when it is full of data. - /// Roll (true) adds new columns on the left overwriting the oldest ones. - /// Scroll (false) slides the whole image to the left and adds new columns to the right. - public void SaveImage(string fileName, double intensity = 1, bool dB = false, double dBScale = 1, bool roll = false) - { - if (FFTs.Count == 0) - throw new InvalidOperationException("Spectrogram contains no data. Use Add() to add signal data."); - - string extension = Path.GetExtension(fileName).ToLower(); - - ImageFormat fmt; - if (extension == ".bmp") - fmt = ImageFormat.Bmp; - else if (extension == ".png") - fmt = ImageFormat.Png; - else if (extension == ".jpg" || extension == ".jpeg") - fmt = ImageFormat.Jpeg; - else if (extension == ".gif") - fmt = ImageFormat.Gif; - else - throw new ArgumentException("unknown file extension"); - - Image.GetBitmap(FFTs, Colormap, intensity, dB, dBScale, roll, NextColumnIndex).Save(fileName, fmt); - } + /// + /// Create a Mel-scaled spectrogram. + /// + /// Total number of output bins to use. Choose a value significantly smaller than Height. + /// Multiply the output by a fixed value to change its brightness. + /// If true, output will be log-transformed. + /// If dB scaling is in use, this multiplier will be applied before log transformation. + /// Behavior of the spectrogram when it is full of data. + /// Roll (true) adds new columns on the left overwriting the oldest ones. + /// Scroll (false) slides the whole image to the left and adds new columns to the right. + public SKBitmap GetBitmapMel(int melBinCount = 25, double intensity = 1, bool dB = false, double dBScale = 1, bool roll = false) => + Image.GetBitmap(GetMelFFTs(melBinCount), Colormap, intensity, dB, dBScale, roll, NextColumnIndex); + + /// + /// Generate the spectrogram and save it as an image file. + /// + /// Path of the file to save. + /// Multiply the output by a fixed value to change its brightness. + /// If true, output will be log-transformed. + /// If dB scaling is in use, this multiplier will be applied before log transformation. + /// Behavior of the spectrogram when it is full of data. + /// Roll (true) adds new columns on the left overwriting the oldest ones. + /// Scroll (false) slides the whole image to the left and adds new columns to the right. + public void SaveImage(string fileName, double intensity = 1, bool dB = false, double dBScale = 1, bool roll = false) + { + if (FFTs.Count == 0) + throw new InvalidOperationException("Spectrogram contains no data. Use Add() to add signal data."); + + string extension = Path.GetExtension(fileName).ToLower(); + + SKEncodedImageFormat fmt; + if (extension == ".bmp") + fmt = SKEncodedImageFormat.Bmp; + else if (extension == ".png") + fmt = SKEncodedImageFormat.Png; + else if (extension == ".jpg" || extension == ".jpeg") + fmt = SKEncodedImageFormat.Jpeg; + else if (extension == ".gif") + fmt = SKEncodedImageFormat.Gif; + else + throw new ArgumentException("unknown file extension"); + + using var image = Image.GetBitmap(FFTs, Colormap, intensity, dB, dBScale, roll, NextColumnIndex); + using var encodedImage = image.Encode(fmt, 80); + using var fileStream = new FileStream(fileName, FileMode.Create); + encodedImage.SaveTo(fileStream); + } - /// - /// Create and return a spectrogram bitmap from the FFTs stored in memory. - /// The output will be scaled-down vertically by binning according to a reduction factor and keeping the brightest pixel value in each bin. - /// - /// Multiply the output by a fixed value to change its brightness. - /// If true, output will be log-transformed. - /// If dB scaling is in use, this multiplier will be applied before log transformation. - /// Behavior of the spectrogram when it is full of data. - /// - public Bitmap GetBitmapMax(double intensity = 1, bool dB = false, double dBScale = 1, bool roll = false, int reduction = 4) + /// + /// Create and return a spectrogram bitmap from the FFTs stored in memory. + /// The output will be scaled-down vertically by binning according to a reduction factor and keeping the brightest pixel value in each bin. + /// + /// Multiply the output by a fixed value to change its brightness. + /// If true, output will be log-transformed. + /// If dB scaling is in use, this multiplier will be applied before log transformation. + /// Behavior of the spectrogram when it is full of data. + /// + public SKBitmap GetBitmapMax(double intensity = 1, bool dB = false, double dBScale = 1, bool roll = false, int reduction = 4) + { + List ffts2 = new List(); + for (int i = 0; i < FFTs.Count; i++) { - List ffts2 = new List(); - for (int i = 0; i < FFTs.Count; i++) - { - double[] d1 = FFTs[i]; - double[] d2 = new double[d1.Length / reduction]; - for (int j = 0; j < d2.Length; j++) - for (int k = 0; k < reduction; k++) - d2[j] = Math.Max(d2[j], d1[j * reduction + k]); - ffts2.Add(d2); - } - return Image.GetBitmap(ffts2, Colormap, intensity, dB, dBScale, roll, NextColumnIndex); + double[] d1 = FFTs[i]; + double[] d2 = new double[d1.Length / reduction]; + for (int j = 0; j < d2.Length; j++) + for (int k = 0; k < reduction; k++) + d2[j] = Math.Max(d2[j], d1[j * reduction + k]); + ffts2.Add(d2); } + return Image.GetBitmap(ffts2, Colormap, intensity, dB, dBScale, roll, NextColumnIndex); + } - /// - /// Defines the total number of FFTs (spectrogram columns) to store in memory. Determines Width. - /// - private int fixedWidth = 0; + /// + /// Defines the total number of FFTs (spectrogram columns) to store in memory. Determines Width. + /// + private int fixedWidth = 0; - /// - /// Configure the Spectrogram to maintain a fixed number of pixel columns. - /// Zeros will be added to padd existing data to achieve this width, and extra columns will be deleted. - /// - public void SetFixedWidth(int width) - { - fixedWidth = width; - PadOrTrimForFixedWidth(); - } + /// + /// Configure the Spectrogram to maintain a fixed number of pixel columns. + /// Zeros will be added to pad existing data to achieve this width, and extra columns will be deleted. + /// + public void SetFixedWidth(int width) + { + fixedWidth = width; + PadOrTrimForFixedWidth(); + } - private void PadOrTrimForFixedWidth() + private void PadOrTrimForFixedWidth() + { + if (fixedWidth > 0) { - if (fixedWidth > 0) - { - int overhang = Width - fixedWidth; - if (overhang > 0) - FFTs.RemoveRange(0, overhang); + int overhang = Width - fixedWidth; + if (overhang > 0) + FFTs.RemoveRange(0, overhang); - while (FFTs.Count < fixedWidth) - FFTs.Insert(0, new double[Height]); - } + while (FFTs.Count < fixedWidth) + FFTs.Insert(0, new double[Height]); } + } - /// - /// Get a vertical image containing ticks and tick labels for the frequency axis. - /// - /// size (pixels) - /// number to add to each tick label - /// length of each tick mark (pixels) - /// bin size for vertical data reduction - public Bitmap GetVerticalScale(int width, int offsetHz = 0, int tickSize = 3, int reduction = 1) - { - return Scale.Vertical(width, Settings, offsetHz, tickSize, reduction); - } + /// + /// Get a vertical image containing ticks and tick labels for the frequency axis. + /// + /// size (pixels) + /// number to add to each tick label + /// length of each tick mark (pixels) + /// bin size for vertical data reduction + public SKBitmap GetVerticalScale(int width, int offsetHz = 0, int tickSize = 3, int reduction = 1) + { + return Scale.Vertical(width, Settings, offsetHz, tickSize, reduction); + } - /// - /// Return the vertical position (pixel units) for the given frequency - /// - public int PixelY(double frequency, int reduction = 1) - { - int pixelsFromZeroHz = (int)(Settings.PxPerHz * frequency / reduction); - int pixelsFromMinFreq = pixelsFromZeroHz - Settings.FftIndex1 / reduction + 1; - int pixelRow = Settings.Height / reduction - 1 - pixelsFromMinFreq; - return pixelRow - 1; - } + /// + /// Return the vertical position (pixel units) for the given frequency + /// + public int PixelY(double frequency, int reduction = 1) + { + int pixelsFromZeroHz = (int)(Settings.PxPerHz * frequency / reduction); + int pixelsFromMinFreq = pixelsFromZeroHz - Settings.FftIndex1 / reduction + 1; + int pixelRow = Settings.Height / reduction - 1 - pixelsFromMinFreq; + return pixelRow - 1; + } - /// - /// Return the list of FFTs in memory underlying the spectrogram. - /// This list may continue to evolve after it is returned. - /// - public List GetFFTs() - { - return FFTs; - } + /// + /// Return the list of FFTs in memory underlying the spectrogram. + /// This list may continue to evolve after it is returned. + /// + public List GetFFTs() + { + return FFTs; + } - /// - /// Return frequency and magnitude of the dominant frequency. - /// - /// If true, only the latest FFT will be assessed. - public (double freqHz, double magRms) GetPeak(bool latestFft = true) - { - if (FFTs.Count == 0) - return (double.NaN, double.NaN); + /// + /// Return frequency and magnitude of the dominant frequency. + /// + /// If true, only the latest FFT will be assessed. + public (double freqHz, double magRms) GetPeak(bool latestFft = true) + { + if (FFTs.Count == 0) + return (double.NaN, double.NaN); - if (latestFft == false) - throw new NotImplementedException("peak of mean of all FFTs not yet supported"); + if (latestFft == false) + throw new NotImplementedException("peak of mean of all FFTs not yet supported"); - double[] freqs = FFTs[FFTs.Count - 1]; + double[] freqs = FFTs[FFTs.Count - 1]; - int peakIndex = 0; - double peakMagnitude = 0; - for (int i = 0; i < freqs.Length; i++) + int peakIndex = 0; + double peakMagnitude = 0; + for (int i = 0; i < freqs.Length; i++) + { + if (freqs[i] > peakMagnitude) { - if (freqs[i] > peakMagnitude) - { - peakMagnitude = freqs[i]; - peakIndex = i; - } + peakMagnitude = freqs[i]; + peakIndex = i; } + } - double maxFreq = SampleRate / 2; - double peakFreqFrac = peakIndex / (double)freqs.Length; - double peakFreqHz = maxFreq * peakFreqFrac; + double maxFreq = SampleRate / 2; + double peakFreqFrac = peakIndex / (double)freqs.Length; + double peakFreqHz = maxFreq * peakFreqFrac; - return (peakFreqHz, peakMagnitude); - } + return (peakFreqHz, peakMagnitude); } } From ab81058bd63d8b9156450c2c8c71ee1af05d3916 Mon Sep 17 00:00:00 2001 From: Aksel Haukanes Date: Fri, 1 Nov 2024 16:00:29 +0100 Subject: [PATCH 61/62] Support non integer sample rate. (#60) * Support non integer sample rate. * Update SpectrogramGenerator.cs --------- Co-authored-by: Aksel Haukanes Co-authored-by: Scott W Harden --- src/Spectrogram/Settings.cs | 6 +++--- src/Spectrogram/SpectrogramGenerator.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Spectrogram/Settings.cs b/src/Spectrogram/Settings.cs index 3ec35f1..d36f137 100644 --- a/src/Spectrogram/Settings.cs +++ b/src/Spectrogram/Settings.cs @@ -6,7 +6,7 @@ namespace Spectrogram { class Settings { - public readonly int SampleRate; + public readonly double SampleRate; // vertical information public readonly int FftSize; @@ -29,7 +29,7 @@ class Settings public readonly double StepOverlapFrac; public readonly double StepOverlapSec; - public Settings(int sampleRate, int fftSize, int stepSize, double minFreq, double maxFreq, int offsetHz) + public Settings(double sampleRate, int fftSize, int stepSize, double minFreq, double maxFreq, int offsetHz) { static bool IsPowerOfTwo(int x) => ((x & (x - 1)) == 0) && (x > 0); @@ -45,7 +45,7 @@ public Settings(int sampleRate, int fftSize, int stepSize, double minFreq, doubl // vertical minFreq = Math.Max(minFreq, 0); FreqNyquist = sampleRate / 2; - HzPerPixel = (double)sampleRate / fftSize; + HzPerPixel = sampleRate / fftSize; PxPerHz = (double)fftSize / sampleRate; FftIndex1 = (minFreq == 0) ? 0 : (int)(minFreq / HzPerPixel); FftIndex2 = (maxFreq >= FreqNyquist) ? fftSize / 2 : (int)(maxFreq / HzPerPixel); diff --git a/src/Spectrogram/SpectrogramGenerator.cs b/src/Spectrogram/SpectrogramGenerator.cs index edb85b4..d022564 100644 --- a/src/Spectrogram/SpectrogramGenerator.cs +++ b/src/Spectrogram/SpectrogramGenerator.cs @@ -56,7 +56,7 @@ public class SpectrogramGenerator /// /// Number of samples per second /// - public int SampleRate { get => Settings.SampleRate; } + public double SampleRate { get => Settings.SampleRate; } /// /// Number of samples to step forward after each FFT is processed. @@ -218,7 +218,7 @@ public List GetMelFFTs(int melBinCount) var fftsMel = new List(); foreach (var fft in FFTs) - fftsMel.Add(FftSharp.Mel.Scale(fft, SampleRate, melBinCount)); + fftsMel.Add(FftSharp.Mel.Scale(fft, (int)SampleRate, melBinCount)); return fftsMel; } From 058e604b012b52a344305b1666dfc345da911363 Mon Sep 17 00:00:00 2001 From: Scott W Harden Date: Fri, 1 Nov 2024 11:29:50 -0400 Subject: [PATCH 62/62] SpectrogramGenerator: separate SaveImage() and GetImageBytes() (#62) resolves #57 Co-authored-by: John McMeen --- src/Spectrogram/SpectrogramGenerator.cs | 43 +++++++++++++------------ 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/src/Spectrogram/SpectrogramGenerator.cs b/src/Spectrogram/SpectrogramGenerator.cs index d022564..2c8d593 100644 --- a/src/Spectrogram/SpectrogramGenerator.cs +++ b/src/Spectrogram/SpectrogramGenerator.cs @@ -262,32 +262,33 @@ public SKBitmap GetBitmapMel(int melBinCount = 25, double intensity = 1, bool dB /// Multiply the output by a fixed value to change its brightness. /// If true, output will be log-transformed. /// If dB scaling is in use, this multiplier will be applied before log transformation. - /// Behavior of the spectrogram when it is full of data. - /// Roll (true) adds new columns on the left overwriting the oldest ones. - /// Scroll (false) slides the whole image to the left and adds new columns to the right. + /// Controls overflow behavior. True wraps new data around to the start. False slides new data in. public void SaveImage(string fileName, double intensity = 1, bool dB = false, double dBScale = 1, bool roll = false) + { + string extension = Path.GetExtension(fileName).ToLower(); + byte[] bytes = GetImageBytes(extension, intensity, dB, dBScale, roll); + File.WriteAllBytes(fileName, bytes); + } + + public byte[] GetImageBytes(string extension, double intensity = 1, bool dB = false, double dBScale = 1, bool roll = false) { if (FFTs.Count == 0) throw new InvalidOperationException("Spectrogram contains no data. Use Add() to add signal data."); - string extension = Path.GetExtension(fileName).ToLower(); - - SKEncodedImageFormat fmt; - if (extension == ".bmp") - fmt = SKEncodedImageFormat.Bmp; - else if (extension == ".png") - fmt = SKEncodedImageFormat.Png; - else if (extension == ".jpg" || extension == ".jpeg") - fmt = SKEncodedImageFormat.Jpeg; - else if (extension == ".gif") - fmt = SKEncodedImageFormat.Gif; - else - throw new ArgumentException("unknown file extension"); - - using var image = Image.GetBitmap(FFTs, Colormap, intensity, dB, dBScale, roll, NextColumnIndex); - using var encodedImage = image.Encode(fmt, 80); - using var fileStream = new FileStream(fileName, FileMode.Create); - encodedImage.SaveTo(fileStream); + SKEncodedImageFormat fmt = extension.ToLower() switch + { + ".bmp" => SKEncodedImageFormat.Bmp, + ".png" => SKEncodedImageFormat.Png, + ".gif" => SKEncodedImageFormat.Gif, + ".jpg" => SKEncodedImageFormat.Jpeg, + ".jpeg" => SKEncodedImageFormat.Jpeg, + _ => throw new ArgumentException("unknown file extension"), + }; + + using SKBitmap image = Image.GetBitmap(FFTs, Colormap, intensity, dB, dBScale, roll, NextColumnIndex); + using SKData encodedImage = image.Encode(fmt, 80); + byte[] bytes = encodedImage.ToArray(); + return bytes; } ///