Skip to content

Commit

Permalink
Adds test cases for harmonic algoirthm
Browse files Browse the repository at this point in the history
Also adds some test assertions for validating objects.

Related to #471
  • Loading branch information
atruskie committed Jun 9, 2021
1 parent 2099caf commit da46bcb
Show file tree
Hide file tree
Showing 9 changed files with 209 additions and 19 deletions.
5 changes: 2 additions & 3 deletions src/Acoustics.Shared/Extensions/EnumerableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -315,12 +315,11 @@ public static string Join<T>(this IEnumerable<T> items, string delimiter = " ",
var result = new StringBuilder();
foreach (var item in items)
{
result.Append(item);
result.Append(delimiter);
result.AppendJoin(string.Empty, prefix, item, suffix, delimiter);
}

// return one delimiter length less because we always add a delimiter on the end
return result.ToString(0, result.Length - delimiter.Length);
return result.ToString(0, Math.Max(0, result.Length - delimiter.Length));
}

public static string JoinFormatted(this IEnumerable<double> items, string formatString = "{0:f2}", string delimiter = " ") =>
Expand Down
7 changes: 4 additions & 3 deletions src/AnalysisPrograms/Recognizers/GenericRecognizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace AnalysisPrograms.Recognizers
{
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Reflection;
Expand Down Expand Up @@ -72,10 +73,10 @@ public static void ValidateProfileTagsMatchAlgorithms(Dictionary<string, object>
{
if (profile is CommonParameters c)
{
var checks = c.Validate(null).Where(v => v is not null);
if (checks.Any())
List<ValidationResult> failures = new();
if (!Validator.TryValidateObject(c, new ValidationContext(c), failures))
{
throw new ConfigFileException(checks, file) { ProfileName = profileName };
throw new ConfigFileException(failures, file) { ProfileName = profileName };
}
}

Expand Down
1 change: 1 addition & 0 deletions tests/Acoustics.Test/Acoustics.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
<ProjectReference Include="..\..\src\AcousticWorkbench\AcousticWorkbench.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="AudioAnalysisTools\HarmonicAnalysis\" />
<Folder Include="Shared\Collections\" />
</ItemGroup>
<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// <copyright file="HarmonicAnalysisTests.cs" company="QutEcoacoustics">
// All code in this file and all associated files are the copyright and property of the QUT Ecoacoustics Research Group (formerly MQUTeR, and formerly QUT Bioacoustics Research Group).
// </copyright>

namespace Acoustics.Test.AudioAnalysisTools.HarmonicAnalysis
{
using System;
using System.IO;
using System.Linq;
using Acoustics.Test.TestHelpers;
using global::AnalysisPrograms.Recognizers.Base;
using global::AudioAnalysisTools;
using global::AudioAnalysisTools.DSP;
using global::AudioAnalysisTools.StandardSpectrograms;
using global::AudioAnalysisTools.WavTools;
using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class HarmonicAlgorithmTests : OutputDirectoryTest
{
private static readonly FileInfo TestAsset = PathHelper.ResolveAsset("harmonic.wav");
private readonly SpectrogramStandard spectrogram;

public HarmonicAlgorithmTests()
{
var recording = new AudioRecording(TestAsset);
this.spectrogram = new SpectrogramStandard(
new SonogramConfig
{
WindowSize = 512,
WindowStep = 512,
WindowOverlap = 0,
NoiseReductionType = NoiseReductionType.None,
NoiseReductionParameter = 0.0,
Duration = recording.Duration,
SampleRate = recording.SampleRate,
},
recording.WavReader);
}

[TestMethod]
public void TestHarmonicsAlgorithmOn440HertzHarmonic()
{
var threshold = -80;
var parameters = new HarmonicParameters
{
MinHertz = 400,
MaxHertz = 5500,
// expected value
//MaxFormantGap = 480,
MaxFormantGap = 3500,// this is the lowest value that would produce a result, 3400 does not
MinFormantGap = 400,
MinDuration = 0.9,
MaxDuration = 1.1,
DecibelThresholds = new double?[] { threshold },
DctThreshold = 0.5,
};
Assert.That.IsValid(parameters);

var (events, plots) = HarmonicParameters.GetComponentsWithHarmonics(
this.spectrogram,
parameters,
threshold,
TimeSpan.Zero,
"440_harmonic");

this.SaveImage(
SpectrogramTools.GetSonogramPlusCharts(this.spectrogram, events, plots, null));

Assert.AreEqual(1, events.Count);
Assert.IsInstanceOfType(events.First(), typeof(HarmonicEvent));

// first harmonic is 440Hz fundamental, with 12 harmonics, stopping at 5280 Hz
var actual = events.First() as HarmonicEvent;
Assert.AreEqual(1.0, actual.EventStartSeconds);
Assert.AreEqual(2.0, actual.EventEndSeconds);
Assert.AreEqual(400, actual.LowFrequencyHertz);
Assert.AreEqual(5400, actual.HighFrequencyHertz);

Assert.Fail("intentionally faulty test");
}

[TestMethod]
public void TestHarmonicsAlgorithmOn1000HertzHarmonic()
{
var threshold = -80;
var parameters = new HarmonicParameters
{
MinHertz = 800,
MaxHertz = 5500,
// expected values
//MaxFormantGap = 1050,
MaxFormantGap = 3200, // this is the lowest value that would produce a result, 3100 does not
MinFormantGap = 950,
MinDuration = 0.9,
MaxDuration = 1.1,
DecibelThresholds = new double?[] { threshold },
DctThreshold = 0.5,
};
Assert.That.IsValid(parameters);

var (events, plots) = HarmonicParameters.GetComponentsWithHarmonics(
this.spectrogram,
parameters,
threshold,
TimeSpan.Zero,
"1000_harmonic");

this.SaveImage(
SpectrogramTools.GetSonogramPlusCharts(this.spectrogram, events, plots, null));

Assert.AreEqual(1, events.Count);
Assert.IsInstanceOfType(events.First(), typeof(HarmonicEvent));

// second harmonic is 1000 Hz fundamental, with 4 harmonics, stopping at 5000 Hz
var actual = events.First() as HarmonicEvent;
Assert.AreEqual(3.0, actual.EventStartSeconds);
Assert.AreEqual(4.0, actual.EventEndSeconds);
Assert.AreEqual(900, actual.LowFrequencyHertz);
Assert.AreEqual(5100, actual.HighFrequencyHertz);

Assert.Fail("intentionally faulty test");

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ public void TestJoinCustomDelimiter()
Assert.AreEqual("0,-,1,-,2,-,3,-,4", actual);
}

[TestMethod]
public void TestJoinCustomDelimiterWithPrefixAndSuffix()
{
var items = new[] { 0, 1, 2, 3, 4 };
var actual = items.Join("/", "`", "~");

Assert.AreEqual("`0~/`1~/`2~/`3~/`4~", actual);
}

[TestMethod]
public void TestJoinNonGeneric()
{
Expand Down
42 changes: 42 additions & 0 deletions tests/Acoustics.Test/TestHelpers/Assertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace Acoustics.Test.TestHelpers
{
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Globalization;
using System.IO;
Expand Down Expand Up @@ -154,6 +155,47 @@ public static void AreEqual(
}
}

public static void IsEmpty<T>(
this CollectionAssert _,
IEnumerable<T> actual,
bool printItems = false,
string message = "")
{
if (actual == null)
{
Assert.Fail("actual is null, expected none");
}

using var actualEnum = actual.GetEnumerator();

if (actualEnum.MoveNext())
{
var items = printItems ? actual.FormatList() : string.Empty;
Assert.Fail($"Actual had {actual.Count()} items and we expected none.\n{items}\n{message}");
}
}

public static void IsValid<T>(
this Assert _,
T actual,
ValidationContext context = null,
string message = "")
where T : IValidatableObject
{
if (actual == null)
{
Assert.Fail("actual is null, expected an object to validate");
}

List<ValidationResult> failures = new();
context ??= new ValidationContext(actual);
if (!Validator.TryValidateObject(actual, context, failures))
{
var items = failures.Select(x => x.MemberNames.Join(", ") + " => " + x.ToString()).FormatList();
Assert.Fail($"Actual was not valid. Returned validation failures:\n{items}\n{message}");
}
}

public static void AreEqual(
this CollectionAssert collectionAssert,
double[,] expected,
Expand Down
14 changes: 1 addition & 13 deletions tests/Acoustics.Test/TestHelpers/GeneratedImageTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,19 +116,7 @@ private void SaveImage(string typeToken, Image<T> image)
{
var extra = this.ExtraName.IsNullOrEmpty() ? string.Empty : "_" + this.ExtraName;

var outName = $"{this.TestContext.TestName}{extra}_{typeToken}.png";
if (image == null)
{
this.TestContext.WriteLine($"Skipping writing expected image `{outName}` because it is null");
return;
}

this.SaveTestOutput(output =>
{
var path = output.CombinePath(outName);
image.Save(path);
return path;
});
this.SaveImage(image, $"{extra}_{typeToken}");
}

private bool ShouldWrite(WriteTestOutput should) =>
Expand Down
21 changes: 21 additions & 0 deletions tests/Acoustics.Test/TestHelpers/OutputDirectoryTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ namespace Acoustics.Test.TestHelpers
using System;
using System.IO;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;

[TestClass]
public class OutputDirectoryTest
Expand Down Expand Up @@ -69,6 +71,25 @@ protected FileInfo SaveTestOutput(Func<DirectoryInfo, FileInfo> callback)
return savedFile;
}

protected void SaveImage(Image image, params string[] tokens)
{
var token = tokens.Join("_");

var outName = $"{this.TestContext.TestName}{token}.png";
if (image == null)
{
this.TestContext.WriteLine($"Skipping writing expected image `{outName}` because it is null");
return;
}

this.SaveTestOutput(output =>
{
var path = output.CombinePath(outName);
image.Save(path);
return path;
});
}

/// <summary>
/// Save a test result.
/// Also saves copies of test results to daily output directories.
Expand Down
3 changes: 3 additions & 0 deletions tests/Fixtures/harmonic.wav
Git LFS file not shown

0 comments on commit da46bcb

Please sign in to comment.