Make active sync use chirp-bin decoder

This commit is contained in:
Meta Vens 2026-05-23 19:22:25 +01:00
commit 67c7627446
11 changed files with 311 additions and 164 deletions

View file

@ -73,6 +73,11 @@ When the user says to prepare for imminent compaction:
- Before substantial edits, restate the current mechanism and intended change.
- Prefer one clear hypothesis per iteration.
- Do not keep corpses of previous attempts at building the Machine. When a new
mechanism supersedes an old one, either delete the old mechanism or demote it
to an explicitly named diagnostic/reference artifact with no live authority.
Do not leave dead pipelines selectable, documented as current, or quietly
reachable through mode flags just because they once worked.
- Verify against OBS ingest behavior and actual sender-side FFmpeg capability, not vibes wearing a lab coat.
- Do not add an OBS plugin, fork, registry, custom transport daemon, or compatibility layer until the script-based SRT design fails a concrete invariant.
- Keep video, audio-source isolation, transport, and OBS composition as separate responsibilities.

View file

@ -52,11 +52,7 @@ timing evidence Mimir is allowed to emit:
buffer.
- `hybrid`: prefer passive program-audio evidence when confidence is high, then
emit a watermark when confidence falls. The passive side uses bounded
GCC-PHAT-style phase correlation. The active fallback now uses
`MimirChirpBinTimeline`: a fixed-slope chirp-bin watermark whose symbols are
decoded by one dechirp and a fixed Goertzel bin bank instead of dense sliding
matched filters. Hybrid emission is pulsed, not continuous: when passive
confidence is weak, Mimir emits one half-second coded burst every two seconds.
GCC-PHAT-style phase correlation.
The mode belongs to the runtime, not the decoder. The decoder should consume
known timing evidence; it should not decide whether Mimir is allowed to make
@ -71,14 +67,19 @@ evidence and carry zero confidence. A single passive window is also capped below
certainty; full confidence belongs to repeated coherent state over time, not
one attractive correlation peak.
The active watermark path is deliberately receiver-cheap. `MimirChirpBinTimeline`
uses a common chirp duration, chirp slope, and Hann window for every symbol.
Symbol identity is the dechirped frequency bin. The synthetic invariant is
The active calibration path is deliberately receiver-cheap in both `chirp-only`
and `hybrid`. `MimirChirpBinTimeline` uses a common chirp duration, chirp slope,
and window for every symbol. Acquisition is a cheap bounded energy/onset pass;
classification is dechirp plus a fixed Goertzel bin bank; timeline placement is
the de Bruijn triplet trellis. A constrained local waveform correlation around
the decoded anchor delay provides the final fractional offset. The synthetic invariant is
`dotnet run --project .\src\Mimir.BufferSmoke\Mimir.BufferSmoke.csproj -- --chirp-bin-self-test`;
it renders the chirp-bin timeline, decodes by dechirp plus Goertzel bins, and
requires code-valid triplet anchors plus a stable source clock. Default hybrid
watermark gain is intentionally low (`watermarkGain`, or
`MIMIR_WATERMARK_GAIN`) and separate from the louder chirp-only lab gain.
requires code-valid triplet anchors plus a stable source clock. `chirp-only`
emits this witness continuously; `hybrid` emits one low-gain half-second coded
burst every two seconds only while passive confidence is weak. Default hybrid
watermark gain is intentionally low (`watermarkGain`, or `MIMIR_WATERMARK_GAIN`)
and separate from the louder chirp-only lab gain.
Research notes:
@ -236,10 +237,9 @@ timeline anchors. Current synthetic proof detects all 15 emitted chirps, keeps
device runs still depend on loopback capture staying live; the local Scarlett
loopback has intermittently stopped advancing during short headless sniffs.
The older `MimirChirpletTimeline` matcher is now the chirp-only lab path, not
the hybrid fallback. `BuildChirpletEnergyTrace` still behaves like a dense
sliding matched-filter bank and should not retake the hot path. The active
hybrid watermark belongs to the chirp-bin machine.
The older `MimirChirpletTimeline` matcher is a diagnostic/reference artifact,
not the active runtime path. `BuildChirpletEnergyTrace` still behaves like a
dense sliding matched-filter bank and should not retake authority.
Next, replace the diagnostic bridge with native audio capture workers that
append typed blocks into `Mimir.Runtime`, then expose buffer depth, clock state,

View file

