Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
* develop:
  Version bump to v8.27.0
  Making sure non-completed realtime query matches are released even if full track does not match.
  This is a dangerous implementation that can lead to memory leaks, let's not publish it for the general public.
  When purging we need to clear tracks as well.
  RealtimeQueryResult can contain the information about ongoing realtime entries.
  • Loading branch information
AddictedCS committed Sep 14, 2023
2 parents 3f27b89 + 69f28c1 commit 656a77d
Show file tree
Hide file tree
Showing 12 changed files with 129 additions and 55 deletions.
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("8.25.0.100")]
[assembly: AssemblyInformationalVersion("8.25.0.100")]
[assembly: AssemblyVersion("8.27.0.100")]
[assembly: AssemblyInformationalVersion("8.27.0.100")]
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ public void SetUp()
{
audioStrategy = new Mock<ICompletionStrategy<ResultEntry>>();
videoStrategy = new Mock<ICompletionStrategy<ResultEntry>>();
avEntryStrategy = new AVResultEntryCompletionStrategy(audioStrategy.Object, videoStrategy.Object);
avEntryStrategy = new AvResultEntryCompletionStrategy(audioStrategy.Object, videoStrategy.Object);
}

[Test]
public void ConstructorThrowsOnInvalidArgs()
{
Assert.Throws<ArgumentNullException>(() => new AVResultEntryCompletionStrategy(null, videoStrategy.Object));
Assert.Throws<ArgumentNullException>(() => new AVResultEntryCompletionStrategy(audioStrategy.Object, null));
Assert.Throws<ArgumentNullException>(() => new AvResultEntryCompletionStrategy(null, videoStrategy.Object));
Assert.Throws<ArgumentNullException>(() => new AvResultEntryCompletionStrategy(audioStrategy.Object, null));
}

[Test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public RealtimeQueryCommandTest()
}

