diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
new file mode 100644
index 0000000..8a2d2bd
--- /dev/null
+++ b/.github/workflows/ci.yaml
@@ -0,0 +1,40 @@
+name: CI/CD
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - main
+ pull_request:
+ types: [opened, synchronize, reopened]
+ release:
+ types:
+ - created
+
+jobs:
+ build:
+ name: Build, Test, and Deploy
+ runs-on: windows-latest
+ steps:
+ - name: 🛒 Checkout
+ uses: actions/checkout@v4
+ - name: ✨ Setup .NET 8
+ uses: actions/setup-dotnet@v4
+ with:
+ 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 --configuration Release
+ - name: 📦 Pack
+ run: dotnet pack src --configuration Release
+ - name: 🔑 Configure Secrets
+ if: github.event_name == 'release'
+ uses: nuget/setup-nuget@v1
+ with:
+ nuget-api-key: ${{ secrets.NUGET_API_KEY }}
+ - name: 🚀 Deploy NuGet Package
+ if: github.event_name == 'release'
+ run: nuget push "src\Spectrogram\bin\Release\*.nupkg" -SkipDuplicate -Source https://api.nuget.org/v3/index.json
diff --git a/.gitignore b/.gitignore
index 3898b62..a2796a9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
+.vscode
+
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
diff --git a/README.md b/README.md
index 884a04c..aee7e88 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,13 @@
# Spectrogram
-[](https://dev.azure.com/swharden/swharden/_build/latest?definitionId=9&branchName=master)
+[](https://github.com/swharden/Spectrogram/actions/workflows/ci.yaml)
[](https://www.nuget.org/packages/Spectrogram/)
-**Spectrogram** is a .NET library for creating spectrograms from pre-recorded signals or live audio from the sound card. Spectrogram uses FFT algorithms and window functions provided by the [FftSharp](https://github.com/swharden/FftSharp) project, and it targets .NET Standard 2.0 so it can be used in .NET Framework and .NET Core projects.
+**Spectrogram** is a .NET library for creating spectrograms from pre-recorded signals or live audio from the sound card. Spectrogram uses FFT algorithms and window functions provided by the [FftSharp](https://github.com/swharden/FftSharp) project, and it targets .NET Standard so it can be used in .NET Framework and .NET Core projects.
-
+
_"I'm sorry Dave... I'm afraid I can't do that"_
@@ -16,45 +16,44 @@ _"I'm sorry Dave... I'm afraid I can't do that"_
## Quickstart
-_Spectrogram is [available on NuGet](https://www.nuget.org/packages/Spectrogram)_
+* This code generates the spectrogram above.
-```cs
-(int sampleRate, double[] audio) = WavFile.ReadMono("hal.wav");
+* Source code for the WAV reading method is at the bottom of this page.
-var spec = new Spectrogram(sampleRate, fftSize: 4096, stepSize: 500, maxFreq: 3000);
-spec.Add(audio);
-spec.SaveImage("hal.png", intensity: .4);
+```cs
+(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");
```
-This code generates the image displayed at the top of this page.
## Windows Forms
If you're using Spectrogram in a graphical application you may find it helpful to retrieve the output as a Bitmap which can be displayed on a Picturebox:
```cs
-Bitmap bmp = spec.GetBitmap();
-pictureBox1.Image = bmp;
+pictureBox1.Image = sg.GetBitmap();
```
I find it helpful to put the Picturebox inside a Panel with auto-scroll enabled, so large spectrograms which are bigger than the size of the window can be interactively displayed.
## Real-Time Spectrogram
-An example program is included in this repository which demonstrates how to use [NAudio](https://github.com/naudio/NAudio) to get samples from the sound card and display them as a spectrogram. Spectrogram was designed to be able to display spectrograms with live or growing data, so this is exceptionally easy to implement.
+An example program is included in this repository which demonstrates how to use NAudio to get samples from the sound card and display them as a spectrogram. Spectrogram was designed to be able to display spectrograms with live or growing data, so this is exceptionally easy to implement.
-**Run this demo: [Spectrogram.MicrophoneDemo.exe](dev/SpectrogramDemo.zip)** 👈 64-bit Windows Application
+* **Click-to-run demo** for 64-bit Windows: [SpectrogramDemo.exe](dev/SpectrogramDemo.zip)

To do this, keep your Spectrogram at the class level:
```cs
-Spectrogram spec;
+SpectrogramGenerator sg;
public Form1()
{
InitializeComponent();
- spec = new Spectrogram(sampleRate, fftSize: 4096, stepSize: 500, maxFreq: 3000);
+ sg = new SpectrogramGenerator(sampleRate, fftSize: 4096, stepSize: 500, maxFreq: 3000);
}
```
@@ -62,14 +61,14 @@ Whenever an audio buffer gets filled, add the data to your Spectrogram:
```cs
private void GotNewBuffer(double[] audio)
{
- spec.Add(audio);
+ sg.Add(audio);
}
```
Then set up a timer to trigger rendering:
```cs
private void timer1_Tick(object sender, EventArgs e){
- Bitmap bmp = spec.GetBitmap(intensity: .4);
+ Bitmap bmp = sg.GetBitmap(intensity: .4);
pictureBox1.Image?.Dispose();
pictureBox1.Image = bmp;
}
@@ -82,16 +81,20 @@ 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
-(int sampleRate, double[] audio) = WavFile.ReadMono("Handel.wav");
+(double[] audio, int sampleRate) = ReadMono("song.wav");
+
+int fftSize = 16384;
+int targetWidthPx = 3000;
+int stepSize = audio.Length / targetWidthPx;
-var spec = new Spectrogram(sampleRate, fftSize: 16384, stepSize: 2500, maxFreq: 2200);
-spec.Add(audio);
-spec.SaveImage("spectrogram-song.jpg", intensity: 5, dB: true);
+var sg = new SpectrogramGenerator(sampleRate, fftSize, stepSize, maxFreq: 2200);
+sg.Add(audio);
+sg.SaveImage("song.png", intensity: 5, dB: true);
```
Notice the optional conversion to Decibels while saving the image.
-
+
If you [listen to the audio track](https://www.youtube.com/watch?v=Mza-xqk770k) while closely inspecting the spectrogram you can identify individual piano notes and chords, and may be surprised by the interesting patterns that emerge around trills and glissandos.
@@ -100,7 +103,7 @@ If you [listen to the audio track](https://www.youtube.com/watch?v=Mza-xqk770k)
The Spectrogram's `ToString()` method displays detailed information about the spectrogram:
```cs
-Console.WriteLine(spec);
+Console.WriteLine(sg);
```
```
@@ -114,12 +117,11 @@ 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
-(int sampleRate, double[] audio) = WavFile.ReadMono("hal.wav");
-int fftSize = 8192;
-var spec = new Spectrogram(sampleRate, fftSize, stepSize: 200, maxFreq: 3000);
-spec.Add(audio);
-spec.SetColormap(Colormap.Jet);
-spec.SaveImage($"hal-Jet.png", intensity: .5);
+(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);
+sg.SaveImage($"jet.png");
```
Viridis | Greens | Blues | Grayscale | GrayscaleR
@@ -132,75 +134,42 @@ Analytical spectrograms aimed at achieving maximum frequency resolution are pres
**To visualize frequency in a way that mimics human perception** we create a spectrogram that represents lower frequencies using a large portion of the image, and condense higher frequency ranges into smaller rows of pixels toward the top of the image. The [Mel Scale](https://en.wikipedia.org/wiki/Mel_scale) is commonly used to represent power spectral density this way, and the resulting _Mel Spectrogram_ has greatly reduced vertical resolution but is a better representation of human frequency perception.
-Cropped Linear Scale (0-1kHz) | Full Linear Scale (0-22 kHz) | Mel Scale (0-22 kHz)
----|---|---
-||
+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
-// Load "I'm sorry dave, I'm afraid I can't do that" audio
-(int sampleRate, double[] audio) = WavFile.ReadMono("hal.wav");
+(double[] audio, int sampleRate) = ReadMono("hal.wav");
+var sg = new SpectrogramGenerator(sampleRate, fftSize: 4096, stepSize: 500, maxFreq: 3000);
+sg.Add(audio);
-// Create a traditional (linear) Spectrogram with dB units
-var spec = new Spectrogram(sampleRate, fftSize: 4096, stepSize: 500, maxFreq: 3000);
-spec.Add(audio);
-spec.SaveImage("hal.png", intensity: 4, dB: true);
+// Create a traditional (linear) Spectrogram
+sg.SaveImage("hal.png");
-// Create a Mel Spectrogram with dB units
-Bitmap bmp = spec.GetBitmapMel(melSizePoints: 250, intensity: 4, dB: true);
+// Create a Mel Spectrogram
+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).
+## Read Data from an Audio File
-### 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.
+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
-(int sampleRate, double[] audio) = WavFile.ReadMono("hal.wav");
-int fftSize = 1 << 12;
-var spec = new Spectrogram(sampleRate, fftSize, stepSize: 700, maxFreq: 2000);
-spec.Add(audio);
-spec.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
\ No newline at end of file
+(double[] audio, int sampleRate) ReadMono(string filePath, double multiplier = 16_000)
+{
+ using var afr = new NAudio.Wave.AudioFileReader(filePath);
+ int sampleRate = afr.WaveFormat.SampleRate;
+ int bytesPerSample = afr.WaveFormat.BitsPerSample / 8;
+ int sampleCount = (int)(afr.Length / bytesPerSample);
+ int channelCount = afr.WaveFormat.Channels;
+ var audio = new List
(sampleCount);
+ var buffer = new float[sampleRate * channelCount];
+ int samplesRead = 0;
+ while ((samplesRead = afr.Read(buffer, 0, buffer.Length)) > 0)
+ audio.AddRange(buffer.Take(samplesRead).Select(x => x * multiplier));
+ return (audio.ToArray(), sampleRate);
+}
+```
\ No newline at end of file
diff --git a/dev/build/azure-pipelines.yml b/dev/build/azure-pipelines.yml
deleted file mode 100644
index 9477383..0000000
--- a/dev/build/azure-pipelines.yml
+++ /dev/null
@@ -1,68 +0,0 @@
-trigger:
-- master
-
-strategy:
- matrix:
-
- 'Build and Test on MacOS':
- purpose: 'library'
- imageName: 'macOS-latest'
-
- 'Build and Test on Linux':
- purpose: 'library'
- imageName: 'ubuntu-latest'
-
- 'Build and Test on Windows':
- purpose: 'library'
- imageName: 'windows-latest'
-
- 'Rebuild Solution on Windows':
- purpose: 'solution'
- imageName: 'windows-latest'
-
-pool:
- vmImage: $(imageName)
-
-steps:
-
-### INSTALL NUGET AND RESTORE PACKAGES
-
-- task: NuGetToolInstaller@1
- displayName: 'Install NuGet'
-
-- task: NuGetCommand@2
- displayName: 'Restore packages'
- inputs:
- restoreSolution: 'src/Spectrogram.sln'
-
-### BUILD THE CORE LIBRARY AND RUN TESTS
-
-- task: DotNetCoreCLI@2
- displayName: 'Build Spectrogram'
- condition: eq(variables['purpose'], 'library')
- inputs:
- command: 'build'
- projects: 'src/Spectrogram/Spectrogram.csproj'
-
-- task: DotNetCoreCLI@2
- displayName: 'Build Tests'
- condition: eq(variables['purpose'], 'library')
- inputs:
- command: 'build'
- projects: 'src/Spectrogram.Tests/Spectrogram.Tests.csproj'
-
-- task: DotNetCoreCLI@2
- displayName: 'Run Tests'
- condition: eq(variables['purpose'], 'library')
- inputs:
- command: test
- projects: 'src/Spectrogram.Tests/Spectrogram.Tests.csproj'
-
-### REBUILD FULL SOLUTION
-
-- task: VSBuild@1
- displayName: 'Build Release (Windows)'
- condition: eq(variables['purpose'], 'solution')
- inputs:
- solution: 'src/Spectrogram.sln'
- configuration: 'release'
\ No newline at end of file
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/dev/graphics/hal-Argo.png b/dev/graphics/hal-Argo.png
index 6f1fc13..6ebfaa9 100644
Binary files a/dev/graphics/hal-Argo.png and b/dev/graphics/hal-Argo.png differ
diff --git a/dev/graphics/hal-Blues.png b/dev/graphics/hal-Blues.png
index 4164b49..af9c929 100644
Binary files a/dev/graphics/hal-Blues.png and b/dev/graphics/hal-Blues.png differ
diff --git a/dev/graphics/hal-Grayscale.png b/dev/graphics/hal-Grayscale.png
index 9577e3a..63927e6 100644
Binary files a/dev/graphics/hal-Grayscale.png and b/dev/graphics/hal-Grayscale.png differ
diff --git a/dev/graphics/hal-GrayscaleR.png b/dev/graphics/hal-GrayscaleR.png
index 066c894..0770ffe 100644
Binary files a/dev/graphics/hal-GrayscaleR.png and b/dev/graphics/hal-GrayscaleR.png differ
diff --git a/dev/graphics/hal-Greens.png b/dev/graphics/hal-Greens.png
index 5baa6ae..a15a81d 100644
Binary files a/dev/graphics/hal-Greens.png and b/dev/graphics/hal-Greens.png differ
diff --git a/dev/graphics/hal-Inferno.png b/dev/graphics/hal-Inferno.png
index fec7ec7..643ba8b 100644
Binary files a/dev/graphics/hal-Inferno.png and b/dev/graphics/hal-Inferno.png differ
diff --git a/dev/graphics/hal-Lopora.png b/dev/graphics/hal-Lopora.png
index 95b5255..d79fd74 100644
Binary files a/dev/graphics/hal-Lopora.png and b/dev/graphics/hal-Lopora.png differ
diff --git a/dev/graphics/hal-Magma.png b/dev/graphics/hal-Magma.png
index c66f679..8dad373 100644
Binary files a/dev/graphics/hal-Magma.png and b/dev/graphics/hal-Magma.png differ
diff --git a/dev/graphics/hal-Plasma.png b/dev/graphics/hal-Plasma.png
index ffe0096..8947860 100644
Binary files a/dev/graphics/hal-Plasma.png and b/dev/graphics/hal-Plasma.png differ
diff --git a/dev/graphics/hal-Turbo.png b/dev/graphics/hal-Turbo.png
index 2293296..3382989 100644
Binary files a/dev/graphics/hal-Turbo.png and b/dev/graphics/hal-Turbo.png differ
diff --git a/dev/graphics/hal-Viridis.png b/dev/graphics/hal-Viridis.png
index 82b06a5..ef34eb3 100644
Binary files a/dev/graphics/hal-Viridis.png and b/dev/graphics/hal-Viridis.png differ
diff --git a/dev/graphics/hal-spectrogram.png b/dev/graphics/hal-spectrogram.png
new file mode 100644
index 0000000..fbdb2d8
Binary files /dev/null and b/dev/graphics/hal-spectrogram.png differ
diff --git a/dev/graphics/hal.png b/dev/graphics/hal.png
index 7b97577..80dc981 100644
Binary files a/dev/graphics/hal.png and b/dev/graphics/hal.png differ
diff --git a/dev/graphics/halMel-LinearCropped.png b/dev/graphics/halMel-LinearCropped.png
new file mode 100644
index 0000000..9bf9e47
Binary files /dev/null and b/dev/graphics/halMel-LinearCropped.png differ
diff --git a/dev/graphics/halMel-MelScale.png b/dev/graphics/halMel-MelScale.png
new file mode 100644
index 0000000..432a64c
Binary files /dev/null and b/dev/graphics/halMel-MelScale.png differ
diff --git a/dev/graphics/halMel.png b/dev/graphics/halMel.png
deleted file mode 100644
index 786c8a3..0000000
Binary files a/dev/graphics/halMel.png and /dev/null differ
diff --git a/dev/graphics/halMelLinearCropped.png b/dev/graphics/halMelLinearCropped.png
deleted file mode 100644
index 60a4224..0000000
Binary files a/dev/graphics/halMelLinearCropped.png and /dev/null differ
diff --git a/dev/graphics/halMelLinearFull.png b/dev/graphics/halMelLinearFull.png
deleted file mode 100644
index e0d50ec..0000000
Binary files a/dev/graphics/halMelLinearFull.png and /dev/null differ
diff --git a/dev/graphics/spectrogram-song.png b/dev/graphics/spectrogram-song.png
new file mode 100644
index 0000000..9486ae5
Binary files /dev/null and b/dev/graphics/spectrogram-song.png differ
diff --git a/dev/python/readwav.py b/dev/python/readwav.py
new file mode 100644
index 0000000..7515c8d
--- /dev/null
+++ b/dev/python/readwav.py
@@ -0,0 +1,15 @@
+"""
+sample rate: 44100
+values: 166671
+value 12345: 4435
+"""
+from scipy.io import wavfile
+import pathlib
+PATH_HERE = pathlib.Path(__file__).parent
+PATH_DATA = PATH_HERE.joinpath("../../data")
+
+if __name__ == "__main__":
+ for wavFilePath in PATH_DATA.glob("*.wav"):
+ wavFilePath = PATH_DATA.joinpath(wavFilePath)
+ samplerate, data = wavfile.read(wavFilePath)
+ print(f"{wavFilePath.name}, {samplerate}, {len(data)}")
diff --git a/dev/sff/SffViewer/SffViewer.csproj b/dev/sff/SffViewer/SffViewer.csproj
index 1e9e478..58946c2 100644
--- a/dev/sff/SffViewer/SffViewer.csproj
+++ b/dev/sff/SffViewer/SffViewer.csproj
@@ -1,93 +1,16 @@
-
-
-
+
- Debug
- AnyCPU
- {9478208D-60C7-4F6A-B2E4-6325D38139DA}
+ net5.0-windows
WinExe
- SffViewer
- SffViewer
- v4.7.2
- 512
- true
- true
+ false
+ true
+ true
-
- AnyCPU
- true
- full
- false
- bin\Debug\
- DEBUG;TRACE
- prompt
- 4
-
-
- AnyCPU
- pdbonly
- true
- bin\Release\
- TRACE
- prompt
- 4
-
-
-
- ..\..\..\src\packages\FftSharp.1.0.5\lib\netstandard2.0\FftSharp.dll
-
-
- ..\..\..\src\packages\Spectrogram.1.2.3\lib\netstandard2.0\Spectrogram.dll
-
-
-
-
- ..\..\..\src\packages\System.Drawing.Common.4.7.0\lib\net461\System.Drawing.Common.dll
-
-
-
-
-
-
-
-
-
-
-
-
-
- Form
-
-
- Form1.cs
-
-
-
-
- Form1.cs
-
-
- ResXFileCodeGenerator
- Resources.Designer.cs
- Designer
-
-
- True
- Resources.resx
-
-
-
- SettingsSingleFileGenerator
- Settings.Designer.cs
-
-
- True
- Settings.settings
- True
-
-
-
+
+
+
+
+
-
\ No newline at end of file
diff --git a/dev/sff/SffViewer/packages.config b/dev/sff/SffViewer/packages.config
index 7d09d04..ed03785 100644
--- a/dev/sff/SffViewer/packages.config
+++ b/dev/sff/SffViewer/packages.config
@@ -1,6 +1,6 @@
-
-
-
+
+
+
\ No newline at end of file
diff --git a/dev/sff/hal.sff b/dev/sff/hal.sff
index c2d4f03..85557c2 100644
Binary files a/dev/sff/hal.sff and b/dev/sff/hal.sff differ
diff --git a/dev/sff/halMel.png b/dev/sff/halMel.png
index 627c388..5bed878 100644
Binary files a/dev/sff/halMel.png and b/dev/sff/halMel.png differ
diff --git a/dev/sff/halMel.sff b/dev/sff/halMel.sff
index 6cc01d5..a23b726 100644
Binary files a/dev/sff/halMel.sff and b/dev/sff/halMel.sff differ
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 9fe0ce8..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
{
@@ -48,7 +49,7 @@ private void Form1_Load(object sender, EventArgs e) { }
private void cbDevice_SelectedIndexChanged(object sender, EventArgs e) => StartListening();
private void cbFftSize_SelectedIndexChanged(object sender, EventArgs e) => StartListening();
- private Spectrogram spec;
+ private SpectrogramGenerator spec;
private Listener listener;
private void StartListening()
{
@@ -60,12 +61,11 @@ private void StartListening()
pbSpectrogram.Image = null;
listener?.Dispose();
listener = new Listener(cbDevice.SelectedIndex, sampleRate);
- spec = new Spectrogram(sampleRate, fftSize, stepSize);
- //spec.SetWindow(FftSharp.Window.Rectangular(fftSize));
+ spec = new SpectrogramGenerator(sampleRate, fftSize, stepSize);
pbSpectrogram.Height = spec.Height;
pbScaleVert.Image?.Dispose();
- pbScaleVert.Image = spec.GetVerticalScale(pbScaleVert.Width);
+ pbScaleVert.Image = spec.GetVerticalScale(pbScaleVert.Width).ToBitmap();
pbScaleVert.Height = spec.Height;
}
@@ -82,11 +82,11 @@ private void timer1_Tick(object sender, EventArgs e)
spec.Process();
spec.SetFixedWidth(pbSpectrogram.Width);
Bitmap bmpSpec = new Bitmap(spec.Width, spec.Height, System.Drawing.Imaging.PixelFormat.Format32bppPArgb);
- using (var bmpSpecIndexed = spec.GetBitmap(multiplier, cbDecibels.Checked, cbRoll.Checked))
+ using (var bmpSpecIndexed = spec.GetBitmap(multiplier, cbDecibels.Checked, roll: cbRoll.Checked))
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);
@@ -106,7 +106,7 @@ private void timer1_Tick(object sender, EventArgs e)
private void cbColormap_SelectedIndexChanged(object sender, EventArgs e)
{
- spec.SetColormap(cmaps[cbColormap.SelectedIndex]);
+ spec.Colormap = cmaps[cbColormap.SelectedIndex];
}
private void btnResetRoll_Click(object sender, EventArgs e)
diff --git a/src/Spectrogram.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
new file mode 100644
index 0000000..e58c01a
--- /dev/null
+++ b/src/Spectrogram.MicrophoneDemo/Spectrogram.Demo.csproj
@@ -0,0 +1,15 @@
+
+
+ net8.0-windows
+ WinExe
+ true
+ true
+ NU1701
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Spectrogram.MicrophoneDemo/Spectrogram.MicrophoneDemo.csproj b/src/Spectrogram.MicrophoneDemo/Spectrogram.MicrophoneDemo.csproj
deleted file mode 100644
index 2b9c9f6..0000000
--- a/src/Spectrogram.MicrophoneDemo/Spectrogram.MicrophoneDemo.csproj
+++ /dev/null
@@ -1,100 +0,0 @@
-
-
-
-
- Debug
- AnyCPU
- {D51ABC6A-53F4-4620-88A1-14EA1D779538}
- WinExe
- Spectrogram.MicrophoneDemo
- Spectrogram.MicrophoneDemo
- v4.7.2
- 512
- true
- true
-
-
- AnyCPU
- true
- full
- false
- bin\Debug\
- DEBUG;TRACE
- prompt
- 4
-
-
- AnyCPU
- pdbonly
- true
- bin\Release\
- TRACE
- prompt
- 4
-
-
-
- ..\packages\FftSharp.1.0.5\lib\netstandard2.0\FftSharp.dll
-
-
- ..\packages\NAudio.1.10.0\lib\net35\NAudio.dll
-
-
-
-
- ..\packages\System.Drawing.Common.4.7.0\lib\net461\System.Drawing.Common.dll
-
-
-
-
-
-
-
-
-
-
-
-
-
- Form
-
-
- FormMicrophone.cs
-
-
-
-
-
- FormMicrophone.cs
-
-
- ResXFileCodeGenerator
- Resources.Designer.cs
- Designer
-
-
- True
- Resources.resx
-
-
-
- SettingsSingleFileGenerator
- Settings.Designer.cs
-
-
- True
- Settings.settings
- True
-
-
-
-
-
-
-
- {6ff83edd-e18a-4edd-8d53-d2281515ac47}
- Spectrogram
-
-
-
-
\ No newline at end of file
diff --git a/src/Spectrogram.MicrophoneDemo/packages.config b/src/Spectrogram.MicrophoneDemo/packages.config
deleted file mode 100644
index 6c64d76..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/AudioFile.cs b/src/Spectrogram.Tests/AudioFile.cs
new file mode 100644
index 0000000..99875c6
--- /dev/null
+++ b/src/Spectrogram.Tests/AudioFile.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Spectrogram.Tests
+{
+ public static class AudioFile
+ {
+ ///
+ /// Use NAudio to read the contents of a WAV file.
+ ///
+ public static (double[] audio, int sampleRate) ReadWAV(string filePath, double multiplier = 16_000)
+ {
+ using var afr = new NAudio.Wave.AudioFileReader(filePath);
+ int sampleRate = afr.WaveFormat.SampleRate;
+ int bytesPerSample = afr.WaveFormat.BitsPerSample / 8;
+ int sampleCount = (int)afr.Length / bytesPerSample;
+ int channelCount = afr.WaveFormat.Channels;
+ var audio = new List(sampleCount);
+ var buffer = new float[sampleRate * channelCount];
+ int samplesRead = 0;
+ while ((samplesRead = afr.Read(buffer, 0, buffer.Length)) > 0)
+ audio.AddRange(buffer.Take(samplesRead).Select(x => x * multiplier));
+ return (audio.ToArray(), sampleRate);
+ }
+
+ ///
+ /// Use MP3Sharp to read the contents of an MP3 file.
+ ///
+ public static double[] ReadMP3(string filePath, int bufferSize = 4096)
+ {
+ List audio = new List();
+ MP3Sharp.MP3Stream stream = new MP3Sharp.MP3Stream(filePath);
+ byte[] buffer = new byte[bufferSize];
+ int bytesReturned = 1;
+ while (bytesReturned > 0)
+ {
+ bytesReturned = stream.Read(buffer, 0, bufferSize);
+ for (int i = 0; i < bytesReturned / 2 - 1; i += 2)
+ audio.Add(BitConverter.ToInt16(buffer, i * 2));
+ }
+ stream.Close();
+ return audio.ToArray();
+ }
+ }
+}
diff --git a/src/Spectrogram.Tests/AudioFileTests.cs b/src/Spectrogram.Tests/AudioFileTests.cs
new file mode 100644
index 0000000..ef99c25
--- /dev/null
+++ b/src/Spectrogram.Tests/AudioFileTests.cs
@@ -0,0 +1,25 @@
+using FluentAssertions;
+using NUnit.Framework;
+
+namespace Spectrogram.Tests
+{
+ class AudioFileTests
+ {
+ ///
+ /// Compare values read from the WAV reader against those read by Python's SciPy module (see script in /dev folder)
+ ///
+ [TestCase("cant-do-that-44100.wav", 44_100, 166_671, 1)]
+ [TestCase("03-02-03-01-02-01-19.wav", 48_000, 214_615, 1)]
+ [TestCase("qrss-10min.wav", 6_000, 3_600_000, 1)]
+ [TestCase("cant-do-that-11025-stereo.wav", 11_025, 41668, 2)]
+ [TestCase("asehgal-original.wav", 40_000, 1_600_000, 1)]
+ public void Test_AudioFile_LengthAndSampleRate(string filename, int knownRate, int knownLength, int channels)
+ {
+ string filePath = $"../../../../../data/{filename}";
+ (double[] audio, int sampleRate) = AudioFile.ReadWAV(filePath);
+
+ 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 1760d9b..0000000
--- a/src/Spectrogram.Tests/ColormapExamples.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-using NuGet.Frameworks;
-using NUnit.Framework;
-using Spectrogram.Colormaps;
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Drawing;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Spectrogram.Tests
-{
- class ColormapExamples
- {
- [Test]
- public void Test_Make_CommonColormaps()
- {
- (int sampleRate, double[] audio) = WavFile.ReadMono("../../../../../data/cant-do-that-44100.wav");
- int fftSize = 1 << 12;
- var spec = new Spectrogram(sampleRate, fftSize, stepSize: 700, maxFreq: 2000);
- spec.SetWindow(FftSharp.Window.Hanning(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.SetColormap(cmap);
- spec.SaveImage($"../../../../../dev/graphics/hal-{cmap.Name}.png", intensity: .5);
- 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 f4e23bb..0000000
--- a/src/Spectrogram.Tests/FileFormat.cs
+++ /dev/null
@@ -1,95 +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()
- {
- (int sampleRate, double[] audio) = WavFile.ReadMono("../../../../../data/cant-do-that-44100.wav");
- int fftSize = 1 << 12;
- var spec = new Spectrogram(sampleRate, fftSize, stepSize: 700, maxFreq: 2000);
- spec.SetWindow(FftSharp.Window.Hanning(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()
- {
- (int sampleRate, double[] audio) = WavFile.ReadMono("../../../../../data/cant-do-that-44100.wav");
- int fftSize = 1 << 12;
- var spec = new Spectrogram(sampleRate, fftSize, stepSize: 700);
- spec.SetWindow(FftSharp.Window.Hanning(fftSize / 3)); // sharper window than typical
- spec.Add(audio);
-
- Bitmap bmp = spec.GetBitmapMel(250, 3, true);
- 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
- (int sampleRate, double[] audio) = WavFile.ReadMono("../../../../../data/03-02-03-01-02-01-19.wav");
- Assert.AreEqual(48000, sampleRate);
-
- // save the SFF
- int fftSize = 1 << 12;
- var spec = new Spectrogram(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
-
- (int sampleRate, double[] audio) = WavFile.ReadMono("../../../../../data/03-02-03-01-02-01-19.wav");
- Assert.AreEqual(48000, sampleRate);
-
- int fftSize = 1 << 12;
- var spec = new Spectrogram(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 4c7eba0..be6b6f3 100644
--- a/src/Spectrogram.Tests/Mel.cs
+++ b/src/Spectrogram.Tests/Mel.cs
@@ -1,25 +1,51 @@
using NUnit.Framework;
using System;
-using System.Collections.Generic;
-using System.Drawing;
-using System.Drawing.Imaging;
-using System.Text;
+using SkiaSharp;
namespace Spectrogram.Tests
{
class Mel
{
[Test]
- public void Test_Mel_Spectrogram()
+ public void Test_MelSpectrogram_MelScale()
{
- (int sampleRate, double[] audio) = WavFile.ReadMono("../../../../../data/cant-do-that-44100.wav");
+ (double[] audio, int sampleRate) = AudioFile.ReadWAV("../../../../../data/cant-do-that-44100.wav");
int fftSize = 4096;
- var spec = new Spectrogram(sampleRate, fftSize, stepSize: 500);
- spec.Add(audio);
- spec.SaveImage("halNotMel.png", 4, true);
-
- Bitmap bmp = spec.GetBitmapMel(250, 4, true);
- bmp.Save("../../../../../dev/graphics/halMel.png", ImageFormat.Png);
+ var sg = new SpectrogramGenerator(sampleRate, fftSize, stepSize: 500);
+ sg.Add(audio);
+
+ // 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);
+
+ // Disegna bmpRaw su bmpCropped usando SKCanvas
+ using (var canvas = new SKCanvas(bmpCropped))
+ {
+ 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);
+ }
+ }
}
[Test]
@@ -30,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.PlotScatter(freq, power, markerSize: 0);
+ var plt1 = new ScottPlot.Plot();
+ plt1.Add.ScatterLine(freq, power);
int filterSize = 25;
@@ -57,10 +83,9 @@ public void Test_Mel_Graph()
double freqCenter = binStartFreqs[binIndex + 1];
double freqHigh = binStartFreqs[binIndex + 2];
- var sctr = plt1.PlotScatter(
- 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);
@@ -77,18 +102,18 @@ public void Test_Mel_Graph()
binValue += power[indexLow + i] * frac;
}
binValue /= binScaleSum;
- plt1.PlotPoint(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]
public void Test_SaveEmpty_Throws()
{
- (int sampleRate, double[] audio) = WavFile.ReadMono("../../../../../data/cant-do-that-44100.wav");
+ (double[] audio, int sampleRate) = AudioFile.ReadWAV("../../../../../data/cant-do-that-44100.wav");
int fftSize = 4096;
- var spec = new Spectrogram(sampleRate, fftSize, stepSize: 500);
+ var spec = new SpectrogramGenerator(sampleRate, fftSize, stepSize: 500);
//spec.Add(audio);
Assert.Throws(() => { spec.SaveImage("empty.png"); });
}
diff --git a/src/Spectrogram.Tests/Mp3.cs b/src/Spectrogram.Tests/Mp3.cs
deleted file mode 100644
index d20a707..0000000
--- a/src/Spectrogram.Tests/Mp3.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Text;
-
-namespace Spectrogram.Tests
-{
- public static class Mp3
- {
- public static double[] Read(string filePath)
- {
- List audio = new List();
-
- MP3Sharp.MP3Stream stream = new MP3Sharp.MP3Stream(filePath);
- int bufferSize = 4096;
- byte[] buffer = new byte[bufferSize];
- int bytesReturned = 1;
- while (bytesReturned > 0)
- {
- bytesReturned = stream.Read(buffer, 0, buffer.Length);
- for (int i = 0; i < bytesReturned / 2 - 1; i += 2) // TODO: -1 needed? better algo?
- audio.Add(BitConverter.ToInt16(buffer, i * 2));
- }
- stream.Close();
-
- return audio.ToArray();
- }
-
- public static double[] Div8(double[] input)
- {
- double[] output = new double[input.Length / 8];
- for (int i = 0; i < output.Length; i++)
- output[i] = input[i * 8];
- return output;
- }
- }
-}
diff --git a/src/Spectrogram.Tests/Quickstart.cs b/src/Spectrogram.Tests/Quickstart.cs
index 022a9a2..ec98436 100644
--- a/src/Spectrogram.Tests/Quickstart.cs
+++ b/src/Spectrogram.Tests/Quickstart.cs
@@ -1,7 +1,7 @@
using NUnit.Framework;
using System;
-using System.Drawing;
-using System.Drawing.Imaging;
+using System.Collections.Generic;
+using System.Linq;
namespace Spectrogram.Tests
{
@@ -10,30 +10,41 @@ public class Quickstart
[Test]
public void Test_Quickstart_Hal()
{
- (int sampleRate, double[] audio) = WavFile.ReadMono("../../../../../data/cant-do-that-44100.wav");
- int fftSize = 4096;
- var spec = new Spectrogram(sampleRate, fftSize, stepSize: 500, maxFreq: 3000);
- spec.Add(audio);
- spec.SaveImage("../../../../../dev/graphics/hal.png", intensity: .2);
+ (double[] audio, int sampleRate) = AudioFile.ReadWAV("../../../../../data/cant-do-that-44100.wav");
+ var sg = new SpectrogramGenerator(sampleRate, fftSize: 4096, stepSize: 500, maxFreq: 3000);
+ sg.Add(audio);
+ sg.SaveImage("../../../../../dev/graphics/hal.png");
+ Console.WriteLine(sg);
+ }
+
+ [Test]
+ public void Test_Readme_HeaderImage()
+ {
+ (double[] audio, int sampleRate) = AudioFile.ReadWAV("../../../../../data/cant-do-that-44100.wav");
+ int fftSize = 2048;
+ var spec = new SpectrogramGenerator(sampleRate, fftSize, stepSize: 400, maxFreq: 6000);
+ spec.Add(audio);
+ spec.SaveImage("../../../../../dev/graphics/hal-spectrogram.png", intensity: 10, dB: true, dBScale: .05);
+
Console.WriteLine(spec);
}
[Test]
public void Test_Quickstart_Handel()
{
- double[] audio = Mp3.Read("../../../../../data/Handel - Air and Variations.mp3");
+ double[] audio = AudioFile.ReadMP3("../../../../../data/Handel - Air and Variations.mp3");
int sampleRate = 44100;
int fftSize = 16384;
int targetWidthPx = 3000;
int stepSize = audio.Length / targetWidthPx;
- var spec = new Spectrogram(sampleRate, fftSize, stepSize, maxFreq: 2200);
- spec.Add(audio);
- spec.SaveImage("../../../../../dev/spectrogram-song.jpg", intensity: 5, dB: true);
+ var sg = new SpectrogramGenerator(sampleRate, fftSize, stepSize, maxFreq: 2200);
+ sg.Add(audio);
+ sg.SaveImage("../../../../../dev/graphics/spectrogram-song.png", intensity: 5, dB: true);
- Console.WriteLine(spec);
+ Console.WriteLine(sg);
/*
Spectrogram (2993, 817)
Vertical (817 px): 0 - 2,199 Hz, FFT size: 16,384 samples, 2.69 Hz/px
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 e8650aa..83fa10d 100644
--- a/src/Spectrogram.Tests/Spectrogram.Tests.csproj
+++ b/src/Spectrogram.Tests/Spectrogram.Tests.csproj
@@ -1,22 +1,22 @@
-
+
-
- netcoreapp3.1
+
+ net8.0
+ false
+
- false
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
diff --git a/src/Spectrogram.Tests/TestAGC.cs b/src/Spectrogram.Tests/TestAGC.cs
index f2e2bad..70cb9e8 100644
--- a/src/Spectrogram.Tests/TestAGC.cs
+++ b/src/Spectrogram.Tests/TestAGC.cs
@@ -12,11 +12,11 @@ class TestAGC
public void Test_AGC_off()
{
string wavFilePath = "../../../../../data/qrss-10min.wav";
- (int sampleRate, double[] L) = WavFile.ReadMono(wavFilePath);
+ (double[] audio, int sampleRate) = AudioFile.ReadWAV(wavFilePath);
int fftSize = 8192;
- var spec = new Spectrogram(sampleRate, fftSize, stepSize: 2000, maxFreq: 3000);
- spec.Add(L);
+ var spec = new SpectrogramGenerator(sampleRate, fftSize, stepSize: 2000, maxFreq: 3000);
+ spec.Add(audio);
spec.SaveImage("qrss-agc-off.png", intensity: 3);
}
@@ -26,11 +26,11 @@ public void Test_AGC_normToNoiseFloor()
// strategy here is to normalize to the magnitude of the quietest 20% of frequencies
string wavFilePath = "../../../../../data/qrss-10min.wav";
- (int sampleRate, double[] L) = WavFile.ReadMono(wavFilePath);
+ (double[] audio, int sampleRate) = AudioFile.ReadWAV(wavFilePath);
int fftSize = 8192;
- var spec = new Spectrogram(sampleRate, fftSize, stepSize: 2000, maxFreq: 3000);
- spec.Add(L);
+ var spec = new SpectrogramGenerator(sampleRate, fftSize, stepSize: 2000, maxFreq: 3000);
+ spec.Add(audio);
var ffts = spec.GetFFTs();
double normalIntensity = 2;
@@ -61,11 +61,11 @@ public void Test_AGC_normWindow()
// strategy here is to create a weighted moving window mean and normalize to that
string wavFilePath = "../../../../../data/qrss-10min.wav";
- (int sampleRate, double[] L) = WavFile.ReadMono(wavFilePath);
+ (double[] audio, int sampleRate) = AudioFile.ReadWAV(wavFilePath);
int fftSize = 8192;
- var spec = new Spectrogram(sampleRate, fftSize, stepSize: 2000, maxFreq: 3000);
- spec.Add(L);
+ var spec = new SpectrogramGenerator(sampleRate, fftSize, stepSize: 2000, maxFreq: 3000);
+ spec.Add(audio);
var ffts = spec.GetFFTs();
for (int i = 0; i < ffts.Count; i++)
@@ -78,7 +78,8 @@ private double[] SubtractMovingWindow(double[] input, int windowSizePx = 100)
{
// return a copy of the input array with the moving window subtracted
- double[] window = FftSharp.Window.Hanning(windowSizePx);
+ var hanningWindow = new FftSharp.Windows.Hanning();
+ double[] window = hanningWindow.Create(windowSizePx);
double windowSum = window.Sum();
double[] windowed = new double[input.Length];
diff --git a/src/Spectrogram.Tests/Wav.cs b/src/Spectrogram.Tests/Wav.cs
deleted file mode 100644
index 495377d..0000000
--- a/src/Spectrogram.Tests/Wav.cs
+++ /dev/null
@@ -1,57 +0,0 @@
-using NUnit.Framework;
-using ScottPlot;
-using System;
-using System.Collections.Generic;
-using System.Text;
-
-namespace Spectrogram.Tests
-{
- class Wav
- {
- [Test]
- public void Test_WavFile_ReadHal1()
- {
- string wavFilePath = "../../../../../data/cant-do-that-44100.wav";
- (int sampleRate, double[] L) = WavFile.ReadMono(wavFilePath);
- Assert.AreEqual(44100, sampleRate);
-
- double lengthSec = (double)L.Length / sampleRate;
- Assert.AreEqual(3.779, lengthSec, .01);
-
- var plt = new Plot();
- plt.PlotSignal(L);
- plt.SaveFig("hal1.png");
- }
-
- [Test]
- public void Test_WavFile_ReadHal2()
- {
- string wavFilePath = "../../../../../data/cant-do-that-11025-stereo.wav";
- (int sampleRate, double[] L, double[] R) = WavFile.ReadStereo(wavFilePath);
- Assert.AreEqual(11025, sampleRate);
-
- double lengthSec = (double)L.Length / sampleRate;
- Assert.AreEqual(3.779, lengthSec, .01);
-
- var plt = new Plot();
- plt.PlotSignal(L);
- plt.PlotSignal(R);
- plt.SaveFig("hal2.png");
- }
-
- [Test]
- public void Test_WavFile_ReadDoor()
- {
- string wavFilePath = "../../../../../data/03-02-03-01-02-01-19.wav";
- (int sampleRate, double[] L) = WavFile.ReadMono(wavFilePath);
- Assert.AreEqual(48000, sampleRate);
-
- double lengthSec = (double)L.Length / sampleRate;
- Assert.AreEqual(4.471, lengthSec, .5);
-
- var plt = new Plot();
- plt.PlotSignal(L);
- plt.SaveFig("door.png");
- }
- }
-}
diff --git a/src/Spectrogram.Tests/WavUsingLibrary.cs b/src/Spectrogram.Tests/WavUsingLibrary.cs
deleted file mode 100644
index c6f5032..0000000
--- a/src/Spectrogram.Tests/WavUsingLibrary.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-using NAudio.Wave;
-using NUnit.Framework;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-
-namespace Spectrogram.Tests
-{
- class WavUsingLibrary
- {
- [Test]
- public void Test_WavFile_ReadASehgal()
- {
- // READ THE WAV FILE WITH NAUDIO
- double[] audio;
- int sampleRate;
- string wavFilePath = "../../../../../data/asehgal-original.wav";
-
- using (var audioFileReader = new AudioFileReader(wavFilePath))
- {
- sampleRate = audioFileReader.WaveFormat.SampleRate;
- var wholeFile = new List((int)(audioFileReader.Length / 4));
- var readBuffer = new float[audioFileReader.WaveFormat.SampleRate * audioFileReader.WaveFormat.Channels];
- int samplesRead = 0;
- while ((samplesRead = audioFileReader.Read(readBuffer, 0, readBuffer.Length)) > 0)
- wholeFile.AddRange(readBuffer.Take(samplesRead));
- audio = Array.ConvertAll(wholeFile.ToArray(), x => (double)x);
- }
-
- // TEST VALUES ARE WHAT WE EXPECT
- double lengthSec = (double)audio.Length / sampleRate;
- Assert.AreEqual(40_000, sampleRate);
- Assert.AreEqual(40, lengthSec, .01);
-
- // CREATE THE SPECTROGRAM
- int fftSize = 8192;
- var spec = new Spectrogram(sampleRate, fftSize, stepSize: 4_000, maxFreq: 2_000);
- spec.Add(audio);
- spec.SaveImage("asehgal.png", intensity: 10_000, dB: true);
- }
- }
-}
diff --git a/src/Spectrogram.sln b/src/Spectrogram.sln
index 2c92801..215c96f 100644
--- a/src/Spectrogram.sln
+++ b/src/Spectrogram.sln
@@ -1,16 +1,14 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.30128.74
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31815.197
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectrogram", "Spectrogram\Spectrogram.csproj", "{6FF83EDD-E18A-4EDD-8D53-D2281515AC47}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spectrogram.MicrophoneDemo", "Spectrogram.MicrophoneDemo\Spectrogram.MicrophoneDemo.csproj", "{D51ABC6A-53F4-4620-88A1-14EA1D779538}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectrogram.Demo", "Spectrogram.MicrophoneDemo\Spectrogram.Demo.csproj", "{D51ABC6A-53F4-4620-88A1-14EA1D779538}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectrogram.Tests", "Spectrogram.Tests\Spectrogram.Tests.csproj", "{E7482801-78C7-41FD-88D1-72A7ED3EFC9D}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SffViewer", "..\dev\sff\SffViewer\SffViewer.csproj", "{9478208D-60C7-4F6A-B2E4-6325D38139DA}"
-EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -29,10 +27,6 @@ Global
{E7482801-78C7-41FD-88D1-72A7ED3EFC9D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E7482801-78C7-41FD-88D1-72A7ED3EFC9D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E7482801-78C7-41FD-88D1-72A7ED3EFC9D}.Release|Any CPU.Build.0 = Release|Any CPU
- {9478208D-60C7-4F6A-B2E4-6325D38139DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {9478208D-60C7-4F6A-B2E4-6325D38139DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {9478208D-60C7-4F6A-B2E4-6325D38139DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {9478208D-60C7-4F6A-B2E4-6325D38139DA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/src/Spectrogram/Colormap.cs b/src/Spectrogram/Colormap.cs
index 826c9d2..1553712 100644
--- a/src/Spectrogram/Colormap.cs
+++ b/src/Spectrogram/Colormap.cs
@@ -1,106 +1,91 @@
using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Drawing;
using System.Linq;
-using System.Text;
+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.Grayscale());
- 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()
- {
- IColormap[] ics = AppDomain.CurrentDomain.GetAssemblies()
- .SelectMany(s => s.GetTypes())
- .Where(p => p.IsInterface == false)
- .Where(p => p.ToString().StartsWith("Spectrogram.Colormaps."))
- .Select(x => x.ToString())
- .Select(path => (IColormap)Activator.CreateInstance(Type.GetType(path)))
- .ToArray();
-
- return ics.Select(x => new Colormap(x)).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 f23dfc5..7413b38 100644
--- a/src/Spectrogram/Image.cs
+++ b/src/Spectrogram/Image.cs
@@ -1,57 +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, 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 + 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/README.md b/src/Spectrogram/README.md
new file mode 100644
index 0000000..959faa7
--- /dev/null
+++ b/src/Spectrogram/README.md
@@ -0,0 +1,36 @@
+**Spectrogram is a .NET library for creating frequency spectrograms from pre-recorded signals, streaming data, or microphone audio from the sound card.** Spectrogram uses FFT algorithms and window functions provided by the [FftSharp](https://github.com/swharden/FftSharp) project, and it targets .NET Standard so it can be used in .NET Framework and .NET Core projects.
+
+[](https://github.com/swharden/Spectrogram)
+
+## Quickstart
+
+```cs
+(double[] audio, int sampleRate) = ReadWavMono("hal.wav");
+var sg = new SpectrogramGenerator(sampleRate, fftSize: 4096, stepSize: 500, maxFreq: 3000);
+sg.Add(audio);
+sg.SaveImage("hal.png");
+```
+
+This example generates the image at the top of the page.
+
+
+## How to Read a WAV File
+
+There are many excellent libraries that read audio files. Consult the documentation _for those libraries_ to learn how to do this well. Here's an example method I use to read audio values from mono WAV files using the NAudio package:
+
+```cs
+(double[] audio, int sampleRate) ReadWavMono(string filePath, double multiplier = 16_000)
+{
+ using var afr = new NAudio.Wave.AudioFileReader(filePath);
+ int sampleRate = afr.WaveFormat.SampleRate;
+ int bytesPerSample = afr.WaveFormat.BitsPerSample / 8;
+ int sampleCount = (int)(afr.Length / bytesPerSample);
+ int channelCount = afr.WaveFormat.Channels;
+ var audio = new List(sampleCount);
+ var buffer = new float[sampleRate * channelCount];
+ int samplesRead = 0;
+ while ((samplesRead = afr.Read(buffer, 0, buffer.Length)) > 0)
+ audio.AddRange(buffer.Take(samplesRead).Select(x => x * multiplier));
+ return (audio.ToArray(), sampleRate);
+}
+```
\ No newline at end of file
diff --git a/src/Spectrogram/SFF.cs b/src/Spectrogram/SFF.cs
deleted file mode 100644
index 3c9f456..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(Spectrogram 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 b765051..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);
@@ -55,7 +57,8 @@ public Settings(int sampleRate, int fftSize, int stepSize, double minFreq, doubl
// horizontal
StepLengthSec = (double)StepSize / sampleRate;
- Window = FftSharp.Window.Hanning(fftSize);
+ var window = new FftSharp.Windows.Hanning();
+ Window = window.Create(fftSize);
StepOverlapSec = FftLengthSec - StepLengthSec;
StepOverlapFrac = StepOverlapSec / FftLengthSec;
}
diff --git a/src/Spectrogram/Spectrogram.cs b/src/Spectrogram/Spectrogram.cs
deleted file mode 100644
index 281d98d..0000000
--- a/src/Spectrogram/Spectrogram.cs
+++ /dev/null
@@ -1,264 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Drawing;
-using System.Drawing.Imaging;
-using System.IO;
-using System.Linq;
-using System.Runtime.InteropServices;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Spectrogram
-{
- public class Spectrogram
- {
- public int Width { get { return ffts.Count; } }
- public int Height { get { return settings.Height; } }
- public int FftSize { get { return settings.FftSize; } }
- public double HzPerPx { get { return settings.HzPerPixel; } }
- public double SecPerPx { get { return settings.StepLengthSec; } }
- public int FftsToProcess { get { return (newAudio.Count - settings.FftSize) / settings.StepSize; } }
- public int FftsProcessed { get; private set; }
- public int NextColumnIndex { get { return (FftsProcessed + rollOffset) % Width; } }
- public int OffsetHz { get { return settings.OffsetHz; } set { settings.OffsetHz = value; } }
- public int SampleRate { get { return settings.SampleRate; } }
- public int StepSize { get { return settings.StepSize; } }
- public double FreqMax { get { return settings.FreqMax; } }
- public double FreqMin { get { return settings.FreqMin; } }
-
- private readonly Settings settings;
- private readonly List ffts = new List();
- private readonly List newAudio = new List();
- private Colormap cmap = Colormap.Viridis;
-
- public Spectrogram(int sampleRate, int fftSize, int stepSize,
- double minFreq = 0, double maxFreq = double.PositiveInfinity,
- int? fixedWidth = null, int offsetHz = 0)
- {
- settings = new Settings(sampleRate, fftSize, stepSize, minFreq, maxFreq, offsetHz);
-
- 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}%";
- }
-
- public void SetColormap(Colormap cmap)
- {
- this.cmap = cmap ?? this.cmap;
- }
-
- 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;
-
- 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) { }
-
- [Obsolete("use the Add() method", true)]
- public void AddCircular(float[] values) { }
-
- [Obsolete("use the Add() method", true)]
- public void AddScroll(float[] values) { }
-
- public void Add(double[] audio, bool process = true)
- {
- newAudio.AddRange(audio);
- if (process)
- Process();
- }
-
- private int rollOffset = 0;
- public void RollReset(int offset = 0)
- {
- rollOffset = -FftsProcessed + offset;
- }
-
- public double[][] Process()
- {
- if (FftsToProcess < 1)
- return null;
-
- int newFftCount = FftsToProcess;
- double[][] newFfts = new double[newFftCount][];
-
- Parallel.For(0, newFftCount, newFftIndex =>
- {
- FftSharp.Complex[] buffer = new FftSharp.Complex[settings.FftSize];
- int sourceIndex = newFftIndex * settings.StepSize;
- for (int i = 0; i < settings.FftSize; i++)
- buffer[i].Real = newAudio[sourceIndex + i] * settings.Window[i];
-
- FftSharp.Transform.FFT(buffer);
-
- newFfts[newFftIndex] = new double[settings.Height];
- for (int i = 0; i < settings.Height; i++)
- newFfts[newFftIndex][i] = buffer[settings.FftIndex1 + i].Magnitude / settings.FftSize;
- });
-
- foreach (var newFft in newFfts)
- ffts.Add(newFft);
- FftsProcessed += newFfts.Length;
-
- newAudio.RemoveRange(0, newFftCount * settings.StepSize);
- PadOrTrimForFixedWidth();
-
- return newFfts;
- }
-
- public List GetMelFFTs(int melBinCount)
- {
- if (settings.FreqMin != 0)
- throw new InvalidOperationException("cannot get Mel spectrogram unless minimum frequency is 0Hz");
-
- var fftsMel = new List();
- foreach(var fft in ffts)
- fftsMel.Add(FftSharp.Transform.MelScale(fft, SampleRate, melBinCount));
-
- return fftsMel;
- }
-
- public Bitmap GetBitmap(double intensity = 1, bool dB = false, bool roll = false) =>
- Image.GetBitmap(ffts, cmap, intensity, dB, roll, NextColumnIndex);
-
- public Bitmap GetBitmapMel(int melBinCount = 25, double intensity = 1, bool dB = false, bool roll = false) =>
- Image.GetBitmap(GetMelFFTs(melBinCount), cmap, intensity, dB, roll, NextColumnIndex);
-
- [Obsolete("use SaveImage()", true)]
- public void SaveBitmap(Bitmap bmp, string fileName) { }
-
- public void SaveImage(string fileName, double intensity = 1, bool dB = false, 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, cmap, intensity, dB, roll, NextColumnIndex).Save(fileName, fmt);
- }
-
- public Bitmap GetBitmapMax(double intensity = 1, bool dB = false, bool roll = false, int reduction = 4)
- {
- 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, cmap, intensity, dB, roll, NextColumnIndex);
- }
-
- public void SaveData(string filePath, int melBinCount = 0)
- {
- if (!filePath.EndsWith(".sff", StringComparison.OrdinalIgnoreCase))
- filePath += ".sff";
- new SFF(this, melBinCount).Save(filePath);
- }
-
- private int fixedWidth = 0;
- public void SetFixedWidth(int width)
- {
- fixedWidth = width;
- PadOrTrimForFixedWidth();
- }
-
- private void PadOrTrimForFixedWidth()
- {
- if (fixedWidth > 0)
- {
- int overhang = Width - fixedWidth;
- if (overhang > 0)
- ffts.RemoveRange(0, overhang);
-
- while (ffts.Count < fixedWidth)
- ffts.Insert(0, new double[Height]);
- }
- }
-
- public Bitmap GetVerticalScale(int width, int offsetHz = 0, int tickSize = 3, int reduction = 1)
- {
- return Scale.Vertical(width, settings, offsetHz, tickSize, reduction);
- }
-
- 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;
- }
-
- public List GetFFTs()
- {
- return ffts;
- }
-
- 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");
-
- double[] freqs = ffts[ffts.Count - 1];
-
- int peakIndex = 0;
- double peakMagnitude = 0;
- for (int i = 0; i < freqs.Length; i++)
- {
- if (freqs[i] > peakMagnitude)
- {
- peakMagnitude = freqs[i];
- peakIndex = i;
- }
- }
-
- double maxFreq = SampleRate / 2;
- double peakFreqFrac = peakIndex / (double)freqs.Length;
- double peakFreqHz = maxFreq * peakFreqFrac;
-
- return (peakFreqHz, peakMagnitude);
- }
- }
-}
diff --git a/src/Spectrogram/Spectrogram.csproj b/src/Spectrogram/Spectrogram.csproj
index 1524099..4bfc42f 100644
--- a/src/Spectrogram/Spectrogram.csproj
+++ b/src/Spectrogram/Spectrogram.csproj
@@ -1,34 +1,34 @@
-
-
-
- netstandard2.0
- 1.2.5
- A .NET Standard library for creating spectrograms from pre-recorded signals or live audio from the sound card.
- 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
- Quickstart: https://github.com/swharden/Spectrogram
-Releases: https://github.com/swharden/Spectrogram/releases
- true
- true
- snupkg
-
-
-
-
-
-
-
-
-
-
- True
-
-
-
-
-
+
+
+ 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
new file mode 100644
index 0000000..2c8d593
--- /dev/null
+++ b/src/Spectrogram/SpectrogramGenerator.cs
@@ -0,0 +1,409 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using SkiaSharp;
+
+namespace Spectrogram;
+
+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)
+ {
+ Settings = new Settings(sampleRate, fftSize, stepSize, minFreq, maxFreq, offsetHz);
+
+ UnprocessedData = initialAudioList ?? new List();
+
+ 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");
+
+ 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);
+ }
+
+ ///
+ /// Load new data into the spectrogram generator
+ ///
+ public void Add(IEnumerable audio, bool process = true)
+ {
+ UnprocessedData.AddRange(audio);
+ if (process)
+ Process();
+ }
+
+ ///
+ /// The roll offset is used to calculate NextColumnIndex and can be set to a positive number
+ /// to begin adding new columns to the center of the spectrogram.
+ /// This can also be used to artificially move the next column index to zero even though some
+ /// data has already been accumulated.
+ ///
+ private int rollOffset = 0;
+
+ ///
+ /// Reset the next column index such that the next processed FFT will appear at the far left of the spectrogram.
+ ///
+ ///
+ public void RollReset(int offset = 0)
+ {
+ rollOffset = -FftsProcessed + offset;
+ }
+
+ ///
+ /// Perform FFT analysis on all unprocessed data
+ ///
+ public double[][] Process()
+ {
+ if (FftsToProcess < 1)
+ return null;
+
+ int newFftCount = FftsToProcess;
+ double[][] newFfts = new double[newFftCount][];
+
+ Parallel.For(0, newFftCount, newFftIndex =>
+ {
+ var buffer = new System.Numerics.Complex[Settings.FftSize];
+ int sourceIndex = newFftIndex * Settings.StepSize;
+ for (int i = 0; i < Settings.FftSize; i++)
+ buffer[i] = new(UnprocessedData[sourceIndex + i] * Settings.Window[i], 0);
+
+ FftSharp.FFT.Forward(buffer);
+
+ newFfts[newFftIndex] = new double[Settings.Height];
+ for (int i = 0; i < Settings.Height; i++)
+ newFfts[newFftIndex][i] = buffer[Settings.FftIndex1 + i].Magnitude / Settings.FftSize;
+ });
+
+ foreach (var newFft in newFfts)
+ FFTs.Add(newFft);
+ FftsProcessed += newFfts.Length;
+
+ UnprocessedData.RemoveRange(0, newFftCount * Settings.StepSize);
+ PadOrTrimForFixedWidth();
+
+ return newFfts;
+ }
+
+ ///
+ /// Return a list of the mel-scaled FFTs contained in this spectrogram
+ ///
+ /// Total number of output bins to use. Choose a value significantly smaller than Height.
+ public List GetMelFFTs(int melBinCount)
+ {
+ if (Settings.FreqMin != 0)
+ throw new InvalidOperationException("cannot get Mel spectrogram unless minimum frequency is 0Hz");
+
+ var fftsMel = new List();
+ foreach (var fft in FFTs)
+ fftsMel.Add(FftSharp.Mel.Scale(fft, (int)SampleRate, melBinCount));
+
+ return fftsMel;
+ }
+
+ ///
+ /// Create and return a spectrogram bitmap from the FFTs stored in memory.
+ ///
+ /// Multiply the output by a fixed value to change its brightness.
+ /// If true, output will be log-transformed.
+ /// If dB scaling is in use, this multiplier will be applied before log transformation.
+ /// Behavior of the spectrogram when it is full of data.
+ /// If True, the image will be rotated so time flows from top to bottom (rather than left to right).
+ /// Roll (true) adds new columns on the left overwriting the oldest ones.
+ /// Scroll (false) slides the whole image to the left and adds new columns to the right.
+ public SKBitmap GetBitmap(double intensity = 1, bool dB = false, double dBScale = 1, bool roll = false, bool rotate = false)
+ {
+ if (FFTs.Count == 0)
+ throw new InvalidOperationException("Not enough data to create an image. " +
+ $"Ensure {nameof(Width)} is >0 before calling {nameof(GetBitmap)}().");
+
+ return Image.GetBitmap(FFTs, Colormap, intensity, dB, dBScale, roll, NextColumnIndex, rotate);
+ }
+
+ ///
+ /// 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);
+ }
+
+ 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.");
+
+ SKEncodedImageFormat fmt = extension.ToLower() switch
+ {
+ ".bmp" => SKEncodedImageFormat.Bmp,
+ ".png" => SKEncodedImageFormat.Png,
+ ".gif" => SKEncodedImageFormat.Gif,
+ ".jpg" => SKEncodedImageFormat.Jpeg,
+ ".jpeg" => SKEncodedImageFormat.Jpeg,
+ _ => throw new ArgumentException("unknown file extension"),
+ };
+
+ using SKBitmap image = Image.GetBitmap(FFTs, Colormap, intensity, dB, dBScale, roll, NextColumnIndex);
+ using SKData encodedImage = image.Encode(fmt, 80);
+ byte[] bytes = encodedImage.ToArray();
+ return bytes;
+ }
+
+ ///
+ /// 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++)
+ {
+ 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;
+
+ ///
+ /// 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()
+ {
+ if (fixedWidth > 0)
+ {
+ int overhang = Width - fixedWidth;
+ if (overhang > 0)
+ FFTs.RemoveRange(0, overhang);
+
+ 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 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 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);
+
+ if (latestFft == false)
+ throw new NotImplementedException("peak of mean of all FFTs not yet supported");
+
+ double[] freqs = FFTs[FFTs.Count - 1];
+
+ int peakIndex = 0;
+ double peakMagnitude = 0;
+ for (int i = 0; i < freqs.Length; i++)
+ {
+ if (freqs[i] > peakMagnitude)
+ {
+ peakMagnitude = freqs[i];
+ peakIndex = i;
+ }
+ }
+
+ double maxFreq = SampleRate / 2;
+ double peakFreqFrac = peakIndex / (double)freqs.Length;
+ double peakFreqHz = maxFreq * peakFreqFrac;
+
+ 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 23a893d..0000000
--- a/src/Spectrogram/WavFile.cs
+++ /dev/null
@@ -1,123 +0,0 @@
-// Simple WAV file reader by Scott Harden released under a MIT license
-// Format here is based on http://soundfile.sapp.org/doc/WaveFormat/
-
-using System;
-using System.Diagnostics;
-using System.IO;
-
-namespace Spectrogram
-{
- 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 riffChunk = ChunkInfo(br, 0);
- Console.WriteLine($"First chunk '{riffChunk.id}' indicates {riffChunk.length:N0} bytes");
- if (riffChunk.id != "RIFF")
- throw new InvalidOperationException($"Unsupported WAV format (first chunk ID was '{riffChunk.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