@ -85,33 +85,31 @@ a named invariant that the native runtime cannot protect yet.
- `MimirRuntime` queues chirplet timeline PCM through Fensalir audio when the
active timing witness is allowed. `MimirAudioSynchronizationSettings.Mode`
selects `chirp-only`, `passive`, or `hybrid`; passive disables active
emission, and hybrid emits the active pilot only while passive confidence is
below threshold.
emission, chirp-only emits the active witness continuously, and hybrid emits
the active pilot only while passive confidence is below threshold.
- `MimirPassiveAudioSynchronizationEstimator` is the first program-audio timing
path. It estimates loopback-to-mic delay with PHAT-weighted cross-spectrum
correlation so music can act as the default timing witness before any audible
watermark is needed.
- `MimirChirpBinTimeline` is the active hybrid watermark path. It renders a
fixed-slope chirp-bin codebook and decodes symbols with one dechirp plus
fixed Goertzel bins, then feeds the same de Bruijn triplet timeline-anchor
machine. The detector scores dechirped bin energy directly, caches chirp
kernels per sample rate, and uses a widened 32-symbol codebook so sub-frame
timing error does not masquerade as neighboring symbol identity. The matched
chirp-bin peak is refined below one sample and reports/states expose delay in
microseconds as well as fractional samples. Hybrid emits this as low-gain
half-second bursts every two seconds only while passive confidence is weak.
- `MimirChirpBinTimeline` is the active calibration path for both `chirp-only`
and `hybrid`. It renders a fixed-slope chirp-bin codebook and decodes symbols
with cheap event-energy proposals, dechirp plus fixed Goertzel bins, and the
same de Bruijn triplet timeline-anchor machine. The detector keeps
time/frequency ambiguity as candidate symbol/offset pairs so code constraints
can choose the coherent path. The analyzer refines the final fractional delay
with constrained local waveform correlation around the decoded active delay.
Reports/states expose delay in microseconds as well as fractional samples.
Hybrid emits this as low-gain half-second bursts every two seconds only while
passive confidence is weak.
Use `--chirp-bin-self-test` to prove the codebook/decoder and
`--hybrid-sync-self-test` to prove that the analyzer can recover a fractional
delay from a one-second rolling-buffer chirp-bin window. The current synthetic
microsecond proof recovers a 317.375-sample delay with 0.369 us error.
- `Mimir.BufferSmoke --render-chirplet-f32` and `--analyze-asio-f32` provide the
current chirp-only Scarlett artifact proof. A 192 kHz run decoded Focusrite
`Loopback 1 -> Loopback 2` at `0.000 us` with 7 matched anchors and 0.896
confidence. Physical input 2 decoded at about 616.226 samples / 3209.508 us
with 3 matched anchors and 0.516 confidence; physical input 1 did not decode
in that capture. This proves the timing evidence can survive the real ASIO
path, but the matched-kernel chirplet decoder took roughly 100 seconds for one
2-second 192 kHz comparison and is not the runtime hot path.
- `Mimir.BufferSmoke --render-chirp-bin-f32` and `--analyze-asio-f32` provide
the current active Scarlett artifact proof. A 192 kHz chirp-bin run decoded
Focusrite `Loopback 1 -> Loopback 2` at `0.000 us` with 12 matched anchors and
0.999 confidence. Physical inputs did not produce matched anchors in that run,
so acoustic robustness remains separate from the clean loopback timing proof.
- `MimirVideoFrameDescriptor` for dimensions, pixel format, stride, device
timestamp, and native/GPU handle metadata.
- `IMimirVideoCaptureDriver` and `MimirVideoCaptureDriverSource` as the live
@ -147,8 +145,8 @@ a named invariant that the native runtime cannot protect yet.
workers for local mic, loopback, and network audio feeds. Use the verified
Focusrite ASIO path for Scarlett capture; WASAPI remains a diagnostic
witness, not the Scarlett hot path.
4. Replace the diagnostic chirp-only matched-kernel decoder with the efficient
streaming transform before using chirp-only at 192 kHz in the hot path.
4. Prove the active chirp-bin path through real microphones, not only Scarlett
loopback; tune acoustic bands/gain/code spacing from measured mic response.
5. Add the synchronization actuator: drive a variable-rate resampler and
fractional delay line per non-reference stream from the smoothed
`MimirAudioSynchronizationState`. First, prove the constrained chirplet

View file

@ -99,19 +99,19 @@ current operating horizon. Mimir continuously queues that timeline through
Fensalir audio and decodes timing through the constrained chirplet-transform
path.
`MimirAudioSynchronizationSettings.Mode` chooses whether active calibration is
allowed: `chirp-only` emits the witness, `passive` stays silent and uses
program-audio phase correlation, and `hybrid` uses passive evidence by default
while emitting active pilot chunks only when passive confidence is weak.
Hybrid does not inherit the passive two-second analysis floor: passive still
needs a longer program-audio window, while chirp-bin fallback can decode short
pilot windows once at least a code-valid triplet is present. The chirp-bin
detector now proposes events from dechirped bin energy rather than broadband RMS
and uses a widened symbol spacing to keep sub-frame timing error from becoming
symbol substitution. Chirp-bin frame timing is refined with a local matched-peak
fit below one sample, and reports/states expose `delayUs` next to fractional
sample delay. `Mimir.BufferSmoke --hybrid-sync-self-test` proves the analyzer
path with a one-second rolling-buffer window and a recovered 317.375-sample
delay at 0.369 us error.
allowed: `chirp-only` emits the active chirp-bin witness, `passive` stays silent
and uses program-audio phase correlation, and `hybrid` uses passive evidence by
default while emitting active pilot chunks only when passive confidence is weak.
Active decoding does not inherit the passive two-second analysis floor: passive
still needs a longer program-audio window, while the chirp-bin witness can decode
short pilot windows once at least a code-valid triplet is present. The chirp-bin
detector now uses cheap bounded energy/onset proposals, dechirp plus fixed
Goertzel bins, explicit time/frequency ambiguity candidates, and a constrained
local waveform correlation for final fractional delay. Reports/states expose
`delayUs` next to fractional sample delay. `Mimir.BufferSmoke
--chirp-only-sync-self-test --sample-rate 192000` and `--hybrid-sync-self-test
--sample-rate 192000` both recover a 1269.5-sample synthetic delay with printed
0.000 us error.
Reports now carry fractional delay and per-band matched energy. The first
`MimirChirpletSymbolCodebook` owns separable symbol definitions; every symbol
has a unique chirp shape, with rhythm as additional evidence. `MimirChirpletStreamDecoder`
@ -144,18 +144,14 @@ runtime analyzer accepts Float32, Int16, Int24, and Int32 PCM windows so
ASIO/native capture can preserve the interface format. Raven also has a
loopback-capable Scarlett ASIO path at 192 kHz for co-streamer/game timing
evidence; Starfire still owns the heavy soundfield and sensor-fusion work.
The ASIO probe can now play a raw mono Float32 chirplet timeline through the
The ASIO probe can now play raw mono Float32 timeline audio through the
Focusrite outputs while capturing every ASIO input as raw interleaved Float32.
`Mimir.BufferSmoke --analyze-asio-f32` feeds those captured channels into the
same runtime chirp-only analyzer. A real 192 kHz Scarlett artifact decoded
`Loopback 1 -> Loopback 2` at exactly `0.000 us` with 7 matched anchors and
0.896 confidence. The physical input path decoded on channel 1 at about
616.226 samples / 3209.508 us with 3 matched anchors and 0.516 confidence;
channel 0 did not produce anchors in that window. The result proves chirp-only
hardware alignment can reach microsecond reporting on clean loopback, but the
current matched-kernel chirplet decoder took roughly 100 seconds for one
2-second 192 kHz comparison. That implementation is diagnostic, not the final
runtime hot path.
same runtime active analyzer. A real 192 kHz chirp-bin Scarlett artifact decoded
`Loopback 1 -> Loopback 2` at exactly `0.000 us` with 12 matched anchors and
0.999 confidence. Physical inputs did not produce matched anchors in that
chirp-bin run; acoustic robustness is now the open problem, not clean loopback
timing or active decoder shape.
## Visual Fusion

