Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
* develop:
  Version bump to v10.3.0
  Adding the ability to add silence in the result set of fingerprints, which may be useful for scenarios when the signal contains a lot of pauses that are considered as silence (podcasts).
  Improving query path reconstruction strategy, specifically in case we have a tone signal inside query and track, current implementation will allow to reconstruct it without any track/query gaps.
  Update README.md
  • Loading branch information
AddictedCS committed May 16, 2024
2 parents b60c0fc + 4081504 commit 24a58d9
Show file tree
Hide file tree
Showing 13 changed files with 191 additions and 66 deletions.
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,14 @@ Read [Supported Media Formats][audio-services-wiki-page] page for details about
### Video fingerprinting support since version 8.0.0
Since `v8.0.0` video fingerprinting support has been added. Similarly to audio fingerprinting, video fingerprints are generated from video frames, and used to insert and later query the datastore for exact and similar matches. You can use `SoundFingerprinting` to fingerprint either audio or video content or both at the same time. More details about video fingerprinting are available [here][video-fingerprinting-wiki-page].

### Version 9
Version 9 was released to accomodate `SoundFingerprinting.Emy` v9.0.0, which upgrades to FFmpeg v5.x (breaking change as v8.x is using FFmpeg v4.x).
If you are not using `SoundFingerprinting.Emy` you can safely upgrade to v9. Version 9.4.0 provides dramatic improvement for long queries (over 1 hour), that match long tracks.
### Version Matrix
If you are using `FFmpegAudioService` as described in the [wiki][audio-services-wiki-page], follow the below version matrix.
| SoundFingerprinting | SoundFingerprinting.Emy | FFmpeg |
| ---- | ------ |-----|
| 8.x | 8.x | 4.x |
| 9.x | 9.x | 5.x |
| 10.x | 10.x | 6.x |



### FAQ
Expand Down
3 changes: 2 additions & 1 deletion src/Emy.ruleset
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@
<Rule Id="SA1025" Action="None" /> <!-- Code should not contain multiple whitespaces in a row-->
<Rule Id="SA1408" Action="None" /> <!-- Conditional expressions should declare precedence-->
<Rule Id="SA1120" Action="None" /> <!-- Comments should contain text-->
<Rule Id="SA1629" Action="None" /> <!-- Documentation should end with a comma-->
<Rule Id="SA1629" Action="None" /> <!-- Documentation should end with a comma-->
<Rule Id="SA1010" Action="None" />
</Rules>
</RuleSet>
Original file line number Diff line number Diff line change
Expand Up @@ -299,18 +299,19 @@ public async Task ShouldCreateFingerprintsFromAudioSamplesQueryWithPreviouslyCre
{
var audioSamples = GetAudioSamples();
var track = new TrackInfo("4321", audioSamples.Origin, audioSamples.Origin);
var fingerprints = await FingerprintCommandBuilder.Instance
var avHashes = await FingerprintCommandBuilder.Instance
.BuildFingerprintCommand()
.From(audioSamples)
.UsingServices(audioService)
.Hash();

var modelService = new InMemoryModelService();
modelService.Insert(track, fingerprints);
modelService.Insert(track, avHashes);

var (queryResult, _) = await QueryCommandBuilder.Instance.BuildQueryCommand()
.From(fingerprints)
.UsingServices(modelService, audioService)
var (queryResult, _) = await QueryCommandBuilder.Instance
.BuildQueryCommand()
.From(avHashes)
.UsingServices(modelService)
.Query();

Assert.That(queryResult, Is.Not.Null);
Expand Down
4 changes: 2 additions & 2 deletions src/SoundFingerprinting.Tests/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]
[assembly: Guid("4cac962e-ebc5-4006-a1e0-7ffb3e2483c2")]
[assembly: AssemblyVersion("10.0.0.100")]
[assembly: AssemblyInformationalVersion("10.0.0.100")]
[assembly: AssemblyVersion("10.3.0.100")]
[assembly: AssemblyInformationalVersion("10.3.0.100")]
8 changes: 8 additions & 0 deletions src/SoundFingerprinting.Tests/TestUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,14 @@ public static List<Gap> GetGaps(double[] gapsStartEnd)
return gaps;
}

public static AudioSamples Concatenate(AudioSamples first, AudioSamples second)
{
float[] concatenated = new float[first.Samples.Length + second.Samples.Length];
Array.Copy(first.Samples, concatenated, first.Samples.Length);
Array.Copy(second.Samples, 0, concatenated, first.Samples.Length, second.Samples.Length);
return new AudioSamples(concatenated, string.Empty, first.SampleRate, first.RelativeTo);
}

