Skip to content

Commit 77c56f3

Browse files
authored
Merge pull request #50 from arthurits/main
FFTfreq: fix off-by-one error
2 parents 454708e + 0809ad7 commit 77c56f3

File tree

8 files changed

+164
-34
lines changed

8 files changed

+164
-34
lines changed

dev/quickstart/fft-windowed.png

-216 Bytes
Loading

dev/quickstart/fft.png

-209 Bytes
Loading

dev/quickstart/periodogram.png

-181 Bytes
Loading

src/FftSharp.Tests/FftFreqTests.cs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
using NUnit.Framework;
2+
using System;
3+
using System.Linq;
4+
5+
namespace FftSharp.Tests
6+
{
7+
internal class FftFreqTests
8+
{
9+
[Test]
10+
public void Test_FftFreq_KnownValues()
11+
{
12+
// https://github.com/swharden/FftSharp/issues/49
13+
14+
// generate signal with 2 Hz sine wave
15+
int sampleCount = 16;
16+
double sampleRateHz = 16;
17+
double sinFrequencyHz = 2;
18+
double[] samples = Enumerable.Range(0, sampleCount)
19+
.Select(i => Math.Sin(2 * Math.PI * sinFrequencyHz * i / sampleRateHz))
20+
.ToArray();
21+
22+
// get FFT
23+
double[] fft = FftSharp.Transform.FFTmagnitude(samples);
24+
double[] fftKnown = { 0, 0, 1, 0, 0, 0, 0, 0, 0 };
25+
Assert.That(fft, Is.EqualTo(fftKnown).Within(1e-10));
26+
27+
// calculate FFT frequencies both ways
28+
double[] fftFreqKnown = { 0, 1, 2, 3, 4, 5, 6, 7, 8 };
29+
double[] fftFreq = FftSharp.Transform.FFTfreq(sampleRateHz, fft.Length);
30+
double[] fftFreq2 = FftSharp.Transform.FFTfreq(sampleRateHz, fft);
31+
Assert.That(fftFreq, Is.EqualTo(fftFreqKnown));
32+
Assert.That(fftFreq2, Is.EqualTo(fftFreqKnown));
33+
34+
ScottPlot.Plot plt1 = new(400, 200);
35+
plt1.AddSignal(samples, sampleRateHz);
36+
TestTools.SaveFig(plt1, "signal");
37+
38+
ScottPlot.Plot plt2 = new(400, 200);
39+
plt2.AddScatter(fftFreq, fft);
40+
plt2.AddVerticalLine(2, System.Drawing.Color.Red, style: ScottPlot.LineStyle.Dash);
41+
TestTools.SaveFig(plt2, "fft");
42+
}
43+
44+
[Test]
45+
public void Test_Freq_Lookup()
46+
{
47+
/* Compare against values generated using Python:
48+
*
49+
* >>> numpy.fft.fftfreq(16, 1/16000)
50+
* array([ 0., 1000., 2000., 3000., 4000., 5000., 6000., 7000.,
51+
* -8000., -7000., -6000., -5000., -4000., -3000., -2000., -1000.])
52+
*
53+
*/
54+
55+
int sampleRate = 16000;
56+
int pointCount = 16;
57+
double[] signal = new double[pointCount];
58+
double[] fft = FftSharp.Transform.FFTmagnitude(signal);
59+
60+
double[] freqsFullKnown = {
61+
0, 1000, 2000, 3000, 4000, 5000, 6000, 7000,
62+
-8000, -7000, -6000, -5000, -4000, -3000, -2000, -1000
63+
};
64+
double[] freqsFull = FftSharp.Transform.FFTfreq(sampleRate, signal, oneSided: false);
65+
double[] freqsFull2 = FftSharp.Transform.FFTfreq(sampleRate, signal.Length, oneSided: false);
66+
Assert.That(freqsFull, Is.EqualTo(freqsFullKnown));
67+
Assert.That(freqsFull2, Is.EqualTo(freqsFullKnown));
68+
69+
double[] freqsHalfKnown = {
70+
0, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000
71+
};
72+
double[] freqsHalf = FftSharp.Transform.FFTfreq(sampleRate, fft, oneSided: true);
73+
double[] freqsHalf2 = FftSharp.Transform.FFTfreq(sampleRate, fft.Length, oneSided: true);
74+
Assert.That(freqsHalf, Is.EqualTo(freqsHalfKnown));
75+
Assert.That(freqsHalf2, Is.EqualTo(freqsHalfKnown));
76+
}
77+
}
78+
}

src/FftSharp.Tests/FreqLookup.cs

Lines changed: 0 additions & 28 deletions
This file was deleted.

