Delay runtime sources until scene ready

This commit is contained in:
Meta Vens 2026-05-25 07:13:09 +01:00
commit 38fa830211
4 changed files with 72 additions and 32 deletions

View file

@ -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<IMimirStreamSource> Sources => ownedSources;
public void Detach(IMimirStreamSource source)
{
ownedSources.Remove(source);

View file

@ -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<MimirStreamSourceFactory> 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<IMimirStreamSource>())
{
}
@ -62,10 +63,23 @@ public sealed class MimirRuntime : IAquariumRuntime
AquariumRuntimeOptions options,
MimirSynchronizationSettings settings,
IEnumerable<IMimirStreamSource> streamSources)
: this(options, settings, Array.Empty<MimirStreamSourceFactory>())
{
foreach (var source in streamSources)
{
synchronization.AddSource(source);
}
}
private MimirRuntime(
AquariumRuntimeOptions options,
MimirSynchronizationSettings settings,
IEnumerable<MimirStreamSourceFactory> 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)

View file

@ -6,7 +6,16 @@ public sealed class MimirRuntimeConfiguration
{
public MimirSynchronizationSettings Settings { get; init; } = new();
public IReadOnlyList<IMimirStreamSource> Sources { get; init; } = [];
public IReadOnlyList<MimirStreamSourceFactory> SourceFactories { get; init; } = [];
public IReadOnlyList<IMimirStreamSource> CreateSources()
{
return SourceFactories
.Select(factory => factory.Create())
.Where(source => source != null)
.Cast<IMimirStreamSource>()
.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<IMimirStreamSource>()
var sourceFactories = sourceModels
.Select(stream => TryCreateSourceFactory(stream, configDirectory))
.Where(factory => factory != null)
.Cast<MimirStreamSourceFactory>()
.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<string>(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<IMimirStreamSource?> Create);
public sealed class MimirRuntimeConfigFile
{
public double BufferSeconds { get; set; } = 5.0;

View file

@ -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."}