private static bool IsInsideGap(MatchedWith matched, IEnumerable<Gap> gaps, double fingerprintLength)
{
return gaps.Any(gap => matched.TrackMatchAt + fingerprintLength >= gap.Start && matched.TrackMatchAt <= gap.End);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class QueryPathReconstructionStrategyTest
[Test]
public void ShouldNotThrowWhenEmptyIsPassed()
{
var result = queryPathReconstructionStrategy.GetBestPaths(Enumerable.Empty<MatchedWith>(), permittedGap: 0);
var result = queryPathReconstructionStrategy.GetBestPaths([], permittedGap: 0);

CollectionAssert.IsEmpty(result);
}
Expand Down Expand Up @@ -49,19 +49,18 @@ public void ShouldIgnoreRepeatingCrossMatches()
/*
* q 1 1 1 4
* t 1 2 3 4
* expected x x x x
* expected x x
* max 1 1 1 2
*/
[Test]
public void ShouldPickAllQueryCandidates()
{
var matchedWiths = new[] { (1, 1), (1, 2), (1, 3), (4, 4) }.Select(tuple =>
new MatchedWith((uint)tuple.Item1, tuple.Item1, (uint)tuple.Item2, tuple.Item2, 0d));
var matchedWiths = new[] { (1, 1), (1, 2), (1, 3), (4, 4) }.Select(tuple => new MatchedWith((uint)tuple.Item1, tuple.Item1, (uint)tuple.Item2, tuple.Item2, 0d));

var result = queryPathReconstructionStrategy.GetBestPaths(matchedWiths, permittedGap: 0).First().ToList();

CollectionAssert.AreEqual(new[] { 1, 1, 1, 4 }, result.Select(_ => (int)_.QuerySequenceNumber));
CollectionAssert.AreEqual(new[] { 1, 2, 3, 4 }, result.Select(_ => (int)_.TrackSequenceNumber));
CollectionAssert.AreEqual(new[] { 1, 4 }, result.Select(_ => (int)_.QuerySequenceNumber));
CollectionAssert.AreEqual(new[] { 1, 4 }, result.Select(_ => (int)_.TrackSequenceNumber));
}

/*
Expand All @@ -82,6 +81,24 @@ public void ShouldPickAllTrackCandidates()
CollectionAssert.AreEqual(new[] { 1, 1, 1, 4 }, result.Select(_ => (int)_.TrackSequenceNumber));
}

/*
* q 1 2 3 4 7 4 5 6
* t 1 2 3 4 6 6 6 6
* expected x x x x
* max 1 2 3 4 5 4 5 6
*/
[Test]
public void ShouldNotUpdateIfQueryMatchReversalDetected()
{
var matchedWiths = new[] { (1, 1), (2, 2), (3, 3), (4, 4), (7, 6), (4, 6), (5, 6), (6, 6) }
.Select(tuple => new MatchedWith((uint)tuple.Item1, tuple.Item1, (uint)tuple.Item2, tuple.Item2, 0d));

var result = queryPathReconstructionStrategy.GetBestPaths(matchedWiths, permittedGap: 0).First().ToList();

CollectionAssert.AreEqual(new[] { 1, 2, 3, 4, 5, 6 }, result.Select(_ => (int)_.QuerySequenceNumber));
CollectionAssert.AreEqual(new[] { 1, 2, 3, 4, 6, 6 }, result.Select(_ => (int)_.TrackSequenceNumber));
}

[Test]
public void ShouldFindLongestIncreasingSequence()
{
Expand Down Expand Up @@ -194,9 +211,10 @@ public void ShouldFindLongestIncreasingSequence2()
*/

var pairs = new[] {(1, 1), (1, 2), (1, 3), (4, 4)};
var expected = new[] {(1, 1), (4, 4)};
var result = queryPathReconstructionStrategy.GetBestPaths(Generate(pairs), permittedGap: 0).First();

AssertResult(pairs, result);
AssertResult(expected, result);
}

[Test]
Expand Down Expand Up @@ -249,9 +267,10 @@ public void ShouldFindLongestIncreasingSequence5()
*/

var pairs = new[] {(1, 1), (2, 2), (3, 3), (4, 3), (4, 4)};
var expected = new[] {(1, 1), (2, 2), (3, 3), (4, 4)};
var result = queryPathReconstructionStrategy.GetBestPaths(Generate(pairs), permittedGap: 0).ToList();

AssertResult(pairs, result[0]);
AssertResult(expected, result[0]);
}

[Test]
Expand Down Expand Up @@ -285,15 +304,15 @@ public void ShouldFindLongestIncreasingSequence7()
/*
* q 1 2 4 3 3
* t 1 2 3 4 5
* expected x x x x
* expected x x x
* max 1 2 3 3 3
*/

var pairs = new[] {(1, 1), (2, 2), (4, 3), (3, 4), (3, 5)};
var result = queryPathReconstructionStrategy.GetBestPaths(Generate(pairs), permittedGap: 0).ToList();

Assert.AreEqual(1, result.Count);
var expected1 = new[] {(1, 1), (2, 2), (3, 4), (3, 5)};
var expected1 = new[] {(1, 1), (2, 2), (3, 4)};
AssertResult(expected1, result[0]);
}

