using Mimir.Runtime.Synchronization; if (args.Any(arg => string.Equals(arg, "--chirplet-self-test", StringComparison.OrdinalIgnoreCase))) { return RunChirpletSelfTest(); } var duration = TimeSpan.FromSeconds(ParseDoubleOption(args, "--seconds", 10.0)); var pollDelay = TimeSpan.FromMilliseconds(ParseDoubleOption(args, "--poll-ms", 10.0)); var requireSamples = args.Any(arg => string.Equals(arg, "--require-samples", StringComparison.OrdinalIgnoreCase)); var syncReference = ParseStringOption(args, "--sync-reference", "loopback-scarlett-speakers"); using var configuration = new DisposableConfiguration(MimirRuntimeConfiguration.Load()); using var hub = new MimirSynchronizationHub(configuration.Value.Settings); foreach (var source in configuration.Value.Sources) { configuration.Detach(source); hub.AddSource(source); } var deadline = DateTimeOffset.UtcNow + duration; var nextSyncPoll = DateTimeOffset.UtcNow + TimeSpan.FromSeconds(1); while (DateTimeOffset.UtcNow < deadline) { hub.PollSources(maxSamplesPerSource: 4096); if (DateTimeOffset.UtcNow >= nextSyncPoll) { hub.AnalyzeAudioSynchronization(syncReference); nextSyncPoll += TimeSpan.FromSeconds(1); } await Task.Delay(pollDelay).ConfigureAwait(false); } hub.PollSources(maxSamplesPerSource: 16384); Console.WriteLine($"sources={hub.SourceCount} ingested={hub.IngestedSamples} buffers={hub.Buffers.Buffers.Count}"); var emptyBuffers = new List(); foreach (var buffer in hub.Buffers.Buffers.OrderBy(buffer => buffer.Descriptor.SourceId, StringComparer.Ordinal)) { var latest = buffer.Latest; if (latest?.VideoFrame is { } frame) { Console.WriteLine( $"{buffer.Descriptor.SourceId}: count={buffer.Count} edgeNs={buffer.EdgeNs} latest={frame.Width}x{frame.Height} {frame.PixelFormat} bytes={latest.Value.ByteLength}"); } else if (latest?.AudioBlock is { } block) { Console.WriteLine( $"{buffer.Descriptor.SourceId}: count={buffer.Count} edgeNs={buffer.EdgeNs} latest={block.Channels}ch {block.SampleRate}Hz {block.SampleFormat} frames={block.FrameCount} bytes={latest.Value.ByteLength}"); } else { Console.WriteLine($"{buffer.Descriptor.SourceId}: count={buffer.Count} edgeNs={buffer.EdgeNs}"); } if (buffer.Count == 0) { emptyBuffers.Add(buffer.Descriptor.SourceId); } } var reports = hub.AnalyzeAudioSynchronization(syncReference); foreach (var report in reports.OrderBy(report => report.SourceId, StringComparer.Ordinal)) { Console.WriteLine( $"sync {report.ReferenceSourceId}->{report.SourceId}: delaySamples={report.DelaySamples} fractionalDelaySamples={report.FractionalDelaySamples:0.000} delayMs={report.DelayMilliseconds:0.000} confidence={report.Confidence:0.000} bands={DescribeBands(report.BandResponses)} compared={report.ComparedSamples}"); } foreach (var state in hub.AudioSynchronizationStates) { Console.WriteLine( $"sync-state {state.ReferenceSourceId}->{state.SourceId}: smoothedDelaySamples={state.SmoothedDelaySamples:0.000} delayMs={state.DelayMilliseconds:0.000} sroPpm={state.SamplingRateOffsetPpm:0.000} confidence={state.Confidence:0.000} bands={DescribeBands(state.BandResponses)}"); } if (requireSamples && emptyBuffers.Count > 0) { Console.Error.WriteLine($"empty buffers: {string.Join(", ", emptyBuffers)}"); return 1; } return 0; static double ParseDoubleOption(IReadOnlyList args, string name, double fallback) { for (var index = 0; index < args.Count - 1; index++) { if (string.Equals(args[index], name, StringComparison.OrdinalIgnoreCase) && double.TryParse(args[index + 1], out var parsed) && parsed > 0) { return parsed; } } return fallback; } static string ParseStringOption(IReadOnlyList args, string name, string fallback) { for (var index = 0; index < args.Count - 1; index++) { if (string.Equals(args[index], name, StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(args[index + 1])) { return args[index + 1]; } } return fallback; } static string DescribeBands(IReadOnlyList bands) { return bands.Count == 0 ? "none" : string.Join(",", bands.Select(band => $"{band.CenterHz:0}Hz:{band.Energy:0.000}")); } static int RunChirpletSelfTest() { var timeline = MimirChirpletTimeline.Default; var decoder = new MimirChirpletStreamDecoder(windowDuration: TimeSpan.FromSeconds(2)); MimirChirpletStreamDecode decode = new([], [], [], null); for (ulong segment = 0; segment < 4; segment++) { decode = decoder.Append(timeline.RenderSegmentMonoFloat(segment)); } var meanAbsoluteError = decode.ClockFit?.MeanAbsoluteErrorSamples ?? double.PositiveInfinity; Console.WriteLine( $"chirplet-self-test frames={decode.Frames.Count} symbols={decode.Symbols.Count} anchors={decode.Anchors.Count} " + $"clock={(decode.ClockFit is null ? "none" : decode.ClockFit.EffectiveSampleRate.ToString("0.000000"))} " + $"confidence={(decode.ClockFit?.Confidence ?? 0.0):0.000} mae={meanAbsoluteError:0.000000}"); foreach (var anchor in decode.Anchors) { var expected = timeline.EventForIndex(anchor.EventIndex).StartSeconds * MimirChirpletTimeline.SampleRate; Console.WriteLine( $"chirplet-anchor event={anchor.EventIndex} actual={anchor.SampleOffset:0.000} expected={expected:0.000} " + $"error={anchor.SampleOffset - expected:0.000} confidence={anchor.Confidence:0.000}"); } if (decode.ClockFit == null || decode.Anchors.Count < 12 || meanAbsoluteError > 0.25) { Console.Error.WriteLine("chirplet self-test failed: canonical timeline did not decode to sub-frame anchors"); return 1; } return 0; } internal sealed class DisposableConfiguration : IDisposable { private readonly List ownedSources; public DisposableConfiguration(MimirRuntimeConfiguration value) { Value = value; ownedSources = value.Sources.ToList(); } public MimirRuntimeConfiguration Value { get; } public void Detach(IMimirStreamSource source) { ownedSources.Remove(source); } public void Dispose() { foreach (var source in ownedSources) { source.Dispose(); } } }