feat(audio): v1 signature producer, bit-exact with the pipeline

`AudioSignature` is the DSP — band table, periodic Hann, radix-2 FFT,
band-mean peak, energy class, packing — and `AudioSignatureService` the
decode, running the FFmpeg binary `IMediaEncoder.EncoderPath` names. The
plugin gained no dependency.

The server specification's prose does not determine a byte stream, so the
parameters it leaves open are pinned by the fixture shared with the
extraction repo and restated at the top of `AudioSignature`: double
throughout, whole frames only, periodic Hann, unnormalised FFT, band mean
rather than sum, argmax ties to the lowest index.

`fixtures/audio/` holds the extraction repo's three files byte-identically
and the computed signature equals the recorded vector exactly. The binding
check regenerates the fixture PCM from `make_fixture.py`'s arithmetic and
verifies it against the recorded decode checksums, so it runs on a host
with no codec at all and a decode divergence stays distinguishable from a
DSP one; the two tests that drive real FFmpeg self-skip without a binary.

The workflow named "Test Plugin" until now only compiled one. A test that
is built and never run is not evidence, and a golden vector shared across
two repos exists precisely so CI fails when they drift.

TRACES: JR-042, JR-043 | SR-003
This commit is contained in:
2026-07-31 16:24:11 +02:00
parent 19aecee646
commit 17be9bcc4e
9 changed files with 1359 additions and 0 deletions
@@ -22,5 +22,10 @@ public class ServiceRegistrator : IPluginServiceRegistrator
// server that is down should be skipped for the whole sweep, not
// retried once per item (JR-037).
serviceCollection.AddSingleton<IManifestExchangeClient, ManifestExchangeClient>();
// Registered concretely: the signature is computed, not yet consumed, so
// there is no second implementation for an interface to abstract over
// and nothing to gain from inventing one (JR-042).
serviceCollection.AddSingleton<AudioSignatureService>();
}
}