View file

@ -61,7 +61,7 @@ Get-Content .\state\evidence.jsonl -Tail 8
but Leap drops hard under shared USB load: 40.12 fps when the PS3 Eye runs
640x480@60, and 81.10 fps when the PS3 Eye runs 320x240@187. The regular
Kiyo and second PS3 Eye were absent, so six-camera viability is not proven.
- Current chirplet-backed smoke uses Scarlett speaker loopback as timing
- Current active sync smoke uses Scarlett speaker loopback as timing
authority. `MimirChirpletTimeline` owns the structured birdsong-like timeline
fingerprint: an order-3 de Bruijn sequence over 32 time/frequency
constellation symbols. Start band, glide shape, duration, and following
@ -80,14 +80,18 @@ Get-Content .\state\evidence.jsonl -Tail 8
it currently detects all 15 emitted chirps and recovers 13 anchors for events
0-12 with a 47999.999990 Hz clock fit and 0.000014 sample MAE. `MimirRuntime` runs sync analysis as a bounded rotating
service and caches reports/states for UI and telemetry; readouts must stay
passive. Audio sync mode is runtime-owned: `chirp-only` emits active
calibration, `passive` disables emission and uses program-audio phase
correlation, and `hybrid` emits active pilot chunks only when passive
confidence is weak. Hybrid chirp-bin fallback now has its own short-window
analyzer floor instead of inheriting the passive two-second gate, and
`Mimir.BufferSmoke --hybrid-sync-self-test` proves a one-second rolling-buffer
window recovers a 317.375-sample fractional delay with 0.369 us error from
code-valid chirp-bin anchors. Reports and sync states now expose `delayUs`.
passive. Audio sync mode is runtime-owned: `chirp-only` emits the active
chirp-bin witness continuously, `passive` disables emission and uses
program-audio phase correlation, and `hybrid` emits active pilot chunks only
when passive confidence is weak. The active chirp-bin path now has its own
short-window analyzer floor instead of inheriting the passive two-second gate.
It uses cheap energy/onset proposals, dechirp plus fixed Goertzel bins,
explicit time/frequency ambiguity candidates, and constrained local waveform
correlation for final fractional delay. `Mimir.BufferSmoke
--chirp-only-sync-self-test --sample-rate 192000` and
`--hybrid-sync-self-test --sample-rate 192000` both recover a 1269.5-sample
synthetic delay with printed 0.000 us error. Reports and sync states expose
`delayUs`.
Actual
Mimir.App testing proves Fensalir audio can wake Scarlett
loopback, keep mic buffers live, and produce confident online passive sync
@ -103,22 +107,20 @@ Get-Content .\state\evidence.jsonl -Tail 8
`Loopback 1/2`, 192-frame preferred buffers, 44.1-192 kHz support, and
captures nonzero 4-channel `Int32LSB` input callbacks at 192 kHz. It can play
raw mono Float32 chirplet timelines through ASIO outputs and capture all ASIO
inputs as raw interleaved Float32 for runtime analysis. A real Scarlett
chirp-only artifact run at 192 kHz decoded `Loopback 1 -> Loopback 2` at
`0.000 us` delay with 7 matched anchors and 0.896 confidence. Physical input
2 decoded against loopback at about 616.226 samples / 3209.508 us with 3
matched anchors and 0.516 confidence; physical input 1 did not decode in that
window. One 2-second 192 kHz chirp-only matched-kernel decode took roughly
100 seconds. Accuracy is present; the current chirplet transform is not a hot
path.
inputs as raw interleaved Float32 for runtime analysis. The old arbitrary
chirplet artifact proved correctness but took roughly 100 seconds per
comparison and is now diagnostic only. A real Scarlett chirp-bin artifact run
at 192 kHz decoded `Loopback 1 -> Loopback 2` at `0.000 us` delay with 12
matched anchors and 0.999 confidence in the normal active analyzer. Physical
inputs did not produce matched anchors in that chirp-bin run, so acoustic
robustness is still open.
- Raven also has a 192 kHz loopback-capable Scarlett for co-streamer/game timing
evidence. Do not move the heavy soundfield or sensor-fusion workload there.
## Immediate Re-entry Instruction
Replace the diagnostic chirp-only matched-kernel decoder with the planned
efficient streaming transform before calling chirp-only runtime-ready at
192 kHz, then feed Scarlett ASIO callbacks into `Mimir.Runtime` and turn decoded
clock fits into the SRO/fractional-delay actuator. Keep loopback as timing
authority. Do not call synchronization analysis from UI/telemetry readouts. Do
not restore deleted script infrastructure because a stale doc once missed it.
Prove the active chirp-bin path through real microphones, then feed Scarlett
ASIO callbacks into `Mimir.Runtime` and turn decoded clock fits into the
SRO/fractional-delay actuator. Keep loopback as timing authority. Do not call
synchronization analysis from UI/telemetry readouts. Do not restore deleted
script infrastructure because a stale doc once missed it.

View file

