├── .github
└── workflows
│ └── build-binaries.yml
├── .gitignore
├── LICENSE
├── README.md
├── build-scripts
├── NSD.UI (linux-x64).bat
└── NSD.UI (win-x64).bat
├── docs
├── v1.0.png
└── v1.4.png
└── source
├── NSD.Benchmarks
├── ComplexFftBenchmark.cs
├── FftTestData.cs
├── GoertzelFilter
│ ├── GoertzelFilter1.cs
│ └── GoertzelFilterBaseline.cs
├── GoertzelFilterBenchmark.cs
├── NSD.Benchmarks.csproj
├── Program.cs
├── README.md
├── RealFftBenchmark.cs
├── WindowBenchmark.cs
└── Windows
│ ├── Windows1.cs
│ └── WindowsBaseline.cs
├── NSD.Generator
├── NSD.Generator.csproj
└── Program.cs
├── NSD.UI
├── .gitignore
├── App.axaml
├── App.axaml.cs
├── LogDecadeMinorTickGenerator.cs
├── MainWindow.axaml
├── MainWindow.axaml.cs
├── MainWindowViewModel.cs
├── NSD.UI.csproj
├── Settings.cs
└── runtimeconfig.template.json
├── NSD.sln
└── NSD
├── Data types
└── Spectrum.cs
├── FFT.cs
├── Filter.cs
├── GoertzelFilter.cs
├── NSD.cs
├── NSD.csproj
├── NsdProcessingException.cs
├── Papers
├── GH_FFT.pdf
└── Improved spectrum estimation from digitized time series on a logaritmic frequency axis.pdf
├── Signals.cs
└── Windows.cs
/.github/workflows/build-binaries.yml:
--------------------------------------------------------------------------------
1 | name: Build binaries
2 | on: [push]
3 | jobs:
4 | build:
5 | runs-on: ${{ matrix.os }}
6 | strategy:
7 | matrix:
8 | dotnet-version: [ '8.0.x' ]
9 | os: [windows-latest]
10 | steps:
11 | - uses: actions/checkout@v4
12 | - name: Setup .NET Core SDK ${{ matrix.dotnet-version }}
13 | uses: actions/setup-dotnet@v3
14 | with:
15 | dotnet-version: ${{ matrix.dotnet-version }}
16 |
17 | - name: Install dependencies
18 | run: dotnet restore
19 | working-directory: source
20 |
21 | - name: Run win-x64 build
22 | working-directory: build-scripts
23 | run: ./"NSD.UI (win-x64).bat"
24 | - name: Upload win-x64 build
25 | uses: actions/upload-artifact@v4
26 | with:
27 | name: NSD-win-x64
28 | path: builds/NSD.UI/win-x64/NSD-win-x64.exe
29 |
30 | - name: Run linux-x64 build
31 | working-directory: build-scripts
32 | run: ./"NSD.UI (linux-x64).bat"
33 | - name: Upload linux-x64 build
34 | uses: actions/upload-artifact@v4
35 | with:
36 | name: NSD-linux-x64
37 | path: builds/NSD.UI/linux-x64/NSD-linux-x64
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Aa][Rr][Mm]/
27 | [Aa][Rr][Mm]64/
28 | bld/
29 | [Bb]in/
30 | [Oo]bj/
31 | [Ll]og/
32 | [Ll]ogs/
33 |
34 | # Visual Studio 2015/2017 cache/options directory
35 | .vs/
36 | # Uncomment if you have tasks that create the project's static files in wwwroot
37 | #wwwroot/
38 |
39 | # Visual Studio 2017 auto generated files
40 | Generated\ Files/
41 |
42 | # MSTest test Results
43 | [Tt]est[Rr]esult*/
44 | [Bb]uild[Ll]og.*
45 |
46 | # NUnit
47 | *.VisualState.xml
48 | TestResult.xml
49 | nunit-*.xml
50 |
51 | # Build Results of an ATL Project
52 | [Dd]ebugPS/
53 | [Rr]eleasePS/
54 | dlldata.c
55 |
56 | # Benchmark Results
57 | BenchmarkDotNet.Artifacts/
58 |
59 | # .NET Core
60 | project.lock.json
61 | project.fragment.lock.json
62 | artifacts/
63 |
64 | # StyleCop
65 | StyleCopReport.xml
66 |
67 | # Files built by Visual Studio
68 | *_i.c
69 | *_p.c
70 | *_h.h
71 | *.ilk
72 | *.meta
73 | *.obj
74 | *.iobj
75 | *.pch
76 | *.pdb
77 | *.ipdb
78 | *.pgc
79 | *.pgd
80 | *.rsp
81 | *.sbr
82 | *.tlb
83 | *.tli
84 | *.tlh
85 | *.tmp
86 | *.tmp_proj
87 | *_wpftmp.csproj
88 | *.log
89 | *.vspscc
90 | *.vssscc
91 | .builds
92 | *.pidb
93 | *.svclog
94 | *.scc
95 |
96 | # Chutzpah Test files
97 | _Chutzpah*
98 |
99 | # Visual C++ cache files
100 | ipch/
101 | *.aps
102 | *.ncb
103 | *.opendb
104 | *.opensdf
105 | *.sdf
106 | *.cachefile
107 | *.VC.db
108 | *.VC.VC.opendb
109 |
110 | # Visual Studio profiler
111 | *.psess
112 | *.vsp
113 | *.vspx
114 | *.sap
115 |
116 | # Visual Studio Trace Files
117 | *.e2e
118 |
119 | # TFS 2012 Local Workspace
120 | $tf/
121 |
122 | # Guidance Automation Toolkit
123 | *.gpState
124 |
125 | # ReSharper is a .NET coding add-in
126 | _ReSharper*/
127 | *.[Rr]e[Ss]harper
128 | *.DotSettings.user
129 |
130 | # TeamCity is a build add-in
131 | _TeamCity*
132 |
133 | # DotCover is a Code Coverage Tool
134 | *.dotCover
135 |
136 | # AxoCover is a Code Coverage Tool
137 | .axoCover/*
138 | !.axoCover/settings.json
139 |
140 | # Visual Studio code coverage results
141 | *.coverage
142 | *.coveragexml
143 |
144 | # NCrunch
145 | _NCrunch_*
146 | .*crunch*.local.xml
147 | nCrunchTemp_*
148 |
149 | # MightyMoose
150 | *.mm.*
151 | AutoTest.Net/
152 |
153 | # Web workbench (sass)
154 | .sass-cache/
155 |
156 | # Installshield output folder
157 | [Ee]xpress/
158 |
159 | # DocProject is a documentation generator add-in
160 | DocProject/buildhelp/
161 | DocProject/Help/*.HxT
162 | DocProject/Help/*.HxC
163 | DocProject/Help/*.hhc
164 | DocProject/Help/*.hhk
165 | DocProject/Help/*.hhp
166 | DocProject/Help/Html2
167 | DocProject/Help/html
168 |
169 | # Click-Once directory
170 | publish/
171 |
172 | # Publish Web Output
173 | *.[Pp]ublish.xml
174 | *.azurePubxml
175 | # Note: Comment the next line if you want to checkin your web deploy settings,
176 | # but database connection strings (with potential passwords) will be unencrypted
177 | *.pubxml
178 | *.publishproj
179 |
180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
181 | # checkin your Azure Web App publish settings, but sensitive information contained
182 | # in these scripts will be unencrypted
183 | PublishScripts/
184 |
185 | # NuGet Packages
186 | *.nupkg
187 | # NuGet Symbol Packages
188 | *.snupkg
189 | # The packages folder can be ignored because of Package Restore
190 | **/[Pp]ackages/*
191 | # except build/, which is used as an MSBuild target.
192 | !**/[Pp]ackages/build/
193 | # Uncomment if necessary however generally it will be regenerated when needed
194 | #!**/[Pp]ackages/repositories.config
195 | # NuGet v3's project.json files produces more ignorable files
196 | *.nuget.props
197 | *.nuget.targets
198 |
199 | # Microsoft Azure Build Output
200 | csx/
201 | *.build.csdef
202 |
203 | # Microsoft Azure Emulator
204 | ecf/
205 | rcf/
206 |
207 | # Windows Store app package directories and files
208 | AppPackages/
209 | BundleArtifacts/
210 | Package.StoreAssociation.xml
211 | _pkginfo.txt
212 | *.appx
213 | *.appxbundle
214 | *.appxupload
215 |
216 | # Visual Studio cache files
217 | # files ending in .cache can be ignored
218 | *.[Cc]ache
219 | # but keep track of directories ending in .cache
220 | !?*.[Cc]ache/
221 |
222 | # Others
223 | ClientBin/
224 | ~$*
225 | *~
226 | *.dbmdl
227 | *.dbproj.schemaview
228 | *.jfm
229 | *.pfx
230 | *.publishsettings
231 | orleans.codegen.cs
232 |
233 | # Including strong name files can present a security risk
234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
235 | #*.snk
236 |
237 | # Since there are multiple workflows, uncomment next line to ignore bower_components
238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
239 | #bower_components/
240 |
241 | # RIA/Silverlight projects
242 | Generated_Code/
243 |
244 | # Backup & report files from converting an old project file
245 | # to a newer Visual Studio version. Backup files are not needed,
246 | # because we have git ;-)
247 | _UpgradeReport_Files/
248 | Backup*/
249 | UpgradeLog*.XML
250 | UpgradeLog*.htm
251 | ServiceFabricBackup/
252 | *.rptproj.bak
253 |
254 | # SQL Server files
255 | *.mdf
256 | *.ldf
257 | *.ndf
258 |
259 | # Business Intelligence projects
260 | *.rdl.data
261 | *.bim.layout
262 | *.bim_*.settings
263 | *.rptproj.rsuser
264 | *- [Bb]ackup.rdl
265 | *- [Bb]ackup ([0-9]).rdl
266 | *- [Bb]ackup ([0-9][0-9]).rdl
267 |
268 | # Microsoft Fakes
269 | FakesAssemblies/
270 |
271 | # GhostDoc plugin setting file
272 | *.GhostDoc.xml
273 |
274 | # Node.js Tools for Visual Studio
275 | .ntvs_analysis.dat
276 | node_modules/
277 |
278 | # Visual Studio 6 build log
279 | *.plg
280 |
281 | # Visual Studio 6 workspace options file
282 | *.opt
283 |
284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
285 | *.vbw
286 |
287 | # Visual Studio LightSwitch build output
288 | **/*.HTMLClient/GeneratedArtifacts
289 | **/*.DesktopClient/GeneratedArtifacts
290 | **/*.DesktopClient/ModelManifest.xml
291 | **/*.Server/GeneratedArtifacts
292 | **/*.Server/ModelManifest.xml
293 | _Pvt_Extensions
294 |
295 | # Paket dependency manager
296 | .paket/paket.exe
297 | paket-files/
298 |
299 | # FAKE - F# Make
300 | .fake/
301 |
302 | # CodeRush personal settings
303 | .cr/personal
304 |
305 | # Python Tools for Visual Studio (PTVS)
306 | __pycache__/
307 | *.pyc
308 |
309 | # Cake - Uncomment if you are using it
310 | # tools/**
311 | # !tools/packages.config
312 |
313 | # Tabs Studio
314 | *.tss
315 |
316 | # Telerik's JustMock configuration file
317 | *.jmconfig
318 |
319 | # BizTalk build output
320 | *.btp.cs
321 | *.btm.cs
322 | *.odx.cs
323 | *.xsd.cs
324 |
325 | # OpenCover UI analysis results
326 | OpenCover/
327 |
328 | # Azure Stream Analytics local run output
329 | ASALocalRun/
330 |
331 | # MSBuild Binary and Structured Log
332 | *.binlog
333 |
334 | # NVidia Nsight GPU debugger configuration file
335 | *.nvuser
336 |
337 | # MFractors (Xamarin productivity tool) working folder
338 | .mfractor/
339 |
340 | # Local History for Visual Studio
341 | .localhistory/
342 |
343 | # BeatPulse healthcheck temp database
344 | healthchecksdb
345 |
346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
347 | MigrationBackup/
348 |
349 | # Ionide (cross platform F# VS Code tools) working folder
350 | .ionide/
351 |
352 | builds
353 | tests
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 macaba
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NSD
2 |
3 | Cross-platform UI tool for estimating noise spectral density (NSD) from time-series data.
4 |
5 | This is intended to be an easy-to-use tool which allows anyone to quickly produce NSD charts that are directly comparable to other people's charts if they are produced by the same version of this tool. Customisation and configurability is very limited therefore power users will prefer their own scripts.
6 |
7 |
8 |
9 | ## Input file format
10 |
11 | Input file format is single column CSV with no header.
12 |
13 | Example:
14 | ```
15 | -5.259715387375001E-07
16 | -4.895393397810999E-07
17 | -5.413877378806E-07
18 | -5.731182876255E-07
19 | -5.228528194452E-07
20 | ```
21 |
22 | Multiple number formats supported, however not exhaustive.
23 |
24 | ## Using release binaries
25 |
26 | ### Windows
27 |
28 | It may need unblocking on properties dialog
29 |
30 | 
31 |
32 | ### Linux
33 |
34 | ```
35 | sudo apt-get install -y libgdiplus
36 | chmod +x NSD.linux-x64
37 | ```
38 |
--------------------------------------------------------------------------------
/build-scripts/NSD.UI (linux-x64).bat:
--------------------------------------------------------------------------------
1 | dotnet publish ../source/NSD.UI -r linux-x64 -c Release --output ../builds/NSD.UI/linux-x64/
2 | ren ..\builds\NSD.UI\linux-x64\NSD.UI NSD-linux-x64
--------------------------------------------------------------------------------
/build-scripts/NSD.UI (win-x64).bat:
--------------------------------------------------------------------------------
1 | dotnet publish ../source/NSD.UI -r win-x64 -c Release --output ../builds/NSD.UI/win-x64/
2 | ren ..\builds\NSD.UI\win-x64\NSD.UI.exe NSD-win-x64.exe
--------------------------------------------------------------------------------
/docs/v1.0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/macaba/NSD/5b9f022c3fcccb7cad7603d5cf9af9413af43637/docs/v1.0.png
--------------------------------------------------------------------------------
/docs/v1.4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/macaba/NSD/5b9f022c3fcccb7cad7603d5cf9af9413af43637/docs/v1.4.png
--------------------------------------------------------------------------------
/source/NSD.Benchmarks/ComplexFftBenchmark.cs:
--------------------------------------------------------------------------------
1 | using BenchmarkDotNet.Attributes;
2 | using BenchmarkDotNet.Jobs;
3 | using System.Numerics;
4 |
5 | namespace NSD.Benchmarks
6 | {
7 | [SimpleJob(RuntimeMoniker.Net80)]
8 | [MemoryDiagnoser]
9 | [Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.FastestToSlowest)]
10 | public class ComplexFftBenchmark
11 | {
12 | private Complex[] values_FftFlat;
13 | private Complex[] values_FftSharp;
14 | private Complex[] values_MathNet;
15 | private Complex[] values_AelianFft;
16 | private Complex[] values_NWaveFft;
17 |
18 | private FftFlat.FastFourierTransform fftFlat;
19 | private NWaves.Transforms.Fft64 nWavesFft;
20 |
21 | private double[] values_NWavesFft_Real;
22 | private double[] values_NWavesFft_Img;
23 |
24 | private StreamWriter log;
25 |
26 | //[Params(256, 512, 1024, 2048, 4096, 8192)]
27 | [Params(8192)]
28 | public int Length;
29 |
30 | [GlobalSetup]
31 | public void Setup()
32 | {
33 | values_FftFlat = FftTestData.CreateComplex(Length);
34 | values_FftSharp = FftTestData.CreateComplex(Length);
35 | values_MathNet = FftTestData.CreateComplex(Length);
36 | values_AelianFft = FftTestData.CreateComplex(Length);
37 | values_NWaveFft = FftTestData.CreateComplex(Length);
38 | values_NWavesFft_Real = values_NWaveFft.Select(x => x.Real).ToArray();
39 | values_NWavesFft_Img = values_NWaveFft.Select(x => x.Imaginary).ToArray();
40 |
41 | fftFlat = new FftFlat.FastFourierTransform(Length);
42 |
43 | Aelian.FFT.FastFourierTransform.Initialize();
44 |
45 | nWavesFft = new NWaves.Transforms.Fft64(Length);
46 |
47 | var logPath = Path.Combine("log" + Length + ".txt");
48 | log = new StreamWriter(logPath);
49 | log.WriteLine("=== BEFORE ===");
50 | log.WriteLine("FftFlat: " + GetMaxValue(values_FftFlat));
51 | log.WriteLine("FftSharp: " + GetMaxValue(values_FftSharp));
52 | log.WriteLine("MathNet: " + GetMaxValue(values_MathNet));
53 | }
54 |
55 | [GlobalCleanup]
56 | public void Cleanup()
57 | {
58 | log.WriteLine("=== AFTER ===");
59 | log.WriteLine("FftFlat: " + GetMaxValue(values_FftFlat));
60 | log.WriteLine("FftSharp: " + GetMaxValue(values_FftSharp));
61 | log.WriteLine("MathNet: " + GetMaxValue(values_MathNet));
62 | log.Dispose();
63 | }
64 |
65 | private static double GetMaxValue(Complex[] data)
66 | {
67 | return data.Select(x => Math.Max(Math.Abs(x.Real), Math.Abs(x.Imaginary))).Max();
68 | }
69 |
70 | [Benchmark(Baseline = true)]
71 | public void FftFlat()
72 | {
73 | fftFlat.Forward(values_FftFlat);
74 | fftFlat.Inverse(values_FftFlat);
75 | }
76 |
77 | [Benchmark]
78 | public void FftSharp()
79 | {
80 | global::FftSharp.FFT.Forward(values_FftSharp);
81 | global::FftSharp.FFT.Inverse(values_FftSharp);
82 | }
83 |
84 | [Benchmark]
85 | public void MathNet()
86 | {
87 | global::MathNet.Numerics.IntegralTransforms.Fourier.Forward(values_MathNet, global::MathNet.Numerics.IntegralTransforms.FourierOptions.AsymmetricScaling);
88 | global::MathNet.Numerics.IntegralTransforms.Fourier.Inverse(values_MathNet, global::MathNet.Numerics.IntegralTransforms.FourierOptions.AsymmetricScaling);
89 | }
90 |
91 | [Benchmark]
92 | public void AelianFft()
93 | {
94 | Aelian.FFT.FastFourierTransform.FFT(values_AelianFft, true);
95 | Aelian.FFT.FastFourierTransform.FFT(values_AelianFft, false);
96 | }
97 |
98 | [Benchmark]
99 | public void NWaves()
100 | {
101 | nWavesFft.Direct(values_NWavesFft_Real, values_NWavesFft_Img);
102 | nWavesFft.Inverse(values_NWavesFft_Real, values_NWavesFft_Img);
103 | }
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/source/NSD.Benchmarks/FftTestData.cs:
--------------------------------------------------------------------------------
1 | using System.Numerics;
2 |
3 | namespace NSD.Benchmarks
4 | {
5 | public static class FftTestData
6 | {
7 | public static Complex[] CreateComplex(int length)
8 | {
9 | var random = new Random(42);
10 | var values = new Complex[length];
11 | for (var i = 0; i < length; i++)
12 | {
13 | values[i] = new Complex(random.NextDouble(), random.NextDouble());
14 | }
15 |
16 | return values;
17 | }
18 |
19 | public static double[] CreateDouble(int length)
20 | {
21 | var random = new Random(42);
22 | var values = new double[length];
23 | for (var i = 0; i < length; i++)
24 | {
25 | values[i] = random.NextDouble();
26 | }
27 |
28 | return values;
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/source/NSD.Benchmarks/GoertzelFilter/GoertzelFilter1.cs:
--------------------------------------------------------------------------------
1 | using System.Numerics;
2 |
3 | namespace NSD
4 | {
5 | public class GoertzelFilter1
6 | {
7 | public double coeff;
8 | public double sine;
9 | public double cosine;
10 |
11 | public GoertzelFilter1(double filterFreq, double sampleFreq)
12 | {
13 | double w = 2 * Math.PI * (filterFreq / sampleFreq);
14 | double wr, wi;
15 |
16 | wr = Math.Cos(w);
17 | wi = Math.Sin(w);
18 | coeff = 2 * wr;
19 | cosine = wr;
20 | sine = wi;
21 | }
22 |
23 | public Complex Process(Span samples)
24 | {
25 | double sprev = 0.0;
26 | double sprev2 = 0.0;
27 | double s, imag, real;
28 |
29 | unsafe
30 | {
31 | fixed (double* samplesP = samples)
32 | {
33 | double* dataPointer = samplesP;
34 | double* endPointer = samplesP + samples.Length;
35 | while(dataPointer < endPointer)
36 | {
37 | s = *dataPointer + coeff * sprev - sprev2;
38 | sprev2 = sprev;
39 | sprev = s;
40 | dataPointer++;
41 | }
42 | }
43 | }
44 |
45 | real = sprev * cosine - sprev2;
46 | imag = -sprev * sine;
47 |
48 | return new Complex(real, imag);
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/source/NSD.Benchmarks/GoertzelFilter/GoertzelFilterBaseline.cs:
--------------------------------------------------------------------------------
1 | using System.Numerics;
2 |
3 | namespace NSD
4 | {
5 | public class GoertzelFilter
6 | {
7 | public double coeff;
8 | public double sine;
9 | public double cosine;
10 |
11 | public GoertzelFilter(double filterFreq, double sampleFreq)
12 | {
13 | double w = 2 * Math.PI * (filterFreq / sampleFreq);
14 | double wr, wi;
15 |
16 | wr = Math.Cos(w);
17 | wi = Math.Sin(w);
18 | coeff = 2 * wr;
19 | cosine = wr;
20 | sine = wi;
21 | }
22 |
23 | public Complex Process(Span samples)
24 | {
25 | double sprev = 0.0;
26 | double sprev2 = 0.0;
27 | double s, imag, real;
28 |
29 | for (int n = 0; n < samples.Length; n++)
30 | {
31 | s = samples[n] + coeff * sprev - sprev2;
32 | sprev2 = sprev;
33 | sprev = s;
34 | }
35 |
36 | real = sprev * cosine - sprev2;
37 | imag = -sprev * sine;
38 |
39 | return new Complex(real, imag);
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/source/NSD.Benchmarks/GoertzelFilterBenchmark.cs:
--------------------------------------------------------------------------------
1 | using BenchmarkDotNet.Attributes;
2 | using BenchmarkDotNet.Jobs;
3 |
4 | namespace NSD.Benchmarks
5 | {
6 | [SimpleJob(RuntimeMoniker.Net80)]
7 | [MemoryDiagnoser]
8 | [Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.FastestToSlowest)]
9 | public class GoertzelFilterBenchmark
10 | {
11 | private double[] samples;
12 |
13 | [GlobalSetup]
14 | public void Setup()
15 | {
16 | samples = new double[10000000];
17 | }
18 |
19 | [Benchmark(Baseline = true)]
20 | public void GoertzelFilterBaseline()
21 | {
22 | var filter = new GoertzelFilter(1000, 10000000);
23 | filter.Process(samples);
24 | }
25 |
26 | [Benchmark]
27 | public void GoertzelFilter1()
28 | {
29 | var filter = new GoertzelFilter1(1000, 10000000);
30 | filter.Process(samples);
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/source/NSD.Benchmarks/NSD.Benchmarks.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net8.0
6 | enable
7 | enable
8 | true
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/source/NSD.Benchmarks/Program.cs:
--------------------------------------------------------------------------------
1 | using BenchmarkDotNet.Configs;
2 | using BenchmarkDotNet.Running;
3 | using NSD.Benchmarks;
4 |
5 | DefaultConfig.Instance.WithOptions(ConfigOptions.JoinSummary);
6 | //_ = BenchmarkRunner.Run();
7 | //_ = BenchmarkRunner.Run();
8 | //_ = BenchmarkRunner.Run();
9 | _ = BenchmarkRunner.Run();
10 | Console.ReadKey();
--------------------------------------------------------------------------------
/source/NSD.Benchmarks/README.md:
--------------------------------------------------------------------------------
1 | Complex:
2 |
3 | | Method | Length | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated |
4 | |---------- |------- |----------:|---------:|---------:|------:|--------:|-------:|----------:|
5 | | FftFlat | 8192 | 81.43 μs | 0.242 μs | 0.215 μs | 1.00 | 0.00 | - | - |
6 | | AelianFft | 8192 | 154.61 μs | 0.142 μs | 0.133 μs | 1.90 | 0.01 | - | - |
7 | | MathNet | 8192 | 330.86 μs | 0.500 μs | 0.390 μs | 4.06 | 0.01 | 3.9063 | 50138 B |
8 | | NWaves | 8192 | 389.83 μs | 1.076 μs | 1.007 μs | 4.79 | 0.02 | - | - |
9 | | FftSharp | 8192 | 980.78 μs | 6.862 μs | 6.419 μs | 12.04 | 0.08 | - | - |
10 |
11 | Real:
12 |
13 | | Method | Length | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Gen2 | Allocated |
14 | |---------- |------- |------------:|----------:|---------:|------:|--------:|--------:|--------:|--------:|----------:|
15 | | FftFlat | 8192 | 48.37 μs | 0.640 μs | 0.599 μs | 1.00 | 0.02 | - | - | - | - |
16 | | AelianFft | 8192 | 79.49 μs | 0.962 μs | 0.900 μs | 1.64 | 0.03 | - | - | - | - |
17 | | NWaves | 8192 | 172.38 μs | 1.866 μs | 1.654 μs | 3.56 | 0.05 | - | - | - | - |
18 | | MathNet | 8192 | 357.23 μs | 4.862 μs | 4.548 μs | 7.39 | 0.13 | 38.0859 | 34.1797 | 34.1797 | 314351 B |
19 | | FftSharp | 8192 | 1,048.16 μs | 10.268 μs | 9.102 μs | 21.67 | 0.32 | 41.0156 | 33.2031 | 31.2500 | 393339 B |
20 |
--------------------------------------------------------------------------------
/source/NSD.Benchmarks/RealFftBenchmark.cs:
--------------------------------------------------------------------------------
1 | using BenchmarkDotNet.Attributes;
2 | using BenchmarkDotNet.Jobs;
3 |
4 | namespace NSD.Benchmarks
5 | {
6 | [SimpleJob(RuntimeMoniker.Net80)]
7 | [MemoryDiagnoser]
8 | [Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.FastestToSlowest)]
9 | public class RealFftBenchmark
10 | {
11 | private double[] values_FftFlat;
12 | private double[] values_FftSharp;
13 | private double[] values_MathNet;
14 | private double[] values_AelianFft;
15 | private double[] values_NWaves;
16 | private double[] values_NWaves_Real;
17 | private double[] values_NWaves_Img;
18 |
19 | private FftFlat.FastFourierTransform fftFlat;
20 | private FftFlat.RealFourierTransform fftFlatReal;
21 | private NWaves.Transforms.RealFft64 nWavesFft;
22 |
23 | private StreamWriter log;
24 |
25 | //[Params(256, 512, 1024, 2048, 4096, 8192)]
26 | [Params(8192)]
27 | public int Length;
28 |
29 | [GlobalSetup]
30 | public void Setup()
31 | {
32 | values_FftFlat = FftTestData.CreateDouble(Length).Append(0.0).Append(0.0).ToArray();
33 | values_FftSharp = FftTestData.CreateDouble(Length).ToArray();
34 | values_MathNet = FftTestData.CreateDouble(Length).Append(0.0).Append(0.0).ToArray();
35 | values_AelianFft = FftTestData.CreateDouble(Length).ToArray();
36 | values_NWaves = FftTestData.CreateDouble(Length).ToArray();
37 | values_NWaves_Real = new double[Length];
38 | values_NWaves_Img = new double[Length];
39 |
40 | fftFlatReal = new FftFlat.RealFourierTransform(Length);
41 |
42 | Aelian.FFT.FastFourierTransform.Initialize();
43 |
44 | nWavesFft = new NWaves.Transforms.RealFft64(Length);
45 |
46 | var logPath = Path.Combine("log" + Length + ".txt");
47 | log = new StreamWriter(logPath);
48 | log.WriteLine("=== BEFORE ===");
49 | log.WriteLine("FftFlatReal: " + GetMaxValue(values_FftFlat));
50 | log.WriteLine("FftSharpReal: " + GetMaxValue(values_FftSharp));
51 | log.WriteLine("MathNetReal: " + GetMaxValue(values_MathNet));
52 | }
53 |
54 | [GlobalCleanup]
55 | public void Cleanup()
56 | {
57 | log.WriteLine("=== AFTER ===");
58 | log.WriteLine("FftFlatReal: " + GetMaxValue(values_FftFlat));
59 | log.WriteLine("FftSharpReal: " + GetMaxValue(values_FftSharp));
60 | log.WriteLine("MathNetReal: " + GetMaxValue(values_MathNet));
61 | log.Dispose();
62 | }
63 |
64 | private static double GetMaxValue(double[] data)
65 | {
66 | return data.Select(x => Math.Abs(x)).Max();
67 | }
68 |
69 | [Benchmark(Baseline = true)]
70 | public void FftFlat()
71 | {
72 | var spectrum = fftFlatReal.Forward(values_FftFlat);
73 | fftFlatReal.Inverse(spectrum);
74 | }
75 |
76 | [Benchmark]
77 | public void FftSharp()
78 | {
79 | var spectrum = global::FftSharp.FFT.ForwardReal(values_FftSharp);
80 | values_FftSharp = global::FftSharp.FFT.InverseReal(spectrum);
81 | }
82 |
83 | [Benchmark]
84 | public void MathNet()
85 | {
86 | global::MathNet.Numerics.IntegralTransforms.Fourier.ForwardReal(values_MathNet, Length, global::MathNet.Numerics.IntegralTransforms.FourierOptions.AsymmetricScaling);
87 | global::MathNet.Numerics.IntegralTransforms.Fourier.InverseReal(values_MathNet, Length, global::MathNet.Numerics.IntegralTransforms.FourierOptions.AsymmetricScaling);
88 | }
89 |
90 | [Benchmark]
91 | public void AelianFft()
92 | {
93 | Aelian.FFT.FastFourierTransform.RealFFT(values_AelianFft, true);
94 | Aelian.FFT.FastFourierTransform.RealFFT(values_AelianFft, false);
95 | }
96 |
97 | [Benchmark]
98 | public void NWaves()
99 | {
100 | nWavesFft.Direct(values_NWaves, values_NWaves_Real, values_NWaves_Img);
101 | nWavesFft.Inverse(values_NWaves, values_NWaves_Real, values_NWaves_Img);
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/source/NSD.Benchmarks/WindowBenchmark.cs:
--------------------------------------------------------------------------------
1 | using BenchmarkDotNet.Attributes;
2 | using BenchmarkDotNet.Jobs;
3 |
4 | namespace NSD.Benchmarks
5 | {
6 | [SimpleJob(RuntimeMoniker.Net80)]
7 | [MemoryDiagnoser]
8 | [Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.FastestToSlowest)]
9 | public class WindowBenchmark
10 | {
11 | [Benchmark(Baseline = true)]
12 | public void GoertzelFilterBaseline()
13 | {
14 | Windows.FTNI(100000, out _, out _);
15 | }
16 |
17 | [Benchmark]
18 | public void GoertzelFilter1()
19 | {
20 | Windows1.FTNI(100000, out _, out _);
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/source/NSD.Benchmarks/Windows/Windows1.cs:
--------------------------------------------------------------------------------
1 | namespace NSD
2 | {
3 | public static class Windows1
4 | {
5 | /// Optimum overlap in ratio of width, used for Welch method
6 | /// Normalized Equivalent Noise BandWidth with unit of bins
7 | public static Memory FTNI(int width, out double optimumOverlap, out double NENBW)
8 | {
9 | // FTNI - https://holometer.fnal.gov/GH_FFT.pdf
10 | // wj = 0.2810639 − 0.5208972 cos(z) + 0.1980399 cos(2z).
11 | optimumOverlap = 0.656;
12 | NENBW = 2.9656;
13 | Memory window = new double[width];
14 | int i = 0;
15 | unsafe
16 | {
17 | fixed (double* windowP = window.Span)
18 | {
19 | double* dataPointer = windowP;
20 | double* endPointer = windowP + window.Length;
21 | while (dataPointer < endPointer)
22 | {
23 | double z = (2.0 * Math.PI * i) / width;
24 | double wj = 0.2810639 - (0.5208972 * Math.Cos(z)) + (0.1980399 * Math.Cos(2 * z));
25 | *dataPointer = wj;
26 | dataPointer++;
27 | i++;
28 | }
29 | }
30 | }
31 | return window;
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/source/NSD.Benchmarks/Windows/WindowsBaseline.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace NSD
4 | {
5 | public static class Windows
6 | {
7 | /// Optimum overlap in ratio of width, used for Welch method
8 | /// Normalized Equivalent Noise BandWidth with unit of bins
9 | public static Memory HFT95(int width, out double optimumOverlap, out double NENBW)
10 | {
11 | // HFT95 - https://holometer.fnal.gov/GH_FFT.pdf
12 | // wj = 1 − 1.9383379 cos(z) + 1.3045202 cos(2z) − 0.4028270 cos(3z) + 0.0350665 cos(4z).
13 | optimumOverlap = 0.756;
14 | NENBW = 3.8112;
15 | Memory window = new double[width];
16 | for (int i = 0; i < width; i++)
17 | {
18 | double z = (2.0 * Math.PI * i) / width;
19 | double wj = 1 - (1.9383379 * Math.Cos(z)) + (1.3045202 * Math.Cos(2 * z)) - (0.4028270 * Math.Cos(3 * z)) + (0.0350665 * Math.Cos(4 * z));
20 | window.Span[i] = wj;
21 | }
22 | return window;
23 | }
24 |
25 | /// Optimum overlap in ratio of width, used for Welch method
26 | /// Normalized Equivalent Noise BandWidth with unit of bins
27 | public static Memory HFT90D(int width, out double optimumOverlap, out double NENBW)
28 | {
29 | // HFT90D - https://holometer.fnal.gov/GH_FFT.pdf
30 | // wj = 1 − 1.942604 cos(z) + 1.340318 cos(2z) − 0.440811 cos(3z) + 0.043097 cos(4z).
31 | optimumOverlap = 0.76;
32 | NENBW = 3.8832;
33 | Memory window = new double[width];
34 | for (int i = 0; i < width; i++)
35 | {
36 | double z = (2.0 * Math.PI * i) / width;
37 | double wj = 1 - (1.942604 * Math.Cos(z)) + (1.340318 * Math.Cos(2 * z)) - (0.440811 * Math.Cos(3 * z)) + (0.043097 * Math.Cos(4 * z));
38 | window.Span[i] = wj;
39 | }
40 |
41 | return window;
42 | }
43 |
44 | /// Optimum overlap in ratio of width, used for Welch method
45 | /// Normalized Equivalent Noise BandWidth with unit of bins
46 | public static Memory FTNI(int width, out double optimumOverlap, out double NENBW)
47 | {
48 | // FTNI - https://holometer.fnal.gov/GH_FFT.pdf
49 | // wj = 0.2810639 − 0.5208972 cos(z) + 0.1980399 cos(2z).
50 | optimumOverlap = 0.656;
51 | NENBW = 2.9656;
52 | Memory window = new double[width];
53 | for (int i = 0; i < width; i++)
54 | {
55 | double z = (2.0 * Math.PI * i) / width;
56 | double wj = 0.2810639 - (0.5208972 * Math.Cos(z)) + (0.1980399 * Math.Cos(2 * z));
57 | window.Span[i] = wj;
58 | }
59 | return window;
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/source/NSD.Generator/NSD.Generator.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net8.0
6 | enable
7 | enable
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/source/NSD.Generator/Program.cs:
--------------------------------------------------------------------------------
1 | using CsvHelper;
2 | using NSD;
3 | using System.Globalization;
4 |
5 | //var noise = Signals.WhiteNoise(1000000, 50, 1e-9);
6 | var noise = Signals.PowerLawGaussian(100000, 1, 1);
7 |
8 | using (StreamWriter writer = new(@$"1nV 1f 50SPS.csv"))
9 | using (CsvWriter csvWrite = new(writer, CultureInfo.InvariantCulture))
10 | {
11 | foreach (var sample in noise.Span)
12 | {
13 | csvWrite.WriteField(sample);
14 | csvWrite.NextRecord();
15 | }
16 | }
--------------------------------------------------------------------------------
/source/NSD.UI/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Ww][Ii][Nn]32/
27 | [Aa][Rr][Mm]/
28 | [Aa][Rr][Mm]64/
29 | bld/
30 | [Bb]in/
31 | [Oo]bj/
32 | [Ll]og/
33 | [Ll]ogs/
34 |
35 | # Visual Studio 2015/2017 cache/options directory
36 | .vs/
37 | # Uncomment if you have tasks that create the project's static files in wwwroot
38 | #wwwroot/
39 |
40 | # Visual Studio 2017 auto generated files
41 | Generated\ Files/
42 |
43 | # MSTest test Results
44 | [Tt]est[Rr]esult*/
45 | [Bb]uild[Ll]og.*
46 |
47 | # NUnit
48 | *.VisualState.xml
49 | TestResult.xml
50 | nunit-*.xml
51 |
52 | # Build Results of an ATL Project
53 | [Dd]ebugPS/
54 | [Rr]eleasePS/
55 | dlldata.c
56 |
57 | # Benchmark Results
58 | BenchmarkDotNet.Artifacts/
59 |
60 | # .NET Core
61 | project.lock.json
62 | project.fragment.lock.json
63 | artifacts/
64 |
65 | # Tye
66 | .tye/
67 |
68 | # ASP.NET Scaffolding
69 | ScaffoldingReadMe.txt
70 |
71 | # StyleCop
72 | StyleCopReport.xml
73 |
74 | # Files built by Visual Studio
75 | *_i.c
76 | *_p.c
77 | *_h.h
78 | *.ilk
79 | *.meta
80 | *.obj
81 | *.iobj
82 | *.pch
83 | *.pdb
84 | *.ipdb
85 | *.pgc
86 | *.pgd
87 | *.rsp
88 | *.sbr
89 | *.tlb
90 | *.tli
91 | *.tlh
92 | *.tmp
93 | *.tmp_proj
94 | *_wpftmp.csproj
95 | *.log
96 | *.vspscc
97 | *.vssscc
98 | .builds
99 | *.pidb
100 | *.svclog
101 | *.scc
102 |
103 | # Chutzpah Test files
104 | _Chutzpah*
105 |
106 | # Visual C++ cache files
107 | ipch/
108 | *.aps
109 | *.ncb
110 | *.opendb
111 | *.opensdf
112 | *.sdf
113 | *.cachefile
114 | *.VC.db
115 | *.VC.VC.opendb
116 |
117 | # Visual Studio profiler
118 | *.psess
119 | *.vsp
120 | *.vspx
121 | *.sap
122 |
123 | # Visual Studio Trace Files
124 | *.e2e
125 |
126 | # TFS 2012 Local Workspace
127 | $tf/
128 |
129 | # Guidance Automation Toolkit
130 | *.gpState
131 |
132 | # ReSharper is a .NET coding add-in
133 | _ReSharper*/
134 | *.[Rr]e[Ss]harper
135 | *.DotSettings.user
136 |
137 | # TeamCity is a build add-in
138 | _TeamCity*
139 |
140 | # DotCover is a Code Coverage Tool
141 | *.dotCover
142 |
143 | # AxoCover is a Code Coverage Tool
144 | .axoCover/*
145 | !.axoCover/settings.json
146 |
147 | # Coverlet is a free, cross platform Code Coverage Tool
148 | coverage*.json
149 | coverage*.xml
150 | coverage*.info
151 |
152 | # Visual Studio code coverage results
153 | *.coverage
154 | *.coveragexml
155 |
156 | # NCrunch
157 | _NCrunch_*
158 | .*crunch*.local.xml
159 | nCrunchTemp_*
160 |
161 | # MightyMoose
162 | *.mm.*
163 | AutoTest.Net/
164 |
165 | # Web workbench (sass)
166 | .sass-cache/
167 |
168 | # Installshield output folder
169 | [Ee]xpress/
170 |
171 | # DocProject is a documentation generator add-in
172 | DocProject/buildhelp/
173 | DocProject/Help/*.HxT
174 | DocProject/Help/*.HxC
175 | DocProject/Help/*.hhc
176 | DocProject/Help/*.hhk
177 | DocProject/Help/*.hhp
178 | DocProject/Help/Html2
179 | DocProject/Help/html
180 |
181 | # Click-Once directory
182 | publish/
183 |
184 | # Publish Web Output
185 | *.[Pp]ublish.xml
186 | *.azurePubxml
187 | # Note: Comment the next line if you want to checkin your web deploy settings,
188 | # but database connection strings (with potential passwords) will be unencrypted
189 | *.pubxml
190 | *.publishproj
191 |
192 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
193 | # checkin your Azure Web App publish settings, but sensitive information contained
194 | # in these scripts will be unencrypted
195 | PublishScripts/
196 |
197 | # NuGet Packages
198 | *.nupkg
199 | # NuGet Symbol Packages
200 | *.snupkg
201 | # The packages folder can be ignored because of Package Restore
202 | **/[Pp]ackages/*
203 | # except build/, which is used as an MSBuild target.
204 | !**/[Pp]ackages/build/
205 | # Uncomment if necessary however generally it will be regenerated when needed
206 | #!**/[Pp]ackages/repositories.config
207 | # NuGet v3's project.json files produces more ignorable files
208 | *.nuget.props
209 | *.nuget.targets
210 |
211 | # Microsoft Azure Build Output
212 | csx/
213 | *.build.csdef
214 |
215 | # Microsoft Azure Emulator
216 | ecf/
217 | rcf/
218 |
219 | # Windows Store app package directories and files
220 | AppPackages/
221 | BundleArtifacts/
222 | Package.StoreAssociation.xml
223 | _pkginfo.txt
224 | *.appx
225 | *.appxbundle
226 | *.appxupload
227 |
228 | # Visual Studio cache files
229 | # files ending in .cache can be ignored
230 | *.[Cc]ache
231 | # but keep track of directories ending in .cache
232 | !?*.[Cc]ache/
233 |
234 | # Others
235 | ClientBin/
236 | ~$*
237 | *~
238 | *.dbmdl
239 | *.dbproj.schemaview
240 | *.jfm
241 | *.pfx
242 | *.publishsettings
243 | orleans.codegen.cs
244 |
245 | # Including strong name files can present a security risk
246 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
247 | #*.snk
248 |
249 | # Since there are multiple workflows, uncomment next line to ignore bower_components
250 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
251 | #bower_components/
252 |
253 | # RIA/Silverlight projects
254 | Generated_Code/
255 |
256 | # Backup & report files from converting an old project file
257 | # to a newer Visual Studio version. Backup files are not needed,
258 | # because we have git ;-)
259 | _UpgradeReport_Files/
260 | Backup*/
261 | UpgradeLog*.XML
262 | UpgradeLog*.htm
263 | ServiceFabricBackup/
264 | *.rptproj.bak
265 |
266 | # SQL Server files
267 | *.mdf
268 | *.ldf
269 | *.ndf
270 |
271 | # Business Intelligence projects
272 | *.rdl.data
273 | *.bim.layout
274 | *.bim_*.settings
275 | *.rptproj.rsuser
276 | *- [Bb]ackup.rdl
277 | *- [Bb]ackup ([0-9]).rdl
278 | *- [Bb]ackup ([0-9][0-9]).rdl
279 |
280 | # Microsoft Fakes
281 | FakesAssemblies/
282 |
283 | # GhostDoc plugin setting file
284 | *.GhostDoc.xml
285 |
286 | # Node.js Tools for Visual Studio
287 | .ntvs_analysis.dat
288 | node_modules/
289 |
290 | # Visual Studio 6 build log
291 | *.plg
292 |
293 | # Visual Studio 6 workspace options file
294 | *.opt
295 |
296 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
297 | *.vbw
298 |
299 | # Visual Studio LightSwitch build output
300 | **/*.HTMLClient/GeneratedArtifacts
301 | **/*.DesktopClient/GeneratedArtifacts
302 | **/*.DesktopClient/ModelManifest.xml
303 | **/*.Server/GeneratedArtifacts
304 | **/*.Server/ModelManifest.xml
305 | _Pvt_Extensions
306 |
307 | # Paket dependency manager
308 | .paket/paket.exe
309 | paket-files/
310 |
311 | # FAKE - F# Make
312 | .fake/
313 |
314 | # CodeRush personal settings
315 | .cr/personal
316 |
317 | # Python Tools for Visual Studio (PTVS)
318 | __pycache__/
319 | *.pyc
320 |
321 | # Cake - Uncomment if you are using it
322 | # tools/**
323 | # !tools/packages.config
324 |
325 | # Tabs Studio
326 | *.tss
327 |
328 | # Telerik's JustMock configuration file
329 | *.jmconfig
330 |
331 | # BizTalk build output
332 | *.btp.cs
333 | *.btm.cs
334 | *.odx.cs
335 | *.xsd.cs
336 |
337 | # OpenCover UI analysis results
338 | OpenCover/
339 |
340 | # Azure Stream Analytics local run output
341 | ASALocalRun/
342 |
343 | # MSBuild Binary and Structured Log
344 | *.binlog
345 |
346 | # NVidia Nsight GPU debugger configuration file
347 | *.nvuser
348 |
349 | # MFractors (Xamarin productivity tool) working folder
350 | .mfractor/
351 |
352 | # Local History for Visual Studio
353 | .localhistory/
354 |
355 | # BeatPulse healthcheck temp database
356 | healthchecksdb
357 |
358 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
359 | MigrationBackup/
360 |
361 | # Ionide (cross platform F# VS Code tools) working folder
362 | .ionide/
363 |
364 | # Fody - auto-generated XML schema
365 | FodyWeavers.xsd
366 |
367 | ##
368 | ## Visual studio for Mac
369 | ##
370 |
371 |
372 | # globs
373 | Makefile.in
374 | *.userprefs
375 | *.usertasks
376 | config.make
377 | config.status
378 | aclocal.m4
379 | install-sh
380 | autom4te.cache/
381 | *.tar.gz
382 | tarballs/
383 | test-results/
384 |
385 | # Mac bundle stuff
386 | *.dmg
387 | *.app
388 |
389 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
390 | # General
391 | .DS_Store
392 | .AppleDouble
393 | .LSOverride
394 |
395 | # Icon must end with two \r
396 | Icon
397 |
398 |
399 | # Thumbnails
400 | ._*
401 |
402 | # Files that might appear in the root of a volume
403 | .DocumentRevisions-V100
404 | .fseventsd
405 | .Spotlight-V100
406 | .TemporaryItems
407 | .Trashes
408 | .VolumeIcon.icns
409 | .com.apple.timemachine.donotpresent
410 |
411 | # Directories potentially created on remote AFP share
412 | .AppleDB
413 | .AppleDesktop
414 | Network Trash Folder
415 | Temporary Items
416 | .apdisk
417 |
418 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
419 | # Windows thumbnail cache files
420 | Thumbs.db
421 | ehthumbs.db
422 | ehthumbs_vista.db
423 |
424 | # Dump file
425 | *.stackdump
426 |
427 | # Folder config file
428 | [Dd]esktop.ini
429 |
430 | # Recycle Bin used on file shares
431 | $RECYCLE.BIN/
432 |
433 | # Windows Installer files
434 | *.cab
435 | *.msi
436 | *.msix
437 | *.msm
438 | *.msp
439 |
440 | # Windows shortcuts
441 | *.lnk
442 |
443 | # JetBrains Rider
444 | .idea/
445 | *.sln.iml
446 |
447 | ##
448 | ## Visual Studio Code
449 | ##
450 | .vscode/*
451 | !.vscode/settings.json
452 | !.vscode/tasks.json
453 | !.vscode/launch.json
454 | !.vscode/extensions.json
455 |
--------------------------------------------------------------------------------
/source/NSD.UI/App.axaml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/source/NSD.UI/App.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Controls.ApplicationLifetimes;
3 | using Avalonia.Markup.Xaml;
4 | using MsBox.Avalonia;
5 | using MsBox.Avalonia.Enums;
6 | using System;
7 | using System.IO;
8 | using System.Threading.Tasks;
9 |
10 | namespace NSD.UI
11 | {
12 | public partial class App : Application
13 | {
14 | public override void Initialize()
15 | {
16 | AvaloniaXamlLoader.Load(this);
17 | SetupExceptionHandling();
18 | }
19 |
20 | private void SetupExceptionHandling()
21 | {
22 | AppDomain.CurrentDomain.UnhandledException += (s, e) =>
23 | LogUnhandledException((Exception)e.ExceptionObject, "AppDomain.CurrentDomain.UnhandledException");
24 |
25 | TaskScheduler.UnobservedTaskException += (s, e) =>
26 | {
27 | LogUnhandledException(e.Exception, "TaskScheduler.UnobservedTaskException");
28 | e.SetObserved();
29 | };
30 | }
31 |
32 | private async void LogUnhandledException(Exception exception, string source)
33 | {
34 | string message = $"Unhandled exception:\n\n{exception}\n";
35 | File.AppendAllText("error.txt", message);
36 | }
37 |
38 | public override void OnFrameworkInitializationCompleted()
39 | {
40 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
41 | desktop.MainWindow = new MainWindow();
42 | base.OnFrameworkInitializationCompleted();
43 | }
44 |
45 | public static int Main(string[] args)
46 | => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
47 |
48 | public static AppBuilder BuildAvaloniaApp()
49 | => AppBuilder.Configure()
50 | .UsePlatformDetect()
51 | .WithInterFont()
52 | .LogToTrace();
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/source/NSD.UI/LogDecadeMinorTickGenerator.cs:
--------------------------------------------------------------------------------
1 | using ScottPlot;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 |
6 | namespace NSD.UI
7 | {
8 | public class LogDecadeMinorTickGenerator : IMinorTickGenerator
9 | {
10 | public int TicksPerDecade { get; set; } = 10;
11 |
12 | public IEnumerable GetMinorTicks(double[] majorPositions, CoordinateRange visibleRange)
13 | {
14 | var minDecadeTick = Math.Floor(visibleRange.Min);
15 | var maxDecadeTick = Math.Ceiling(visibleRange.Max);
16 |
17 | if(minDecadeTick == maxDecadeTick)
18 | return [];
19 |
20 | // pre-calculate the log-distributed offset positions between major ticks
21 | IEnumerable minorTickOffsets = Enumerable.Range(1, TicksPerDecade - 1)
22 | .Select(x => Math.Log10(x * 10 / TicksPerDecade));
23 |
24 | // iterate major ticks and collect minor ticks with offsets
25 | List minorTicks = [];
26 | for (double major = minDecadeTick; major <= maxDecadeTick; major += 1)
27 | {
28 | minorTicks.AddRange(minorTickOffsets.Select(offset => major + offset));
29 | }
30 |
31 | return minorTicks;
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/source/NSD.UI/MainWindow.axaml:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | CSV has header?
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | NPLC (50Hz)
62 | NPLC (60Hz)
63 | s
64 | ms
65 | μs
66 | ns
67 | SPS
68 | kSPS
69 | MSPS
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | Logarithmic
80 | Linear
81 | Linear stacking
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | 2
95 | 4
96 | 8
97 | 16
98 | 32
99 | 64
100 | 128
101 | 256
102 | 512
103 | 1024
104 |
105 |
106 |
107 |
108 |
109 | 64
110 | 128
111 | 256
112 | 512
113 | 1024
114 | 2048
115 | 4096
116 | 8192
117 | 16384
118 | 32768
119 | 65536
120 | 131072
121 | 262144
122 | 524288
123 | 1048576
124 |
125 |
126 |
127 |
128 |
129 | 64
130 | 128
131 | 256
132 | 512
133 | 1024
134 | 2048
135 | 4096
136 | 8192
137 | 16384
138 | 32768
139 | 65536
140 | 131072
141 | 262144
142 | 524288
143 | 1048576
144 |
145 |
146 |
147 | 64
148 | 128
149 | 256
150 | 512
151 | 1024
152 | 2048
153 | 4096
154 | 8192
155 | 16384
156 | 32768
157 | 65536
158 | 131072
159 | 262144
160 | 524288
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
--------------------------------------------------------------------------------
/source/NSD.UI/MainWindow.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia.Controls;
2 | using Avalonia.Interactivity;
3 | using CsvHelper.Configuration;
4 | using MsBox.Avalonia;
5 | using ScottPlot.TickGenerators;
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Globalization;
9 | using System.IO;
10 | using System.Linq;
11 | using System.Runtime.InteropServices;
12 | using System.Text.RegularExpressions;
13 | using System.Threading.Tasks;
14 |
15 | namespace NSD.UI
16 | {
17 | public partial class MainWindow : Window
18 | {
19 | private readonly MainWindowViewModel viewModel;
20 | private Spectrum spectrum = new();
21 | private Settings settings;
22 |
23 | public MainWindow()
24 | {
25 | InitializeComponent();
26 | settings = Settings.Load();
27 | viewModel = new(settings, this);
28 | DataContext = viewModel;
29 | InitNsdChart();
30 | }
31 |
32 | public async void BtnSearch_Click(object sender, RoutedEventArgs e)
33 | {
34 | if (!Directory.Exists(viewModel.ProcessWorkingFolder))
35 | {
36 | var messageBoxStandardWindow = MessageBoxManager.GetMessageBoxStandard("Folder not found", "Search folder not found");
37 | await messageBoxStandardWindow.ShowAsync();
38 | return;
39 | }
40 |
41 | viewModel.InputFilePaths.Clear();
42 | viewModel.InputFileNames.Clear();
43 | var files = Directory.EnumerateFiles(viewModel.ProcessWorkingFolder, "*.csv");
44 | foreach (var file in files)
45 | {
46 | viewModel.InputFilePaths.Add(file);
47 | viewModel.InputFileNames.Add(Path.GetFileName(file));
48 | }
49 | files = Directory.EnumerateFiles(viewModel.ProcessWorkingFolder, "*.f32"); // Hidden functionality that supports F32 bin files
50 | foreach (var file in files)
51 | {
52 | viewModel.InputFilePaths.Add(file);
53 | viewModel.InputFileNames.Add(Path.GetFileName(file));
54 | }
55 |
56 | viewModel.SelectedInputFileIndex = 0;
57 | }
58 |
59 | public async void btnRun_Click(object sender, RoutedEventArgs e)
60 | {
61 | viewModel.Enabled = false;
62 | try
63 | {
64 | var path = viewModel.GetSelectedInputFilePath();
65 | if (!File.Exists(path))
66 | {
67 | viewModel.Status = "Error: Input CSV file not found";
68 | return;
69 | }
70 | if (!double.TryParse(viewModel.AcquisitionTime, out double acquisitionTime))
71 | {
72 | viewModel.Status = "Error: Invalid acquisition time value";
73 | return;
74 | }
75 | double acquisitionTimeSeconds = (string)(viewModel.SelectedAcquisitionTimebaseItem).Content switch
76 | {
77 | "NPLC (50Hz)" => acquisitionTime * (1.0 / 50.0),
78 | "NPLC (60Hz)" => acquisitionTime * (1.0 / 60.0),
79 | "s" => acquisitionTime,
80 | "ms" => acquisitionTime / 1e3,
81 | "μs" => acquisitionTime / 1e6,
82 | "ns" => acquisitionTime / 1e9,
83 | "SPS" => 1.0 / acquisitionTime,
84 | "kSPS" => 1.0 / (acquisitionTime * 1000.0),
85 | "MSPS" => 1.0 / (acquisitionTime * 1000000.0),
86 | _ => throw new ApplicationException("Acquisition time combobox value not handled")
87 | };
88 | //if (!double.TryParse(viewModel.DataRate, out double dataRateTime))
89 | //{
90 | // viewModel.Status = "Error: Invalid data rate value";
91 | // return;
92 | //}
93 | //double dataRateTimeSeconds = (string)viewModel.SelectedDataRateUnitItem.Content switch
94 | //{
95 | // "Samples per second" => 1.0 / dataRateTime,
96 | // "Seconds per sample" => dataRateTime,
97 | // _ => throw new ApplicationException("Data rate combobox value not handled")
98 | //};
99 |
100 | switch ((string)viewModel.SelectedNsdAlgorithm.Content)
101 | {
102 | case "Logarithmic":
103 | break;
104 | case "Linear":
105 | break;
106 | case "Linear stacking":
107 | {
108 | var fftWidth = int.Parse((string)viewModel.SelectedLinearStackingLengthItem.Content);
109 | var stackingFftWidth = int.Parse((string)viewModel.SelectedLinearStackingMinLengthItem.Content);
110 | if (stackingFftWidth >= fftWidth)
111 | {
112 | viewModel.Status = "Error: Invalid minimum stacking FFT width";
113 | return;
114 | }
115 | break;
116 | }
117 | }
118 |
119 | if (!double.TryParse(viewModel.InputScaling, out double inputScaling))
120 | {
121 | viewModel.Status = "Error: Invalid input scaling value";
122 | return;
123 | }
124 |
125 | viewModel.Status = "Status: Loading CSV...";
126 |
127 | using var stream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
128 | List records = [];
129 | DateTimeOffset fileParseStart = DateTimeOffset.UtcNow;
130 | DateTimeOffset fileParseFinish = DateTimeOffset.UtcNow;
131 |
132 | switch (Path.GetExtension(path))
133 | {
134 | case ".csv":
135 | {
136 | //using var reader = new StreamReader(stream);
137 | //using var csv = new CsvReader(reader, CultureInfo.InvariantCulture);
138 | //var records = await csv.GetRecordsAsync().ToListAsync();
139 |
140 | fileParseStart = DateTimeOffset.UtcNow;
141 | await Task.Run(() =>
142 | {
143 | using var streamReader = new StreamReader(stream);
144 | var csvReader = new NReco.Csv.CsvReader(streamReader, ",");
145 | if (viewModel.CsvHasHeader)
146 | csvReader.Read();
147 | int columnIndex = viewModel.CsvColumnIndex;
148 | while (csvReader.Read())
149 | {
150 | var number = double.Parse(csvReader[columnIndex]);
151 | if (number > 1e12) // Catches the overrange samples from DMM6500
152 | continue;
153 | records.Add(number);
154 | }
155 | });
156 | fileParseFinish = DateTimeOffset.UtcNow;
157 | break;
158 | }
159 | case ".f32":
160 | {
161 | fileParseStart = DateTimeOffset.UtcNow;
162 | await Task.Run(() =>
163 | {
164 | const int ChunkSizeBytes = 8 * 1024 * 1024; // 8 MiB
165 | byte[] buffer = new byte[ChunkSizeBytes];
166 | int bytesRead;
167 |
168 | while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0)
169 | {
170 | // Only process full float32 values (4 bytes each)
171 | int validBytes = bytesRead - (bytesRead % 4);
172 | if (validBytes == 0)
173 | continue;
174 |
175 | ReadOnlySpan byteSpan = buffer.AsSpan(0, validBytes);
176 | ReadOnlySpan floatSpan = MemoryMarshal.Cast(byteSpan);
177 |
178 | foreach (var f32 in floatSpan)
179 | {
180 | records.Add(f32);
181 | }
182 | }
183 | });
184 | fileParseFinish = DateTimeOffset.UtcNow;
185 | break;
186 | }
187 | }
188 |
189 | if (records.Count == 0)
190 | {
191 | viewModel.Status = "Error: No CSV records found";
192 | return;
193 | }
194 |
195 | switch ((string)viewModel.SelectedNsdAlgorithm.Content)
196 | {
197 | case "Logarithmic":
198 | break;
199 | case "Linear":
200 | {
201 | var fftWidth = int.Parse((string)viewModel.SelectedLinearLengthItem.Content);
202 | if (fftWidth > records.Count)
203 | {
204 | viewModel.Status = "Error: FFT width is longer than input data";
205 | return;
206 | }
207 | break;
208 | }
209 | case "Linear stacking":
210 | {
211 | var fftWidth = int.Parse((string)viewModel.SelectedLinearStackingLengthItem.Content);
212 | if (fftWidth > records.Count)
213 | {
214 | viewModel.Status = "Error: FFT width is longer than input data";
215 | return;
216 | }
217 | break;
218 | }
219 | }
220 |
221 |
222 | //records = Signals.WhiteNoise(100000, sampleRate, 1e-9).ToArray().ToList();
223 |
224 | viewModel.Status = "Status: Calculating NSD...";
225 |
226 | for (int i = 0; i < records.Count; i++)
227 | {
228 | records[i] *= inputScaling;
229 | }
230 |
231 | DateTimeOffset nsdComputeStart = DateTimeOffset.UtcNow;
232 | //double spectralValueCorrection = Math.Sqrt(dataRateTimeSeconds / acquisitionTimeSeconds);
233 | //double spectralValueCorrection = 1.0;
234 | //double frequencyBinCorrection = Math.Sqrt(dataRateTimeSeconds / acquisitionTimeSeconds);
235 | //double frequencyBinCorrection = 1.0;
236 | switch ((string)viewModel.SelectedNsdAlgorithm.Content)
237 | {
238 | case "Logarithmic":
239 | {
240 | var pointsPerDecade = int.Parse(viewModel.LogNsdPointsDecade);
241 | var pointsPerDecadeScaling = double.Parse(viewModel.LogNsdPointsDecadeScaling);
242 | var minAverages = int.Parse(viewModel.LogNsdMinAverages);
243 | //var minLength = int.Parse(viewModel.LogNsdMinLength);
244 | var minLength = int.Parse((string)viewModel.SelectedLogNsdMinLength.Content);
245 | var nsd = await Task.Factory.StartNew(() => NSD.Log(
246 | input: records.ToArray(),
247 | sampleRateHz: 1.0 / acquisitionTimeSeconds,
248 | freqMin: viewModel.XMin,
249 | freqMax: viewModel.XMax,
250 | pointsPerDecade,
251 | minAverages,
252 | minLength,
253 | pointsPerDecadeScaling));
254 | spectrum = nsd;
255 | break;
256 | }
257 | case "Linear":
258 | {
259 | var fftWidth = int.Parse((string)viewModel.SelectedLinearLengthItem.Content);
260 | var nsd = await Task.Factory.StartNew(() => NSD.Linear(input: records.ToArray(), 1.0 / acquisitionTimeSeconds, outputWidth: fftWidth));
261 | spectrum = nsd;
262 | break;
263 | }
264 | case "Linear stacking":
265 | {
266 | var fftMaxWidth = int.Parse((string)viewModel.SelectedLinearStackingLengthItem.Content);
267 | var fftMinWidth = int.Parse((string)viewModel.SelectedLinearStackingMinLengthItem.Content);
268 | var nsd = await Task.Factory.StartNew(() => NSD.StackedLinear(input: records.ToArray(), 1.0 / acquisitionTimeSeconds, maxWidth: fftMaxWidth, minWidth: fftMinWidth));
269 | spectrum = nsd;
270 | break;
271 | }
272 | }
273 | DateTimeOffset nsdComputeFinish = DateTimeOffset.UtcNow;
274 | Memory yArray;
275 | if (viewModel.SgFilterChecked)
276 | yArray = new SavitzkyGolayFilter(5, 1).Process(spectrum.Values.Span);
277 | else
278 | yArray = spectrum.Values;
279 |
280 | //for (int i = 0; i < yArray.Length; i++)
281 | //{
282 | // yArray.Span[i] /= spectralValueCorrection;
283 | //}
284 |
285 | //for (int i = 0; i < spectrum.Frequencies.Length; i++)
286 | //{
287 | // spectrum.Frequencies.Span[i] *= frequencyBinCorrection;
288 | //}
289 |
290 | UpdateNSDChart(spectrum.Frequencies, yArray);
291 | var fileParseTimeSec = fileParseFinish.Subtract(fileParseStart).TotalSeconds;
292 | var nsdComputeTimeSec = nsdComputeFinish.Subtract(nsdComputeStart).TotalSeconds;
293 | if (spectrum.Stacking > 1)
294 | viewModel.Status = $"Status: Processing complete, {records.Count} input points, averaged {spectrum.Averages} spectrums over {spectrum.Stacking} stacking FFT widths. File parse time: {fileParseTimeSec:F3}s, NSD compute time: {nsdComputeTimeSec:F3}s.";
295 | else
296 | viewModel.Status = $"Status: Processing complete, {records.Count} input points, averaged {spectrum.Averages} spectrums. File parse time: {fileParseTimeSec:F3}s, NSD compute time: {nsdComputeTimeSec:F3}s.";
297 | }
298 | catch (Exception ex)
299 | {
300 | await ShowError("Exception", ex.Message);
301 | }
302 | finally
303 | {
304 | viewModel.Enabled = true;
305 | }
306 | }
307 |
308 | public class NsdSample
309 | {
310 | public double Frequency { get; set; }
311 | public double Noise { get; set; }
312 | }
313 |
314 | public async void BtnGenerate_Click(object sender, RoutedEventArgs e)
315 | {
316 | var outputFilePath = Path.Combine(viewModel.ProcessWorkingFolder, viewModel.OutputFileName);
317 |
318 | CsvConfiguration config = new(CultureInfo.InvariantCulture);
319 | config.Delimiter = ",";
320 | using var writer = new StreamWriter(outputFilePath);
321 | using var csvWriter = new CsvHelper.CsvWriter(writer, config);
322 | csvWriter.WriteHeader();
323 | csvWriter.NextRecord();
324 |
325 | for (int i = 0; i < spectrum.Frequencies.Length; i++)
326 | {
327 | csvWriter.WriteField(spectrum.Frequencies.Span[i]);
328 | csvWriter.WriteField(spectrum.Values.Span[i]);
329 | csvWriter.NextRecord();
330 | }
331 | }
332 |
333 | private void btnSetAxis_Click(object sender, RoutedEventArgs e)
334 | {
335 | if (spectrum != null)
336 | {
337 | Memory yArray;
338 | if (viewModel.SgFilterChecked)
339 | yArray = new SavitzkyGolayFilter(5, 1).Process(spectrum.Values.Span);
340 | else
341 | yArray = spectrum.Values;
342 | UpdateNSDChart(spectrum.Frequencies, yArray);
343 | }
344 | SetChartLimitsAndRefresh();
345 | }
346 |
347 | public void UpdateNSDChart(Memory x, Memory y)
348 | {
349 | WpfPlot1.Plot.Clear();
350 | double[] logXs = x.ToArray().Select(pt => Math.Log10(pt)).ToArray();
351 | double[] logYs = y.ToArray().Select(pt => Math.Log10(pt * 1E9)).ToArray();
352 | var scatter = WpfPlot1.Plot.Add.ScatterLine(logXs, logYs);
353 | //var scatter = WpfPlot1.Plot.Add.Scatter(logXs, logYs);
354 | if (viewModel.MarkersChecked)
355 | {
356 | scatter.MarkerStyle.Shape = ScottPlot.MarkerShape.FilledCircle;
357 | scatter.MarkerStyle.Size = 3;
358 | scatter.MarkerStyle.IsVisible = true;
359 | }
360 | CommonChartConfig();
361 | }
362 |
363 | private void InitNsdChart()
364 | {
365 | WpfPlot1.Plot.Clear();
366 | CommonChartConfig();
367 | }
368 |
369 | private void CommonChartConfig()
370 | {
371 | static string logTickLabels(double y) => Math.Pow(10, y).ToString(); // "N0"
372 | NumericAutomatic xTickGenerator = new()
373 | {
374 | LabelFormatter = logTickLabels,
375 | MinorTickGenerator = new LogDecadeMinorTickGenerator() { TicksPerDecade = 10 },
376 | IntegerTicksOnly = true,
377 | TargetTickCount = 10
378 | };
379 | NumericAutomatic yTickGenerator = new()
380 | {
381 | LabelFormatter = logTickLabels,
382 | MinorTickGenerator = new LogDecadeMinorTickGenerator() { TicksPerDecade = 10 },
383 | IntegerTicksOnly = true,
384 | TargetTickCount = 10
385 | };
386 | WpfPlot1.Plot.Axes.Bottom.TickGenerator = xTickGenerator;
387 | WpfPlot1.Plot.Axes.Left.TickGenerator = yTickGenerator;
388 | WpfPlot1.Plot.Axes.Hairline(true);
389 | WpfPlot1.Plot.XLabel("Frequency (Hz)", 14);
390 | WpfPlot1.Plot.YLabel("Noise (nV/rHz)", 14);
391 | WpfPlot1.Plot.Axes.Bottom.Label.Bold = false;
392 | WpfPlot1.Plot.Axes.Left.Label.Bold = false;
393 | WpfPlot1.Plot.Title("NSD estimation", size: 14);
394 | WpfPlot1.Plot.Axes.Title.Label.Bold = false;
395 | WpfPlot1.Plot.Grid.MinorLineWidth = 1;
396 | WpfPlot1.Plot.Grid.MinorLineColor = ScottPlot.Color.FromARGB(0x14000000);
397 | WpfPlot1.Plot.Grid.MajorLineColor = ScottPlot.Color.FromARGB(0x50000000);
398 | SetChartLimitsAndRefresh();
399 | }
400 |
401 | private void SetChartLimitsAndRefresh()
402 | {
403 | double fudgeFactor = 0.001;
404 | var left = Math.Log10(viewModel.XMin - (viewModel.XMin * fudgeFactor));
405 | var right = Math.Log10(viewModel.XMax + (viewModel.XMax * fudgeFactor));
406 | var top = Math.Log10(viewModel.YMax + (viewModel.YMax * fudgeFactor));
407 | var bottom = Math.Log10(viewModel.YMin - (viewModel.YMin * fudgeFactor));
408 | WpfPlot1.Plot.Axes.SetLimits(left, right, bottom, top);
409 | WpfPlot1.Refresh();
410 | }
411 |
412 | private async Task ShowError(string title, string message)
413 | {
414 | var messageBoxStandardWindow = MessageBoxManager.GetMessageBoxStandard(title, message);
415 | await messageBoxStandardWindow.ShowAsync();
416 | }
417 | }
418 | }
419 |
--------------------------------------------------------------------------------
/source/NSD.UI/MainWindowViewModel.cs:
--------------------------------------------------------------------------------
1 | using Avalonia.Controls;
2 | using Avalonia.Media;
3 | using CommunityToolkit.Mvvm.ComponentModel;
4 | using System;
5 | using System.Collections.ObjectModel;
6 | using System.Reflection;
7 |
8 | namespace NSD.UI
9 | {
10 | // https://docs.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/generators/overview
11 | public partial class MainWindowViewModel : ObservableObject
12 | {
13 | // Loaded from settings file
14 | [ObservableProperty] string? processWorkingFolder;
15 | [ObservableProperty] string? acquisitionTime;
16 |
17 | [ObservableProperty] string status = "Status: Idle";
18 | [ObservableProperty] bool enabled = true;
19 | [ObservableProperty] ObservableCollection inputFilePaths = new();
20 | [ObservableProperty] ObservableCollection inputFileNames = new();
21 | [ObservableProperty] int selectedInputFileIndex = -1;
22 | [ObservableProperty] string outputFileName = "output.nsd";
23 | [ObservableProperty] bool sgFilterChecked = false;
24 | [ObservableProperty] bool markersChecked = false;
25 | [ObservableProperty] IBrush statusBackground = Brushes.WhiteSmoke;
26 | [ObservableProperty] string inputScaling = "1.0";
27 | [ObservableProperty] string logNsdPointsDecade = "5";
28 | [ObservableProperty] string logNsdPointsDecadeScaling = "2.0";
29 | [ObservableProperty] string logNsdMinAverages = "1";
30 | [ObservableProperty] string logNsdMinLength = "128";
31 |
32 | public ComboBoxItem? SelectedAcquisitionTimebaseItem { get; set; }
33 |
34 | private ComboBoxItem? selectedNsdAlgorithm;
35 | public ComboBoxItem? SelectedNsdAlgorithm
36 | {
37 | get => selectedNsdAlgorithm; set
38 | {
39 | selectedNsdAlgorithm = value;
40 | switch ((string)selectedNsdAlgorithm.Content)
41 | {
42 | case "Logarithmic":
43 | AlgorithmLog = true;
44 | AlgorithmLin = false;
45 | AlgorithmLinStack = false;
46 | break;
47 | case "Linear":
48 | AlgorithmLog = false;
49 | AlgorithmLin = true;
50 | AlgorithmLinStack = false;
51 | break;
52 | case "Linear stacking":
53 | AlgorithmLog = false;
54 | AlgorithmLin = false;
55 | AlgorithmLinStack = true;
56 | break;
57 | }
58 | }
59 | }
60 | public ComboBoxItem? SelectedLinearLengthItem { get; set; }
61 | public ComboBoxItem? SelectedLinearStackingLengthItem { get; set; }
62 | public ComboBoxItem? SelectedLinearStackingMinLengthItem { get; set; }
63 | public ComboBoxItem? SelectedLogNsdMinLength { get; set; }
64 | [ObservableProperty] bool algorithmLog = true; // Controls visibility of sub-stack panel
65 | [ObservableProperty] bool algorithmLin = false; // Controls visibility of sub-stack panel
66 | [ObservableProperty] bool algorithmLinStack = false; // Controls visibility of sub-stack panel
67 |
68 | public ComboBoxItem? SelectedFileFormatItem { get; set; }
69 | public double XMin { get; set; } = 0.001;
70 | public double XMax { get; set; } = 100;
71 | public double YMin { get; set; } = 0.1;
72 | public double YMax { get; set; } = 100;
73 | public string WindowTitle { get { Version version = Assembly.GetExecutingAssembly().GetName().Version; return "NSD v" + version.Major + "." + version.Minor; } }
74 | [ObservableProperty] bool csvHasHeader = false;
75 | [ObservableProperty] int csvColumnIndex = 0;
76 |
77 | private Settings settings;
78 |
79 |
80 | public MainWindowViewModel(Settings settings, MainWindow window)
81 | {
82 | this.settings = settings;
83 | processWorkingFolder = settings.ProcessWorkingFolder;
84 | acquisitionTime = settings.AcquisitionTime;
85 |
86 | window.cbTime.SelectedIndex = settings.AcquisitionTimeUnit switch
87 | {
88 | "NPLC (50Hz)" => 0,
89 | "NPLC (60Hz)" => 1,
90 | "s" => 2,
91 | "ms" => 3,
92 | "μs" => 4,
93 | "ns" => 5,
94 | "SPS" => 6,
95 | "kSPS" => 7,
96 | "MSPS" => 8,
97 | //_ => throw new Exception("Invalid AcquisitionTimeUnit")
98 | _ => 0
99 | };
100 | }
101 |
102 | partial void OnProcessWorkingFolderChanged(string? value)
103 | {
104 | settings.ProcessWorkingFolder = value;
105 | settings.Save();
106 | }
107 |
108 | partial void OnAcquisitionTimeChanged(string? value)
109 | {
110 | settings.AcquisitionTime = value;
111 | settings.Save();
112 | }
113 |
114 | partial void OnStatusChanged(string value)
115 | {
116 | if (value.Contains("Error"))
117 | {
118 | StatusBackground = Brushes.Red;
119 | }
120 | else
121 | {
122 | StatusBackground = Brushes.WhiteSmoke;
123 | }
124 | }
125 |
126 | public string GetSelectedInputFilePath()
127 | {
128 | if (inputFilePaths.Count > 0 && selectedInputFileIndex < inputFilePaths.Count)
129 | return inputFilePaths[selectedInputFileIndex];
130 | else
131 | return "";
132 | }
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/source/NSD.UI/NSD.UI.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | WinExe
4 | net8.0
5 | 1.4
6 | enable
7 | true
8 | en
9 | true
10 | true
11 | true
12 | partial
13 | true
14 | true
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/source/NSD.UI/Settings.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Text.Json;
4 | using System.Text.Json.Serialization;
5 |
6 | namespace NSD.UI
7 | {
8 | [JsonSourceGenerationOptions(WriteIndented = true)]
9 | [JsonSerializable(typeof(Settings))]
10 | internal partial class SourceGenerationContext : JsonSerializerContext
11 | {
12 | }
13 |
14 | public class Settings
15 | {
16 | public string? ProcessWorkingFolder { get; set; }
17 | public string? AcquisitionTime { get; set; }
18 | public string? AcquisitionTimeUnit { get; set; }
19 | public string? DataRate { get; set; }
20 | public string? DataRateUnit { get; set; }
21 |
22 | public static Settings Default()
23 | {
24 | return new Settings()
25 | {
26 | ProcessWorkingFolder = Directory.GetCurrentDirectory(),
27 | AcquisitionTime = "1",
28 | AcquisitionTimeUnit = "NPLC (50Hz)",
29 | DataRate = "50",
30 | DataRateUnit = "Samples per second"
31 | };
32 | }
33 |
34 | public static Settings Load()
35 | {
36 | if (!File.Exists("settings.json"))
37 | return Default();
38 | var json = File.ReadAllText("settings.json");
39 | if (json.Contains("SampleRate"))
40 | return Default(); // Ignore old settings file
41 | if (string.IsNullOrWhiteSpace(json))
42 | return Default();
43 | var settings = JsonSerializer.Deserialize(json, SourceGenerationContext.Default.Settings);
44 | if (settings != null)
45 | return settings;
46 | else
47 | return Default();
48 | }
49 |
50 | public void Save()
51 | {
52 | var json = JsonSerializer.Serialize(this, SourceGenerationContext.Default.Settings);
53 | File.WriteAllText("settings.json", json);
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/source/NSD.UI/runtimeconfig.template.json:
--------------------------------------------------------------------------------
1 | {
2 | "configProperties": {
3 | "System.Drawing.EnableUnixSupport": true
4 | }
5 | }
--------------------------------------------------------------------------------
/source/NSD.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.1.32421.90
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NSD.UI", "NSD.UI\NSD.UI.csproj", "{A5492109-BC61-49D4-A830-300B844F8A67}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NSD", "NSD\NSD.csproj", "{EBA91A52-FAE6-4E77-8714-79CBE5A2EEB8}"
9 | EndProject
10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NSD.Generator", "NSD.Generator\NSD.Generator.csproj", "{37A8D3A3-4EA6-4EEF-A094-5BFECF33C4C7}"
11 | EndProject
12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NSD.Benchmarks", "NSD.Benchmarks\NSD.Benchmarks.csproj", "{CAE57A71-6251-4BF3-82F9-9546AB76EC64}"
13 | EndProject
14 | Global
15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
16 | Debug|Any CPU = Debug|Any CPU
17 | Release|Any CPU = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
20 | {A5492109-BC61-49D4-A830-300B844F8A67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {A5492109-BC61-49D4-A830-300B844F8A67}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {A5492109-BC61-49D4-A830-300B844F8A67}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {A5492109-BC61-49D4-A830-300B844F8A67}.Release|Any CPU.Build.0 = Release|Any CPU
24 | {EBA91A52-FAE6-4E77-8714-79CBE5A2EEB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
25 | {EBA91A52-FAE6-4E77-8714-79CBE5A2EEB8}.Debug|Any CPU.Build.0 = Debug|Any CPU
26 | {EBA91A52-FAE6-4E77-8714-79CBE5A2EEB8}.Release|Any CPU.ActiveCfg = Release|Any CPU
27 | {EBA91A52-FAE6-4E77-8714-79CBE5A2EEB8}.Release|Any CPU.Build.0 = Release|Any CPU
28 | {37A8D3A3-4EA6-4EEF-A094-5BFECF33C4C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
29 | {37A8D3A3-4EA6-4EEF-A094-5BFECF33C4C7}.Debug|Any CPU.Build.0 = Debug|Any CPU
30 | {37A8D3A3-4EA6-4EEF-A094-5BFECF33C4C7}.Release|Any CPU.ActiveCfg = Release|Any CPU
31 | {37A8D3A3-4EA6-4EEF-A094-5BFECF33C4C7}.Release|Any CPU.Build.0 = Release|Any CPU
32 | {CAE57A71-6251-4BF3-82F9-9546AB76EC64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
33 | {CAE57A71-6251-4BF3-82F9-9546AB76EC64}.Debug|Any CPU.Build.0 = Debug|Any CPU
34 | {CAE57A71-6251-4BF3-82F9-9546AB76EC64}.Release|Any CPU.ActiveCfg = Release|Any CPU
35 | {CAE57A71-6251-4BF3-82F9-9546AB76EC64}.Release|Any CPU.Build.0 = Release|Any CPU
36 | EndGlobalSection
37 | GlobalSection(SolutionProperties) = preSolution
38 | HideSolutionNode = FALSE
39 | EndGlobalSection
40 | GlobalSection(ExtensibilityGlobals) = postSolution
41 | SolutionGuid = {D5FEEFF6-2F41-4325-9E64-D2FBC754142E}
42 | EndGlobalSection
43 | EndGlobal
44 |
--------------------------------------------------------------------------------
/source/NSD/Data types/Spectrum.cs:
--------------------------------------------------------------------------------
1 | public class Spectrum
2 | {
3 | public Memory Frequencies { get; set; }
4 | public Memory Values { get; set; }
5 | public int Averages { get; set; }
6 | public int Stacking { get; set; } = 1;
7 |
8 | public static Spectrum FromValues(Memory values, double sampleRate, int averages)
9 | {
10 | int length = (values.Length / 2);
11 | Memory frequencies = new double[length];
12 | double dT = (sampleRate / values.Length);
13 | for (int i = 0; i < length; i++)
14 | {
15 | frequencies.Span[i] = i * dT;
16 | }
17 | return new Spectrum() { Frequencies = frequencies, Values = values.Slice(0, length), Averages = averages };
18 | }
19 |
20 | bool trimmedDC = false;
21 | public void TrimDC()
22 | {
23 | if (!trimmedDC)
24 | {
25 | trimmedDC = true;
26 | Frequencies = Frequencies[1..];
27 | Values = Values[1..];
28 | }
29 | }
30 |
31 | // It is well known that windowing of data segments is necessary in the WOSA method to reduce the bias
32 | // of the spectral estimate[14]. When calculating onesided spectral estimates containing only positive
33 | // Fourier frequencies windowing causes a bias at low frequency bins—a fact that is also well known:
34 | // one cannot trust the lowest frequency bins on the spectrum analyzer.The bias stems from aliasing of
35 | // power from negative bins and bin zero to the lowest positive frequency bins.Aliasing from bin zero can
36 | // be eliminated by subtracting the mean data value from the segment.Aliasing from negative bins however, cannot be reduced that way.Hence we propose
37 | // not to use the first few frequency bins.The first frequency bin that yields unbiased spectral estimates
38 | // depends on the window function used.The bin is given by the effective half-width of the window
39 | // transfer function.
40 | int trimmedStartBins = 0;
41 | public void TrimStart(int bins)
42 | {
43 | if (trimmedStartBins != 0)
44 | throw new Exception($"TrimStart already called with bins: {trimmedStartBins}");
45 | trimmedStartBins = bins;
46 | Frequencies = Frequencies.Slice(bins, Frequencies.Length - bins);
47 | Values = Values.Slice(bins, Values.Length - bins);
48 | }
49 | }
--------------------------------------------------------------------------------
/source/NSD/FFT.cs:
--------------------------------------------------------------------------------
1 | using System.Numerics;
2 |
3 | namespace NSD
4 | {
5 | internal class FFT
6 | {
7 | private readonly int length;
8 | private readonly Complex[] complexBuffer;
9 | private readonly FftFlat.FastFourierTransform fft;
10 |
11 | public FFT(int length)
12 | {
13 | this.length = length;
14 | complexBuffer = new Complex[length];
15 | fft = new FftFlat.FastFourierTransform(length);
16 | }
17 |
18 | public void PSD(ReadOnlyMemory inputData, Memory outputPsd, ReadOnlyMemory window, double sampleRate, double windowS2)
19 | {
20 | if (inputData.Length != length || outputPsd.Length != length || window.Length != length)
21 | throw new ArgumentException("Array lengths don't match");
22 |
23 | // Apply window to data
24 | for (int i = 0; i < length; i++)
25 | {
26 | complexBuffer[i] = new Complex(inputData.Span[i] * window.Span[i], 0);
27 | }
28 |
29 | // Apply transform
30 | //MathNet.Numerics.IntegralTransforms.Fourier.Forward(fourierData, MathNet.Numerics.IntegralTransforms.FourierOptions.NoScaling);
31 | fft.Forward(complexBuffer);
32 |
33 | // Convert to magnitude spectrum
34 | for (int i = 0; i < length; i++)
35 | {
36 | outputPsd.Span[i] = (2.0 * Math.Pow(complexBuffer[i].Magnitude, 2)) / (sampleRate * windowS2); //"The factor 2 originates from the fact that we presumably use an efficient FFT algorithm that does not compute the redundant results for negative frequencies"
37 | //outputPsd.Span[i] = (Math.Pow(Math.Abs(fourierData[i].Magnitude), 2)) / (sampleRate * s2);
38 | if (double.IsNaN(outputPsd.Span[i]) || outputPsd.Span[i] > 1000000000000)
39 | throw new Exception();
40 | }
41 | }
42 |
43 | public void PS(ReadOnlyMemory inputData, Memory outputPs, ReadOnlyMemory window, double windowS1)
44 | {
45 | if (inputData.Length != length || outputPs.Length != length || window.Length != length)
46 | throw new ArgumentException("Array lengths don't match");
47 |
48 | // Apply window to data
49 | for (int i = 0; i < window.Length; i++)
50 | {
51 | complexBuffer[i] = new Complex(inputData.Span[i] * window.Span[i], 0);
52 | }
53 |
54 | // Apply transform
55 | //MathNet.Numerics.IntegralTransforms.Fourier.Forward(fourierData, MathNet.Numerics.IntegralTransforms.FourierOptions.NoScaling);
56 | fft.Forward(complexBuffer);
57 |
58 | // Convert to magnitude spectrum
59 | for (int i = 0; i < inputData.Length; i++)
60 | {
61 | outputPs.Span[i] = (2.0 * Math.Pow(complexBuffer[i].Magnitude, 2)) / (windowS1 * windowS1); //"The factor 2 originates from the fact that we presumably use an efficient FFT algorithm that does not compute the redundant results for negative frequencies"
62 | //outputPsd.Span[i] = (Math.Pow(Math.Abs(fourierData[i].Magnitude), 2)) / (s1 * s1);
63 | if (double.IsNaN(outputPs.Span[i]) || outputPs.Span[i] > 1000000000000)
64 | throw new Exception();
65 | }
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/source/NSD/Filter.cs:
--------------------------------------------------------------------------------
1 | using MathNet.Numerics.LinearAlgebra;
2 | using System;
3 |
4 | namespace NSD
5 | {
6 | ///
7 | /// Implements a Savitzky-Golay smoothing filter, as found in [1].
8 | /// [1] Sophocles J.Orfanidis. 1995. Introduction to Signal Processing. Prentice-Hall, Inc., Upper Saddle River, NJ, USA.
9 | ///
10 | public sealed class SavitzkyGolayFilter
11 | {
12 | private readonly int sidePoints;
13 |
14 | private Matrix coefficients;
15 |
16 | public SavitzkyGolayFilter(int sidePoints, int polynomialOrder)
17 | {
18 | this.sidePoints = sidePoints;
19 | Design(polynomialOrder);
20 | }
21 |
22 | ///
23 | /// Smoothes the input samples.
24 | ///
25 | ///
26 | ///
27 | public double[] Process(Span samples)
28 | {
29 | int length = samples.Length;
30 | double[] output = new double[length];
31 | int frameSize = (sidePoints << 1) + 1;
32 | double[] frame = samples.Slice(0, frameSize).ToArray();
33 |
34 | for (int i = 0; i < sidePoints; ++i)
35 | {
36 | output[i] = coefficients.Column(i).DotProduct(Vector.Build.DenseOfArray(frame));
37 | }
38 |
39 | for (int n = sidePoints; n < length - sidePoints; ++n)
40 | {
41 | Array.ConstrainedCopy(samples.ToArray(), n - sidePoints, frame, 0, frameSize);
42 | output[n] = coefficients.Column(sidePoints).DotProduct(Vector.Build.DenseOfArray(frame));
43 | }
44 |
45 | Array.ConstrainedCopy(samples.ToArray(), length - frameSize, frame, 0, frameSize);
46 |
47 | for (int i = 0; i < sidePoints; ++i)
48 | {
49 | output[length - sidePoints + i] = coefficients.Column(sidePoints + 1 + i).DotProduct(Vector.Build.Dense(frame));
50 | }
51 |
52 | return output;
53 | }
54 |
55 | private void Design(int polynomialOrder)
56 | {
57 | double[,] a = new double[(sidePoints << 1) + 1, polynomialOrder + 1];
58 |
59 | for (int m = -sidePoints; m <= sidePoints; ++m)
60 | {
61 | for (int i = 0; i <= polynomialOrder; ++i)
62 | {
63 | a[m + sidePoints, i] = Math.Pow(m, i);
64 | }
65 | }
66 |
67 | Matrix s = Matrix.Build.DenseOfArray(a);
68 | coefficients = s.Multiply(s.TransposeThisAndMultiply(s).Inverse()).Multiply(s.Transpose());
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/source/NSD/GoertzelFilter.cs:
--------------------------------------------------------------------------------
1 | using System.Numerics;
2 |
3 | namespace NSD
4 | {
5 | public class GoertzelFilter
6 | {
7 | private readonly double coeff;
8 | private readonly double sine;
9 | private readonly double cosine;
10 |
11 | public GoertzelFilter(double filterFreq, double sampleFreq)
12 | {
13 | double w = 2.0 * Math.PI * (filterFreq / sampleFreq);
14 | cosine = Math.Cos(w);
15 | sine = Math.Sin(w);
16 | coeff = 2.0 * cosine;
17 | }
18 |
19 | public Complex Process(Span samples)
20 | {
21 | double Q0 = 0.0;
22 | double Q1 = 0.0;
23 | double Q2 = 0.0;
24 |
25 | for (int n = 0; n < samples.Length; n++)
26 | {
27 | Q0 = coeff * Q1 - Q2 + samples[n];
28 | Q2 = Q1;
29 | Q1 = Q0;
30 | }
31 |
32 | var real = Q1 * cosine - Q2;
33 | var imag = -Q1 * sine;
34 | return new Complex(real, imag);
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/source/NSD/NSD.cs:
--------------------------------------------------------------------------------
1 | using System.Data;
2 |
3 | namespace NSD
4 | {
5 | public class NSD
6 | {
7 | public static Spectrum Linear(ReadOnlyMemory input, double sampleRate, int outputWidth = 2048)
8 | {
9 | //var window = Windows.HFT90D(outputWidth, out double optimumOverlap, out double NENBW);
10 | var window = Windows.FTNI(outputWidth, out double optimumOverlap, out double NENBW);
11 | //var window = Windows.HFT95(outputWidth, out double optimumOverlap, out double NENBW);
12 | // Switched from HFT90D to FTNI
13 | // FTNI has the useful feature where Math.Ceiling(NENBW) is 1 less than most flaptop windows,
14 | // therefore showing one more usable frequency point at the low frequency end of the spectrum.
15 | var windowS2 = S2(window.Span);
16 | var fft = new FFT(outputWidth);
17 | int startIndex = 0;
18 | int endIndex = outputWidth;
19 | int overlap = (int)(outputWidth * (1.0 - optimumOverlap));
20 | int spectrumCount = 0;
21 | Memory workBuffer = new double[outputWidth];
22 | Memory workSpectrum = new double[outputWidth];
23 | Memory lineFitOutput = new double[outputWidth];
24 | Memory psdMemory = new double[outputWidth];
25 | while (endIndex < input.Length)
26 | {
27 | var lineFitInput = input.Slice(startIndex, outputWidth);
28 | SubtractLineFit(lineFitInput, lineFitOutput, workBuffer);
29 | fft.PSD(lineFitOutput, psdMemory, window, sampleRate, windowS2);
30 | AddPSDToWorkSpectrum(psdMemory, workSpectrum); spectrumCount++;
31 | startIndex += overlap;
32 | endIndex += overlap;
33 | }
34 | ConvertWorkSpectrumToAverageVSDInPlace(workSpectrum, spectrumCount);
35 | var nsd = Spectrum.FromValues(workSpectrum, sampleRate, spectrumCount);
36 | //nsd.TrimDC(); // Don't need to trim DC if trimming start/end
37 | nsd.TrimStart((int)Math.Ceiling(NENBW * 2));
38 | return nsd;
39 | }
40 |
41 | public static Spectrum StackedLinear(Memory input, double sampleRate, int maxWidth = 2048, int minWidth = 64)
42 | {
43 | // Compute all the possible widths between maxWidth & minWidth
44 | List widths = [maxWidth];
45 | int width = maxWidth;
46 | while (width > minWidth)
47 | {
48 | width /= 2;
49 | widths.Add(width);
50 | }
51 | // Order by smallest to largest
52 | widths.Reverse();
53 |
54 | // Run parallel NSDs
55 | var spectrums = new Dictionary();
56 | Parallel.ForEach(widths, new ParallelOptions { MaxDegreeOfParallelism = 8 }, width =>
57 | {
58 | spectrums[width] = Linear(input, sampleRate, width);
59 | });
60 |
61 | // Combine all the NSDs into one
62 | double lowestFrequency = double.MaxValue;
63 | var outputFrequencies = new List();
64 | var outputValues = new List();
65 | int averages = 0;
66 | foreach (var computedWidth in widths)
67 | {
68 | var nsd = spectrums[computedWidth];
69 | averages += nsd.Averages;
70 | for (int i = nsd.Frequencies.Length - 1; i >= 0; i--)
71 | {
72 | if (nsd.Frequencies.Span[i] < lowestFrequency)
73 | {
74 | lowestFrequency = nsd.Frequencies.Span[i];
75 | outputFrequencies.Add(nsd.Frequencies.Span[i]);
76 | outputValues.Add(nsd.Values.Span[i]);
77 | }
78 | }
79 | }
80 |
81 | // Order by frequencies smallest to largest
82 | outputFrequencies.Reverse();
83 | outputValues.Reverse();
84 | return new Spectrum() { Frequencies = outputFrequencies.ToArray(), Values = outputValues.ToArray(), Averages = averages, Stacking = widths.Count };
85 | }
86 |
87 | private record WelchGoertzelJob(double Frequency, int SpectrumLength, int CalculatedAverages);
88 | public static Spectrum Log(Memory input, double sampleRateHz, double freqMin, double freqMax, int pointsPerDecade, int minimumAverages, int minimumFourierLength, double pointsPerDecadeScaling)
89 | {
90 | if (freqMax <= freqMin)
91 | throw new ArgumentException("freqMax must be greater than freqMin");
92 | if (pointsPerDecade <= 0 || minimumAverages <= 0 || minimumFourierLength <= 0)
93 | throw new ArgumentException("pointsPerDecade, minimumAverages, and minimumFourierLength must be positive");
94 | if (sampleRateHz <= 0)
95 | throw new ArgumentException("sampleRateHz must be positive");
96 |
97 | Windows.FTNI(1, out double optimumOverlap, out double NENBW);
98 | int firstUsableBinForWindow = (int)Math.Ceiling(NENBW);
99 |
100 | // To do:
101 | // For the purposes of the frequencies calculation, round freqMax/freqMin to nearest major decade line.
102 | // This ensures consistency of X-coordinate over various view widths.
103 | double decadeMin = RoundToDecade(freqMin, RoundingMode.Down);
104 | double decadeMax = RoundToDecade(freqMax, RoundingMode.Up);
105 | int decadeMinExponent = (int)Math.Log10(decadeMin);
106 | int decadeMaxExponent = (int)Math.Log10(decadeMax);
107 |
108 | List frequencyList = [];
109 | int pointsPerDecadeScaled = pointsPerDecade;
110 | for (int decadeExponent = decadeMinExponent; decadeExponent < decadeMaxExponent; decadeExponent++)
111 | {
112 | double currentDecadeMin = Math.Pow(10, decadeExponent);
113 | double currentDecadeMax = Math.Pow(10, decadeExponent + 1);
114 | double multiple = Math.Log(currentDecadeMax) - Math.Log(currentDecadeMin);
115 | var decadeFrequencies = Enumerable.Range(0, pointsPerDecadeScaled - 1).Select(i => currentDecadeMin * Math.Exp(i * multiple / (pointsPerDecadeScaled - 1))).ToArray();
116 | frequencyList.AddRange(decadeFrequencies);
117 | pointsPerDecadeScaled = (int)(pointsPerDecadeScaled * pointsPerDecadeScaling);
118 | }
119 |
120 | double g = Math.Log(decadeMax) - Math.Log(decadeMin);
121 | double[] frequencies = frequencyList.ToArray();
122 | double[] spectrumResolution = frequencies.Select(freq => freq / firstUsableBinForWindow).ToArray();
123 | // spectrumResolution contains the 'desired resolutions' for each frequency bin, respecting the rule that we want the first usuable bin for the given window.
124 | int[] spectrumLengths = spectrumResolution.Select(resolution => (int)Math.Round(sampleRateHz / resolution)).ToArray();
125 |
126 | // Create a job list of valid points to calculate
127 | double nyquistMax = sampleRateHz / 2;
128 | List jobs = [];
129 | for (int i = 0; i < frequencies.Length; i++)
130 | {
131 | if (frequencies[i] > nyquistMax)
132 | continue;
133 | if (TryCalculateAverages(input.Length, spectrumLengths[i], optimumOverlap, out var averages))
134 | {
135 | if (averages >= minimumAverages)
136 | {
137 | // Increase spectrum length until minimumLength is met, or averages drops below minimumAverages.
138 | // This increases the spectral resolution at the top end of the chart, allowing 50Hz spikes (& similar) to be more visible
139 | var spectrumLength = spectrumLengths[i];
140 | bool continueLoop = true;
141 | while (continueLoop)
142 | {
143 | if (spectrumLength < minimumFourierLength && averages > minimumAverages)
144 | {
145 | var success = TryCalculateAverages(input.Length, spectrumLength * 2, optimumOverlap, out var newAverages);
146 | if (!success)
147 | break;
148 | if (averages > minimumAverages)
149 | {
150 | spectrumLength *= 2;
151 | averages = newAverages;
152 | continueLoop = true;
153 | }
154 | else
155 | {
156 | continueLoop = false;
157 | break;
158 | }
159 | }
160 | else
161 | {
162 | continueLoop = false;
163 | }
164 | }
165 | jobs.Add(new WelchGoertzelJob(frequencies[i], spectrumLength, averages));
166 | }
167 | }
168 | }
169 |
170 | var spectrum = new Dictionary();
171 | for (int i = 0; i < jobs.Count; i++)
172 | {
173 | spectrum[jobs[i].Frequency] = double.NaN;
174 | }
175 | object averageLock = new();
176 | int cumulativeAverage = 0;
177 | //foreach(var job in jobs)
178 | Parallel.ForEach(jobs, new ParallelOptions { MaxDegreeOfParallelism = 8 }, job =>
179 | {
180 | var result = RunWelchGoertzel(input, job.SpectrumLength, job.Frequency, sampleRateHz, out var actualAverages);
181 | if (job.CalculatedAverages != actualAverages)
182 | throw new Exception("Actual averages does not match calculated averages");
183 | spectrum[job.Frequency] = result;
184 | lock (averageLock)
185 | {
186 | cumulativeAverage += actualAverages;
187 | }
188 | }
189 | );
190 |
191 | var output = new Spectrum
192 | {
193 | Frequencies = spectrum.Keys.ToArray(),
194 | Values = spectrum.Values.ToArray(),
195 | Averages = cumulativeAverage
196 | };
197 | return output;
198 | }
199 |
200 | private static void AddPSDToWorkSpectrum(Memory inputPSD, Memory workingMemory)
201 | {
202 | for (int i = 0; i < inputPSD.Length; i++)
203 | {
204 | workingMemory.Span[i] += inputPSD.Span[i];
205 | }
206 | }
207 |
208 | private static void ConvertWorkSpectrumToAverageVSDInPlace(Memory workingMemory, int count)
209 | {
210 | double divisor = count;
211 | for (int i = 0; i < workingMemory.Length; i++)
212 | {
213 | workingMemory.Span[i] = workingMemory.Span[i] / divisor;
214 | }
215 |
216 | // Convert to VSD
217 | for (int i = 0; i < workingMemory.Length; i++)
218 | {
219 | workingMemory.Span[i] = Math.Sqrt(workingMemory.Span[i]);
220 | }
221 | }
222 |
223 | ///
224 | /// Least-Squares fitting the points (x,y) to a line y : x -> a+b*x, returning its best fitting parameters as (a, b) tuple, where a is the intercept and b the slope.
225 | ///
226 | private static (double A, double B) LineFit(ReadOnlySpan x, ReadOnlySpan y)
227 | {
228 | if (x.Length != y.Length)
229 | {
230 | throw new ArgumentException($"All sample vectors must have the same length.");
231 | }
232 |
233 | if (x.Length <= 1)
234 | {
235 | throw new ArgumentException($"A regression of the requested order requires at least {2} samples. Only {x.Length} samples have been provided.");
236 | }
237 |
238 | // First Pass: Mean (Less robust but faster than ArrayStatistics.Mean)
239 | double mx = 0.0;
240 | double my = 0.0;
241 | for (int i = 0; i < x.Length; i++)
242 | {
243 | mx += x[i];
244 | my += y[i];
245 | }
246 |
247 | mx /= x.Length;
248 | my /= y.Length;
249 |
250 | // Second Pass: Covariance/Variance
251 | double covariance = 0.0;
252 | double variance = 0.0;
253 | for (int i = 0; i < x.Length; i++)
254 | {
255 | double diff = x[i] - mx;
256 | covariance += diff * (y[i] - my);
257 | variance += diff * diff;
258 | }
259 |
260 | var b = covariance / variance;
261 | return (my - b * mx, b);
262 | }
263 |
264 | ///
265 | /// Calculate Least-squares line fit and subtract from input, storing in output. Buffer is temporary variable memory.
266 | ///
267 | private static void SubtractLineFit(ReadOnlyMemory input, Memory output, Memory buffer)
268 | {
269 | if (input.Length != output.Length || input.Length != buffer.Length)
270 | throw new ArgumentException("Lengths don't match");
271 |
272 | var x = buffer.Span;
273 | var y = input.Span;
274 |
275 | for (int i = 0; i < input.Length; i++)
276 | x[i] = i;
277 |
278 | var (A, B) = LineFit(x, y);
279 | var outputSpan = output.Span;
280 | var inputSpan = input.Span;
281 | for (int i = 0; i < input.Length; i++)
282 | {
283 | outputSpan[i] = (inputSpan[i] - (A + B * i));
284 | }
285 | }
286 |
287 | private static double RunWelchGoertzel(Memory input, int runLength, double frequency, double sampleRateHz, out int spectrumCount2)
288 | {
289 | var window = Windows.FTNI(runLength, out double optimumOverlap, out double NENBW);
290 | double s2 = S2(window.Span);
291 | int startIndex = 0;
292 | int endIndex = runLength;
293 | int overlap = (int)(runLength * (1.0 - optimumOverlap));
294 | int spectrumCount = 0;
295 | double average = 0;
296 | Memory waveformBuffer = new double[runLength];
297 | Memory workBuffer = new double[runLength];
298 |
299 | while (endIndex < input.Length)
300 | {
301 | var lineFitInput = input.Slice(startIndex, runLength);
302 | SubtractLineFit(lineFitInput, waveformBuffer, workBuffer);
303 | for (int i = 0; i < runLength; i++)
304 | {
305 | waveformBuffer.Span[i] = waveformBuffer.Span[i] * window.Span[i];
306 | }
307 |
308 | var filter = new GoertzelFilter(frequency, sampleRateHz); // Specific form of 1 bin DFT
309 | var power = filter.Process(waveformBuffer.Span);
310 | average += 2.0 * Math.Pow(power.Magnitude, 2) / (sampleRateHz * s2);
311 | spectrumCount++;
312 | startIndex += overlap;
313 | endIndex += overlap;
314 | }
315 |
316 | spectrumCount2 = spectrumCount;
317 | return Math.Sqrt(average / spectrumCount);
318 | }
319 |
320 | private static double S1(ReadOnlySpan window)
321 | {
322 | double sum = 0;
323 | for (int i = 0; i < window.Length; i++)
324 | {
325 | sum += window[i];
326 | }
327 | return sum;
328 | }
329 |
330 | private static double S2(ReadOnlySpan window)
331 | {
332 | double sumSquared = 0;
333 | for (int i = 0; i < window.Length; i++)
334 | {
335 | sumSquared += Math.Pow(window[i], 2);
336 | }
337 | return sumSquared;
338 | }
339 |
340 | enum RoundingMode { Nearest, Up, Down }
341 | private static double RoundToDecade(double value, RoundingMode mode)
342 | {
343 | if (value <= 0)
344 | throw new ArgumentOutOfRangeException(nameof(value), "Value must be positive.");
345 |
346 | double log10 = Math.Log10(value);
347 | double exponent = mode switch
348 | {
349 | RoundingMode.Nearest => Math.Round(log10),
350 | RoundingMode.Up => Math.Ceiling(log10),
351 | RoundingMode.Down => Math.Floor(log10),
352 | _ => throw new ArgumentOutOfRangeException(nameof(mode), "Invalid rounding mode.")
353 | };
354 |
355 | return Math.Pow(10, exponent);
356 | }
357 |
358 | private static bool TryCalculateAverages(int dataLength, int spectrumLength, double optimumOverlap, out int averages)
359 | {
360 | averages = 0;
361 | int overlap = (int)(spectrumLength * (1.0 - optimumOverlap));
362 | if (overlap < 1)
363 | return false;
364 | int endIndex = spectrumLength;
365 | while (endIndex < dataLength)
366 | {
367 | averages++;
368 | endIndex += overlap;
369 | }
370 | return true;
371 | }
372 | }
373 | }
374 |
375 |
--------------------------------------------------------------------------------
/source/NSD/NSD.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 | true
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/source/NSD/NsdProcessingException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace NSD
4 | {
5 | public class NsdProcessingException : Exception
6 | {
7 | public NsdProcessingException() { }
8 | public NsdProcessingException(string message) : base(message) { }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/source/NSD/Papers/GH_FFT.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/macaba/NSD/5b9f022c3fcccb7cad7603d5cf9af9413af43637/source/NSD/Papers/GH_FFT.pdf
--------------------------------------------------------------------------------
/source/NSD/Papers/Improved spectrum estimation from digitized time series on a logaritmic frequency axis.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/macaba/NSD/5b9f022c3fcccb7cad7603d5cf9af9413af43637/source/NSD/Papers/Improved spectrum estimation from digitized time series on a logaritmic frequency axis.pdf
--------------------------------------------------------------------------------
/source/NSD/Signals.cs:
--------------------------------------------------------------------------------
1 | using MathNet.Numerics.IntegralTransforms;
2 | using System;
3 | using System.Numerics;
4 |
5 | namespace NSD
6 | {
7 | public class Signals
8 | {
9 | public static Memory TenNanoVoltRmsTestSignal()
10 | {
11 | var sine = MathNet.Numerics.Generate.Sinusoidal(1000000, 50, 5.1, 1.41e-8);
12 | for (int i = 0; i < sine.Length; i++)
13 | {
14 | sine[i] = Math.Round(sine[i], 11);
15 | }
16 | return sine;
17 | }
18 |
19 | public static Memory OneVoltRmsTestSignal()
20 | {
21 | var noiseSource = new MathNet.Filtering.DataSources.WhiteGaussianNoiseSource();
22 | var sine = MathNet.Numerics.Generate.Sinusoidal(1000000, 50, 5.1, 1.41);
23 | for (int i = 0; i < sine.Length; i++)
24 | {
25 | sine[i] = sine[i] + noiseSource.ReadNextSample();
26 | }
27 | return sine;
28 | }
29 |
30 | public static Memory WhiteNoise(int samples, double sampleRate, double volt)
31 | {
32 | var noiseSource = new MathNet.Filtering.DataSources.WhiteGaussianNoiseSource(0, 1);
33 | Memory data = new double[samples];
34 | for (int i = 0; i < data.Length; i++)
35 | {
36 | data.Span[i] = noiseSource.ReadNextSample() * volt * Math.Sqrt(sampleRate / 2);
37 | }
38 | return data;
39 | }
40 |
41 | // https://raw.githubusercontent.com/felixpatzelt/colorednoise/master/colorednoise.py
42 | // exponent - 0: gaussian, 1: pink (1/f), 2: brown (1/f^2), -1: blue, -2: violet
43 | public static Memory PowerLawGaussian(int samples, double sampleRate, double exponent)
44 | {
45 | var frequencies = Fourier.FrequencyScale(samples, sampleRate).Take((samples / 2) + 1).ToArray();
46 | var scalingFactors = frequencies.ToArray();
47 | for (int i = 1; i < scalingFactors.Length; i++)
48 | {
49 | scalingFactors[i] = Math.Pow(frequencies[i], -exponent / 2.0);
50 | }
51 | scalingFactors[0] = scalingFactors[1];
52 |
53 | var noiseSource = new MathNet.Filtering.DataSources.WhiteGaussianNoiseSource();
54 | Complex[] fourierData = new Complex[scalingFactors.Length];
55 | for (int i = 0; i < fourierData.Length; i++)
56 | {
57 | fourierData[i] = new Complex(noiseSource.ReadNextSample() * scalingFactors[i], noiseSource.ReadNextSample() * scalingFactors[i]);
58 | }
59 | fourierData[0] = new Complex(fourierData[0].Real * Math.Sqrt(2), 0);
60 |
61 | Fourier.Inverse(fourierData);
62 |
63 | double[] data = new double[fourierData.Length];
64 | for (int i = 0; i < data.Length; i++)
65 | {
66 | data[i] = fourierData[i].Magnitude;
67 | }
68 |
69 | return data;
70 | }
71 |
72 | //public static Memory WhiteWithSlopeNoise(int samples, double sampleRate, double volt, double slopeCornerFrequency, double slope)
73 | //{
74 | // var white = WhiteNoise(samples, sampleRate, volt);
75 | // //var slope = something;
76 |
77 | // return white;
78 | //}
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/source/NSD/Windows.cs:
--------------------------------------------------------------------------------
1 | namespace NSD
2 | {
3 | public static class Windows
4 | {
5 | /// Optimum overlap in ratio of width, used for Welch method
6 | /// Normalized Equivalent Noise BandWidth with unit of bins
7 | public static Memory HFT95(int width, out double optimumOverlap, out double NENBW)
8 | {
9 | // HFT95 - https://holometer.fnal.gov/GH_FFT.pdf
10 | // wj = 1 − 1.9383379 cos(z) + 1.3045202 cos(2z) − 0.4028270 cos(3z) + 0.0350665 cos(4z).
11 | optimumOverlap = 0.756;
12 | NENBW = 3.8112;
13 | Memory window = new double[width];
14 | var windowSpan = window.Span;
15 | double angleIncrement = 2.0 * Math.PI / width;
16 | for (int i = 0; i < width; i++)
17 | {
18 | double z = angleIncrement * i;
19 | double wj = 1 - (1.9383379 * Math.Cos(z)) + (1.3045202 * Math.Cos(2 * z)) - (0.4028270 * Math.Cos(3 * z)) + (0.0350665 * Math.Cos(4 * z));
20 | windowSpan[i] = wj;
21 | }
22 | return window;
23 | }
24 |
25 | /// Optimum overlap in ratio of width, used for Welch method
26 | /// Normalized Equivalent Noise BandWidth with unit of bins
27 | public static Memory HFT90D(int width, out double optimumOverlap, out double NENBW)
28 | {
29 | // HFT90D - https://holometer.fnal.gov/GH_FFT.pdf
30 | // wj = 1 − 1.942604 cos(z) + 1.340318 cos(2z) − 0.440811 cos(3z) + 0.043097 cos(4z).
31 | optimumOverlap = 0.76;
32 | NENBW = 3.8832;
33 | Memory window = new double[width];
34 | var windowSpan = window.Span;
35 | double angleIncrement = 2.0 * Math.PI / width;
36 | for (int i = 0; i < width; i++)
37 | {
38 | double z = angleIncrement * i;
39 | double wj = 1 - (1.942604 * Math.Cos(z)) + (1.340318 * Math.Cos(2 * z)) - (0.440811 * Math.Cos(3 * z)) + (0.043097 * Math.Cos(4 * z));
40 | windowSpan[i] = wj;
41 | }
42 |
43 | return window;
44 | }
45 |
46 | /// Optimum overlap in ratio of width, used for Welch method
47 | /// Normalized Equivalent Noise BandWidth with unit of bins
48 | public static Memory FTNI(int width, out double optimumOverlap, out double NENBW)
49 | {
50 | // FTNI - https://holometer.fnal.gov/GH_FFT.pdf
51 | // wj = 0.2810639 − 0.5208972 cos(z) + 0.1980399 cos(2z).
52 | optimumOverlap = 0.656;
53 | NENBW = 2.9656;
54 | Memory window = new double[width];
55 | var windowSpan = window.Span;
56 | double angleIncrement = 2.0 * Math.PI / width;
57 | for (int i = 0; i < width; i++)
58 | {
59 | double z = angleIncrement * i;
60 | double wj = 0.2810639 - (0.5208972 * Math.Cos(z)) + (0.1980399 * Math.Cos(2 * z));
61 | windowSpan[i] = wj;
62 | }
63 | return window;
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------