diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 07d59c7..8a2d2bd 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -1,10 +1,10 @@
-name: CI
+name: CI/CD
on:
workflow_dispatch:
push:
branches:
- - master
+ - main
pull_request:
types: [opened, synchronize, reopened]
release:
@@ -12,41 +12,29 @@ 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
+ uses: actions/checkout@v4
+ - name: ✨ Setup .NET 8
+ uses: actions/setup-dotnet@v4
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 }}
+ dotnet-version: "8.0.x"
- name: 🚚 Restore
run: dotnet restore src
- name: 🛠️ Build
run: dotnet build src --configuration Release
- name: 🧪 Test
- run: dotnet test src
+ run: dotnet test src --configuration Release
- name: 📦 Pack
run: dotnet pack src --configuration Release
- - name: 💾 Store
- uses: actions/upload-artifact@v2
+ - name: 🔑 Configure Secrets
+ if: github.event_name == 'release'
+ uses: nuget/setup-nuget@v1
with:
- name: Packages
- retention-days: 1
- path: |
- src/Spectrogram/bin/Release/*.nupkg
- src/Spectrogram/bin/Release/*.snupkg
- - name: 🚀 Publish
+ nuget-api-key: ${{ secrets.NUGET_API_KEY }}
+ - 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
\ No newline at end of file
+ run: nuget push "src\Spectrogram\bin\Release\*.nupkg" -SkipDuplicate -Source https://api.nuget.org/v3/index.json
diff --git a/README.md b/README.md
index 417ebd8..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,64 +153,12 @@ 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.
-
-
-
-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()
-```
-
-
-
-## 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
+## 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;
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
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 bb31c5c..e58c01a 100644
--- a/src/Spectrogram.MicrophoneDemo/Spectrogram.Demo.csproj
+++ b/src/Spectrogram.MicrophoneDemo/Spectrogram.Demo.csproj
@@ -1,19 +1,15 @@
-
- net5.0-windows
- WinExe
- false
- true
- true
-
-
-
-
-
-
-
-
-
-
-
+
+ net8.0-windows
+ WinExe
+ 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/AddTests.cs b/src/Spectrogram.Tests/AddTests.cs
new file mode 100644
index 0000000..396e879
--- /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);
+ Assert.Throws(() => sg.GetBitmap());
+ }
+ }
+}
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($"");
- }
- }
-
- [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/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.Tests/ImageTests.cs b/src/Spectrogram.Tests/ImageTests.cs
new file mode 100644
index 0000000..c0fe5ab
--- /dev/null
+++ b/src/Spectrogram.Tests/ImageTests.cs
@@ -0,0 +1,22 @@
+using NUnit.Framework;
+using SkiaSharp;
+
+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);
+
+ SKBitmap bmp1 = sg.GetBitmap(rotate: false);
+ bmp1.SaveTo("test-image-original.png", SKEncodedImageFormat.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 073a9df..83fa10d 100644
--- a/src/Spectrogram.Tests/Spectrogram.Tests.csproj
+++ b/src/Spectrogram.Tests/Spectrogram.Tests.csproj
@@ -1,21 +1,22 @@
-
+
-
- net5.0
- false
-
+
+ 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 18df297..7413b38 100644
--- a/src/Spectrogram/Image.cs
+++ b/src/Spectrogram/Image.cs
@@ -1,64 +1,26 @@
-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,
- bool dB = false,
- double dBScale = 1,
- bool roll = false,
- int rollOffset = 0)
+ 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)
{
- if (ffts.Count == 0)
- throw new ArgumentException("This Spectrogram contains no FFTs (likely because no signal was added)");
- 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..c55ef99
--- /dev/null
+++ b/src/Spectrogram/ImageMaker.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using SkiaSharp;
+
+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 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;
+
+ var imageInfo = new SKImageInfo(width, height, SKColorType.Gray8);
+ var bitmap = new SKBitmap(imageInfo);
+
+ int pixelCount = width * height;
+ byte[] pixelBuffer = new byte[pixelCount];
+
+ 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[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) * width + col;
+ pixelBuffer[bytePosition] = (byte)value;
+ }
+ });
+
+ IntPtr pixelPtr = bitmap.GetPixels();
+ Marshal.Copy(pixelBuffer, 0, pixelPtr, pixelBuffer.Length);
+
+ SKBitmap newBitmap = Colormap.ApplyFilter(bitmap);
+ bitmap.Dispose();
+ return newBitmap;
+ }
+ }
+}
diff --git a/src/Spectrogram/SFF.cs b/src/Spectrogram/SFF.cs
deleted file mode 100644
index 8d68d9b..0000000
--- a/src/Spectrogram/SFF.cs
+++ /dev/null
@@ -1,286 +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
-{
- // Spectrogram File Format reader/writer
- 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/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..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,9 +29,11 @@ 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)
{
- 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
@@ -43,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/Spectrogram.cs b/src/Spectrogram/Spectrogram.cs
deleted file mode 100644
index 9844130..0000000
--- a/src/Spectrogram/Spectrogram.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using System;
-
-namespace Spectrogram
-{
- [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) :
- base(sampleRate, fftSize, stepSize, minFreq, maxFreq, fixedWidth, offsetHz)
- { }
- }
-}
diff --git a/src/Spectrogram/Spectrogram.csproj b/src/Spectrogram/Spectrogram.csproj
index bd9cb66..4bfc42f 100644
--- a/src/Spectrogram/Spectrogram.csproj
+++ b/src/Spectrogram/Spectrogram.csproj
@@ -1,41 +1,34 @@
-
-
-
- netstandard2.0
- 1.4.3
- 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
-
-
-
-
-
-
-
-
-
-
-
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
- all
-
-
-
-
+
+
+ netstandard2.0
+ 2.0.0-alpha
+ 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
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Spectrogram/SpectrogramGenerator.cs b/src/Spectrogram/SpectrogramGenerator.cs
index b86106b..2c8d593 100644
--- a/src/Spectrogram/SpectrogramGenerator.cs
+++ b/src/Spectrogram/SpectrogramGenerator.cs
@@ -1,449 +1,409 @@
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 double 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 => (FftsProcessed + rollOffset) % Width; }
-
- ///
- /// 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);
+ Settings = new Settings(sampleRate, fftSize, stepSize, minFreq, maxFreq, offsetHz);
- UnprocessedData = initialAudioList ?? new List();
+ UnprocessedData = initialAudioList ?? new List();
- if (fixedWidth.HasValue)
- SetFixedWidth(fixedWidth.Value);
- }
-
- 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;
- }
+ if (fixedWidth.HasValue)
+ SetFixedWidth(fixedWidth.Value);
+ }
- ///
- /// 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");
+ ///
+ /// 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");
- 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)]
- public void AddExtend(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 AddCircular(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;
+ }
- [Obsolete("use the Add() method", true)]
- public void AddScroll(float[] values) { }
+ ///
+ /// Perform FFT analysis on all unprocessed data
+ ///
+ public double[][] Process()
+ {
+ if (FftsToProcess < 1)
+ return null;
- ///
- /// Load new data into the spectrogram generator
- ///
- public void Add(IEnumerable audio, bool process = true)
- {
- UnprocessedData.AddRange(audio);
- if (process)
- Process();
- }
+ int newFftCount = FftsToProcess;
+ double[][] newFfts = new double[newFftCount][];
- ///
- /// 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)
+ Parallel.For(0, newFftCount, newFftIndex =>
{
- rollOffset = -FftsProcessed + offset;
- }
+ 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);
- ///
- /// Perform FFT analysis on all unprocessed data
- ///
- public double[][] Process()
- {
- if (FftsToProcess < 1)
- return null;
+ FftSharp.FFT.Forward(buffer);
- int newFftCount = FftsToProcess;
- double[][] newFfts = new double[newFftCount][];
+ newFfts[newFftIndex] = new double[Settings.Height];
+ for (int i = 0; i < Settings.Height; i++)
+ newFfts[newFftIndex][i] = buffer[Settings.FftIndex1 + i].Magnitude / Settings.FftSize;
+ });
- 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];
+ foreach (var newFft in newFfts)
+ FFTs.Add(newFft);
+ FftsProcessed += newFfts.Length;
- FftSharp.Transform.FFT(buffer);
+ UnprocessedData.RemoveRange(0, newFftCount * Settings.StepSize);
+ PadOrTrimForFixedWidth();
- newFfts[newFftIndex] = new double[Settings.Height];
- for (int i = 0; i < Settings.Height; i++)
- newFfts[newFftIndex][i] = buffer[Settings.FftIndex1 + i].Magnitude / Settings.FftSize;
- });
+ return newFfts;
+ }
- foreach (var newFft in newFfts)
- FFTs.Add(newFft);
- FftsProcessed += newFfts.Length;
+ ///
+ /// 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");
- UnprocessedData.RemoveRange(0, newFftCount * Settings.StepSize);
- PadOrTrimForFixedWidth();
+ var fftsMel = new List();
+ foreach (var fft in FFTs)
+ fftsMel.Add(FftSharp.Mel.Scale(fft, (int)SampleRate, melBinCount));
- return newFfts;
- }
+ return fftsMel;
+ }
- ///
- /// 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");
+ ///
+ /// 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)}().");
- var fftsMel = new List();
- foreach (var fft in FFTs)
- fftsMel.Add(FftSharp.Transform.MelScale(fft, SampleRate, melBinCount));
+ return Image.GetBitmap(FFTs, Colormap, intensity, dB, dBScale, roll, NextColumnIndex, rotate);
+ }
- return fftsMel;
- }
+ ///
+ /// 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.
+ /// 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);
+ }
- ///
- /// 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, Colormap, 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), 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);
- }
+ 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.");
- ///
- /// 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)
+ SKEncodedImageFormat fmt = extension.ToLower() switch
{
- 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);
- }
+ ".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;
+ }
- ///
- /// Export spectrogram data using the Spectrogram File Format (SFF)
- ///
- public void SaveData(string filePath, int melBinCount = 0)
+ ///
+ /// 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++)
{
- if (!filePath.EndsWith(".sff", StringComparison.OrdinalIgnoreCase))
- filePath += ".sff";
- new SFF(this, melBinCount).Save(filePath);
+ 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);
}
}
diff --git a/src/Spectrogram/Tools.cs b/src/Spectrogram/Tools.cs
deleted file mode 100644
index 2186644..0000000
--- a/src/Spectrogram/Tools.cs
+++ /dev/null
@@ -1,91 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-
-namespace Spectrogram
-{
- public static class Tools
- {
- ///
- /// 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;
- }
-
- ///
- /// 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;
- }
-
- 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