Improve bioacoustic language scoring
This commit is contained in:
parent
ecc5231af9
commit
b3117bb2e9
4 changed files with 387 additions and 25 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
];
|
||||
|
|
|
|||
Loading…
Reference in a new issue