Expand Down
48 changes: 44 additions & 4 deletions src/SoundFingerprinting.Tests/Unit/Query/QueryCommandTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -228,10 +228,6 @@ public async Task ShouldRemoveCrossMatches()

await InsertFingerprints(track, modelService);

// when allow multiple matches is specified it should return all four matches (cross matches included)
var multipleMatches = await GetQueryResult(query, modelService);
// Assert.AreEqual(4, multipleMatches.ResultEntries.Count());

var singleMatch = await GetQueryResult(query, modelService);
Assert.AreEqual(1, singleMatch.ResultEntries.Count());
var coverage = singleMatch.ResultEntries.First().Coverage;
Expand Down Expand Up @@ -286,6 +282,50 @@ public async Task ShouldIdentifySameMatchTwiceQueryLengthIsSmall()
Assert.AreEqual(60d, entries[1].TrackMatchStartsAt, 1f);
Assert.AreEqual(0d, entries[1].QueryMatchStartsAt, 1f);
}

[Test(Description = "Should cross match tone signal")]
public async Task ShouldBeAbleToCrossMatchToneSignal()
{
var first = TestUtilities.GenerateRandomAudioSamples(15 * 5512);
var silenceGap = new AudioSamples(Enumerable.Repeat((float)0.5, 10 * 5512).ToArray(), string.Empty, 5512);
var second = TestUtilities.GenerateRandomAudioSamples(15 * 5512);

var samples = TestUtilities.Concatenate(TestUtilities.Concatenate(first, silenceGap), second);

var avHashes = await FingerprintCommandBuilder.Instance
.BuildFingerprintCommand()
.From(samples)
.WithFingerprintConfig(config =>
{
config.Audio.TreatSilenceAsSignal = true;
return config;
})
.UsingServices(new SoundFingerprintingAudioService())
.Hash();

var modelService = new InMemoryModelService();

modelService.Insert(new TrackInfo("id", "title", "artist"), avHashes);

var query = await QueryCommandBuilder
.Instance
.BuildQueryCommand()
.From(avHashes)
.UsingServices(modelService)
.Query();

var result = query.ResultEntries.First();

Assert.That(result, Is.Not.Null);
var queryGaps = result.Audio!.Coverage.QueryGaps.ToList();
var trackGaps = result.Audio.Coverage.TrackGaps.ToList();

Assert.That(trackGaps, Is.Empty);
Assert.That(queryGaps, Is.Empty);

Assert.That(result.Audio.Confidence, Is.EqualTo(1).Within(0.1));
Assert.That(result.Audio.TrackRelativeCoverage, Is.EqualTo(1).Within(0.1));
}

private static float[] GetRandomSamplesWithRegions(float[] m1, float[] m2)
{
Expand Down
10 changes: 10 additions & 0 deletions src/SoundFingerprinting/Configuration/FingerprintConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,5 +108,15 @@ public FrequencyRange FrequencyRange
/// Frame normalization allows to apply
/// </remarks>
public IFrameNormalization FrameNormalizationTransform { get; set; } = null!;

/// <summary>
/// Gets or sets a value indicating whether to include silence fingerprints into the fingerprinted result set.
/// </summary>
/// <remarks>
/// Keep in mind that silence fingerprints will always cross-match with any other silence fingerprints. <br />
/// May be useful in scenarios when the dataset is small, and the content you are fingerprinting contains a lot of speech. <br />
/// Default value is false. <br/>
/// </remarks>
public bool TreatSilenceAsSignal { get; set; }
}
}
2 changes: 1 addition & 1 deletion src/SoundFingerprinting/FingerprintService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ internal IEnumerable<Fingerprint> CreateOriginalFingerprintsFromFrames(IEnumerab
waveletDecomposition.DecomposeImageInPlace(rowCols, frame.Rows, frame.Cols, configuration.HaarWaveletNorm);
RangeUtils.PopulateIndexes(length, cachedIndexes);
var image = fingerprintDescriptor.ExtractTopWavelets(rowCols, configuration.TopWavelets, cachedIndexes);
if (!image.IsSilence)
if (!image.IsSilence || configuration.TreatSilenceAsSignal)
{
fingerprints.Add(new Fingerprint(image, frame.StartsAt, frame.SequenceNumber, originalPoint));
}
Expand Down
14 changes: 5 additions & 9 deletions src/SoundFingerprinting/LCS/MaxAt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,12 @@ namespace SoundFingerprinting.LCS
{
using SoundFingerprinting.Query;

internal class MaxAt
internal record MaxAt(int Length, MatchedWith MatchedWith)
{
public MaxAt(int length, MatchedWith matchedWith)
{
Length = length;
MatchedWith = matchedWith;
}
public int Length { get; } = Length;

public int Length { get; }
public MatchedWith MatchedWith { get; }
public MatchedWith MatchedWith { get; } = MatchedWith;

public float QueryTrackDistance => System.Math.Abs(MatchedWith.QueryMatchAt - MatchedWith.TrackMatchAt);
}
}
Loading

0 comments on commit 24a58d9

Please sign in to comment.