feat(audio): signature reader, offset recovery, and version refusal
Closes the consumer halves of JR-044 and JR-045, which were blocked on there being no reader at all. `AudioSignatureMatcher` implements the specification's slide — ±600 frames, scoring the fraction of overlapping frames whose peak band agrees — and returns the tier and offset. JR-045: `TryParseFrames` refuses any prefix but `v1:`. A `v2:` signature from a future producer describes a DSP chain this build does not implement, so scoring it as v1 would be a confident wrong answer where declining is a correct one — the item drops to the runtime tier, which is the entire reason the prefix is separate from `schema_version`. JR-044: a runtime under 120 s yields no match and therefore no offset, read off the runtime rather than inferred from a missing string, because the runtime is what both producers test. The boundary is asserted on one file at 119.999 s and 120.000 s, so a null cannot be blamed on the decode. The offset has two terms, which is easy to miss: the recovered slide, and the difference between where the two windows are anchored, since both are centred on their own file's midpoint. A release carrying 40 s of extra head material recovers 20 s from each. One parameter is not from the specification and is marked as such in the code: an alignment must overlap by at least 64 frames before its score counts, or the extreme offsets compare a handful of frames where a chance agreement scores 1.0 and beats the true alignment. TRACES: JR-044, JR-045 | SR-003
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
using System;
|
||||
using Jellyfin.Plugin.JRay.Configuration;
|
||||
|
||||
namespace Jellyfin.Plugin.JRay.Services;
|
||||
|
||||
/// <summary>
|
||||
/// The outcome of comparing a local audio signature against a remote one.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Produced only when the two signatures actually align; a comparison that
|
||||
/// reaches no tier yields no result at all rather than a zero-scored one, so a
|
||||
/// caller cannot mistake "did not match" for "matched at the bottom".
|
||||
/// </remarks>
|
||||
public readonly struct AudioSignatureMatch : IEquatable<AudioSignatureMatch>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the fraction of overlapping frames whose peak band agreed, in [0, 1].
|
||||
/// </summary>
|
||||
public double Score { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the best-scoring alignment, in frames, between the two analysis
|
||||
/// windows.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Positive means the local window lags the remote one. This is a
|
||||
/// window-relative quantity and is <b>not</b> the offset to apply to
|
||||
/// timings — see <see cref="OffsetSec"/>, which additionally accounts for
|
||||
/// the two windows being anchored at different points in their files.
|
||||
/// </remarks>
|
||||
public int OffsetFrames { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the seconds to add to every remote window to bring it into the local
|
||||
/// file's timebase.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is the quantity JR-030 applies at store time, and the one
|
||||
/// <see cref="ManifestConverter.ToTruthFile"/> takes.
|
||||
/// </remarks>
|
||||
public double OffsetSec { get; init; }
|
||||
|
||||
/// <summary>Gets the tier this score earns.</summary>
|
||||
public MatchTier Tier { get; init; }
|
||||
|
||||
/// <summary>Compares two matches for equality.</summary>
|
||||
/// <param name="left">Left operand.</param>
|
||||
/// <param name="right">Right operand.</param>
|
||||
/// <returns><c>true</c> when the two are equal.</returns>
|
||||
public static bool operator ==(AudioSignatureMatch left, AudioSignatureMatch right) => left.Equals(right);
|
||||
|
||||
/// <summary>Compares two matches for inequality.</summary>
|
||||
/// <param name="left">Left operand.</param>
|
||||
/// <param name="right">Right operand.</param>
|
||||
/// <returns><c>true</c> when the two differ.</returns>
|
||||
public static bool operator !=(AudioSignatureMatch left, AudioSignatureMatch right) => !left.Equals(right);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(AudioSignatureMatch other)
|
||||
=> Score.Equals(other.Score)
|
||||
&& OffsetFrames == other.OffsetFrames
|
||||
&& OffsetSec.Equals(other.OffsetSec)
|
||||
&& Tier == other.Tier;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj) => obj is AudioSignatureMatch other && Equals(other);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode() => HashCode.Combine(Score, OffsetFrames, OffsetSec, Tier);
|
||||
}
|
||||
Reference in New Issue
Block a user