From 96331c0093695bb472573965d4bf22432a7f2af7 Mon Sep 17 00:00:00 2001 From: Sergiu Ciumac Date: Fri, 5 Apr 2024 10:01:08 +0300 Subject: [PATCH 1/4] Update README.md Adding FFmpeg version matrix --- README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 86cafe6d..2847c8ca 100644 --- a/README.md +++ b/README.md @@ -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 From 3e12ad5f66fd64813b9c54edbb21d0279dfaa35d Mon Sep 17 00:00:00 2001 From: Sergiu Ciumac Date: Thu, 16 May 2024 11:44:01 +0300 Subject: [PATCH 2/4] 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. --- src/Emy.ruleset | 3 +- .../FingerprintCommandBuilderIntTest.cs | 11 ++- .../Properties/AssemblyInfo.cs | 4 +- .../TestUtilities.cs | 8 ++ .../QueryPathReconstructionStrategyTest.cs | 39 ++++++-- .../Unit/Query/QueryCommandTest.cs | 48 ++++++++- src/SoundFingerprinting/LCS/MaxAt.cs | 14 +-- .../LCS/QueryPathReconstructionStrategy.cs | 98 +++++++++++++------ .../Properties/AssemblyInfo.cs | 4 +- .../SoundFingerprinting.csproj | 5 +- 10 files changed, 172 insertions(+), 62 deletions(-) diff --git a/src/Emy.ruleset b/src/Emy.ruleset index 6dbef2ef..2d82f788 100644 --- a/src/Emy.ruleset +++ b/src/Emy.ruleset @@ -115,6 +115,7 @@ - + + \ No newline at end of file diff --git a/src/SoundFingerprinting.Tests/Integration/FingerprintCommandBuilderIntTest.cs b/src/SoundFingerprinting.Tests/Integration/FingerprintCommandBuilderIntTest.cs index edc3b1da..7ec2e653 100644 --- a/src/SoundFingerprinting.Tests/Integration/FingerprintCommandBuilderIntTest.cs +++ b/src/SoundFingerprinting.Tests/Integration/FingerprintCommandBuilderIntTest.cs @@ -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); diff --git a/src/SoundFingerprinting.Tests/Properties/AssemblyInfo.cs b/src/SoundFingerprinting.Tests/Properties/AssemblyInfo.cs index 5e316759..114ab638 100644 --- a/src/SoundFingerprinting.Tests/Properties/AssemblyInfo.cs +++ b/src/SoundFingerprinting.Tests/Properties/AssemblyInfo.cs @@ -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")] diff --git a/src/SoundFingerprinting.Tests/TestUtilities.cs b/src/SoundFingerprinting.Tests/TestUtilities.cs index 59c08f09..6a7e446b 100644 --- a/src/SoundFingerprinting.Tests/TestUtilities.cs +++ b/src/SoundFingerprinting.Tests/TestUtilities.cs @@ -163,6 +163,14 @@ public static List 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 gaps, double fingerprintLength) { return gaps.Any(gap => matched.TrackMatchAt + fingerprintLength >= gap.Start && matched.TrackMatchAt <= gap.End); diff --git a/src/SoundFingerprinting.Tests/Unit/LCS/QueryPathReconstructionStrategyTest.cs b/src/SoundFingerprinting.Tests/Unit/LCS/QueryPathReconstructionStrategyTest.cs index c5eb07d7..0d684b9c 100644 --- a/src/SoundFingerprinting.Tests/Unit/LCS/QueryPathReconstructionStrategyTest.cs +++ b/src/SoundFingerprinting.Tests/Unit/LCS/QueryPathReconstructionStrategyTest.cs @@ -16,7 +16,7 @@ public class QueryPathReconstructionStrategyTest [Test] public void ShouldNotThrowWhenEmptyIsPassed() { - var result = queryPathReconstructionStrategy.GetBestPaths(Enumerable.Empty(), permittedGap: 0); + var result = queryPathReconstructionStrategy.GetBestPaths([], permittedGap: 0); CollectionAssert.IsEmpty(result); } @@ -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)); } /* @@ -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() { @@ -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] @@ -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] @@ -285,7 +304,7 @@ 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 */ @@ -293,7 +312,7 @@ public void ShouldFindLongestIncreasingSequence7() 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]); } diff --git a/src/SoundFingerprinting.Tests/Unit/Query/QueryCommandTest.cs b/src/SoundFingerprinting.Tests/Unit/Query/QueryCommandTest.cs index bc1c5893..83ec3a9f 100644 --- a/src/SoundFingerprinting.Tests/Unit/Query/QueryCommandTest.cs +++ b/src/SoundFingerprinting.Tests/Unit/Query/QueryCommandTest.cs @@ -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; @@ -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) { diff --git a/src/SoundFingerprinting/LCS/MaxAt.cs b/src/SoundFingerprinting/LCS/MaxAt.cs index bb8e74a9..5c82a11f 100644 --- a/src/SoundFingerprinting/LCS/MaxAt.cs +++ b/src/SoundFingerprinting/LCS/MaxAt.cs @@ -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); } } \ No newline at end of file diff --git a/src/SoundFingerprinting/LCS/QueryPathReconstructionStrategy.cs b/src/SoundFingerprinting/LCS/QueryPathReconstructionStrategy.cs index 86df2c4b..5ad925f1 100644 --- a/src/SoundFingerprinting/LCS/QueryPathReconstructionStrategy.cs +++ b/src/SoundFingerprinting/LCS/QueryPathReconstructionStrategy.cs @@ -1,6 +1,7 @@ namespace SoundFingerprinting.LCS; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using SoundFingerprinting.Query; @@ -24,7 +25,7 @@ private IEnumerable> GetIncreasingSequences(IEnumerable var bestPaths = new List>(); while (matchedWiths.Any()) { - var (sequence, badSequence) = GetLongestIncreasingSequence(matchedWiths, permittedGap); + var (sequence, exclusions) = GetLongestIncreasingSequence(matchedWiths, permittedGap); var withs = sequence as MatchedWith[] ?? sequence.ToArray(); if (!withs.Any()) { @@ -32,17 +33,14 @@ private IEnumerable> GetIncreasingSequences(IEnumerable } bestPaths.Add(withs); - matchedWiths = matchedWiths.Except(withs.Concat(badSequence)).ToList(); + matchedWiths = matchedWiths.Except(withs.Concat(exclusions)).ToList(); } - // this may seem as redundant but it is not, since we can pick the first candidates from not the same sequences + // this may seem as redundant, but it is not, since we can pick the first candidates from not the same sequences return bestPaths.OrderByDescending(_ => _.Count()); } - private static bool IsSameSequence(MatchedWith a, MatchedWith b, double maxGap) - { - return Math.Abs(a.QueryMatchAt - b.QueryMatchAt) <= maxGap && Math.Abs(a.TrackMatchAt - b.TrackMatchAt) <= maxGap; - } + private MaxAt[] MaxIncreasingQuerySequenceOptimal(IReadOnlyList matches, double maxGap, out int max, out int maxIndex) { @@ -97,49 +95,93 @@ private LongestIncreasingSequence GetLongestIncreasingSequence(IEnumerable x.TrackSequenceNumber).ThenBy(_ => _.TrackMatchAt).ToList(); if (!matches.Any()) { - return new LongestIncreasingSequence(Enumerable.Empty(), Enumerable.Empty()); + return new LongestIncreasingSequence([], []); } double maxGap = GetMaxGap(matches, permittedGap); + + // locking second dimension - query sequence number var maxArray = MaxIncreasingQuerySequenceOptimal(matches, maxGap, out int max, out int maxIndex); - var maxs = new Stack(maxArray.Take(maxIndex + 1)); - var result = new Stack(); + // initializing the datastructures with first element set to max + var maxs = new Stack(maxArray.Take(maxIndex)); var excluded = new List(); - while (maxs.TryPop(out var candidate) && max > 0) + var result = new ConcurrentDictionary {[max--] = maxArray[maxIndex]}; + var lastPicked = maxArray[maxIndex]; + + while (maxs.TryPop(out var candidate)) { if (candidate!.Length != max) { - // out of order element need to be excluded if it is part of the same sequence - if (result.TryPeek(out var lastPicked) && IsSameSequence(candidate.MatchedWith, lastPicked!.MatchedWith, maxGap)) + // check if the candidate is part of the same decreasing sequence + if (IsSameSequence(candidate, lastPicked, maxGap)) { - excluded.Add(candidate); + // check if we previously picked a sequence with the same length, if yes we should try picking the best one + if (candidate.Length > max) + { + TryUpdateResultSelection(result, candidate, excluded); + } + else + { + // start of a shorter sequence, we should exclude it + excluded.Add(candidate); + } } - + continue; } max--; + do { - bool firstElementInSequence = !result.TryPeek(out var lastPicked); - bool querySequenceDecreasing = IsQuerySequenceDecreasing(candidate!, lastPicked); - bool sameSequence = firstElementInSequence || IsSameSequence(candidate!.MatchedWith, lastPicked!.MatchedWith, maxGap); - - switch (querySequenceDecreasing) + switch (IsQuerySequenceDecreasing(candidate, lastPicked)) { - case true when sameSequence: - result.Push(candidate!); + case true when IsSameSequence(candidate, lastPicked, maxGap): + lastPicked = TryUpdateResultSelection(result, candidate, excluded); break; - case false when sameSequence: - excluded.Add(candidate!); + case false when IsSameSequence(candidate, lastPicked, maxGap): + excluded.Add(candidate); break; } } - while (maxs.TryPeek(out var lookAhead) && EqualMaxLength(candidate!, lookAhead!) && maxs.TryPop(out candidate)); + while (maxs.TryPeek(out var lookAhead) && EqualMaxLength(candidate!, lookAhead!) && maxs.TryPop(out candidate!)); } - return new LongestIncreasingSequence(result.Select(_ => _.MatchedWith), excluded.Select(_ => _.MatchedWith)); + return new LongestIncreasingSequence(result.OrderBy(_ => _.Key).Select(_ => _.Value.MatchedWith), excluded.Select(_ => _.MatchedWith)); + } + + private static MaxAt TryUpdateResultSelection(ConcurrentDictionary result, MaxAt candidate, List excluded) + { + // check if the candidate is closer to the diagonal than the previous element, pick best and exclude the other + return result.AddOrUpdate(candidate.Length, candidate, (_, previous) => + { + double prevQueryTrackDistance = previous.QueryTrackDistance; + double currentQueryTrackDistance = candidate.QueryTrackDistance; + + // possible when the candidate is part of a different decreasing sequence with equal maxAt + if (!IsQuerySequenceDecreasing(candidate, previous)) + { + excluded.Add(candidate); + return previous; + } + + // if the current element is closer to the diagonal, we should pick it + var pickedValue = prevQueryTrackDistance < currentQueryTrackDistance ? previous : candidate!; + var excludedValue = prevQueryTrackDistance < currentQueryTrackDistance ? candidate : previous; + excluded.Add(excludedValue); + return pickedValue; + }); + } + + private static bool IsSameSequence(MaxAt first, MaxAt second, double maxGap) + { + return IsSameSequence(first.MatchedWith, second.MatchedWith, maxGap); + } + + private static bool IsSameSequence(MatchedWith first, MatchedWith second, double maxGap) + { + return Math.Abs(first.QueryMatchAt - second.QueryMatchAt) <= maxGap && Math.Abs(first.TrackMatchAt - second.TrackMatchAt) <= maxGap; } private static double GetMaxGap(List matches, double permittedGap) @@ -156,9 +198,9 @@ private static double GetMaxGap(List matches, double permittedGap) return Math.Max(permittedGap, Math.Min(queryMatchAtMax - queryMatchAtMin, trackMatchAtMax - trackMatchAtMin)); } - private static bool IsQuerySequenceDecreasing(MaxAt lookAhead, MaxAt? lastPicked) + private static bool IsQuerySequenceDecreasing(MaxAt lookAhead, MaxAt lastPicked) { - return !(lookAhead.MatchedWith.QuerySequenceNumber > lastPicked?.MatchedWith.QuerySequenceNumber); + return !(lookAhead.MatchedWith.QuerySequenceNumber > lastPicked.MatchedWith.QuerySequenceNumber); } private static bool EqualMaxLength(MaxAt current, MaxAt lookAhead) diff --git a/src/SoundFingerprinting/Properties/AssemblyInfo.cs b/src/SoundFingerprinting/Properties/AssemblyInfo.cs index d3b98508..16205e02 100644 --- a/src/SoundFingerprinting/Properties/AssemblyInfo.cs +++ b/src/SoundFingerprinting/Properties/AssemblyInfo.cs @@ -19,5 +19,5 @@ [assembly: InternalsVisibleTo("SoundFingerprinting.FFT.FFTW")] [assembly: InternalsVisibleTo("SoundFingerprinting.FFT.FFTW.Tests")] -[assembly: AssemblyVersion("10.0.0.100")] -[assembly: AssemblyInformationalVersion("10.0.0.100")] +[assembly: AssemblyVersion("10.3.0.100")] +[assembly: AssemblyInformationalVersion("10.3.0.100")] diff --git a/src/SoundFingerprinting/SoundFingerprinting.csproj b/src/SoundFingerprinting/SoundFingerprinting.csproj index de8fb30e..c8dae99d 100644 --- a/src/SoundFingerprinting/SoundFingerprinting.csproj +++ b/src/SoundFingerprinting/SoundFingerprinting.csproj @@ -4,13 +4,16 @@ true false enable - 10.0.0 + 10.3.0-beta3 Sergiu Ciumac SoundFingerprinting is a C# framework that implements an efficient algorithm of audio fingerprinting and identification. Designed for developers, enthusiasts, researchers in the fields of audio processing, data mining, digital signal processing. https://github.com/addictedcs/soundfingerprinting https://github.com/AddictedCS/soundfingerprinting git + Version 10.3.0 + - Improved the ability to reconstruct coverage from tone signal matches (silence can be treated as a tone signal). + - Added a fingerprinting flag that allows including silence fingerprints in the generated result set. Version 10 - Accomodating SoundFingerprinting.Emy upgrade to FFmpeg 6.x Version 9.5.0 From f008168356d27ef6d5b4e28fbae4e08351818198 Mon Sep 17 00:00:00 2001 From: Sergiu Ciumac Date: Thu, 16 May 2024 11:44:45 +0300 Subject: [PATCH 3/4] 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). --- .../Configuration/FingerprintConfiguration.cs | 10 ++++++++++ src/SoundFingerprinting/FingerprintService.cs | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/SoundFingerprinting/Configuration/FingerprintConfiguration.cs b/src/SoundFingerprinting/Configuration/FingerprintConfiguration.cs index 29fdea3e..dde365fd 100644 --- a/src/SoundFingerprinting/Configuration/FingerprintConfiguration.cs +++ b/src/SoundFingerprinting/Configuration/FingerprintConfiguration.cs @@ -108,5 +108,15 @@ public FrequencyRange FrequencyRange /// Frame normalization allows to apply /// public IFrameNormalization FrameNormalizationTransform { get; set; } = null!; + + /// + /// Gets or sets a value indicating whether to include silence fingerprints into the fingerprinted result set. + /// + /// + /// Keep in mind that silence fingerprints will always cross-match with any other silence fingerprints.
+ /// May be useful in scenarios when the dataset is small, and the content you are fingerprinting contains a lot of speech.
+ /// Default value is false.
+ ///
+ public bool TreatSilenceAsSignal { get; set; } } } \ No newline at end of file diff --git a/src/SoundFingerprinting/FingerprintService.cs b/src/SoundFingerprinting/FingerprintService.cs index a0b14843..4527c27e 100644 --- a/src/SoundFingerprinting/FingerprintService.cs +++ b/src/SoundFingerprinting/FingerprintService.cs @@ -99,7 +99,7 @@ internal IEnumerable 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)); } From 4081504381bf1af08a75f2b56bd481202d96bb4b Mon Sep 17 00:00:00 2001 From: Sergiu Ciumac Date: Thu, 16 May 2024 11:45:12 +0300 Subject: [PATCH 4/4] Version bump to v10.3.0 --- src/SoundFingerprinting/SoundFingerprinting.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SoundFingerprinting/SoundFingerprinting.csproj b/src/SoundFingerprinting/SoundFingerprinting.csproj index c8dae99d..a0fde2ed 100644 --- a/src/SoundFingerprinting/SoundFingerprinting.csproj +++ b/src/SoundFingerprinting/SoundFingerprinting.csproj @@ -4,7 +4,7 @@ true false enable - 10.3.0-beta3 + 10.3.0 Sergiu Ciumac SoundFingerprinting is a C# framework that implements an efficient algorithm of audio fingerprinting and identification. Designed for developers, enthusiasts, researchers in the fields of audio processing, data mining, digital signal processing. https://github.com/addictedcs/soundfingerprinting