Der Kern des Vorspann-Abgleichs — ohne Anschluss, aber geprüft
Ein Vorspann ist das, was in jeder Folge einer Staffel gleich klingt. Sonst ist an einer Folge nichts, woran er sich erkennen liesse: er sieht aus wie Handlung und klingt wie Handlung, er wiederholt sich nur. `Tonabgleich` sucht das längste Stück, das zwei Abdruckreihen teilen — reine Rechnung, ohne Datei und ohne ffmpeg, deshalb mit sieben Prüfungen belegt. Darunter die wichtigere Richtung: zwei Folgen ohne Gemeinsamkeit ergeben nichts. Ein nicht gefundener Vorspann kostet einen Knopf, ein erfundener schneidet dem Zuschauer eine halbe Minute Handlung weg. Die Abdrücke holt `fpcalc` aus Chromaprint, nicht selbstgeschriebene Signalverarbeitung. Dasselbe tut Jellyfins Intro Skipper, aus demselben Grund. Das Programk liegt ab jetzt im Abbild; fehlt es, gibt es eben keine Marken — sie sind eine Zugabe, kein Teil der Wiedergabe. Noch ruft das niemand auf. Tabelle, Lauf und Knöpfe kommen als Nächstes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
bfe263d495
commit
bcbe5557d7
@@ -0,0 +1,165 @@
|
||||
using Flaro.Core.Media;
|
||||
|
||||
namespace Flaro.Core.Tests;
|
||||
|
||||
/// <summary>Was zwei Folgen gemeinsam haben.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <b>Mit erfundenen Abdrücken statt mit Mediendateien.</b> Was hier geprüft
|
||||
/// wird, ist die Suche nach dem gemeinsamen Stück — nicht, ob Chromaprint
|
||||
/// rechnen kann. Eine Prüfung, die eine Serie mit echten Folgen braucht,
|
||||
/// liefe nirgends ausser auf dem Rechner dessen, der sie geschrieben hat.
|
||||
/// </remarks>
|
||||
public class TonabgleichTests
|
||||
{
|
||||
/// <summary>Chromaprint mit Standardeinstellungen.</summary>
|
||||
private const double JeAbdruck = 0.1238;
|
||||
|
||||
/// <summary>Eine Folge von Abdrücken, die sich nicht wiederholt.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// Aus einem festen Startwert gerechnet, nicht gewürfelt: dieselbe
|
||||
/// Prüfung soll morgen dasselbe Ergebnis haben.
|
||||
/// </remarks>
|
||||
private static uint[] Rauschen(int anzahl, int saat)
|
||||
{
|
||||
var werte = new uint[anzahl];
|
||||
var stand = (uint)saat * 2654435761u + 1u;
|
||||
for (var i = 0; i < anzahl; i++)
|
||||
{
|
||||
stand ^= stand << 13;
|
||||
stand ^= stand >> 17;
|
||||
stand ^= stand << 5;
|
||||
werte[i] = stand;
|
||||
}
|
||||
return werte;
|
||||
}
|
||||
|
||||
/// <summary>Hängt Abschnitte aneinander.</summary>
|
||||
private static uint[] Aneinander(params uint[][] teile) =>
|
||||
teile.SelectMany(t => t).ToArray();
|
||||
|
||||
/// <summary>Wie viele Zeitscheiben so viele Sekunden sind.</summary>
|
||||
private static int Scheiben(double sekunden) => (int)Math.Round(sekunden / JeAbdruck);
|
||||
|
||||
[Test]
|
||||
public void Ein_gemeinsamer_Vorspann_wird_gefunden()
|
||||
{
|
||||
var vorspann = Rauschen(Scheiben(30), saat: 1);
|
||||
|
||||
// Folge 1: zehn Sekunden Handlung, dann der Vorspann, dann weiter.
|
||||
var a = Aneinander(Rauschen(Scheiben(10), 2), vorspann, Rauschen(Scheiben(60), 3));
|
||||
// Folge 2: der Vorspann liegt viel später.
|
||||
var b = Aneinander(Rauschen(Scheiben(70), 4), vorspann, Rauschen(Scheiben(20), 5));
|
||||
|
||||
var treffer = Tonabgleich.Finden(a, b, JeAbdruck);
|
||||
|
||||
Assert.That(treffer, Is.Not.Null);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(treffer!.AVon, Is.EqualTo(10).Within(1.0));
|
||||
Assert.That(treffer.ABis, Is.EqualTo(40).Within(1.0));
|
||||
Assert.That(treffer.BVon, Is.EqualTo(70).Within(1.0));
|
||||
Assert.That(treffer.BBis, Is.EqualTo(100).Within(1.0));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Zwei Folgen ohne Gemeinsamkeit ergeben keinen Fund.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <b>Der wichtigere der beiden Fälle.</b> Ein Vorspann, der nicht
|
||||
/// gefunden wird, kostet einen Knopf. Ein Vorspann, der erfunden wird,
|
||||
/// schneidet dem Zuschauer eine halbe Minute Handlung weg.
|
||||
/// </remarks>
|
||||
[Test]
|
||||
public void Ohne_Gemeinsamkeit_kommt_nichts()
|
||||
{
|
||||
var a = Rauschen(Scheiben(120), 11);
|
||||
var b = Rauschen(Scheiben(120), 12);
|
||||
|
||||
Assert.That(Tonabgleich.Finden(a, b, JeAbdruck), Is.Null);
|
||||
}
|
||||
|
||||
/// <summary>Ein zu kurzes gemeinsames Stück zählt nicht.</summary>
|
||||
[Test]
|
||||
public void Drei_Sekunden_sind_kein_Vorspann()
|
||||
{
|
||||
var kurz = Rauschen(Scheiben(3), 21);
|
||||
var a = Aneinander(Rauschen(Scheiben(30), 22), kurz, Rauschen(Scheiben(30), 23));
|
||||
var b = Aneinander(Rauschen(Scheiben(50), 24), kurz, Rauschen(Scheiben(10), 25));
|
||||
|
||||
Assert.That(Tonabgleich.Finden(a, b, JeAbdruck), Is.Null);
|
||||
}
|
||||
|
||||
/// <summary>Ein paar abweichende Bits machen den Fund nicht kaputt.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// Derselbe Vorspann aus zwei Dateien ist nie bitgleich: andere Bitrate,
|
||||
/// anderer Codec, andere Lautstärke. Wäre Gleichheit verlangt, fände der
|
||||
/// Abgleich an echten Folgen nie etwas.
|
||||
/// </remarks>
|
||||
[Test]
|
||||
public void Kleine_Abweichungen_stoeren_nicht()
|
||||
{
|
||||
var vorspann = Rauschen(Scheiben(30), 31);
|
||||
var leichtAnders = vorspann.Select((w, i) => i % 3 == 0 ? w ^ 0b101u : w).ToArray();
|
||||
|
||||
var a = Aneinander(Rauschen(Scheiben(5), 32), vorspann, Rauschen(Scheiben(40), 33));
|
||||
var b = Aneinander(Rauschen(Scheiben(20), 34), leichtAnders, Rauschen(Scheiben(40), 35));
|
||||
|
||||
var treffer = Tonabgleich.Finden(a, b, JeAbdruck);
|
||||
|
||||
Assert.That(treffer, Is.Not.Null);
|
||||
Assert.That(treffer!.Laenge, Is.EqualTo(30).Within(2.0));
|
||||
}
|
||||
|
||||
/// <summary>Eine eingeblendete Ansage mitten im Vorspann reisst ihn nicht entzwei.</summary>
|
||||
[Test]
|
||||
public void Eine_kurze_Luecke_haelt_den_Lauf_zusammen()
|
||||
{
|
||||
var vorspann = Rauschen(Scheiben(30), 41);
|
||||
// Eine Sekunde mittendrin ist in der einen Folge anders.
|
||||
var mitAnsage = (uint[])vorspann.Clone();
|
||||
var mitte = mitAnsage.Length / 2;
|
||||
for (var i = mitte; i < mitte + Scheiben(1); i++) mitAnsage[i] = 0xDEADBEEF;
|
||||
|
||||
var a = Aneinander(Rauschen(Scheiben(5), 42), vorspann, Rauschen(Scheiben(30), 43));
|
||||
var b = Aneinander(Rauschen(Scheiben(15), 44), mitAnsage, Rauschen(Scheiben(30), 45));
|
||||
|
||||
var treffer = Tonabgleich.Finden(a, b, JeAbdruck);
|
||||
|
||||
Assert.That(treffer, Is.Not.Null);
|
||||
Assert.That(treffer!.Laenge, Is.EqualTo(30).Within(2.0),
|
||||
"Die Lücke gehoert zum Vorspann, nicht dahinter");
|
||||
}
|
||||
|
||||
/// <summary>Eine ganze Staffel auf einmal.</summary>
|
||||
[Test]
|
||||
public void Jede_Folge_der_Staffel_bekommt_ihre_Stelle()
|
||||
{
|
||||
var vorspann = Rauschen(Scheiben(25), 51);
|
||||
var folgen = new List<(int, IReadOnlyList<uint>)>();
|
||||
for (var i = 0; i < 5; i++)
|
||||
{
|
||||
folgen.Add((i, Aneinander(
|
||||
Rauschen(Scheiben(5 + i * 10), 60 + i), vorspann, Rauschen(Scheiben(30), 70 + i))));
|
||||
}
|
||||
|
||||
var gefunden = Tonabgleich.ReiheAbgleichen(folgen, JeAbdruck);
|
||||
|
||||
Assert.That(gefunden, Has.Count.EqualTo(5), "auch die letzte Folge");
|
||||
for (var i = 0; i < 5; i++)
|
||||
{
|
||||
Assert.That(gefunden[i].Von, Is.EqualTo(5 + i * 10).Within(1.5), $"Folge {i}");
|
||||
Assert.That(gefunden[i].Bis - gefunden[i].Von, Is.EqualTo(25).Within(2.0), $"Folge {i}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Eine einzelne Folge hat niemanden zum Vergleichen.</summary>
|
||||
[Test]
|
||||
public void Eine_Folge_allein_ergibt_nichts()
|
||||
{
|
||||
var eine = new List<(int, IReadOnlyList<uint>)> { (0, Rauschen(Scheiben(60), 81)) };
|
||||
Assert.That(Tonabgleich.ReiheAbgleichen(eine, JeAbdruck), Is.Empty);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using Flaro.Media;
|
||||
|
||||
namespace Flaro.Media.Tests;
|
||||
|
||||
/// <summary>Die Ausgabe von <c>fpcalc -raw</c> lesen.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <b>Ohne das Programm selbst.</b> Ob Chromaprint rechnen kann, ist nicht
|
||||
/// unsere Frage; ob wir seine Ausgabe richtig verstehen, schon. Die Zahlen
|
||||
/// kommen mit Vorzeichen, gemeint sind die Bitmuster — wer das verwechselt,
|
||||
/// bekommt einen Abdruck, der nie zu einem zweiten passt.
|
||||
/// </remarks>
|
||||
public class TonabdruckleserTests
|
||||
{
|
||||
[Test]
|
||||
public void Die_Zahlen_werden_als_Bitmuster_gelesen()
|
||||
{
|
||||
var abdruck = Tonabdruckleser.Lesen("DURATION=120\nFINGERPRINT=1,-1,2147483647,-2147483648\n");
|
||||
|
||||
Assert.That(abdruck, Is.EqualTo(new uint[] { 1u, 0xFFFFFFFFu, 0x7FFFFFFFu, 0x80000000u }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Ohne_Abdruckzeile_kommt_nichts()
|
||||
{
|
||||
Assert.That(Tonabdruckleser.Lesen("DURATION=120\n"), Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Eine_leere_Zeile_ergibt_keinen_Abdruck()
|
||||
{
|
||||
Assert.That(Tonabdruckleser.Lesen("FINGERPRINT=\n"), Is.Empty);
|
||||
}
|
||||
|
||||
/// <summary>Eine Zeitscheibe ist rund 124 Millisekunden.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// Daran hängt jede Zeitangabe, die aus einem Abdruck entsteht. Stimmt
|
||||
/// die Zahl nicht, liegt jede Marke um denselben Faktor daneben — und
|
||||
/// das sähe aus wie ein Fehler in der Erkennung.
|
||||
/// </remarks>
|
||||
[Test]
|
||||
public void Eine_Zeitscheibe_dauert_124_Millisekunden()
|
||||
{
|
||||
Assert.That(Tonabdruckleser.SekundenJeAbdruck, Is.EqualTo(0.1238).Within(0.0005));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user