Mimir/src/Mimir.BufferSmoke/Program.cs

177 lines
6.4 KiB
C#
Raw Normal View History

using Mimir.Runtime.Synchronization;
2026-05-22 16:46:50 +01:00
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));
2026-05-22 11:40:56 +01:00
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;
2026-05-22 12:46:51 +01:00
var nextSyncPoll = DateTimeOffset.UtcNow + TimeSpan.FromSeconds(1);
while (DateTimeOffset.UtcNow < deadline)
{
hub.PollSources(maxSamplesPerSource: 4096);
2026-05-22 12:46:51 +01:00
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<string>();
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);
}
}
2026-05-22 11:40:56 +01:00
var reports = hub.AnalyzeAudioSynchronization(syncReference);
2026-05-22 01:22:36 +01:00
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}");
2026-05-22 01:22:36 +01:00
}
2026-05-22 12:46:51 +01:00
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<string> 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;
}
2026-05-22 11:40:56 +01:00
static string ParseStringOption(IReadOnlyList<string> 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<MimirChirpletBandResponse> bands)
{
return bands.Count == 0
? "none"
: string.Join(",", bands.Select(band => $"{band.CenterHz:0}Hz:{band.Energy:0.000}"));
}
2026-05-22 16:46:50 +01:00
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<IMimirStreamSource> 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();
}
}
}