Improve bioacoustic language scoring

This commit is contained in:
Meta Vens 2026-05-24 20:07:15 +01:00
commit b3117bb2e9
4 changed files with 387 additions and 25 deletions

View file

@ -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

View file

@ -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<int> 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<BioacousticContestantResult>();
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<int> 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<int> 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<int> RunBioacousticContestantsAsync(
confidence,
timingAccuracy,
frequencyAccuracy,
payloadAccuracy,
convergence,
realtime,
payloadBitrate,
languageScore,
contestScore,
decodeMs,
analysis.MelBins,
@ -799,10 +825,16 @@ static async Task<int> 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<int> 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<CepstralWordObservation> 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<CepstralWordObservation> 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<double> first, IReadOnlyList<double> second)
{
var sum = 0.0;
@ -2936,12 +3017,15 @@ internal sealed record CepstralRoundTripAnalysis(
internal sealed record CepstralWordTemplate(
ulong EventIndex,
int PayloadSymbol,
IReadOnlyList<double> Feature);
internal sealed record CepstralWordObservation(
ulong EventIndex,
double SampleOffset,
double Confidence);
double Confidence,
int PayloadSymbol,
double ShapeAccuracy);
internal sealed record CepstralWordIndex(
IReadOnlyList<CepstralWordTemplate> 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);

View file

@ -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<MimirBioacousticContestantProfile> 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<ulong> 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,

View file

@ -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
];