src/FftSharp.Tests/Transform.cs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,5 +155,68 @@ public void Test_FftInput_ThrowsIfLengthIsNotPowerOfTwo(int length, bool shouldT
155155
Assert.DoesNotThrow(realSpanRFFT);
156156
}
157157
}
158+
159+
[TestCase(0, true)]
160+
[TestCase(1, false)]
161+
[TestCase(2, false)]
162+
[TestCase(4, false)]
163+
[TestCase(8, false)]
164+
[TestCase(16, false)]
165+
[TestCase(32, false)]
166+
[TestCase(64, false)]
167+
public void Test_Fft_Complex_DifferentLengths(int pointCount, bool shouldFail)
168+
{
169+
Complex[] signal = new Complex[pointCount];
170+
if (shouldFail)
171+
{
172+
Assert.Throws<ArgumentException>(() => FftSharp.Transform.FFT(signal));
173+
}
174+
else
175+
{
176+
Assert.DoesNotThrow(() => FftSharp.Transform.FFT(signal));
177+
}
178+
}
179+
180+
[TestCase(0, true)]
181+
[TestCase(1, false)]
182+
[TestCase(2, false)]
183+
[TestCase(4, false)]
184+
[TestCase(8, false)]
185+
[TestCase(16, false)]
186+
[TestCase(32, false)]
187+
[TestCase(64, false)]
188+
public void Test_Fft_Double_DifferentLengths(int pointCount, bool shouldFail)
189+
{
190+
double[] signal = new double[pointCount];
191+
if (shouldFail)
192+
{
193+
Assert.Throws<ArgumentException>(() => FftSharp.Transform.FFT(signal));
194+
}
195+
else
196+
{
197+
Assert.DoesNotThrow(() => FftSharp.Transform.FFT(signal));
198+
}
199+
}
200+
201+
[TestCase(0, true)]
202+
[TestCase(1, true)]
203+
[TestCase(2, true)]
204+
[TestCase(4, true)]
205+
[TestCase(8, true)]
206+
[TestCase(16, false)]
207+
[TestCase(32, false)]
208+
[TestCase(64, false)]
209+
public void Test_Fft_Magnitude_DifferentLengths(int pointCount, bool shouldFail)
210+
{
211+
double[] signal = new double[pointCount];
212+
if (shouldFail)
213+
{
214+
Assert.Throws<ArgumentException>(() => FftSharp.Transform.FFTmagnitude(signal));
215+
}
216+
else
217+
{
218+
Assert.DoesNotThrow(() => FftSharp.Transform.FFTmagnitude(signal));
219+
}
220+
}
158221
}
159222
}

src/FftSharp/FftSharp.csproj

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
<PackageReleaseNotes>https://github.com/swharden/FftSharp/releases</PackageReleaseNotes>
1717
<IncludeSymbols>true</IncludeSymbols>
1818
<DocumentationFile>FftSharp.xml</DocumentationFile>
19-
<Version>1.1.5</Version>
20-
<AssemblyVersion>1.1.5.0</AssemblyVersion>
21-
<FileVersion>1.1.5.0</FileVersion>
19+
<Version>1.1.6</Version>
20+
<AssemblyVersion>1.1.6.0</AssemblyVersion>
21+
<FileVersion>1.1.6.0</FileVersion>
2222
<NoWarn>1591</NoWarn>
2323
<DebugType>portable</DebugType>
2424
<IncludeSymbols>true</IncludeSymbols>
@@ -34,15 +34,15 @@
3434
</ItemGroup>
3535

3636
<ItemGroup>
37-
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0">
37+
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1">
3838
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
3939
<PrivateAssets>all</PrivateAssets>
4040
</PackageReference>
4141
</ItemGroup>
4242

4343
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
4444
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
45-
<PackageReference Include="System.Memory" Version="4.5.4" />
45+
<PackageReference Include="System.Memory" Version="4.5.5" />
4646
</ItemGroup>
4747

4848
</Project>

src/FftSharp/Transform.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,13 +122,16 @@ private static int BitReverse(int value, int maxValue)
122122
/// <summary>
123123
/// Calculate sample frequency for each point in a FFT
124124
/// </summary>
125+
/// <param name="sampleRate">Sample rate (Hz) of the original signal</param>
126+
/// <param name="pointCount">Number of points to generate (typically the length of the FFT)</param>
127+
/// <param name="oneSided">Whether or not frequencies are for a one-sided FFT (containing only real numbers)</param>
125128
public static double[] FFTfreq(double sampleRate, int pointCount, bool oneSided = true)
126129
{
127130
double[] freqs = new double[pointCount];
128131

129132
if (oneSided)
130133
{
131-
double fftPeriodHz = sampleRate / pointCount / 2;
134+
double fftPeriodHz = sampleRate / (pointCount - 1) / 2;
132135

133136
// freqs start at 0 and approach maxFreq
134137
for (int i = 0; i < pointCount; i++)
@@ -151,6 +154,17 @@ public static double[] FFTfreq(double sampleRate, int pointCount, bool oneSided
151154
}
152155
}
153156

157+
/// <summary>
158+
/// Calculate sample frequency for each point in a FFT
159+
/// </summary>
160+
/// <param name="sampleRate">Sample rate (Hz) of the original signal</param>
161+
/// <param name="fft">FFT array for which frequencies should be generated</param>
162+
/// <param name="oneSided">Whether or not frequencies are for a one-sided FFT (containing only real numbers)</param>
163+
public static double[] FFTfreq(double sampleRate, double[] fft, bool oneSided = true)
164+
{
165+
return FFTfreq(sampleRate, fft.Length, oneSided);
166+
}
167+
154168
/// <summary>
155169
/// Return the distance between each FFT point in frequency units (Hz)
156170
/// </summary>
@@ -294,6 +308,9 @@ public static double[] FFTmagnitude(double[] input)
294308
/// <param name="input">real input</param>
295309
public static void FFTmagnitude(Span<double> destination, Span<double> input)
296310
{
311+
if (input.Length < 16)
312+
throw new ArgumentException("This overload requires an input with at least 16 points");
313+
297314
if (!IsPowerOfTwo(input.Length))
298315
throw new ArgumentException("Input length must be an even power of 2");
299316

0 commit comments

Comments
 (0)