[Test]
public async Task RealtimeQueryShouldMatchOnlySelectedClusters()
public async Task RealtimeQueryShouldMatchOnlySelectedMetaFieldsFilters()
{
var modelService = new InMemoryModelService();
int count = 10, foundWithClusters = 0, foundWithWrongClusters = 0, testWaitTime = 3000;
Expand All @@ -56,7 +56,9 @@ public async Task RealtimeQueryShouldMatchOnlySelectedClusters()
modelService.Insert(new TrackInfo("312", "Bohemian Rhapsody", "Queen", new Dictionary<string, string>{{ "country", "USA" }}), hashes);

var cancellationTokenSource = new CancellationTokenSource(testWaitTime);
var wrong = QueryCommandBuilder.Instance.BuildRealtimeQueryCommand()
var wrong = QueryCommandBuilder
.Instance
.BuildRealtimeQueryCommand()
.From(SimulateRealtimeAudioQueryData(data, jitterLength: 0))
.WithRealtimeQueryConfig(config =>
{
Expand Down Expand Up @@ -93,8 +95,7 @@ public async Task RealtimeQueryStrideShouldBeUsed()
var modelService = new InMemoryModelService();
int staticStride = 1024;
double permittedGap = (double) minSamplesPerFingerprint / sampleRate;
int count = 10, found = 0, didNotPassThreshold = 0, fingerprintsCount = 0;
int testWaitTime = 3000;
int count = 10, found = 0, didNotPassThreshold = 0, fingerprintsCount = 0, testWaitTime = 3000;
var data = GenerateRandomAudioChunks(count, 1, DateTime.UtcNow);
var concatenated = Concatenate(data);
var hashes = await FingerprintCommandBuilder.Instance
Expand All @@ -107,7 +108,8 @@ public async Task RealtimeQueryStrideShouldBeUsed()
var collection = SimulateRealtimeAudioQueryData(data, jitterLength: 0);
var cancellationTokenSource = new CancellationTokenSource(testWaitTime);

double duration = await QueryCommandBuilder.Instance.BuildRealtimeQueryCommand()
double duration = await QueryCommandBuilder.Instance
.BuildRealtimeQueryCommand()
.From(collection)
.WithRealtimeQueryConfig(config =>
{
Expand All @@ -129,13 +131,69 @@ public async Task RealtimeQueryStrideShouldBeUsed()
Assert.AreEqual((double)count * minSamplesPerFingerprint / 5512, duration, 0.00001);
}

[Test]
public async Task ShouldCaptureRealtimeQueryResultThatOccursOnTheEdgeOfQueryMatches()
{
var modelService = new InMemoryModelService();
int staticStride = 512;
double permittedGap = (double) minSamplesPerFingerprint / sampleRate, queryLength = 0d;
int count = 10, found = 0, didNotPassThreshold = 0, hashesCount = 0, testWaitTime = 3000;
var data = GenerateRandomAudioChunks(count, 1, DateTime.UtcNow);
var concatenated = Concatenate(data);
var hashes = await FingerprintCommandBuilder.Instance
.BuildFingerprintCommand()
.From(concatenated)
.Hash();

modelService.Insert(new TrackInfo("312", "Bohemian Rhapsody", "Queen"), hashes);

// query samples contain all but last chunk of the track and more random samples that should not match
var querySamples = data
.Take(count - 1)
.Concat(GenerateRandomAudioChunks(count, seed: 100, relativeTo: DateTime.UtcNow.AddSeconds(minSizeChunkDuration * (count - 1)))).ToList();

var collection = SimulateRealtimeAudioQueryData(querySamples, jitterLength: 0);
var cancellationTokenSource = new CancellationTokenSource(testWaitTime);

await QueryCommandBuilder.Instance
.BuildRealtimeQueryCommand()
.From(collection)
.WithRealtimeQueryConfig(config =>
{
config.ResultEntryFilter = new TrackRelativeCoverageLengthEntryFilter(0.2d, waitTillCompletion: true);
config.QueryConfiguration.Audio.Stride = new IncrementalStaticStride(staticStride);
config.SuccessCallback = _ =>
{
// we want to capture match not when final results are purged
// making sure we call the accumulators even on empty results
if (hashesCount <= count + 1)
{
Interlocked.Increment(ref found);
}
};
config.DidNotPassFilterCallback = _ => Interlocked.Increment(ref didNotPassThreshold);
config.QueryConfiguration.Audio.PermittedGap = permittedGap;
return config;
})
.InterceptHashes(fingerprints =>
{
Interlocked.Increment(ref hashesCount);
return fingerprints;
})
.UsingServices(modelService)
.Query(cancellationTokenSource.Token);

Assert.AreEqual(1, found);
Assert.AreEqual(count * 2 - 1, hashesCount);
}

[Test]
public async Task ShouldQueryInRealtime()
{
var modelService = new InMemoryModelService();

double minSizeChunk = (double)minSamplesPerFingerprint / sampleRate; // length in seconds of one query chunk ~1.8577
const double totalTrackLength = 210; // length of the track 3 minutes 30 seconds.
const double totalTrackLength = 210; // length of the track 3 minutes 30 seconds.
int count = (int)Math.Round(totalTrackLength / minSizeChunk), fingerprintsCount = 0, queryMatchLength = 10, ongoingCalls = 0;
var data = GenerateRandomAudioChunks(count, seed: 1, DateTime.UtcNow);
var concatenated = Concatenate(data);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ public void ShouldNotFailWithNullObjectPass()
var aggregator = new StatefulRealtimeResultEntryAggregator(
new TrackMatchLengthEntryFilter(5d),
new NoPassRealtimeResultEntryFilter(),
_ => { },
new AVResultEntryCompletionStrategy(new ResultEntryCompletionStrategy(3d), new ResultEntryCompletionStrategy(1.75d)),
new AvResultEntryCompletionStrategy(new ResultEntryCompletionStrategy(3d), new ResultEntryCompletionStrategy(1.75d)),
new ResultEntryConcatenator(loggerFactory, false),
new ResultEntryConcatenator(loggerFactory, false),
new StatefulQueryHashesConcatenator());
Expand All @@ -52,8 +51,7 @@ public void ShouldWaitAsTrackLengthPermits()
var aggregator = new StatefulRealtimeResultEntryAggregator(
new TrackMatchLengthEntryFilter(10d),
new NoPassRealtimeResultEntryFilter(),
_ => { },
new AVResultEntryCompletionStrategy(new ResultEntryCompletionStrategy(0d), new ResultEntryCompletionStrategy(0d)),
new AvResultEntryCompletionStrategy(new ResultEntryCompletionStrategy(0d), new ResultEntryCompletionStrategy(0d)),
new ResultEntryConcatenator(loggerFactory, false),
new ResultEntryConcatenator(loggerFactory, false),
new StatefulQueryHashesConcatenator());
Expand Down Expand Up @@ -90,8 +88,7 @@ public void ShouldMergeResults()
double permittedGap = 2d;
var aggregator = new StatefulRealtimeResultEntryAggregator(new TrackMatchLengthEntryFilter(5d),
new NoPassRealtimeResultEntryFilter(),
_ => { },
new AVResultEntryCompletionStrategy(new ResultEntryCompletionStrategy(3d), new ResultEntryCompletionStrategy(1.75d)),
new AvResultEntryCompletionStrategy(new ResultEntryCompletionStrategy(3d), new ResultEntryCompletionStrategy(1.75d)),
new ResultEntryConcatenator(loggerFactory, false),
new ResultEntryConcatenator(loggerFactory, false),
new StatefulQueryHashesConcatenator());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace SoundFingerprinting.Command
/// <summary>
/// No pass realtime result entry filter.
/// </summary>
public class NoPassRealtimeResultEntryFilter : IRealtimeResultEntryFilter
internal class NoPassRealtimeResultEntryFilter : IRealtimeResultEntryFilter
{
/// <summary>
/// Never pass result entry filter.
Expand Down
33 changes: 17 additions & 16 deletions src/SoundFingerprinting/Command/RealtimeQueryCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -252,10 +252,10 @@ private async IAsyncEnumerable<AVHashes> ConvertToAvHashes(IAsyncEnumerable<AVTr
/// <exception cref="ObjectDisposedException">Object disposed exception (invoked on cancellation token).</exception>
private async Task<double> QueryRealtimeSource(CancellationToken cancellationToken)
{
var resultsAggregator = new StatefulRealtimeResultEntryAggregator(configuration.ResultEntryFilter,
var resultsAggregator = new StatefulRealtimeResultEntryAggregator(
configuration.ResultEntryFilter,
configuration.OngoingResultEntryFilter,
configuration.OngoingSuccessCallback,
new AVResultEntryCompletionStrategy(configuration.QueryConfiguration),
new AvResultEntryCompletionStrategy(configuration.QueryConfiguration),
new ResultEntryConcatenator(loggerFactory, configuration.AutomaticSkipDetection),
new ResultEntryConcatenator(loggerFactory, configuration.AutomaticSkipDetection),
configuration.IncludeQueryHashesInResponse ? new StatefulQueryHashesConcatenator() : new NoOpQueryHashesConcatenator(loggerFactory));
Expand Down Expand Up @@ -301,8 +301,8 @@ private async Task<double> QueryRealtimeSource(CancellationToken cancellationTok
{
// let's purge stateful results aggregator to safe-guard ourselves from memory issues when certain tracks get stuck in the aggregator
var purged = resultsAggregator.Purge();
InvokeSuccessHandler(purged.SuccessEntries);
InvokeDidNotPassFilterHandler(purged.DidNotPassThresholdEntries);
InvokeCallbackHandler(purged.SuccessEntries, configuration?.SuccessCallback);
InvokeCallbackHandler(purged.DidNotPassThresholdEntries, configuration?.DidNotPassFilterCallback);
}
}
}
Expand All @@ -327,9 +327,10 @@ private async Task QueryFromRealtimeAndOffline(IRealtimeResultEntryAggregator re

private void ConsumeQueryResult(AVQueryResult queryResult, IRealtimeResultEntryAggregator resultsAggregator)
{
var aggregatedResult = resultsAggregator.Consume(queryResult);
InvokeSuccessHandler(aggregatedResult.SuccessEntries);
InvokeDidNotPassFilterHandler(aggregatedResult.DidNotPassThresholdEntries);
var (ongoingEntries, successEntries, didNotPassThresholdEntries) = resultsAggregator.Consume(queryResult);
InvokeOngoingCallbackHandler(ongoingEntries);
InvokeCallbackHandler(successEntries, configuration.SuccessCallback);
InvokeCallbackHandler(didNotPassThresholdEntries, configuration.DidNotPassFilterCallback);
}

private void HandleQueryFailure(AVHashes? hashes, Exception e)
Expand Down Expand Up @@ -485,20 +486,20 @@ private async Task<AVQueryResult> GetAvQueryResult(AVHashes hashes)
return avQueryResult;
}

private void InvokeDidNotPassFilterHandler(IEnumerable<AVQueryResult> queryResults)
private void InvokeOngoingCallbackHandler(IEnumerable<AVResultEntry> ongoingResultEntries)
{
foreach (var queryResult in queryResults)
foreach (var resultEntry in ongoingResultEntries)
{
configuration?.DidNotPassFilterCallback(queryResult);
configuration?.OngoingSuccessCallback(resultEntry);
}
}

private void InvokeSuccessHandler(IEnumerable<AVQueryResult> queryResults)
private static void InvokeCallbackHandler(IEnumerable<AVQueryResult> results, Action<AVQueryResult>? callback)
{
foreach (var queryResult in queryResults)
foreach (var queryResult in results)
{
configuration?.SuccessCallback(queryResult);
}
callback?.Invoke(queryResult);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ public TrackRelativeCoverageLengthEntryFilter(double coverage, bool waitTillComp
/// <inheritdoc cref="IRealtimeResultEntryFilter.Pass"/>
public bool Pass(AVResultEntry entry, bool canContinueInTheNextQuery)
{
return !waitTillCompletion ? (entry.Audio?.TrackRelativeCoverage > coverage || entry.Video?.TrackRelativeCoverage > coverage) : (entry.Audio?.TrackRelativeCoverage > coverage || entry.Video?.TrackRelativeCoverage > coverage) && !canContinueInTheNextQuery;
return !waitTillCompletion ?
(entry.Audio?.TrackRelativeCoverage > coverage || entry.Video?.TrackRelativeCoverage > coverage) :
(entry.Audio?.TrackRelativeCoverage > coverage || entry.Video?.TrackRelativeCoverage > coverage) && !canContinueInTheNextQuery;
}
}
}
4 changes: 2 additions & 2 deletions src/SoundFingerprinting/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@
[assembly: InternalsVisibleTo("SoundFingerprinting.FFT.FFTW")]
[assembly: InternalsVisibleTo("SoundFingerprinting.FFT.FFTW.Tests")]

[assembly: AssemblyVersion("8.26.0.100")]
[assembly: AssemblyInformationalVersion("8.26.0.100")]
[assembly: AssemblyVersion("8.27.0.100")]
[assembly: AssemblyInformationalVersion("8.27.0.100")]
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@ namespace SoundFingerprinting.Query
using System;
using SoundFingerprinting.Configuration;

internal sealed class AVResultEntryCompletionStrategy : ICompletionStrategy<AVResultEntry>
internal sealed class AvResultEntryCompletionStrategy : ICompletionStrategy<AVResultEntry>
{
private readonly ICompletionStrategy<ResultEntry> audioStrategy;
private readonly ICompletionStrategy<ResultEntry> videoStrategy;

public AVResultEntryCompletionStrategy(ICompletionStrategy<ResultEntry> audioStrategy, ICompletionStrategy<ResultEntry> videoStrategy)
public AvResultEntryCompletionStrategy(ICompletionStrategy<ResultEntry> audioStrategy, ICompletionStrategy<ResultEntry> videoStrategy)
{
this.audioStrategy = audioStrategy ?? throw new ArgumentNullException(nameof(audioStrategy));
this.videoStrategy = videoStrategy ?? throw new ArgumentNullException(nameof(videoStrategy));
}

public AVResultEntryCompletionStrategy(AVQueryConfiguration config) : this(
public AvResultEntryCompletionStrategy(AVQueryConfiguration config) : this(
new ResultEntryCompletionStrategy(config.Audio.PermittedGap),
new ResultEntryCompletionStrategy(config.Video.PermittedGap))
{
Expand Down
22 changes: 19 additions & 3 deletions src/SoundFingerprinting/Query/RealtimeQueryResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,23 @@ namespace SoundFingerprinting.Query
using System.Collections.Generic;
using SoundFingerprinting.Command;

public class RealtimeQueryResult
internal class RealtimeQueryResult
{
public RealtimeQueryResult(IEnumerable<AVQueryResult> successEntries, IEnumerable<AVQueryResult> didNotPassThresholdEntries)
public RealtimeQueryResult(
IEnumerable<AVResultEntry> ongoingEntries,
IEnumerable<AVQueryResult> successEntries,
IEnumerable<AVQueryResult> didNotPassThresholdEntries)
{
OngoingEntries = ongoingEntries;
SuccessEntries = successEntries;
DidNotPassThresholdEntries = didNotPassThresholdEntries;
}


/// <summary>
/// Gets list of ongoing matches.
/// </summary>
public IEnumerable<AVResultEntry> OngoingEntries { get; }

/// <summary>
/// Gets list of aggregated successful matches.
/// </summary>
Expand All @@ -23,5 +32,12 @@ public RealtimeQueryResult(IEnumerable<AVQueryResult> successEntries, IEnumerabl
/// See implementations of <see cref="IRealtimeResultEntryFilter"/> interface.
/// </remarks>
public IEnumerable<AVQueryResult> DidNotPassThresholdEntries { get; }

public void Deconstruct(out IEnumerable<AVResultEntry> ongoingEntries, out IEnumerable<AVQueryResult> successEntries, out IEnumerable<AVQueryResult> didNotPassThresholdEntries)
{
ongoingEntries = OngoingEntries;
successEntries = SuccessEntries;
didNotPassThresholdEntries = DidNotPassThresholdEntries;
}
}
}
Loading

0 comments on commit 656a77d

Please sign in to comment.