2024-10-25 14:31:50 -04:00
|
|
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
|
|
|
// SPDX-License-Identifier: GPL-3.0-only.
|
2024-10-25 14:15:12 -04:00
|
|
|
|
2022-10-28 02:25:57 -05:00
|
|
|
using System;
|
|
|
|
using System.Collections.Generic;
|
2024-06-15 13:16:47 +02:00
|
|
|
using System.Linq;
|
2022-10-28 02:25:57 -05:00
|
|
|
using System.Numerics;
|
|
|
|
using System.Threading;
|
2024-11-02 18:17:22 +01:00
|
|
|
using System.Threading.Tasks;
|
2024-10-19 23:50:41 +02:00
|
|
|
using IntroSkipper.Configuration;
|
|
|
|
using IntroSkipper.Data;
|
2022-10-28 02:25:57 -05:00
|
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
|
2024-10-19 23:50:41 +02:00
|
|
|
namespace IntroSkipper.Analyzers;
|
2024-04-20 12:58:29 +02:00
|
|
|
|
2022-10-28 02:25:57 -05:00
|
|
|
/// <summary>
|
2024-11-21 15:42:55 +01:00
|
|
|
/// Initializes a new instance of the <see cref="ChromaprintAnalyzer"/> class.
|
2022-10-28 02:25:57 -05:00
|
|
|
/// </summary>
|
2024-11-21 15:42:55 +01:00
|
|
|
/// <param name="logger">Logger.</param>
|
|
|
|
public class ChromaprintAnalyzer(ILogger<ChromaprintAnalyzer> logger) : IMediaFileAnalyzer
|
2022-10-28 02:25:57 -05:00
|
|
|
{
|
|
|
|
/// <summary>
|
|
|
|
/// Seconds of audio in one fingerprint point.
|
|
|
|
/// This value is defined by the Chromaprint library and should not be changed.
|
|
|
|
/// </summary>
|
2024-05-10 14:05:59 +02:00
|
|
|
private const double SamplesToSeconds = 0.1238;
|
2024-11-21 15:42:55 +01:00
|
|
|
private readonly PluginConfiguration _config = Plugin.Instance?.Configuration ?? new PluginConfiguration();
|
|
|
|
private readonly ILogger<ChromaprintAnalyzer> _logger = logger;
|
|
|
|
private readonly Dictionary<Guid, Dictionary<uint, int>> _invertedIndexCache = [];
|
2024-10-16 16:05:59 +02:00
|
|
|
private AnalysisMode _analysisMode;
|
2022-10-31 01:00:39 -05:00
|
|
|
|
2022-10-28 02:25:57 -05:00
|
|
|
/// <inheritdoc />
|
2024-11-02 18:17:22 +01:00
|
|
|
public async Task<IReadOnlyList<QueuedEpisode>> AnalyzeMediaFiles(
|
2024-10-05 19:30:30 +02:00
|
|
|
IReadOnlyList<QueuedEpisode> analysisQueue,
|
2024-10-16 16:05:59 +02:00
|
|
|
AnalysisMode mode,
|
2022-10-28 02:25:57 -05:00
|
|
|
CancellationToken cancellationToken)
|
|
|
|
{
|
2024-11-21 15:42:55 +01:00
|
|
|
// Episodes that were not analyzed.
|
|
|
|
var episodeAnalysisQueue = analysisQueue.Where(e => !e.IsAnalyzed).ToList();
|
2022-10-28 02:25:57 -05:00
|
|
|
|
2024-11-21 15:42:55 +01:00
|
|
|
if (episodeAnalysisQueue.Count <= 1)
|
2024-06-15 13:16:47 +02:00
|
|
|
{
|
|
|
|
return analysisQueue;
|
|
|
|
}
|
|
|
|
|
2024-11-21 15:42:55 +01:00
|
|
|
_analysisMode = mode;
|
2024-06-15 13:16:47 +02:00
|
|
|
|
2024-11-21 15:42:55 +01:00
|
|
|
// All intros for this season.
|
|
|
|
var seasonIntros = new Dictionary<Guid, Segment>();
|
2024-06-15 13:16:47 +02:00
|
|
|
|
2024-11-21 15:42:55 +01:00
|
|
|
// Cache of all fingerprints for this season.
|
|
|
|
var fingerprintCache = new Dictionary<Guid, uint[]>();
|
2024-06-15 13:16:47 +02:00
|
|
|
|
2022-10-28 02:25:57 -05:00
|
|
|
// Compute fingerprints for all episodes in the season
|
2024-11-21 15:42:55 +01:00
|
|
|
foreach (var episode in episodeAnalysisQueue)
|
2022-10-28 02:25:57 -05:00
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
2022-10-31 01:00:39 -05:00
|
|
|
fingerprintCache[episode.EpisodeId] = FFmpegWrapper.Fingerprint(episode, mode);
|
2022-10-28 02:25:57 -05:00
|
|
|
|
2024-04-26 15:06:41 +02:00
|
|
|
// Use reversed fingerprints for credits
|
2024-10-16 16:05:59 +02:00
|
|
|
if (_analysisMode == AnalysisMode.Credits)
|
2024-04-26 15:06:41 +02:00
|
|
|
{
|
|
|
|
Array.Reverse(fingerprintCache[episode.EpisodeId]);
|
|
|
|
}
|
|
|
|
|
2022-10-28 02:25:57 -05:00
|
|
|
if (cancellationToken.IsCancellationRequested)
|
|
|
|
{
|
|
|
|
return analysisQueue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
catch (FingerprintException ex)
|
|
|
|
{
|
2022-11-06 21:25:23 -06:00
|
|
|
_logger.LogDebug("Caught fingerprint error: {Ex}", ex);
|
|
|
|
WarningManager.SetFlag(PluginWarning.InvalidChromaprintFingerprint);
|
2022-10-28 02:25:57 -05:00
|
|
|
|
|
|
|
// Fallback to an empty fingerprint on any error
|
2024-09-10 18:08:42 +02:00
|
|
|
fingerprintCache[episode.EpisodeId] = [];
|
2022-10-28 02:25:57 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// While there are still episodes in the queue
|
2024-11-21 15:42:55 +01:00
|
|
|
while (episodeAnalysisQueue.Count > 0)
|
2022-10-28 02:25:57 -05:00
|
|
|
{
|
|
|
|
// Pop the first episode from the queue
|
2024-11-21 15:42:55 +01:00
|
|
|
var currentEpisode = episodeAnalysisQueue[0];
|
|
|
|
episodeAnalysisQueue.RemoveAt(0);
|
2022-10-28 02:25:57 -05:00
|
|
|
|
|
|
|
// Search through all remaining episodes.
|
2024-11-21 15:42:55 +01:00
|
|
|
foreach (var remainingEpisode in episodeAnalysisQueue)
|
2022-10-28 02:25:57 -05:00
|
|
|
{
|
|
|
|
// Compare the current episode to all remaining episodes in the queue.
|
|
|
|
var (currentIntro, remainingIntro) = CompareEpisodes(
|
|
|
|
currentEpisode.EpisodeId,
|
|
|
|
fingerprintCache[currentEpisode.EpisodeId],
|
|
|
|
remainingEpisode.EpisodeId,
|
|
|
|
fingerprintCache[remainingEpisode.EpisodeId]);
|
|
|
|
|
|
|
|
// Ignore this comparison result if:
|
|
|
|
// - one of the intros isn't valid, or
|
|
|
|
// - the introduction exceeds the configured limit
|
|
|
|
if (
|
|
|
|
!remainingIntro.Valid ||
|
2024-10-16 16:05:59 +02:00
|
|
|
(_analysisMode == AnalysisMode.Introduction && remainingIntro.Duration > Plugin.Instance!.Configuration.MaximumIntroDuration))
|
2022-10-28 02:25:57 -05:00
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2022-10-31 01:00:39 -05:00
|
|
|
/* Since the Fingerprint() function returns an array of Chromaprint points without time
|
|
|
|
* information, the times reported from the index search function start from 0.
|
|
|
|
*
|
|
|
|
* While this is desired behavior for detecting introductions, it breaks credit
|
|
|
|
* detection, as the audio we're analyzing was extracted from some point into the file.
|
|
|
|
*
|
2024-04-26 15:06:41 +02:00
|
|
|
* To fix this, the starting and ending times need to be switched, as they were previously reversed
|
|
|
|
* and subtracted from the episode duration to get the reported time range.
|
2022-10-31 01:00:39 -05:00
|
|
|
*/
|
2024-10-16 16:05:59 +02:00
|
|
|
if (_analysisMode == AnalysisMode.Credits)
|
2022-10-31 01:00:39 -05:00
|
|
|
{
|
2024-04-26 15:06:41 +02:00
|
|
|
// Calculate new values for the current intro
|
2024-09-12 08:37:47 +00:00
|
|
|
double currentOriginalIntroStart = currentIntro.Start;
|
|
|
|
currentIntro.Start = currentEpisode.Duration - currentIntro.End;
|
|
|
|
currentIntro.End = currentEpisode.Duration - currentOriginalIntroStart;
|
2024-04-26 15:06:41 +02:00
|
|
|
|
|
|
|
// Calculate new values for the remaining intro
|
2024-09-12 08:37:47 +00:00
|
|
|
double remainingIntroOriginalStart = remainingIntro.Start;
|
|
|
|
remainingIntro.Start = remainingEpisode.Duration - remainingIntro.End;
|
|
|
|
remainingIntro.End = remainingEpisode.Duration - remainingIntroOriginalStart;
|
2022-10-31 01:00:39 -05:00
|
|
|
}
|
|
|
|
|
2022-10-28 02:25:57 -05:00
|
|
|
// Only save the discovered intro if it is:
|
|
|
|
// - the first intro discovered for this episode
|
|
|
|
// - longer than the previously discovered intro
|
|
|
|
if (
|
|
|
|
!seasonIntros.TryGetValue(currentIntro.EpisodeId, out var savedCurrentIntro) ||
|
|
|
|
currentIntro.Duration > savedCurrentIntro.Duration)
|
|
|
|
{
|
|
|
|
seasonIntros[currentIntro.EpisodeId] = currentIntro;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
|
|
|
!seasonIntros.TryGetValue(remainingIntro.EpisodeId, out var savedRemainingIntro) ||
|
|
|
|
remainingIntro.Duration > savedRemainingIntro.Duration)
|
|
|
|
{
|
|
|
|
seasonIntros[remainingIntro.EpisodeId] = remainingIntro;
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2024-11-21 15:42:55 +01:00
|
|
|
// If an intro is found for this episode, adjust its times and save it else add it to the list of episodes without intros.
|
|
|
|
if (seasonIntros.TryGetValue(currentEpisode.EpisodeId, out var intro))
|
2024-05-08 16:27:41 +02:00
|
|
|
{
|
2024-11-21 15:42:55 +01:00
|
|
|
currentEpisode.IsAnalyzed = true;
|
|
|
|
await Plugin.Instance!.UpdateTimestampAsync(intro, mode).ConfigureAwait(false);
|
2024-05-08 16:27:41 +02:00
|
|
|
}
|
2022-10-28 02:25:57 -05:00
|
|
|
}
|
|
|
|
|
2024-11-21 15:42:55 +01:00
|
|
|
return analysisQueue;
|
2022-10-28 02:25:57 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Analyze two episodes to find an introduction sequence shared between them.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="lhsId">First episode id.</param>
|
|
|
|
/// <param name="lhsPoints">First episode fingerprint points.</param>
|
|
|
|
/// <param name="rhsId">Second episode id.</param>
|
|
|
|
/// <param name="rhsPoints">Second episode fingerprint points.</param>
|
|
|
|
/// <returns>Intros for the first and second episodes.</returns>
|
2024-09-12 08:37:47 +00:00
|
|
|
public (Segment Lhs, Segment Rhs) CompareEpisodes(
|
2022-10-28 02:25:57 -05:00
|
|
|
Guid lhsId,
|
|
|
|
uint[] lhsPoints,
|
|
|
|
Guid rhsId,
|
|
|
|
uint[] rhsPoints)
|
|
|
|
{
|
|
|
|
// Creates an inverted fingerprint point index for both episodes.
|
|
|
|
// For every point which is a 100% match, search for an introduction at that point.
|
|
|
|
var (lhsRanges, rhsRanges) = SearchInvertedIndex(lhsId, lhsPoints, rhsId, rhsPoints);
|
|
|
|
|
|
|
|
if (lhsRanges.Count > 0)
|
|
|
|
{
|
|
|
|
_logger.LogTrace("Index search successful");
|
|
|
|
|
|
|
|
return GetLongestTimeRange(lhsId, lhsRanges, rhsId, rhsRanges);
|
|
|
|
}
|
|
|
|
|
|
|
|
_logger.LogTrace(
|
|
|
|
"Unable to find a shared introduction sequence between {LHS} and {RHS}",
|
|
|
|
lhsId,
|
|
|
|
rhsId);
|
|
|
|
|
2024-09-12 08:37:47 +00:00
|
|
|
return (new Segment(lhsId), new Segment(rhsId));
|
2022-10-28 02:25:57 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Locates the longest range of similar audio and returns an Intro class for each range.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="lhsId">First episode id.</param>
|
|
|
|
/// <param name="lhsRanges">First episode shared timecodes.</param>
|
|
|
|
/// <param name="rhsId">Second episode id.</param>
|
|
|
|
/// <param name="rhsRanges">Second episode shared timecodes.</param>
|
|
|
|
/// <returns>Intros for the first and second episodes.</returns>
|
2024-09-25 17:23:25 +02:00
|
|
|
private static (Segment Lhs, Segment Rhs) GetLongestTimeRange(
|
2022-10-28 02:25:57 -05:00
|
|
|
Guid lhsId,
|
|
|
|
List<TimeRange> lhsRanges,
|
|
|
|
Guid rhsId,
|
|
|
|
List<TimeRange> rhsRanges)
|
|
|
|
{
|
|
|
|
// Store the longest time range as the introduction.
|
|
|
|
lhsRanges.Sort();
|
|
|
|
rhsRanges.Sort();
|
|
|
|
|
|
|
|
var lhsIntro = lhsRanges[0];
|
|
|
|
var rhsIntro = rhsRanges[0];
|
|
|
|
|
|
|
|
// If the intro starts early in the episode, move it to the beginning.
|
|
|
|
if (lhsIntro.Start <= 5)
|
|
|
|
{
|
|
|
|
lhsIntro.Start = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (rhsIntro.Start <= 5)
|
|
|
|
{
|
|
|
|
rhsIntro.Start = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create Intro classes for each time range.
|
2024-09-12 08:37:47 +00:00
|
|
|
return (new Segment(lhsId, lhsIntro), new Segment(rhsId, rhsIntro));
|
2022-10-28 02:25:57 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Search for a shared introduction sequence using inverted indexes.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="lhsId">LHS ID.</param>
|
|
|
|
/// <param name="lhsPoints">Left episode fingerprint points.</param>
|
|
|
|
/// <param name="rhsId">RHS ID.</param>
|
|
|
|
/// <param name="rhsPoints">Right episode fingerprint points.</param>
|
|
|
|
/// <returns>List of shared TimeRanges between the left and right episodes.</returns>
|
|
|
|
private (List<TimeRange> Lhs, List<TimeRange> Rhs) SearchInvertedIndex(
|
|
|
|
Guid lhsId,
|
|
|
|
uint[] lhsPoints,
|
|
|
|
Guid rhsId,
|
|
|
|
uint[] rhsPoints)
|
|
|
|
{
|
|
|
|
var lhsRanges = new List<TimeRange>();
|
|
|
|
var rhsRanges = new List<TimeRange>();
|
|
|
|
|
|
|
|
// Generate inverted indexes for the left and right episodes.
|
2024-11-21 15:42:55 +01:00
|
|
|
var lhsIndex = CreateInvertedIndex(lhsId, lhsPoints);
|
|
|
|
var rhsIndex = CreateInvertedIndex(rhsId, rhsPoints);
|
2022-10-28 02:25:57 -05:00
|
|
|
var indexShifts = new HashSet<int>();
|
|
|
|
|
|
|
|
// For all audio points in the left episode, check if the right episode has a point which matches exactly.
|
|
|
|
// If an exact match is found, calculate the shift that must be used to align the points.
|
|
|
|
foreach (var kvp in lhsIndex)
|
|
|
|
{
|
|
|
|
var originalPoint = kvp.Key;
|
|
|
|
|
2024-11-21 15:42:55 +01:00
|
|
|
for (var i = -1 * _config.InvertedIndexShift; i <= _config.InvertedIndexShift; i++)
|
2022-10-28 02:25:57 -05:00
|
|
|
{
|
|
|
|
var modifiedPoint = (uint)(originalPoint + i);
|
|
|
|
|
2024-03-05 09:12:57 -05:00
|
|
|
if (rhsIndex.TryGetValue(modifiedPoint, out var rhsModifiedPoint))
|
2022-10-28 02:25:57 -05:00
|
|
|
{
|
2024-04-20 12:58:29 +02:00
|
|
|
var lhsFirst = lhsIndex[originalPoint];
|
|
|
|
var rhsFirst = rhsModifiedPoint;
|
2022-10-28 02:25:57 -05:00
|
|
|
indexShifts.Add(rhsFirst - lhsFirst);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Use all discovered shifts to compare the episodes.
|
|
|
|
foreach (var shift in indexShifts)
|
|
|
|
{
|
|
|
|
var (lhsIndexContiguous, rhsIndexContiguous) = FindContiguous(lhsPoints, rhsPoints, shift);
|
|
|
|
if (lhsIndexContiguous.End > 0 && rhsIndexContiguous.End > 0)
|
|
|
|
{
|
|
|
|
lhsRanges.Add(lhsIndexContiguous);
|
|
|
|
rhsRanges.Add(rhsIndexContiguous);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return (lhsRanges, rhsRanges);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Finds the longest contiguous region of similar audio between two fingerprints using the provided shift amount.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="lhs">First fingerprint to compare.</param>
|
|
|
|
/// <param name="rhs">Second fingerprint to compare.</param>
|
|
|
|
/// <param name="shiftAmount">Amount to shift one fingerprint by.</param>
|
|
|
|
private (TimeRange Lhs, TimeRange Rhs) FindContiguous(
|
|
|
|
uint[] lhs,
|
|
|
|
uint[] rhs,
|
|
|
|
int shiftAmount)
|
|
|
|
{
|
|
|
|
var leftOffset = 0;
|
|
|
|
var rightOffset = 0;
|
|
|
|
|
|
|
|
// Calculate the offsets for the left and right hand sides.
|
|
|
|
if (shiftAmount < 0)
|
|
|
|
{
|
|
|
|
leftOffset -= shiftAmount;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
rightOffset += shiftAmount;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Store similar times for both LHS and RHS.
|
|
|
|
var lhsTimes = new List<double>();
|
|
|
|
var rhsTimes = new List<double>();
|
|
|
|
var upperLimit = Math.Min(lhs.Length, rhs.Length) - Math.Abs(shiftAmount);
|
|
|
|
|
|
|
|
// XOR all elements in LHS and RHS, using the shift amount from above.
|
|
|
|
for (var i = 0; i < upperLimit; i++)
|
|
|
|
{
|
|
|
|
// XOR both samples at the current position.
|
|
|
|
var lhsPosition = i + leftOffset;
|
|
|
|
var rhsPosition = i + rightOffset;
|
|
|
|
var diff = lhs[lhsPosition] ^ rhs[rhsPosition];
|
|
|
|
|
|
|
|
// If the difference between the samples is small, flag both times as similar.
|
2024-11-21 15:42:55 +01:00
|
|
|
if (CountBits(diff) > _config.MaximumFingerprintPointDifferences)
|
2022-10-28 02:25:57 -05:00
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
var lhsTime = lhsPosition * SamplesToSeconds;
|
|
|
|
var rhsTime = rhsPosition * SamplesToSeconds;
|
|
|
|
|
|
|
|
lhsTimes.Add(lhsTime);
|
|
|
|
rhsTimes.Add(rhsTime);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure the last timestamp is checked
|
|
|
|
lhsTimes.Add(double.MaxValue);
|
|
|
|
rhsTimes.Add(double.MaxValue);
|
|
|
|
|
|
|
|
// Now that both fingerprints have been compared at this shift, see if there's a contiguous time range.
|
2024-11-21 15:42:55 +01:00
|
|
|
var lContiguous = TimeRangeHelpers.FindContiguous([.. lhsTimes], _config.MaximumTimeSkip);
|
|
|
|
if (lContiguous is null || lContiguous.Duration < _config.MinimumIntroDuration)
|
2022-10-28 02:25:57 -05:00
|
|
|
{
|
|
|
|
return (new TimeRange(), new TimeRange());
|
|
|
|
}
|
|
|
|
|
|
|
|
// Since LHS had a contiguous time range, RHS must have one also.
|
2024-11-21 15:42:55 +01:00
|
|
|
var rContiguous = TimeRangeHelpers.FindContiguous([.. rhsTimes], _config.MaximumTimeSkip)!;
|
2022-10-28 02:25:57 -05:00
|
|
|
return (lContiguous, rContiguous);
|
|
|
|
}
|
|
|
|
|
2024-11-21 15:42:55 +01:00
|
|
|
/// <summary>
|
|
|
|
/// Adjusts the end timestamps of all intros so that they end at silence.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="episode">QueuedEpisode to adjust.</param>
|
|
|
|
/// <param name="originalIntro">Original introduction.</param>
|
|
|
|
private Segment AdjustIntroTimes(
|
|
|
|
QueuedEpisode episode,
|
|
|
|
Segment originalIntro)
|
|
|
|
{
|
|
|
|
_logger.LogTrace(
|
|
|
|
"{Name} original intro: {Start} - {End}",
|
|
|
|
episode.Name,
|
|
|
|
originalIntro.Start,
|
|
|
|
originalIntro.End);
|
|
|
|
|
|
|
|
var originalIntroStart = new TimeRange(
|
|
|
|
Math.Max(0, (int)originalIntro.Start - 5),
|
|
|
|
(int)originalIntro.Start + 10);
|
|
|
|
|
|
|
|
var originalIntroEnd = new TimeRange(
|
|
|
|
(int)originalIntro.End - 10,
|
|
|
|
Math.Min(episode.Duration, (int)originalIntro.End + 5));
|
|
|
|
|
|
|
|
// Try to adjust based on chapters first, fall back to silence detection for intros
|
|
|
|
if (!AdjustIntroBasedOnChapters(episode, originalIntro, originalIntroStart, originalIntroEnd) &&
|
|
|
|
_analysisMode == AnalysisMode.Introduction)
|
|
|
|
{
|
|
|
|
AdjustIntroBasedOnSilence(episode, originalIntro, originalIntroEnd);
|
|
|
|
}
|
|
|
|
|
|
|
|
_logger.LogTrace(
|
|
|
|
"{Name} adjusted intro: {Start} - {End}",
|
|
|
|
episode.Name,
|
|
|
|
originalIntro.Start,
|
|
|
|
originalIntro.End);
|
|
|
|
|
|
|
|
return originalIntro;
|
|
|
|
}
|
|
|
|
|
|
|
|
private bool AdjustIntroBasedOnChapters(
|
|
|
|
QueuedEpisode episode,
|
|
|
|
Segment intro,
|
|
|
|
TimeRange originalIntroStart,
|
|
|
|
TimeRange originalIntroEnd)
|
|
|
|
{
|
|
|
|
var chapters = Plugin.Instance?.GetChapters(episode.EpisodeId) ?? [];
|
|
|
|
double previousTime = 0;
|
|
|
|
|
|
|
|
for (int i = 0; i <= chapters.Count; i++)
|
|
|
|
{
|
|
|
|
double currentTime = i < chapters.Count
|
|
|
|
? TimeSpan.FromTicks(chapters[i].StartPositionTicks).TotalSeconds
|
|
|
|
: episode.Duration;
|
|
|
|
|
|
|
|
if (IsTimeWithinRange(previousTime, originalIntroStart))
|
|
|
|
{
|
|
|
|
intro.Start = previousTime;
|
|
|
|
_logger.LogTrace("{Name} chapter found close to intro start: {Start}", episode.Name, previousTime);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (IsTimeWithinRange(currentTime, originalIntroEnd))
|
|
|
|
{
|
|
|
|
intro.End = currentTime;
|
|
|
|
_logger.LogTrace("{Name} chapter found close to intro end: {End}", episode.Name, currentTime);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
previousTime = currentTime;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
private void AdjustIntroBasedOnSilence(QueuedEpisode episode, Segment intro, TimeRange originalIntroEnd)
|
|
|
|
{
|
|
|
|
var silenceRanges = FFmpegWrapper.DetectSilence(episode, originalIntroEnd);
|
|
|
|
|
|
|
|
foreach (var silenceRange in silenceRanges)
|
|
|
|
{
|
|
|
|
_logger.LogTrace("{Name} silence: {Start} - {End}", episode.Name, silenceRange.Start, silenceRange.End);
|
|
|
|
|
|
|
|
if (IsValidSilenceForIntroAdjustment(silenceRange, originalIntroEnd, intro))
|
|
|
|
{
|
|
|
|
intro.End = silenceRange.Start;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private bool IsValidSilenceForIntroAdjustment(
|
|
|
|
TimeRange silenceRange,
|
|
|
|
TimeRange originalIntroEnd,
|
|
|
|
Segment adjustedIntro)
|
|
|
|
{
|
|
|
|
return originalIntroEnd.Intersects(silenceRange) &&
|
|
|
|
silenceRange.Duration >= _config.SilenceDetectionMinimumDuration &&
|
|
|
|
silenceRange.Start >= adjustedIntro.Start;
|
|
|
|
}
|
|
|
|
|
|
|
|
private static bool IsTimeWithinRange(double time, TimeRange range)
|
|
|
|
{
|
|
|
|
return range.Start < time && time < range.End;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Transforms a Chromaprint into an inverted index of fingerprint points to the last index it appeared at.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="id">Episode ID.</param>
|
|
|
|
/// <param name="fingerprint">Chromaprint fingerprint.</param>
|
|
|
|
/// <returns>Inverted index.</returns>
|
|
|
|
public Dictionary<uint, int> CreateInvertedIndex(Guid id, uint[] fingerprint)
|
|
|
|
{
|
|
|
|
if (_invertedIndexCache.TryGetValue(id, out var cached))
|
|
|
|
{
|
|
|
|
return cached;
|
|
|
|
}
|
|
|
|
|
|
|
|
var invIndex = new Dictionary<uint, int>();
|
|
|
|
|
|
|
|
for (int i = 0; i < fingerprint.Length; i++)
|
|
|
|
{
|
|
|
|
// Get the current point.
|
|
|
|
var point = fingerprint[i];
|
|
|
|
|
|
|
|
// Append the current sample's timecode to the collection for this point.
|
|
|
|
invIndex[point] = i;
|
|
|
|
}
|
|
|
|
|
|
|
|
_invertedIndexCache[id] = invIndex;
|
|
|
|
|
|
|
|
return invIndex;
|
|
|
|
}
|
|
|
|
|
2022-10-28 02:25:57 -05:00
|
|
|
/// <summary>
|
|
|
|
/// Count the number of bits that are set in the provided number.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="number">Number to count bits in.</param>
|
|
|
|
/// <returns>Number of bits that are equal to 1.</returns>
|
2024-11-21 15:42:55 +01:00
|
|
|
public static int CountBits(uint number)
|
2022-10-28 02:25:57 -05:00
|
|
|
{
|
|
|
|
return BitOperations.PopCount(number);
|
|
|
|
}
|
|
|
|
}
|