diff --git a/src/Mimir.BufferSmoke/Program.cs b/src/Mimir.BufferSmoke/Program.cs index 742b821..9c854e2 100644 --- a/src/Mimir.BufferSmoke/Program.cs +++ b/src/Mimir.BufferSmoke/Program.cs @@ -254,7 +254,7 @@ using var configuration = new DisposableConfiguration(MimirRuntimeConfiguration. using var hub = new MimirSynchronizationHub(configuration.Value.Settings); var syncMode = configuration.Value.Settings.Audio.Mode; -foreach (var source in configuration.Value.Sources) +foreach (var source in configuration.Sources.ToArray()) { configuration.Detach(source); hub.AddSource(source); @@ -4997,11 +4997,13 @@ internal sealed class DisposableConfiguration : IDisposable public DisposableConfiguration(MimirRuntimeConfiguration value) { Value = value; - ownedSources = value.Sources.ToList(); + ownedSources = value.CreateSources().ToList(); } public MimirRuntimeConfiguration Value { get; } + public IReadOnlyList Sources => ownedSources; + public void Detach(IMimirStreamSource source) { ownedSources.Remove(source); diff --git a/src/Mimir.Runtime/MimirRuntime.cs b/src/Mimir.Runtime/MimirRuntime.cs index 18c6914..9d68a10 100644 --- a/src/Mimir.Runtime/MimirRuntime.cs +++ b/src/Mimir.Runtime/MimirRuntime.cs @@ -20,6 +20,7 @@ public sealed class MimirRuntime : IAquariumRuntime private const float DefaultSpectrumUpdateIntervalSeconds = 0.2f; private readonly MimirSynchronizationHub synchronization; private readonly MimirAudioSpectrumAnalyzer spectrumAnalyzer; + private readonly IReadOnlyList sourceFactories; private readonly AquariumUiDocument ui; private readonly AquariumAudioDocument audio = new(); private readonly MimirAudioSynchronizationSettings audioSyncSettings; @@ -49,12 +50,12 @@ public sealed class MimirRuntime : IAquariumRuntime } public MimirRuntime(AquariumRuntimeOptions options, MimirRuntimeConfiguration configuration) - : this(options, configuration.Settings, configuration.Sources) + : this(options, configuration.Settings, configuration.SourceFactories) { } public MimirRuntime(AquariumRuntimeOptions options, MimirSynchronizationSettings settings) - : this(options, settings, []) + : this(options, settings, Array.Empty()) { } @@ -62,10 +63,23 @@ public sealed class MimirRuntime : IAquariumRuntime AquariumRuntimeOptions options, MimirSynchronizationSettings settings, IEnumerable streamSources) + : this(options, settings, Array.Empty()) + { + foreach (var source in streamSources) + { + synchronization.AddSource(source); + } + } + + private MimirRuntime( + AquariumRuntimeOptions options, + MimirSynchronizationSettings settings, + IEnumerable sourceFactories) { Options = options; synchronization = new MimirSynchronizationHub(settings); spectrumAnalyzer = new MimirAudioSpectrumAnalyzer(ParseSpectrumFftSize()); + this.sourceFactories = sourceFactories.ToArray(); audioSyncSettings = settings.Audio; telemetryIntervalSeconds = ParseTelemetryIntervalSeconds(); audioSyncUpdateIntervalSeconds = ParseAudioSyncIntervalSeconds(); @@ -80,11 +94,6 @@ public sealed class MimirRuntime : IAquariumRuntime : null; nextAudioSyncSeconds = audioSyncUpdateIntervalSeconds; nextTelemetrySeconds = telemetryIntervalSeconds; - foreach (var source in streamSources) - { - synchronization.AddSource(source); - } - ui = CreateUi(); } @@ -124,15 +133,16 @@ public sealed class MimirRuntime : IAquariumRuntime public void Update(float deltaSeconds, InputState input) { - runtimeSeconds += Math.Max(deltaSeconds, 0.0f); - lastPollCount = synchronization.PollSources(); - if (sceneReady) + if (!sceneReady) { - QueueCalibrationTimeline(); - UpdateAudioSpectra(); - UpdateAudioSynchronization(); + return; } + runtimeSeconds += Math.Max(deltaSeconds, 0.0f); + lastPollCount = synchronization.PollSources(); + QueueCalibrationTimeline(); + UpdateAudioSpectra(); + UpdateAudioSynchronization(); EmitTelemetry(); } @@ -144,6 +154,7 @@ public sealed class MimirRuntime : IAquariumRuntime } sceneReady = true; + StartConfiguredSources(); nextAudioSyncSeconds = runtimeSeconds + audioSyncUpdateIntervalSeconds; nextSpectrumSeconds = runtimeSeconds; Console.WriteLine($"Mimir Fensalir scene ready at {runtimeSeconds:0.000}s; runtime audio tests and spectra enabled."); @@ -365,6 +376,18 @@ public sealed class MimirRuntime : IAquariumRuntime nextSpectrumSeconds = runtimeSeconds + spectrumUpdateIntervalSeconds; } + private void StartConfiguredSources() + { + foreach (var factory in sourceFactories) + { + var source = factory.Create(); + if (source != null) + { + synchronization.AddSource(source); + } + } + } + private void EmitTelemetry() { if (telemetryIntervalSeconds <= 0.0f || runtimeSeconds < nextTelemetrySeconds) diff --git a/src/Mimir.Runtime/Synchronization/MimirRuntimeConfiguration.cs b/src/Mimir.Runtime/Synchronization/MimirRuntimeConfiguration.cs index f7ae5b1..828fabb 100644 --- a/src/Mimir.Runtime/Synchronization/MimirRuntimeConfiguration.cs +++ b/src/Mimir.Runtime/Synchronization/MimirRuntimeConfiguration.cs @@ -6,7 +6,16 @@ public sealed class MimirRuntimeConfiguration { public MimirSynchronizationSettings Settings { get; init; } = new(); - public IReadOnlyList Sources { get; init; } = []; + public IReadOnlyList SourceFactories { get; init; } = []; + + public IReadOnlyList CreateSources() + { + return SourceFactories + .Select(factory => factory.Create()) + .Where(source => source != null) + .Cast() + .ToArray(); + } public static MimirRuntimeConfiguration Load() { @@ -39,10 +48,10 @@ public sealed class MimirRuntimeConfiguration var streams = sourceModels .SelectMany(ToDescriptors) .ToArray(); - var sources = sourceModels - .Select(stream => TryCreateSource(stream, configDirectory)) - .Where(source => source != null) - .Cast() + var sourceFactories = sourceModels + .Select(stream => TryCreateSourceFactory(stream, configDirectory)) + .Where(factory => factory != null) + .Cast() .ToArray(); return new MimirRuntimeConfiguration @@ -53,7 +62,7 @@ public sealed class MimirRuntimeConfiguration Audio = audio, Streams = streams, }, - Sources = sources, + SourceFactories = sourceFactories, }; } @@ -113,11 +122,12 @@ public sealed class MimirRuntimeConfiguration } } - private static IMimirStreamSource? TryCreateSource(MimirStreamConfig stream, string? configDirectory) + private static MimirStreamSourceFactory? TryCreateSourceFactory(MimirStreamConfig stream, string? configDirectory) { + var descriptor = ToDescriptor(stream); if (string.Equals(stream.Adapter, "native", StringComparison.OrdinalIgnoreCase)) { - return new MimirNativeIngestStreamSource(ToDescriptor(stream)); + return new MimirStreamSourceFactory(descriptor, () => new MimirNativeIngestStreamSource(descriptor)); } if (string.Equals(stream.Adapter, "asio", StringComparison.OrdinalIgnoreCase)) @@ -128,13 +138,13 @@ public sealed class MimirRuntimeConfiguration } #pragma warning disable CA1416 - return new MimirAsioStreamSource( - ToDescriptor(stream), + return new MimirStreamSourceFactory(descriptor, () => new MimirAsioStreamSource( + descriptor, new MimirAsioStreamSourceOptions( ResolveCommand(stream.Command, configDirectory), stream.DriverClsid, stream.SampleRate, - stream.AcceptSourceIds)); + stream.AcceptSourceIds))); #pragma warning restore CA1416 } @@ -146,14 +156,14 @@ public sealed class MimirRuntimeConfiguration return null; } - return new MimirFrameEventProcessStreamSource( - ToDescriptor(stream), + return new MimirStreamSourceFactory(descriptor, () => new MimirFrameEventProcessStreamSource( + descriptor, new MimirFrameEventProcessStreamSourceOptions( ResolveCommand(stream.Command, configDirectory), stream.Arguments, stream.AcceptSourceIds.Length > 0 ? new HashSet(stream.AcceptSourceIds, StringComparer.Ordinal) - : null)); + : null))); } if (!string.Equals(stream.Adapter, "process", StringComparison.OrdinalIgnoreCase)) @@ -166,12 +176,12 @@ public sealed class MimirRuntimeConfiguration return null; } - return new MimirProcessStreamSource( - ToDescriptor(stream), + return new MimirStreamSourceFactory(descriptor, () => new MimirProcessStreamSource( + descriptor, new MimirProcessStreamSourceOptions( ResolveCommand(stream.Command, configDirectory), stream.Arguments, - Math.Max(1024, stream.ChunkBytes))); + Math.Max(1024, stream.ChunkBytes)))); } private static string ResolveCommand(string command, string? configDirectory) @@ -201,6 +211,10 @@ public sealed class MimirRuntimeConfiguration } } +public sealed record MimirStreamSourceFactory( + MimirStreamDescriptor Descriptor, + Func Create); + public sealed class MimirRuntimeConfigFile { public double BufferSeconds { get; set; } = 5.0; diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 89b9787..d541879 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -110,3 +110,4 @@ {"ts": "2026-05-25T06:50:00+01:00", "type": "implementation", "status": "ok", "note": "Corrected `MimirDirectPathTracker` confidence scoring. The old score normalized direct-cluster confidence against all alternate matched-filter lobes, which made a nearly perfect loopback lock report only 0.126 confidence. Confidence now primarily belongs to the selected direct cluster: direct-hit count, residual tightness, phase coherence, and prediction agreement; nearby competing lobes apply only an ambiguity penalty. Reanalysis of `scarlett-canary-packet-live-20260525-063229-f32.raw` leaves delays unchanged but raises Loopback 1 -> Loopback 2 confidence to 0.971, cardioid confidence to 0.488, and the weaker/disconnected-looking channel to 0.335."} {"ts": "2026-05-25T06:59:00+01:00", "type": "correction", "status": "invalidated", "note": "Invalidated the apparent cardioid timing proof in `calibration/bioacoustic/complex-contour-live-20260525-063229.md`. The log-mel spectrogram and channel statistics show `asio-ch0` is not a real acoustic signal in that capture: RMS is almost flat (`0.00208350` to `0.00209815` by 0.1s block), peak is only `0.00334287`, and zero-lag correlation with loopback is `0.000032`. The near-seed +1.547 us contour estimate was a seeded tracker fit against noise, not physical mic evidence. The receipt remains valid as a loopback/interface proof (`asio-ch2` -> `asio-ch3` at -0.014 us, confidence 0.971) plus a weak/non-authoritative input witness."} {"ts": "2026-05-25T07:05:34+01:00", "type": "implementation", "status": "ok", "note": "Added runtime FFT spectra and Fensalir readiness gating. Fensalir now calls IAquariumRuntime.OnSceneReady after the first presented ready frame; Mimir waits for that callback before emitting bioacoustic test audio, running sync analysis, or updating the cached spectrum diagnostics. MimirAudioSpectrumAnalyzer computes bounded Hann-windowed radix-2 FFT snapshots from rolling audio buffers, exposes top peaks/log bands/RMS/peak, and Mimir renders cached spectra in the DirectWrite UI plus telemetry. Verification run with ASIO config showed scene-ready before bioacoustic chunks and live 8192-point spectra for asio-ch0 through asio-ch3; asio-ch0 remains flat low-level noise while loopback/mic-bearing channels show song-band peaks."} +{"ts": "2026-05-25T07:11:48+01:00", "type": "correction", "status": "ok", "note": "Tightened the Fensalir scene-ready gate after runtime spectra testing showed the app still created sources and emitted telemetry before the render path was ready. Runtime config now stores lazy stream-source factories; Mimir.App creates/registers ASIO/process/native sources only inside OnSceneReady, MimirRuntime.Update returns without polling, spectra, sync, telemetry, or audio emission until that callback, and Fensalir constructs the WASAPI/AquaSynth host only after the first presented ready frame. Verification log artifacts/runtime/mimir-ready-gate-20260525-071121.out.log shows no ASIO buffer polling, spectrum telemetry, or bioacoustic batch before D3D12 shader pipelines ready and Mimir Fensalir scene ready."}