diff --git a/research/bioacoustic-contestants-2026-05-24.md b/research/bioacoustic-contestants-2026-05-24.md index 8f9d3fb..6c0a9cc 100644 --- a/research/bioacoustic-contestants-2026-05-24.md +++ b/research/bioacoustic-contestants-2026-05-24.md @@ -6,7 +6,7 @@ This pass moves the harness from "one current song, many decoder knobs" toward actual contestants: different generated birdcall word shapes fight against the same indexed cepstral decoder family and the same cepstral warp/blur damage. -The score is intentionally brutal: +The original score was intentionally brutal: ```text performance = sqrt(clamp(realtime / 50x, 0..1) * convergence) @@ -29,6 +29,42 @@ score = performance * anchor - `thrush-ladder`: repeated interval ladder with a shifted reply. - `thornbill-zigzag`: high-band fast zig-zag syllables. - `nightingale-cascade`: dense pretty cascade; not yet in the fast receipt. +- `aquasynth-formant-weaver`: AquaSynth-shaped formant/FM word with payload + ornaments. +- `canary-packet-trill`: six-syllable packet call with hard timing chips and + payload-colored formant notes. +- `finch-burst-packet`: shorter five-syllable boundary candidate. Useful as a + speed/blur failure witness, not the current recommendation. + +## Language Score Update + +The harness now reports the score the project actually cares about: + +```text +language_score = realtime_multiplier + * timing_accuracy + * frequency_accuracy + * payload_bitrate_bps +``` + +This is raw enough to hurt. It rewards a receiver that is fast, places anchors +accurately in time, preserves frequency/shape identity, and recovers +data-bearing words. `payload_bitrate_bps` is currently computed from the +profile's payload bits per event, event spacing, and payload accuracy. The +payload stream is deterministic test data, not yet an arbitrary encrypted +external message, but it does force the call shape and decoder to carry more +than clock identity. + +The score also stopped using word precision as a fake frequency score. Each +decoded observation now carries a cepstral shape accuracy from the winning +template distance. That is still a compact proxy, but it is pointed at the +frequency/formant surface instead of identity bookkeeping. + +Clock fitting now uses a global propagation-delay hypothesis. Candidate anchors +vote for one delay, and only anchors coherent with that delay are allowed to own +the clock fit. Observations outside the expected fully captured window remain +diagnostics; they no longer poison the timing kernel. This fixed a real harness +bug where truncated end-of-window calls were being counted as expected anchors. The first fast panel intentionally tested only the first four contestants, two decoders, and clean/blur degradations so the loop could produce receipts @@ -68,6 +104,78 @@ anchors. This may be correct for clock acquisition, but the next pass should separate "lock fast" from "lock beautifully once found" so the harness cannot overvalue sparse perfection. +## Packet Contest Receipts + +The best current receipt is: + +```powershell +dotnet run --no-build --project .\src\Mimir.BufferSmoke\Mimir.BufferSmoke.csproj -- --bioacoustic-contestants --seconds 0.75 --song canary-packet-trill --decoder packet-razor-index --max-songs 1 --max-decoders 1 --max-degradations 5 --output artifacts/bioacoustic-contestants +``` + +Receipt: + +`artifacts/bioacoustic-contestants/contestants-20260524-185328/contestant-summary.json` + +Best result: + +```text +song=canary-packet-trill +decoder=packet-razor-index +degradation=warp-light-blur +language_score=396.683 +realtime=6.7x +timing=1.000 +frequency=0.922 +payload_bitrate=64.0 bps +payload_accuracy=1.000 +correct=5/5 +``` + +Worst result in that five-degradation panel: + +```text +degradation=warp-light +language_score=199.244 +realtime=5.1x +timing=1.000 +frequency=0.724 +payload_bitrate=54.1 bps +correct=5/5 +``` + +The `packet-razor-index` profile is the current speed ceiling: 512-point FFT, +16 mel bins, 6 cepstral coefficients, one projection table, 8-bit hashes, and a +tight proposal budget. It only became viable after adding the missing +warp-light template augmentation; without that, plain warp-light collapsed to +`0.720`. + +Boundary tests: + +- `packet-sprint-index` is steadier than razor but slower. Best canary result: + `255.908`, realtime `4.5x`, timing `1.000`, frequency `0.976`. +- `packet-needle-index` buys more speed and loses some frequency fidelity. Best + canary result: `258.298`, realtime `5.2x`, frequency `0.845`. +- `finch-burst-packet` proves the current lower bound is too short: its best + razor result was `280.146`, but blur-light collapsed to `4.358`; with sprint + it recovered blur but topped out at `218.368`. + +Broad panel sanity check: + +```powershell +dotnet run --no-build --project .\src\Mimir.BufferSmoke\Mimir.BufferSmoke.csproj -- --bioacoustic-contestants --seconds 0.75 --max-songs 8 --max-decoders 5 --max-degradations 2 --output artifacts/bioacoustic-contestants +``` + +Receipt: + +`artifacts/bioacoustic-contestants/contestants-20260524-185708/contestant-summary.json` + +This clean/blur-only panel still picked `canary-packet-trill + +packet-razor-index` as best, with `language_score=264.923`, `realtime=6.0x`, +`timing=1.000`, `frequency=0.944`, and `payload_bitrate=46.7 bps` on clean +roundtrip. The score is lower than the targeted five-degradation best because +the broad panel did not include the warp-blur cases where canary/razor hit +`396.683`. + ## Lessons - The old decoder recognized words but emitted coarse proposal offsets. Timing @@ -82,6 +190,16 @@ overvalue sparse perfection. inside the motif instead of at the canonical motif start. - The next scoring improvement should expose per-formant/frequency residuals directly instead of using word identity as a proxy. +- The 96 ms six-syllable canary packet is the best current tradeoff. Pushing the + motif down near 78 ms increases nominal bitrate but loses too much + degradation resistance. +- The global delay hypothesis is not optional. A single smeared anchor can move + a least-squares fit by hundreds of samples if it is allowed to claim clock + authority directly. +- AquaSynth's useful pressure is the separation of authoring intent from + realtime DSP: these packet songs should become precompiled/formally described + emission surfaces, with runtime controls exposed as stable parameters rather + than regenerated inside the audio callback. ## Real Bird References diff --git a/src/Mimir.BufferSmoke/Program.cs b/src/Mimir.BufferSmoke/Program.cs index d88290c..28bbc2a 100644 --- a/src/Mimir.BufferSmoke/Program.cs +++ b/src/Mimir.BufferSmoke/Program.cs @@ -52,7 +52,9 @@ if (args.Any(arg => string.Equals(arg, "--bioacoustic-contestants", StringCompar ParseStringOption(args, "--output", "artifacts/bioacoustic-contestants"), ParseIntOption(args, "--max-songs", MimirBioacousticContestants.BuiltIn.Count), ParseIntOption(args, "--max-decoders", MimirBioacousticDecoderConfiguration.BuiltInProfiles.Count), - ParseIntOption(args, "--max-degradations", CepstralTrainingDegradationSettings().Count)).ConfigureAwait(false); + ParseIntOption(args, "--max-degradations", CepstralTrainingDegradationSettings().Count), + ParseStringOption(args, "--song", ""), + ParseStringOption(args, "--decoder", "")).ConfigureAwait(false); } if (args.Any(arg => string.Equals(arg, "--bioacoustic-actuator-self-test", StringComparison.OrdinalIgnoreCase))) @@ -716,17 +718,24 @@ static async Task RunBioacousticContestantsAsync( string outputRoot, int maxSongs, int maxDecoders, - int maxDegradations) + int maxDegradations, + string songFilter, + string decoderFilter) { var started = DateTimeOffset.UtcNow; var runId = $"contestants-{started:yyyyMMdd-HHmmss}"; var runDirectory = Path.GetFullPath(Path.Combine(outputRoot, runId)); Directory.CreateDirectory(runDirectory); var degradations = CepstralTrainingDegradationSettings().Take(Math.Max(1, maxDegradations)).ToArray(); - var decoders = BioacousticTrainingHypotheses().Take(Math.Max(1, maxDecoders)).ToArray(); + var decoders = BioacousticTrainingHypotheses() + .Where(hypothesis => string.IsNullOrWhiteSpace(decoderFilter) || string.Equals(hypothesis.Id, decoderFilter, StringComparison.OrdinalIgnoreCase)) + .Take(Math.Max(1, maxDecoders)) + .ToArray(); var results = new List(); - foreach (var profile in MimirBioacousticContestants.BuiltIn.Take(Math.Max(1, maxSongs))) + foreach (var profile in MimirBioacousticContestants.BuiltIn + .Where(profile => string.IsNullOrWhiteSpace(songFilter) || string.Equals(profile.Id, songFilter, StringComparison.OrdinalIgnoreCase)) + .Take(Math.Max(1, maxSongs))) { var renderer = new MimirBioacousticContestantRenderer(profile); var source = renderer.RenderSequenceMonoFloat(seconds, sampleRate); @@ -750,15 +759,25 @@ static async Task RunBioacousticContestantsAsync( index, renderer); var decodeMs = Stopwatch.GetElapsedTime(stamp).TotalMilliseconds; - var correct = observations.Count(observation => expectedEvents.Contains(observation.EventIndex)); + var expectedObservations = observations + .Where(observation => expectedEvents.Contains(observation.EventIndex)) + .ToArray(); + var correct = expectedObservations.Length; + var payloadCorrect = expectedObservations.Count(observation => + observation.PayloadSymbol == renderer.PayloadSymbolForEvent(observation.EventIndex)); var precision = observations.Count == 0 ? 0.0 : correct / (double)observations.Count; var recall = expectedEvents.Count == 0 ? 0.0 : correct / (double)expectedEvents.Count; + var payloadAccuracy = observations.Count == 0 || expectedEvents.Count == 0 + ? 0.0 + : Math.Sqrt(payloadCorrect / (double)observations.Count * payloadCorrect / (double)expectedEvents.Count); var confidence = observations.Count == 0 ? 0.0 : observations.Average(observation => observation.Confidence); - var clock = FitContestantClockHypothesis(observations, renderer, sampleRate, expectedEvents.Count); + var clock = FitContestantClockHypothesis(expectedObservations, renderer, sampleRate, expectedEvents.Count); var timingAccuracy = clock == null ? 0.0 : 1.0 / (1.0 + clock.MeanAbsoluteErrorSamples / Math.Max(1.0, sampleRate * 0.00025)); - var frequencyAccuracy = precision; + var frequencyAccuracy = expectedObservations.Length == 0 + ? 0.0 + : expectedObservations.Average(observation => observation.ShapeAccuracy); var convergence = clock == null ? 0.0 : Math.Clamp(clock.AnchorCoverage, 0.0, 1.0) * Math.Clamp(clock.Confidence, 0.0, 1.0); @@ -766,6 +785,10 @@ static async Task RunBioacousticContestantsAsync( var performance = Math.Sqrt(Math.Clamp(realtime / 50.0, 0.0, 1.0) * Math.Max(0.0, convergence)); var anchorAccuracy = Math.Sqrt(Math.Max(0.0, timingAccuracy) * Math.Max(0.0, frequencyAccuracy)); var contestScore = performance * anchorAccuracy; + var payloadBitrate = profile.EventSpacingSeconds <= 0.0 + ? 0.0 + : profile.PayloadBitsPerEvent * payloadAccuracy / profile.EventSpacingSeconds; + var languageScore = realtime * timingAccuracy * frequencyAccuracy * payloadBitrate; var result = new BioacousticContestantResult( $"{runId}:{profile.Id}:{decoder.Id}:{degradation.Name}", runId, @@ -783,8 +806,11 @@ static async Task RunBioacousticContestantsAsync( confidence, timingAccuracy, frequencyAccuracy, + payloadAccuracy, convergence, realtime, + payloadBitrate, + languageScore, contestScore, decodeMs, analysis.MelBins, @@ -799,10 +825,16 @@ static async Task RunBioacousticContestantsAsync( clock.Confidence, clock.Score, clock.AnchorCoverage), + observations.Select(observation => new BioacousticContestantObservation( + observation.EventIndex, + observation.PayloadSymbol, + observation.SampleOffset, + observation.Confidence, + observation.ShapeAccuracy)).ToArray(), profile.BeautyNotes); results.Add(result); Console.WriteLine( - $"bioacoustic-contestant song={profile.Id} decoder={decoder.Id} degradation={degradation.Name} score={contestScore:0.000} perf={performance:0.000} anchor={anchorAccuracy:0.000} timing={timingAccuracy:0.000} freq={frequencyAccuracy:0.000} convergence={convergence:0.000} realtime={realtime:0.0}x correct={correct}/{expectedEvents.Count} observations={observations.Count} confidence={confidence:0.000}"); + $"bioacoustic-contestant song={profile.Id} decoder={decoder.Id} degradation={degradation.Name} languageScore={languageScore:0.000} bitrate={payloadBitrate:0.0}bps payload={payloadAccuracy:0.000} score={contestScore:0.000} perf={performance:0.000} anchor={anchorAccuracy:0.000} timing={timingAccuracy:0.000} freq={frequencyAccuracy:0.000} convergence={convergence:0.000} realtime={realtime:0.0}x correct={correct}/{expectedEvents.Count} observations={observations.Count} confidence={confidence:0.000}"); } } } @@ -816,22 +848,22 @@ static async Task RunBioacousticContestantsAsync( startedAtUtc = started.ToString("O"), sampleRate, seconds, - best = results.OrderByDescending(result => result.ContestScore).FirstOrDefault(), + best = results.OrderByDescending(result => result.LanguageScore).FirstOrDefault(), bySong = results .GroupBy(result => result.SongId) .Select(group => new { song = group.Key, - average = group.Average(result => result.ContestScore), - best = group.Max(result => result.ContestScore), - worst = group.Min(result => result.ContestScore) + average = group.Average(result => result.LanguageScore), + best = group.Max(result => result.LanguageScore), + worst = group.Min(result => result.LanguageScore) }) .OrderByDescending(row => row.average) .ToArray(), results }, new JsonSerializerOptions { WriteIndented = true })).ConfigureAwait(false); - Console.WriteLine($"bioacoustic-contestants-run path={runDirectory} results={results.Count} best={results.OrderByDescending(result => result.ContestScore).First().SongId} score={results.Max(result => result.ContestScore):0.000}"); - return results.Count > 0 && results.Max(result => result.ContestScore) > 0.0 ? 0 : 1; + Console.WriteLine($"bioacoustic-contestants-run path={runDirectory} results={results.Count} best={results.OrderByDescending(result => result.LanguageScore).First().SongId} languageScore={results.Max(result => result.LanguageScore):0.000}"); + return results.Count > 0 && results.Max(result => result.LanguageScore) > 0.0 ? 0 : 1; } static int RunPassiveSyncSelfTest() @@ -1969,7 +2001,7 @@ static IReadOnlyList DecodeCepstralIndexedWordsCore( { var template = templateIndex.Templates[index]; var distance = CepstralDistance(feature, template.Feature); - var timePenalty = Math.Abs((long)template.EventIndex - (long)predictedEvent) * 0.20; + var timePenalty = Math.Abs((long)template.EventIndex - (long)predictedEvent) * 3.0; return (Template: template, Distance: distance + timePenalty); }) .OrderBy(pair => pair.Distance) @@ -1985,7 +2017,10 @@ static IReadOnlyList DecodeCepstralIndexedWordsCore( var refinedOffset = refineOffset == null ? offset : refineOffset(offset, best.Template.EventIndex, motifSamples); - observations.Add(new CepstralWordObservation(best.Template.EventIndex, refinedOffset, confidence)); + var shapeAccuracy = double.IsFinite(best.Distance) + ? 1.0 / (1.0 + best.Distance / 4.0) + : 0.0; + observations.Add(new CepstralWordObservation(best.Template.EventIndex, refinedOffset, confidence, best.Template.PayloadSymbol, shapeAccuracy)); } return observations @@ -2093,7 +2128,7 @@ static CepstralWordIndex BuildCepstralWordIndex(int sampleRate, CepstralDecoderO var templateSamples = setting.BlurPasses == 0 && setting.WarpFrames == 0.0 && setting.WarpCoefficients == 0.0 ? samples : RoundTripThroughDegradedCepstrum(samples, sampleRate, setting, out _); - templates.Add(new CepstralWordTemplate(eventIndex, CepstralFingerprintWithOptions(templateSamples, sampleRate, options))); + templates.Add(new CepstralWordTemplate(eventIndex, 0, CepstralFingerprintWithOptions(templateSamples, sampleRate, options))); } } @@ -2114,7 +2149,7 @@ static CepstralWordIndex BuildCepstralContestantWordIndex( var templateSamples = setting.BlurPasses == 0 && setting.WarpFrames == 0.0 && setting.WarpCoefficients == 0.0 ? samples : RoundTripThroughDegradedCepstrum(samples, sampleRate, setting, out _); - templates.Add(new CepstralWordTemplate(eventIndex, CepstralFingerprintWithOptions(templateSamples, sampleRate, options))); + templates.Add(new CepstralWordTemplate(eventIndex, contestant.PayloadSymbolForEvent(eventIndex), CepstralFingerprintWithOptions(templateSamples, sampleRate, options))); } } @@ -2260,6 +2295,28 @@ static MimirBioacousticClockHypothesis? FitContestantClockHypothesis( return null; } + if (anchors.Length >= 3) + { + var delayHypothesis = WeightedMedian(anchors + .Select(anchor => ( + Value: anchor.SampleOffset - anchor.TimelineSeconds * sampleRate, + Weight: Math.Clamp(anchor.Confidence, 0.001, 1.0))) + .ToArray()); + var residuals = anchors + .Select(anchor => Math.Abs(anchor.SampleOffset - (delayHypothesis + anchor.TimelineSeconds * sampleRate))) + .Order() + .ToArray(); + var medianResidual = residuals[residuals.Length / 2]; + var inlierRadius = Math.Max(sampleRate * 0.003, medianResidual * 3.0 + sampleRate * 0.00025); + var inliers = anchors + .Where(anchor => Math.Abs(anchor.SampleOffset - (delayHypothesis + anchor.TimelineSeconds * sampleRate)) <= inlierRadius) + .ToArray(); + if (inliers.Length >= 2) + { + anchors = inliers; + } + } + var totalWeight = anchors.Sum(anchor => Math.Max(1.0e-6, anchor.Confidence)); var meanTimeline = anchors.Sum(anchor => anchor.TimelineSeconds * Math.Max(1.0e-6, anchor.Confidence)) / totalWeight; var meanSample = anchors.Sum(anchor => anchor.SampleOffset * Math.Max(1.0e-6, anchor.Confidence)) / totalWeight; @@ -2302,6 +2359,30 @@ static MimirBioacousticClockHypothesis? FitContestantClockHypothesis( anchors); } +static double WeightedMedian(IReadOnlyList<(double Value, double Weight)> values) +{ + if (values.Count == 0) + { + return 0.0; + } + + var ordered = values + .OrderBy(value => value.Value) + .ToArray(); + var half = ordered.Sum(value => Math.Max(0.0, value.Weight)) * 0.5; + var cumulative = 0.0; + foreach (var value in ordered) + { + cumulative += Math.Max(0.0, value.Weight); + if (cumulative >= half) + { + return value.Value; + } + } + + return ordered[^1].Value; +} + static double CepstralDistance(IReadOnlyList first, IReadOnlyList second) { var sum = 0.0; @@ -2936,12 +3017,15 @@ internal sealed record CepstralRoundTripAnalysis( internal sealed record CepstralWordTemplate( ulong EventIndex, + int PayloadSymbol, IReadOnlyList Feature); internal sealed record CepstralWordObservation( ulong EventIndex, double SampleOffset, - double Confidence); + double Confidence, + int PayloadSymbol, + double ShapeAccuracy); internal sealed record CepstralWordIndex( IReadOnlyList Templates, @@ -3035,11 +3119,22 @@ public sealed record BioacousticContestantResult( double Confidence, double TimingAccuracy, double FrequencyAccuracy, + double PayloadAccuracy, double Convergence, double RealtimeFactor, + double PayloadBitrate, + double LanguageScore, double ContestScore, double DecodeMilliseconds, int RoundTripMelBins, int RoundTripCepstralCoefficients, BioacousticClockHypothesisSnapshot ClockHypothesis, + BioacousticContestantObservation[] Observations, string BeautyNotes); + +public sealed record BioacousticContestantObservation( + ulong EventIndex, + int PayloadSymbol, + double SampleOffset, + double Confidence, + double ShapeAccuracy); diff --git a/src/Mimir.Runtime/Synchronization/MimirBioacousticContestant.cs b/src/Mimir.Runtime/Synchronization/MimirBioacousticContestant.cs index 6b1467d..93294da 100644 --- a/src/Mimir.Runtime/Synchronization/MimirBioacousticContestant.cs +++ b/src/Mimir.Runtime/Synchronization/MimirBioacousticContestant.cs @@ -7,7 +7,10 @@ public enum MimirBioacousticContestantKind RobinWarble, ThrushLadder, ThornbillZigZag, - NightingaleCascade + NightingaleCascade, + AquaSynthFormantWeaver, + CanaryPacketTrill, + FinchBurstPacket } public sealed record MimirBioacousticContestantProfile( @@ -20,6 +23,7 @@ public sealed record MimirBioacousticContestantProfile( double LowestRootHz, double HighestRootHz, double Gain, + int PayloadBitsPerEvent, string BeautyNotes); public static class MimirBioacousticContestants @@ -39,6 +43,7 @@ public static class MimirBioacousticContestants LowestRootHz: 2_600.0, HighestRootHz: 9_600.0, Gain: 0.030, + PayloadBitsPerEvent: 4, "Musical enough, but still sounds like a lab bird with a clipboard."); public static MimirBioacousticContestantProfile RedpollTrill { get; } = new( @@ -51,6 +56,7 @@ public static class MimirBioacousticContestants LowestRootHz: 3_200.0, HighestRootHz: 10_800.0, Gain: 0.025, + PayloadBitsPerEvent: 5, "Pretty, bright, and temporally sharp; risks weak identity if the trills blur together."); public static MimirBioacousticContestantProfile RobinWarble { get; } = new( @@ -63,6 +69,7 @@ public static class MimirBioacousticContestants LowestRootHz: 2_200.0, HighestRootHz: 8_600.0, Gain: 0.024, + PayloadBitsPerEvent: 6, "Most plausible room sound; slower lock and softer onsets are the price."); public static MimirBioacousticContestantProfile ThrushLadder { get; } = new( @@ -75,6 +82,7 @@ public static class MimirBioacousticContestants LowestRootHz: 2_400.0, HighestRootHz: 9_200.0, Gain: 0.027, + PayloadBitsPerEvent: 6, "Good identity geometry: repetition plus interval ratios. Less subtle."); public static MimirBioacousticContestantProfile ThornbillZigZag { get; } = new( @@ -87,6 +95,7 @@ public static class MimirBioacousticContestants LowestRootHz: 4_200.0, HighestRootHz: 13_200.0, Gain: 0.021, + PayloadBitsPerEvent: 7, "Excellent frequency fingerprint if the path keeps high band alive; fragile on dull mics."); public static MimirBioacousticContestantProfile NightingaleCascade { get; } = new( @@ -99,8 +108,48 @@ public static class MimirBioacousticContestants LowestRootHz: 1_800.0, HighestRootHz: 11_200.0, Gain: 0.023, + PayloadBitsPerEvent: 8, "The prettiest contestant; expensive and easiest to confuse under heavy smearing."); + public static MimirBioacousticContestantProfile AquaSynthFormantWeaver { get; } = new( + "aquasynth-formant-weaver", + "AquaSynth-inspired formant/FM word: two timing syllables, payload ornaments, and a final parity flourish.", + MimirBioacousticContestantKind.AquaSynthFormantWeaver, + SyllableCount: 7, + MotifDurationSeconds: 0.136, + EventSpacingSeconds: 0.168, + LowestRootHz: 2_050.0, + HighestRootHz: 9_800.0, + Gain: 0.026, + PayloadBitsPerEvent: 8, + "Less literal bird imitation, more compact singing modem: readable envelopes, formants, FM motion, and bit-bearing ornaments."); + + public static MimirBioacousticContestantProfile CanaryPacketTrill { get; } = new( + "canary-packet-trill", + "Short bright packet song: two hard timing chips plus four payload-colored formant notes.", + MimirBioacousticContestantKind.CanaryPacketTrill, + SyllableCount: 6, + MotifDurationSeconds: 0.096, + EventSpacingSeconds: 0.125, + LowestRootHz: 2_800.0, + HighestRootHz: 10_600.0, + Gain: 0.024, + PayloadBitsPerEvent: 8, + "A higher-throughput singing modem candidate: compact, bright, and less apologetic about being a packet."); + + public static MimirBioacousticContestantProfile FinchBurstPacket { get; } = new( + "finch-burst-packet", + "Very short five-syllable burst packet with hard onset chips and payload-bent middle notes.", + MimirBioacousticContestantKind.FinchBurstPacket, + SyllableCount: 5, + MotifDurationSeconds: 0.078, + EventSpacingSeconds: 0.105, + LowestRootHz: 3_050.0, + HighestRootHz: 11_400.0, + Gain: 0.023, + PayloadBitsPerEvent: 8, + "Throughput brute made musical enough to tolerate: fewer syllables, sharper anchors, and more packets per second."); + public static IReadOnlyList BuiltIn { get; } = [ CurrentBirdcall, @@ -108,7 +157,10 @@ public static class MimirBioacousticContestants RobinWarble, ThrushLadder, ThornbillZigZag, - NightingaleCascade + NightingaleCascade, + AquaSynthFormantWeaver, + CanaryPacketTrill, + FinchBurstPacket ]; } @@ -139,19 +191,26 @@ public sealed class MimirBioacousticContestantRenderer(MimirBioacousticContestan } public int ExpectedEventCount(double seconds) => - Math.Max(0, (int)Math.Floor((seconds - MimirBioacousticContestants.FirstEventSeconds) / Profile.EventSpacingSeconds) + 1); + Math.Max(0, (int)Math.Floor((seconds - MimirBioacousticContestants.FirstEventSeconds - Profile.MotifDurationSeconds) / Profile.EventSpacingSeconds) + 1); public IReadOnlySet ExpectedEvents(double seconds) => Enumerable.Range(0, ExpectedEventCount(seconds)).Select(index => (ulong)index).ToHashSet(); + public int PayloadSymbolForEvent(ulong eventIndex) + { + var mask = (1 << Math.Clamp(Profile.PayloadBitsPerEvent, 0, 15)) - 1; + return mask == 0 ? 0 : (int)(Mix((int)(eventIndex & 0x7fff), (uint)Profile.Kind + 0xC0DEu) & (uint)mask); + } + private void AddEvent(float[] output, int sampleRate, ulong eventIndex, double eventStartSeconds, double bufferStartSeconds) { var symbolId = SymbolForEvent(eventIndex); + var payload = PayloadSymbolForEvent(eventIndex); var root = RootForSymbol(symbolId); - var rng = Mix(symbolId, (uint)Profile.Kind); + var rng = Mix(symbolId ^ (payload << 9), (uint)Profile.Kind); for (var syllable = 0; syllable < Profile.SyllableCount; syllable++) { - var contour = SyllableContour(symbolId, syllable, root, rng); + var contour = SyllableContour(symbolId, payload, syllable, root, rng); var start = eventStartSeconds - bufferStartSeconds + contour.StartSeconds; AddSyllable(output, sampleRate, start, contour.DurationSeconds, contour.StartHz, contour.EndHz, Profile.Gain * contour.Weight); } @@ -159,12 +218,15 @@ public sealed class MimirBioacousticContestantRenderer(MimirBioacousticContestan private (double StartSeconds, double DurationSeconds, double StartHz, double EndHz, double Weight) SyllableContour( int symbolId, + int payload, int syllable, double root, uint rng) { var jitter = (((rng >> (syllable % 12)) & 7) - 3) * 0.0013; var phase = ((symbolId * 17 + syllable * 31) & 255) / 255.0; + var payloadPhase = ((payload * 29 + syllable * 47) & 255) / 255.0; + var payloadStep = ((payload >> (syllable % Math.Max(1, Profile.PayloadBitsPerEvent))) & 7) - 3; return Profile.Kind switch { MimirBioacousticContestantKind.RedpollTrill => ( @@ -197,6 +259,27 @@ public sealed class MimirBioacousticContestantRenderer(MimirBioacousticContestan StartHz: root * Ratio(0.62 + 0.09 * ((symbolId + syllable) % 8)), EndHz: root * Ratio(1.20 - 0.07 * ((symbolId / 3 + syllable) % 7)), Weight: syllable is 2 or 5 or 8 ? 0.95 : 0.58), + MimirBioacousticContestantKind.AquaSynthFormantWeaver => ( + StartSeconds: syllable * Profile.MotifDurationSeconds / (Profile.SyllableCount + 0.45) + + 0.0022 * Math.Sin((payloadPhase + syllable * 0.11) * Math.Tau), + DurationSeconds: Profile.MotifDurationSeconds * (0.088 + 0.018 * ((payload + syllable) & 3)), + StartHz: root * Ratio(-2.5 + 0.72 * syllable + payloadStep * 0.21 + 0.36 * Math.Sin(payloadPhase * Math.Tau)), + EndHz: root * Ratio(-1.4 + 0.58 * ((syllable + 2) % Profile.SyllableCount) - payloadStep * 0.18 + 0.42 * Math.Cos(payloadPhase * Math.Tau)), + Weight: syllable is 0 or 3 or 6 ? 0.94 : 0.56 + 0.22 * Math.Sin(payloadPhase * Math.Tau) * Math.Sin(payloadPhase * Math.Tau)), + MimirBioacousticContestantKind.CanaryPacketTrill => ( + StartSeconds: syllable * Profile.MotifDurationSeconds / (Profile.SyllableCount + 0.75) + + (syllable < 2 ? 0.0 : 0.0014 * Math.Sin(payloadPhase * Math.Tau)), + DurationSeconds: Profile.MotifDurationSeconds * (syllable < 2 ? 0.070 : 0.086 + 0.010 * ((payload >> syllable) & 1)), + StartHz: root * Ratio((syllable < 2 ? -1.0 + syllable * 1.85 : -0.55 + syllable * 0.52) + payloadStep * 0.24), + EndHz: root * Ratio((syllable < 2 ? 1.25 + syllable * 1.35 : 0.10 + ((payload + syllable) & 7) * 0.19) - payloadStep * 0.16), + Weight: syllable < 2 ? 1.0 : 0.66 + 0.18 * (((payload >> syllable) & 1) == 0 ? 0.0 : 1.0)), + MimirBioacousticContestantKind.FinchBurstPacket => ( + StartSeconds: syllable * Profile.MotifDurationSeconds / (Profile.SyllableCount + 0.65) + + (syllable is 0 or 1 ? 0.0 : 0.0011 * Math.Sin((payloadPhase + 0.17) * Math.Tau)), + DurationSeconds: Profile.MotifDurationSeconds * (syllable is 0 or 1 ? 0.064 : 0.078 + 0.008 * ((payload >> syllable) & 1)), + StartHz: root * Ratio(-0.70 + syllable * 0.92 + payloadStep * 0.20 + 0.18 * Math.Sin(payloadPhase * Math.Tau)), + EndHz: root * Ratio(0.95 + syllable * 0.44 - payloadStep * 0.14 + 0.22 * Math.Cos(payloadPhase * Math.Tau)), + Weight: syllable is 0 or 1 ? 1.0 : 0.70 + 0.16 * (((payload >> (syllable + 2)) & 1) == 0 ? 0.0 : 1.0)), _ => ( StartSeconds: syllable * Profile.MotifDurationSeconds / (Profile.SyllableCount + 0.6) + jitter, DurationSeconds: Profile.MotifDurationSeconds * 0.11, diff --git a/src/Mimir.Runtime/Synchronization/MimirBioacousticDecoderConfiguration.cs b/src/Mimir.Runtime/Synchronization/MimirBioacousticDecoderConfiguration.cs index f26e1b2..861ddae 100644 --- a/src/Mimir.Runtime/Synchronization/MimirBioacousticDecoderConfiguration.cs +++ b/src/Mimir.Runtime/Synchronization/MimirBioacousticDecoderConfiguration.cs @@ -65,6 +65,69 @@ public sealed record MimirBioacousticDecoderConfiguration( CepstralDegradationProfiles.WarpBlur ]); + public static MimirBioacousticDecoderConfiguration PacketSprintIndex { get; } = new( + "packet-sprint-index", + "Smallest packet-song receiver profile: fewer mel bins, fewer projections, and a tighter proposal budget for language-score throughput pressure.", + FftSize: 1024, + HopSize: 256, + MelBins: 24, + CepstralCoefficients: 8, + MinFrequencyHz: 600.0, + MaxFrequencyHz: 14_500.0, + ProjectionTableCount: 2, + ProjectionHashBits: 10, + NearHashRadius: 2, + DenseStepSeconds: 0.050, + ProposalBudgetMultiplier: 4.0, + TemplateAugmentations: + [ + CepstralDegradationProfiles.Clean, + CepstralDegradationProfiles.Blur, + CepstralDegradationProfiles.WarpBlur + ]); + + public static MimirBioacousticDecoderConfiguration PacketNeedleIndex { get; } = new( + "packet-needle-index", + "Tiny fast receiver for short packet-song words; tests whether smaller FFT/features beat the loss in frequency identity.", + FftSize: 512, + HopSize: 128, + MelBins: 20, + CepstralCoefficients: 7, + MinFrequencyHz: 700.0, + MaxFrequencyHz: 14_500.0, + ProjectionTableCount: 2, + ProjectionHashBits: 9, + NearHashRadius: 2, + DenseStepSeconds: 0.060, + ProposalBudgetMultiplier: 3.0, + TemplateAugmentations: + [ + CepstralDegradationProfiles.Clean, + CepstralDegradationProfiles.Blur, + CepstralDegradationProfiles.WarpBlur + ]); + + public static MimirBioacousticDecoderConfiguration PacketRazorIndex { get; } = new( + "packet-razor-index", + "Aggressive speed floor for packet-song decoding; intentionally risks identity for realtime multiplier evidence.", + FftSize: 512, + HopSize: 128, + MelBins: 16, + CepstralCoefficients: 6, + MinFrequencyHz: 850.0, + MaxFrequencyHz: 14_000.0, + ProjectionTableCount: 1, + ProjectionHashBits: 8, + NearHashRadius: 1, + DenseStepSeconds: 0.075, + ProposalBudgetMultiplier: 2.25, + TemplateAugmentations: + [ + CepstralDegradationProfiles.Clean, + CepstralDegradationProfiles.WarpLight, + CepstralDegradationProfiles.WarpBlur + ]); + public static MimirBioacousticDecoderConfiguration RobustWideIndex { get; } = new( "robust-wide-index", "Wider cepstral and augmentation surface for path-damaged acoustic evidence.", @@ -114,6 +177,9 @@ public sealed record MimirBioacousticDecoderConfiguration( [ BaselineMfccIndex, CompactFastIndex, + PacketSprintIndex, + PacketNeedleIndex, + PacketRazorIndex, RobustWideIndex, HighbandRoomIndex ];