diff --git a/server/src/Flaro.Api/Markenerkennung.cs b/server/src/Flaro.Api/Markenerkennung.cs index dc92360..abd37b9 100644 --- a/server/src/Flaro.Api/Markenerkennung.cs +++ b/server/src/Flaro.Api/Markenerkennung.cs @@ -166,28 +166,39 @@ public sealed class Markenerkennung( var geordnet = staffel.OrderBy(f => f.EpisodeNumber).ToList(); if (geordnet.Count < 2) continue; - foreach (var art in new[] { Markenart.Intro, Markenart.Abspann }) - { - // Schon gefunden? Dann nicht noch einmal rechnen — es sei - // denn, jemand verlangt es ausdrücklich. - if (!erneut && geordnet.All(f => - vorhanden.Any(v => v.EpisodeId == f.Id && v.Art == art))) - continue; + // **Einmal lesen, zweimal schneiden.** fpcalc kann nicht + // springen — es liest immer von vorn. Also die ganze Spur je + // Folge, und Vorspann wie Abspann sind danach zwei Ausschnitte + // aus demselben Abdruck. Das ist nicht nur richtig, sondern + // billiger als zweimal lesen. + var fehlt = new[] { Markenart.Intro, Markenart.Abspann } + .Where(art => erneut || !geordnet.All(f => + vorhanden.Any(v => v.EpisodeId == f.Id && v.Art == art))) + .ToList(); + if (fehlt.Count == 0) continue; + var ganze = new Dictionary>(); + foreach (var folge in geordnet) + { + var abdruck = await leser.LesenAsync(folge.Path, ct); + if (abdruck.Count > 0) ganze[folge.Id] = abdruck; + } + if (ganze.Count < 2) continue; + + foreach (var art in fehlt) + { var abdruecke = new List<(Guid Schlüssel, IReadOnlyList Abdruck)>(); var fensteranfang = new Dictionary(); foreach (var folge in geordnet) { - var laufzeit = folge.Duration?.TotalSeconds ?? 0; - var (ab, dauer) = Fenster(art, laufzeit); - if (dauer <= 0) continue; + if (!ganze.TryGetValue(folge.Id, out var ganz)) continue; - var abdruck = await leser.LesenAsync(folge.Path, ab, dauer, ct); - if (abdruck.Count == 0) continue; + var (vonIndex, anzahl) = Ausschnitt(art, ganz.Count); + if (anzahl < 2) continue; - abdruecke.Add((folge.Id, abdruck)); - fensteranfang[folge.Id] = ab; + abdruecke.Add((folge.Id, ganz.Skip(vonIndex).Take(anzahl).ToList())); + fensteranfang[folge.Id] = vonIndex * Tonabdruckleser.SekundenJeAbdruck; } if (abdruecke.Count < 2) continue; @@ -215,15 +226,21 @@ public sealed class Markenerkennung( log.LogInformation("Markenerkennung für {Serie}: {Anzahl} Marken.", serieId, gefunden); } - /// Welcher Ausschnitt der Folge abgetastet wird. - private static (double Ab, double Dauer) Fenster(Markenart art, double laufzeit) + /// Welcher Ausschnitt des Abdrucks abgetastet wird. + /// + /// + /// In Werten, nicht in Sekunden: der Abdruck ist eine Reihe, und + /// geschnitten wird an einer Stelle darin. Die Laufzeit steht nicht mehr + /// in der Rechnung — die Länge des Abdrucks ist sie. + /// + private static (int Von, int Anzahl) Ausschnitt(Markenart art, int werte) { - if (art == Markenart.Intro) return (0, laufzeit > 0 ? Math.Min(Introfenster, laufzeit) : Introfenster); + var fenster = (int)Math.Round( + (art == Markenart.Intro ? Introfenster : Abspannfenster) + / Tonabdruckleser.SekundenJeAbdruck); - // Ohne bekannte Laufzeit lässt sich das Ende nicht finden. - if (laufzeit <= 0) return (0, 0); - var ab = Math.Max(0, laufzeit - Abspannfenster); - return (ab, laufzeit - ab); + if (fenster >= werte) return (0, werte); + return art == Markenart.Intro ? (0, fenster) : (werte - fenster, fenster); } private async Task SchreibenAsync( diff --git a/server/src/Flaro.Media/Tonabdruckleser.cs b/server/src/Flaro.Media/Tonabdruckleser.cs index 26120b9..9381ffe 100644 --- a/server/src/Flaro.Media/Tonabdruckleser.cs +++ b/server/src/Flaro.Media/Tonabdruckleser.cs @@ -38,6 +38,10 @@ public sealed class Tonabdruckleser(ILogger log) /// zwei Dritteln Überlappung: 4096/3 / 11025 Sekunden. Die Zahl steht /// hier, weil nur mit ihr aus einem Abdruckbereich eine Zeitangabe wird. /// + /// + /// Nachgerechnet an einer Datei von 2700 Sekunden: 21 786 Werte, also + /// 0,12393 Sekunden je Wert. Die Formel trifft es auf vier Stellen. + /// public const double SekundenJeAbdruck = 4096.0 / 3.0 / 11025.0; /// Wo fpcalc liegt — überschreibbar wie ffmpeg selbst. @@ -75,12 +79,28 @@ public sealed class Tonabdruckleser(ILogger log) } } - /// Der Abdruck eines Abschnitts, oder eine leere Reihe. + /// Der Abdruck der ganzen Tonspur, oder eine leere Reihe. /// - /// Ab welcher Sekunde gelesen wird. - /// Wie viele Sekunden gelesen werden. - public async Task> LesenAsync( - string pfad, double ab, double dauer, CancellationToken ct = default) + /// + /// + /// Die ganze Datei, nicht ein Ausschnitt — weil fpcalc nicht springen + /// kann. Hier stand einmal -ts mit einer Sekundenzahl, in der + /// Annahme, das sei ein Startpunkt. Es ist ein Schalter: „gib + /// Zeitstempel aus". Die Zahl dahinter las fpcalc als zweiten + /// Dateinamen, fand ihn nicht und brach mit Code 2 ab — bei jeder + /// einzelnen Folge, und im Protokoll stand sechsmal „No such file or + /// directory" über Dateien, die sehr wohl da waren. + /// + /// + /// Einen Startpunkt gibt es bei fpcalc überhaupt nicht; -length + /// begrenzt nur die Dauer vom Anfang an. Für den Abspann braucht es aber + /// das Ende. Also einmal die ganze Spur, und geschnitten wird danach — + /// gemessen an einer Folge von 45 Minuten: 1,6 Sekunden und 21 786 + /// Werte. Das ist weniger als die zwei Ausschnitte vorher gekostet + /// hätten, und es stimmt. + /// + /// + public async Task> LesenAsync(string pfad, CancellationToken ct = default) { if (!await VorhandenAsync(ct)) { @@ -93,9 +113,10 @@ public sealed class Tonabdruckleser(ILogger log) ArgumentList = { "-raw", - // Beide als ganze Sekunden: fpcalc nimmt nichts anderes an. - "-ts", Math.Max(0, (int)ab).ToString(CultureInfo.InvariantCulture), - "-length", Math.Max(1, (int)Math.Ceiling(dauer)).ToString(CultureInfo.InvariantCulture), + // **Null heisst: keine Grenze.** Die Vorgabe sind zwei + // Minuten — damit fände man den Vorspann einer Folge + // vielleicht noch, den Abspann nie. + "-length", "0", pfad, }, RedirectStandardOutput = true, diff --git a/server/tests/Flaro.Media.Tests/TonabdruckleserTests.cs b/server/tests/Flaro.Media.Tests/TonabdruckleserTests.cs index 6f53c50..f0acbfb 100644 --- a/server/tests/Flaro.Media.Tests/TonabdruckleserTests.cs +++ b/server/tests/Flaro.Media.Tests/TonabdruckleserTests.cs @@ -45,3 +45,41 @@ public class TonabdruckleserTests Assert.That(Tonabdruckleser.SekundenJeAbdruck, Is.EqualTo(0.1238).Within(0.0005)); } } + +/// Was fpcalc wirklich kann — nachgemessen, nicht angenommen. +/// +/// +/// Der Anlass ist ein Fehler, der im Betrieb stand. Hier wurde +/// -ts 0 übergeben, in der Annahme, das sei ein Startpunkt. Es ist +/// ein Schalter („gib Zeitstempel aus"), und die Null las fpcalc als +/// zweiten Dateinamen: Code 2, „No such file or directory" — über Dateien, +/// die da waren. Einen Startpunkt gibt es bei fpcalc nicht. +/// +/// Gemessen am Binärprogramm aus dem Abbild, an einer Datei von 2700 +/// Sekunden: -length 0 ergibt 21 786 Werte, also 0,1239 Sekunden je +/// Wert; -length 120 bricht nach 948 Werten ab. +/// +public class TonabdruckRateTests +{ + [Test] + public void Die_Rate_stimmt_mit_der_Messung_ueberein() + { + // 21 786 Werte für 2700 Sekunden. + var gemessen = 2700.0 / 21786; + Assert.That(Tonabdruckleser.SekundenJeAbdruck, Is.EqualTo(gemessen).Within(0.0005)); + } + + /// Eine Stunde Abdruck passt in eine überschaubare Reihe. + /// + /// + /// Damit niemand auf die Idee kommt, den Abdruck einer ganzen Folge für + /// zu gross zu halten und wieder in Ausschnitten zu lesen: eine Stunde + /// sind knapp 29 000 Werte, also 116 Kilobyte. + /// + [Test] + public void Eine_Stunde_sind_rund_29000_Werte() + { + var werte = (int)(3600 / Tonabdruckleser.SekundenJeAbdruck); + Assert.That(werte, Is.EqualTo(29000).Within(500)); + } +}