Make active sync use chirp-bin decoder
This commit is contained in:
parent
8667f6450d
commit
67c7627446
11 changed files with 311 additions and 164 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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."}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue