„No such file or directory" über Dateien, die da waren

Im Protokoll stand für jede einzelne Folge, fpcalc könne die Datei nicht
öffnen — während der Spieler dieselbe Datei im selben Augenblick
umpackte. Null Marken, und der Grund stand nicht da.

`-ts` ist bei fpcalc kein Zeitstempel, sondern ein Schalter: „gib
Zeitstempel aus". Die Sekundenzahl dahinter las fpcalc als zweiten
Dateinamen, fand ihn nicht und brach ab, bevor es die echte Datei
überhaupt ansah.

Nachgemessen am Binärprogramm aus dem Abbild: einen Startpunkt gibt es
bei fpcalc gar nicht. `-length` begrenzt nur die Dauer vom Anfang an —
für den Abspann braucht es aber das Ende. Also einmal die ganze Spur je
Folge, und Vorspann wie Abspann sind danach zwei Ausschnitte aus
demselben Abdruck.

Das ist nicht nur richtig, sondern billiger: eine Folge von 45 Minuten
kostet 1,6 Sekunden und ergibt 21 786 Werte — gegenüber zwei
Ausschnitten, die zusammen mehr gekostet hätten und nie funktionierten.
Die Rate von 0,1239 Sekunden je Wert steht jetzt als Prüfung daneben.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-12 22:09:56 +02:00
co-authored by Claude Opus 5
parent 2c535e6f47
commit fd79fb4022
3 changed files with 105 additions and 29 deletions
+38 -21
View File
@@ -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<Guid, IReadOnlyList<uint>>();
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<uint> Abdruck)>();
var fensteranfang = new Dictionary<Guid, double>();
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);
}
/// <summary>Welcher Ausschnitt der Folge abgetastet wird.</summary>
private static (double Ab, double Dauer) Fenster(Markenart art, double laufzeit)
/// <summary>Welcher Ausschnitt des Abdrucks abgetastet wird.</summary>
///
/// <remarks>
/// 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.
/// </remarks>
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<bool> SchreibenAsync(
+29 -8
View File
@@ -38,6 +38,10 @@ public sealed class Tonabdruckleser(ILogger<Tonabdruckleser> log)
/// zwei Dritteln Überlappung: 4096/3 / 11025 Sekunden. Die Zahl steht
/// hier, weil nur mit ihr aus einem Abdruckbereich eine Zeitangabe wird.
/// </remarks>
/// <remarks>
/// Nachgerechnet an einer Datei von 2700 Sekunden: 21 786 Werte, also
/// 0,12393 Sekunden je Wert. Die Formel trifft es auf vier Stellen.
/// </remarks>
public const double SekundenJeAbdruck = 4096.0 / 3.0 / 11025.0;
/// <summary>Wo <c>fpcalc</c> liegt — überschreibbar wie ffmpeg selbst.</summary>
@@ -75,12 +79,28 @@ public sealed class Tonabdruckleser(ILogger<Tonabdruckleser> log)
}
}
/// <summary>Der Abdruck eines Abschnitts, oder eine leere Reihe.</summary>
/// <summary>Der Abdruck der ganzen Tonspur, oder eine leere Reihe.</summary>
///
/// <param name="ab">Ab welcher Sekunde gelesen wird.</param>
/// <param name="dauer">Wie viele Sekunden gelesen werden.</param>
public async Task<IReadOnlyList<uint>> LesenAsync(
string pfad, double ab, double dauer, CancellationToken ct = default)
/// <remarks>
/// <para>
/// <b>Die ganze Datei, nicht ein Ausschnitt — weil fpcalc nicht springen
/// kann.</b> Hier stand einmal <c>-ts</c> 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.
/// </para>
/// <para>
/// Einen Startpunkt gibt es bei fpcalc überhaupt nicht; <c>-length</c>
/// 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.
/// </para>
/// </remarks>
public async Task<IReadOnlyList<uint>> LesenAsync(string pfad, CancellationToken ct = default)
{
if (!await VorhandenAsync(ct))
{
@@ -93,9 +113,10 @@ public sealed class Tonabdruckleser(ILogger<Tonabdruckleser> 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,
@@ -45,3 +45,41 @@ public class TonabdruckleserTests
Assert.That(Tonabdruckleser.SekundenJeAbdruck, Is.EqualTo(0.1238).Within(0.0005));
}
}
/// <summary>Was fpcalc wirklich kann — nachgemessen, nicht angenommen.</summary>
///
/// <remarks>
/// <b>Der Anlass ist ein Fehler, der im Betrieb stand.</b> Hier wurde
/// <c>-ts 0</c> ü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: <c>-length 0</c> ergibt 21 786 Werte, also 0,1239 Sekunden je
/// Wert; <c>-length 120</c> bricht nach 948 Werten ab.
/// </remarks>
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));
}
/// <summary>Eine Stunde Abdruck passt in eine überschaubare Reihe.</summary>
///
/// <remarks>
/// 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.
/// </remarks>
[Test]
public void Eine_Stunde_sind_rund_29000_Werte()
{
var werte = (int)(3600 / Tonabdruckleser.SekundenJeAbdruck);
Assert.That(werte, Is.EqualTo(29000).Within(500));
}
}