@ -20,6 +20,11 @@ if (args.Any(arg => string.Equals(arg, "--hybrid-sync-self-test", StringComparis
return RunHybridSyncSelfTest(ParseIntOption(args, "--sample-rate", MimirChirpBinTimeline.SampleRate));
}
if (args.Any(arg => string.Equals(arg, "--chirp-only-sync-self-test", StringComparison.OrdinalIgnoreCase)))
{
return RunActiveSyncSelfTest(ParseIntOption(args, "--sample-rate", MimirChirpBinTimeline.SampleRate), MimirAudioSyncMode.ChirpOnly);
}
if (args.Any(arg => string.Equals(arg, "--render-chirplet-f32", StringComparison.OrdinalIgnoreCase)))
{
return RenderChirpletFloat32(
@ -28,6 +33,14 @@ if (args.Any(arg => string.Equals(arg, "--render-chirplet-f32", StringComparison
ParseDoubleOption(args, "--seconds", 3.0));
}
if (args.Any(arg => string.Equals(arg, "--render-chirp-bin-f32", StringComparison.OrdinalIgnoreCase)))
{
return RenderChirpBinFloat32(
ParseStringOption(args, "--output", "artifacts/asio/chirp-bin-f32.raw"),
ParseIntOption(args, "--sample-rate", MimirChirpBinTimeline.SampleRate),
ParseDoubleOption(args, "--seconds", 3.0));
}
if (args.Any(arg => string.Equals(arg, "--analyze-asio-f32", StringComparison.OrdinalIgnoreCase)))
{
return AnalyzeAsioFloat32(
@ -275,6 +288,11 @@ static int RunPassiveSyncSelfTest()
}
static int RunHybridSyncSelfTest(int sampleRate)
{
return RunActiveSyncSelfTest(sampleRate, MimirAudioSyncMode.Hybrid);
}
static int RunActiveSyncSelfTest(int sampleRate, MimirAudioSyncMode mode)
{
const string referenceSourceId = "loopback-test";
const string candidateSourceId = "mic-test";
@ -298,23 +316,23 @@ static int RunHybridSyncSelfTest(int sampleRate)
AppendFloatBlock(candidateBuffer, candidateSourceId, candidate, sampleRate);
var analyzer = new MimirAudioSynchronizationAnalyzer();
var reports = analyzer.Analyze([referenceBuffer, candidateBuffer], referenceSourceId, MimirAudioSyncMode.Hybrid);
var reports = analyzer.Analyze([referenceBuffer, candidateBuffer], referenceSourceId, mode);
var report = reports.SingleOrDefault();
foreach (var trace in analyzer.LastDecodeTraces)
{
Console.WriteLine(
$"hybrid-sync-trace {trace.ReferenceSourceId}->{trace.SourceId}: status={trace.Status} compared={trace.ComparedSamples} refFrames={trace.ReferenceFrames} refAnchors={trace.ReferenceAnchors} candFrames={trace.CandidateFrames} candAnchors={trace.CandidateAnchors} matched={trace.MatchedEvents} confidence={trace.Confidence:0.000}");
$"{SyncModeLabel(mode)}-sync-trace {trace.ReferenceSourceId}->{trace.SourceId}: status={trace.Status} compared={trace.ComparedSamples} refFrames={trace.ReferenceFrames} refAnchors={trace.ReferenceAnchors} candFrames={trace.CandidateFrames} candAnchors={trace.CandidateAnchors} matched={trace.MatchedEvents} confidence={trace.Confidence:0.000}");
}
if (report == null)
{
Console.Error.WriteLine("hybrid sync self-test failed: analyzer did not report from a short chirp-bin window");
Console.Error.WriteLine($"{SyncModeLabel(mode)} sync self-test failed: analyzer did not report from a short chirp-bin window");
return 1;
}
var error = Math.Abs(report.FractionalDelaySamples - delaySamples);
Console.WriteLine(
$"hybrid-sync-self-test evidence={report.EvidenceKind} delaySamples={report.FractionalDelaySamples:0.000000} delayUs={report.DelayMicroseconds:0.000} expected={delaySamples:0.000000} errorSamples={error:0.000000} errorUs={error * 1_000_000.0 / report.SampleRate:0.000} confidence={report.Confidence:0.000} events={report.TimelineMatchedEvents} compared={report.ComparedSamples}");
$"{SyncModeLabel(mode)}-sync-self-test evidence={report.EvidenceKind} delaySamples={report.FractionalDelaySamples:0.000000} delayUs={report.DelayMicroseconds:0.000} expected={delaySamples:0.000000} errorSamples={error:0.000000} errorUs={error * 1_000_000.0 / report.SampleRate:0.000} confidence={report.Confidence:0.000} events={report.TimelineMatchedEvents} compared={report.ComparedSamples}");
return string.Equals(report.EvidenceKind, "chirp-bin", StringComparison.Ordinal) &&
report.TimelineMatchedEvents >= 3 &&
report.Confidence > 0.70 &&
@ -323,6 +341,11 @@ static int RunHybridSyncSelfTest(int sampleRate)
: 1;
}
static string SyncModeLabel(MimirAudioSyncMode mode)
{
return mode == MimirAudioSyncMode.ChirpOnly ? "chirp-only" : "hybrid";
}
static int RenderChirpletFloat32(string outputPath, int sampleRate, double seconds)
{
var segmentCount = Math.Max(1, (int)Math.Ceiling(seconds / MimirChirpletTimeline.SegmentSeconds));
@ -355,6 +378,38 @@ static int RenderChirpletFloat32(string outputPath, int sampleRate, double secon
return 0;
}
static int RenderChirpBinFloat32(string outputPath, int sampleRate, double seconds)
{
var segmentCount = Math.Max(1, (int)Math.Ceiling(seconds / MimirChirpBinTimeline.SegmentSeconds));
var samples = new List<float>(Math.Max(1, (int)Math.Ceiling(seconds * sampleRate)));
for (var segment = 0; segment < segmentCount; segment++)
{
samples.AddRange(MimirChirpBinTimeline.Default.RenderSegmentMonoFloat((ulong)segment, sampleRate));
}
var requestedSamples = Math.Max(1, (int)Math.Round(seconds * sampleRate));
if (samples.Count > requestedSamples)
{
samples.RemoveRange(requestedSamples, samples.Count - requestedSamples);
}
var directory = Path.GetDirectoryName(Path.GetFullPath(outputPath));
if (!string.IsNullOrEmpty(directory))
{
Directory.CreateDirectory(directory);
}
var bytes = new byte[samples.Count * sizeof(float)];
for (var index = 0; index < samples.Count; index++)
{
BitConverter.TryWriteBytes(bytes.AsSpan(index * sizeof(float), sizeof(float)), samples[index]);
}
File.WriteAllBytes(outputPath, bytes);
Console.WriteLine($"chirp-bin-render-f32 path={outputPath} sampleRate={sampleRate} seconds={seconds:0.000} samples={samples.Count}");
return 0;
}
static int AnalyzeAsioFloat32(string inputPath, int sampleRate, int channels, int referenceChannel, int candidateChannel)
{
if (channels <= 0)

View file

@ -179,7 +179,7 @@ public sealed class MimirRuntime : IAquariumRuntime
var batch = RenderCalibrationBatchPcm16Base64(
calibrationSegmentIndex,
segmentCount,
audioSyncSettings.Mode == MimirAudioSyncMode.Hybrid,
UseChirpBinTimeline(audioSyncSettings.Mode),
out var peak);
visualRuntime.Audio.EnqueuePcm16Base64(
batch,
@ -246,6 +246,11 @@ public sealed class MimirRuntime : IAquariumRuntime
return Convert.ToBase64String(bytes);
}
private static bool UseChirpBinTimeline(MimirAudioSyncMode mode)
{
return mode is MimirAudioSyncMode.ChirpOnly or MimirAudioSyncMode.Hybrid;
}
private string DescribeBuffers()
{
return string.Join(" | ", synchronization.Buffers.Buffers.Select(DescribeBuffer));

View file

@ -143,7 +143,8 @@ public sealed class MimirAudioSynchronizationAnalyzer
}
}
var activeMinSeconds = mode == MimirAudioSyncMode.Hybrid
var useChirpBinTimeline = mode is MimirAudioSyncMode.ChirpOnly or MimirAudioSyncMode.Hybrid;
var activeMinSeconds = useChirpBinTimeline
? MinChirpBinWindowSeconds
: MinChirpletWindowSeconds;
var minActiveSamples = MinWindowSamples(activeReferenceBlock.SampleRate, activeMinSeconds);
@ -154,16 +155,16 @@ public sealed class MimirAudioSynchronizationAnalyzer
buffer.Descriptor.SourceId,
activeReferenceBlock.SampleRate,
compared,
mode == MimirAudioSyncMode.Hybrid
useChirpBinTimeline
? "chirp-bin-insufficient-window"
: "chirplet-insufficient-window"));
continue;
}
var comparedReferenceDecode = mode == MimirAudioSyncMode.Hybrid
var comparedReferenceDecode = useChirpBinTimeline
? MimirChirpBinTimeline.Default.DecodeStreamWindow(referenceWindow, activeReferenceBlock.SampleRate)
: MimirChirpletTimeline.Default.DecodeStreamWindow(referenceWindow, activeReferenceBlock.SampleRate);
var candidateDecode = mode == MimirAudioSyncMode.Hybrid
var candidateDecode = useChirpBinTimeline
? MimirChirpBinTimeline.Default.DecodeStreamWindow(candidateWindow, activeReferenceBlock.SampleRate)
: MimirChirpletTimeline.Default.DecodeStreamWindow(candidateWindow, activeReferenceBlock.SampleRate);
var deterministicFit = EstimateDelayFromDecodedTimeline(comparedReferenceDecode, candidateDecode);
@ -188,8 +189,10 @@ public sealed class MimirAudioSynchronizationAnalyzer
continue;
}
var decodedDelaySamples = deterministicFit.DelaySamples;
var bandResponses = mode == MimirAudioSyncMode.Hybrid
var decodedDelaySamples = useChirpBinTimeline
? RefineDelayFromWaveform(referenceWindow, candidateWindow, deterministicFit.DelaySamples, activeReferenceBlock.SampleRate)
: deterministicFit.DelaySamples;
var bandResponses = useChirpBinTimeline
? []
: MimirChirpletTimeline.Default.EstimateBandResponse(candidateWindow, activeReferenceBlock.SampleRate);
reports.Add(new MimirAudioSynchronizationReport(
@ -207,7 +210,7 @@ public sealed class MimirAudioSynchronizationAnalyzer
buffer.Latest?.Sequence ?? 0,
deterministicFit.MatchedEvents,
deterministicFit.Confidence,
mode == MimirAudioSyncMode.Hybrid ? "chirp-bin" : "chirplet"));
useChirpBinTimeline ? "chirp-bin" : "chirplet"));
}
return reports;
@ -305,6 +308,75 @@ public sealed class MimirAudioSynchronizationAnalyzer
return (clockDelay, confidence, Math.Min(reference.ClockFit.AnchorCount, candidate.ClockFit.AnchorCount));
}
private static double RefineDelayFromWaveform(
ReadOnlySpan<float> reference,
ReadOnlySpan<float> candidate,
double initialDelaySamples,
int sampleRate)
{
var radius = Math.Max(16, (int)Math.Ceiling(sampleRate * 0.00035));
var center = (int)Math.Round(initialDelaySamples);
var firstLag = Math.Max(0, center - radius);
var lastLag = Math.Min(candidate.Length - 2, center + radius);
if (reference.Length == 0 || candidate.Length == 0 || firstLag > lastLag)
{
return initialDelaySamples;
}
var bestLag = center;
var bestScore = double.NegativeInfinity;
for (var lag = firstLag; lag <= lastLag; lag++)
{
var score = NormalizedLagScore(reference, candidate, lag);
if (score > bestScore)
{
bestScore = score;
bestLag = lag;
}
}
if (bestLag <= firstLag || bestLag >= lastLag)
{
return bestLag;
}
var left = NormalizedLagScore(reference, candidate, bestLag - 1);
var middle = NormalizedLagScore(reference, candidate, bestLag);
var right = NormalizedLagScore(reference, candidate, bestLag + 1);
var denominator = left - 2.0 * middle + right;
if (Math.Abs(denominator) <= 1.0e-12)
{
return bestLag;
}
return bestLag + Math.Clamp(0.5 * (left - right) / denominator, -0.5, 0.5);
}
private static double NormalizedLagScore(ReadOnlySpan<float> reference, ReadOnlySpan<float> candidate, int lag)
{
var count = Math.Min(reference.Length, candidate.Length - lag);
if (count <= 0)
{
return double.NegativeInfinity;
}
var dot = 0.0;
var referenceEnergy = 0.0;
var candidateEnergy = 0.0;
for (var index = 0; index < count; index++)
{
var referenceSample = reference[index];
var candidateSample = candidate[index + lag];
dot += referenceSample * candidateSample;
referenceEnergy += referenceSample * referenceSample;
candidateEnergy += candidateSample * candidateSample;
}
return referenceEnergy <= 1.0e-12 || candidateEnergy <= 1.0e-12
? double.NegativeInfinity
: dot / Math.Sqrt(referenceEnergy * candidateEnergy);
}
private static float[] ExtractMonoWindow(
MimirRollingStreamBuffer buffer,
out MimirAudioBlockDescriptor? latestBlock,

View file

@ -31,6 +31,7 @@ public sealed class MimirChirpBinTimeline
private const double Gain = 0.070;
private const int MaxSymbolCandidatesPerFrame = 1;
private const int MaxTimingBinShift = 4;
private const double ProposalBudgetMultiplier = 2.5;
private static readonly int[] TimelineSymbols = RotateToDistinctOpening(BuildDeBruijn(SymbolCount, TimelineOrder));
private static readonly Dictionary<int, int> TripleToIndex = BuildTripleIndex(TimelineSymbols);
@ -119,8 +120,8 @@ public sealed class MimirChirpBinTimeline
return [];
}
var hopSamples = Math.Max(1, sampleRate / 1_200);
var energyTrace = BuildDechirpedEnergyTrace(samples, sampleRate, chirpSamples, hopSamples);
var hopSamples = Math.Max(1, sampleRate / 1_000);
var energyTrace = BuildWindowEnergyTrace(samples, chirpSamples, hopSamples);
var threshold = AdaptiveThreshold(energyTrace);
var proposals = new List<int>();
for (var index = 1; index < energyTrace.Length - 1; index++)
@ -133,10 +134,16 @@ public sealed class MimirChirpBinTimeline
}
}
var maxExpectedEvents = Math.Max(
8,
(int)Math.Ceiling(samples.Length / (double)sampleRate / EventSpacingSeconds * ProposalBudgetMultiplier));
var frames = new List<MimirChirpletTransformFrame>();
foreach (var proposal in proposals)
foreach (var proposal in proposals
.OrderByDescending(offset => energyTrace[Math.Clamp(offset / hopSamples, 0, energyTrace.Length - 1)])
.Take(maxExpectedEvents)
.Order())
{
var frame = ClassifyAt(samples, sampleRate, proposal, sampleRate / 120);
var frame = ClassifyAt(samples, sampleRate, proposal, sampleRate / 1_000);
if (frame != null)
{
frames.Add(frame);
@ -163,7 +170,7 @@ public sealed class MimirChirpBinTimeline
var bestOffset = predictedOffset;
MimirChirpletSymbolCandidate[] bestCandidates = [];
var bestEnergy = 0.0;
var step = Math.Max(2, searchRadiusSamples / 24);
var step = Math.Max(2, sampleRate / 4_000);
for (var offset = start; offset <= end; offset += step)
{
var candidates = ScoreBins(samples.Slice(offset, chirpSamples), sampleRate, offset);
@ -176,9 +183,10 @@ public sealed class MimirChirpBinTimeline
}
}
var localStart = Math.Max(start, bestOffset - step);
var localEnd = Math.Min(end, bestOffset + step);
for (var offset = localStart; offset <= localEnd; offset++)
var localStep = Math.Max(1, step / 8);
var localStart = Math.Max(start, bestOffset - localStep * 4);
var localEnd = Math.Min(end, bestOffset + localStep * 4);
for (var offset = localStart; offset <= localEnd; offset += localStep)
{
var candidates = ScoreBins(samples.Slice(offset, chirpSamples), sampleRate, offset);
var energy = candidates.Length == 0 ? 0.0 : candidates[0].Energy;
@ -196,9 +204,37 @@ public sealed class MimirChirpBinTimeline
}
var refinedOffset = RefineOffset(samples, sampleRate, bestOffset, bestCandidates[0].SymbolId, chirpSamples);
var timingBinSamples = BinShiftSamples(sampleRate);
var correctedCandidates = new List<MimirChirpletSymbolCandidate>(bestCandidates.Length * (MaxTimingBinShift * 2 + 1));
foreach (var candidate in bestCandidates)
{
for (var timingBinShift = -MaxTimingBinShift; timingBinShift <= MaxTimingBinShift; timingBinShift++)
{
var canonicalSymbol = ShiftSymbol(candidate.SymbolId, -timingBinShift);
var canonicalOffset = refinedOffset - timingBinShift * timingBinSamples;
var roundedOffset = (int)Math.Round(canonicalOffset);
if (roundedOffset <= 0 || roundedOffset >= samples.Length - chirpSamples - 1)
{
continue;
}
var correctedOffset = RefineOffset(samples, sampleRate, roundedOffset, canonicalSymbol, chirpSamples);
var correctedEnergy = ScoreSymbolEnergy(
samples.Slice((int)Math.Round(correctedOffset), chirpSamples),
sampleRate,
canonicalSymbol);
correctedCandidates.Add(new MimirChirpletSymbolCandidate(
canonicalSymbol,
correctedOffset,
Math.Max(candidate.Energy, correctedEnergy) / (1.0 + Math.Abs(timingBinShift) * 0.15)));
}
}
return new MimirChirpletTransformFrame(
refinedOffset,
bestCandidates.Select(candidate => candidate with { SampleOffset = refinedOffset }).ToArray());
correctedCandidates
.OrderByDescending(candidate => candidate.Energy)
.ToArray());
}
private static MimirChirpletSymbolCandidate[] ScoreBins(
@ -234,29 +270,6 @@ public sealed class MimirChirpBinTimeline
.ToArray();
}
private static double ScoreBestBinEnergy(ReadOnlySpan<float> samples, int sampleRate)
{
var kernels = KernelSets.GetOrAdd(sampleRate, BuildKernelSet);
var sampleEnergy = 0.0;
for (var index = 0; index < samples.Length; index++)
{
sampleEnergy += samples[index] * samples[index];
}
if (sampleEnergy <= 1.0e-12 || kernels.ReferenceEnergy <= 1.0e-12)
{
return 0.0;
}
var best = 0.0;
foreach (var kernel in kernels.Symbols)
{
best = Math.Max(best, DechirpedBin(samples, kernel));
}
return 2.0 * best / Math.Sqrt(sampleEnergy * kernels.ReferenceEnergy);
}
private static double ScoreSymbolEnergy(ReadOnlySpan<float> samples, int sampleRate, int symbolId)
{
var kernels = KernelSets.GetOrAdd(sampleRate, BuildKernelSet);
@ -349,19 +362,14 @@ public sealed class MimirChirpBinTimeline
foreach (var first in firstFrame.Candidates)
foreach (var second in secondFrame.Candidates)
foreach (var third in thirdFrame.Candidates)
for (var timingBinShift = -MaxTimingBinShift; timingBinShift <= MaxTimingBinShift; timingBinShift++)
{
var code = TripleCode(
ShiftSymbol(first.SymbolId, -timingBinShift),
ShiftSymbol(second.SymbolId, -timingBinShift),
ShiftSymbol(third.SymbolId, -timingBinShift));
var code = TripleCode(first.SymbolId, second.SymbolId, third.SymbolId);
if (!TripleToIndex.TryGetValue(code, out var eventIndex))
{
continue;
}
var firstEvent = Default.EventForIndex((ulong)eventIndex);
var correctedFirstOffset = first.SampleOffset - timingBinShift * BinShiftSamples(sampleRate);
var measuredGapA = (second.SampleOffset - first.SampleOffset) / sampleRate;
var measuredGapB = (third.SampleOffset - second.SampleOffset) / sampleRate;
var gapError = Math.Abs(measuredGapA - EventSpacingSeconds) + Math.Abs(measuredGapB - EventSpacingSeconds);
@ -375,12 +383,12 @@ public sealed class MimirChirpBinTimeline
candidates.Add(new MimirChirpletTimelineAnchor(
(ulong)eventIndex,
firstEvent.StartSeconds,
correctedFirstOffset,
(gapConfidence * 0.60 + energyConfidence * 0.40) / (1.0 + Math.Abs(timingBinShift) * 0.15),
first.SampleOffset,
gapConfidence * 0.60 + energyConfidence * 0.40,
[
new MimirChirpletSymbolObservation(ShiftSymbol(first.SymbolId, -timingBinShift), correctedFirstOffset, first.Energy),
new MimirChirpletSymbolObservation(ShiftSymbol(second.SymbolId, -timingBinShift), second.SampleOffset - timingBinShift * BinShiftSamples(sampleRate), second.Energy),
new MimirChirpletSymbolObservation(ShiftSymbol(third.SymbolId, -timingBinShift), third.SampleOffset - timingBinShift * BinShiftSamples(sampleRate), third.Energy),
new MimirChirpletSymbolObservation(first.SymbolId, first.SampleOffset, first.Energy),
new MimirChirpletSymbolObservation(second.SymbolId, second.SampleOffset, second.Energy),
new MimirChirpletSymbolObservation(third.SymbolId, third.SampleOffset, third.Energy),
]));
}
}
@ -495,13 +503,19 @@ public sealed class MimirChirpBinTimeline
meanAbsoluteError);
}
private static float[] BuildDechirpedEnergyTrace(ReadOnlySpan<float> samples, int sampleRate, int windowSamples, int hopSamples)
private static float[] BuildWindowEnergyTrace(ReadOnlySpan<float> samples, int windowSamples, int hopSamples)
{
var output = new float[1 + (samples.Length - windowSamples) / hopSamples];
var prefix = new double[samples.Length + 1];
for (var index = 0; index < samples.Length; index++)
{
prefix[index + 1] = prefix[index] + samples[index] * samples[index];
}
for (var frame = 0; frame < output.Length; frame++)
{
var offset = frame * hopSamples;
output[frame] = (float)ScoreBestBinEnergy(samples.Slice(offset, windowSamples), sampleRate);
output[frame] = (float)((prefix[offset + windowSamples] - prefix[offset]) / windowSamples);
}
return output;

View file

@ -67,3 +67,4 @@
{"ts": "2026-05-23T17:56:26+01:00", "type": "verification", "status": "partial", "note": "Added monitor-sweep mode to the ASIO cadence probe and ran Starfire Scarlett Solo 4th Gen at 192 kHz with 0.02 full-scale output bursts from 8-40 kHz. ASIO loopback channels detected the emitted tone at about 0.01949 amplitude for every tested frequency through 40 kHz. Acoustic input channel 1 detected weak tone energy around 8 kHz and 12 kHz (~0.00041, about -33.5 dB relative to loopback), dropped by 14-16 kHz, and was extremely weak above 20 kHz (roughly -58 dB at 20 kHz, -62 dB at 24 kHz, -80 dB at 32 kHz). Current studio-monitor/acoustic path is not a strong ultrasonic sync carrier; ASIO loopback is excellent for local reference and acoustic calibration should stay near the highest measured robust audible band unless stronger transducers/mics prove otherwise."}
{"ts": "2026-05-23T18:46:59+01:00", "type": "verification", "status": "ok", "note": "Added sample-rate selection to the hybrid chirp-bin self-test and removed the high-rate timing plateau bias from MimirChirpBinTimeline while preserving the 48 kHz compatibility behavior. Synthetic active chirp-bin timing now scales with sample rate: 48 kHz recovered 317.392716 vs 317.375 samples (0.369 us error), 96 kHz recovered 634.766098 vs 634.75 samples (0.168 us error), and 192 kHz recovered 1269.499997 vs 1269.5 samples (about 0.000003 samples, below printed microsecond precision). This proves the timing kernel benefits from 192 kHz in memory; live hardware proof still requires feeding ASIO callbacks into Mimir.Runtime instead of the diagnostic WASAPI/process path."}
{"ts": "2026-05-23T19:03:44+01:00", "type": "measurement", "status": "ok", "note": "Scarlett ASIO chirp-only artifact at 192 kHz: Loopback 1 -> Loopback 2 decoded at 0.000 us with 7 anchors and 0.896 confidence; physical input 2 decoded at 616.226 samples / 3209.508 us with 3 anchors and 0.516 confidence; physical input 1 had no anchors. One 2-second matched-kernel chirplet comparison took roughly 100 seconds, so the current chirp-only decoder is diagnostic, not the runtime hot path."}
{"ts": "2026-05-23T19:22:00+01:00", "type": "implementation", "status": "ok", "note": "Made the active audio sync path use the receiver-cheap chirp-bin decoder for both chirp-only and hybrid modes. The old arbitrary chirplet matcher remains diagnostic/reference only. The chirp-bin decoder now uses bounded energy/onset proposals, dechirp plus fixed Goertzel bins, explicit time/frequency ambiguity candidates, and constrained local waveform correlation for final fractional delay. Synthetic 192 kHz chirp-only and hybrid self-tests both recover a 1269.5-sample delay with printed 0.000 us error; a real Scarlett ASIO chirp-bin artifact decodes Loopback 1 -> Loopback 2 at 0.000 us with 12 anchors and 0.999 confidence, while physical mic channels still need acoustic robustness work."}

View file

@ -102,29 +102,29 @@ accepted_design:
windows before any watermark is needed. Negative passive lags are
contradictory evidence and carry zero confidence; single-window passive
confidence is capped below certainty until repeated state earns it.
`MimirChirpBinTimeline` is the active hybrid watermark path: one fixed chirp
slope/window, 32 widened symbol frequency bins, dechirped bin-energy
proposal scoring, cached per-sample-rate kernels, sub-sample matched-peak
refinement, then the existing de Bruijn triplet anchor and clock-fit
machine. Reports and sync states expose delay in microseconds as well as
fractional samples. Hybrid emits it as low-gain half-second bursts every two
seconds while passive confidence is weak, never as catch-up backlog. The
analyzer no longer applies the passive two-second window floor to chirp-bin
fallback; `dotnet run --project
.\src\Mimir.BufferSmoke\Mimir.BufferSmoke.csproj -- --hybrid-sync-self-test`
proves a one-second rolling-buffer chirp-bin window can recover a
317.375-sample fractional delay from code-valid anchors with 0.369 us
error. The synthetic codebook proof remains `dotnet run --project
`MimirChirpBinTimeline` is the active calibration path for both chirp-only
and hybrid: one fixed chirp slope/window, 32 widened symbol frequency bins,
cheap bounded energy/onset proposals, dechirp plus fixed Goertzel bin
classification, explicit time/frequency ambiguity candidates, then the
existing de Bruijn triplet anchor and clock-fit machine. The analyzer refines
final active delay with constrained local waveform correlation around the
decoded anchor delay. Reports and sync states expose delay in microseconds
as well as fractional samples. Hybrid emits it as low-gain half-second
bursts every two seconds while passive confidence is weak, never as catch-up
backlog. The analyzer no longer applies the passive two-second window floor
to active fallback; `dotnet run --project
.\src\Mimir.BufferSmoke\Mimir.BufferSmoke.csproj -- --chirp-only-sync-self-test --sample-rate 192000`
and `--hybrid-sync-self-test --sample-rate 192000` both recover a
1269.5-sample synthetic delay with printed 0.000 us error. The synthetic
codebook proof remains `dotnet run --project
.\src\Mimir.BufferSmoke\Mimir.BufferSmoke.csproj -- --chirp-bin-self-test`.
`Mimir.BufferSmoke --render-chirplet-f32` plus
`Mimir.BufferSmoke --render-chirp-bin-f32` plus
`native/probes/asio_audio_cadence --play-f32-mono --record-f32-interleaved`
now provides a real Scarlett ASIO artifact proof for chirp-only mode.
At 192 kHz, Loopback 1 -> Loopback 2 decoded at 0.000 us with 7 anchors and
0.896 confidence. Physical input 2 decoded at about 616.226 samples /
3209.508 us with 3 anchors and 0.516 confidence; physical input 1 did not
decode in that capture. The proof cost roughly 100 seconds for one 2-second
192 kHz matched-kernel comparison, so the current chirplet decoder is a
diagnostic witness, not the runtime hot path.
now provides a real Scarlett ASIO artifact proof for active mode. At 192 kHz,
Loopback 1 -> Loopback 2 decoded at 0.000 us with 12 anchors and 0.999
confidence. Physical inputs did not produce matched anchors in that chirp-bin
run, so acoustic robustness remains open. The old arbitrary-chirplet matcher
is a diagnostic/reference artifact only, not the runtime hot path.
- `IMimirVideoCaptureDriver` and `MimirVideoCaptureDriverSource` are the live
seam for Leap, Media Foundation, DirectShow, libusb, LeapC, or shared
texture capture.
@ -223,10 +223,9 @@ current_status:
Kiyo 30.18 fps, PS3 Eye 0 59.66 fps, and PS3 Eye 1 59.95 fps. A mouse in
the Leap USB neighborhood does not reproduce the Focusrite-induced collapse.
next_action: >
Replace the diagnostic chirp-only matched-kernel decoder with the efficient
streaming transform before calling chirp-only runtime-ready at 192 kHz, then
feed Scarlett ASIO callbacks directly into Mimir.Runtime and add the
synchronization actuator.
Prove the active chirp-bin path through real microphones, then feed Scarlett
ASIO callbacks directly into Mimir.Runtime and add the synchronization
actuator.
Current app testing proves Fensalir WASAPI output wakes Scarlett speaker
loopback, live mic buffers stay populated, runtime sync analysis now runs as
a bounded rotating service without UI/telemetry re-entry, and the continuous