Files
MagnetismHeroes/tools/android/compare-audio.ps1
T

59 lines
4.2 KiB
PowerShell

param([Parameter(Mandatory)][string]$ReferencePath, [Parameter(Mandatory)][string]$CapturedPath, [Parameter(Mandatory)][string]$OutputPath)
$ErrorActionPreference='Stop'
Add-Type -TypeDefinition @'
using System;
using System.IO;
public static class AndroidPcmComparison {
static short[] Read(string path) {
using(var r=new BinaryReader(File.OpenRead(path))) {
if(new string(r.ReadChars(4))!="RIFF") throw new Exception("RIFF WAV required");
r.ReadInt32(); if(new string(r.ReadChars(4))!="WAVE") throw new Exception("WAVE required");
bool format=false; int rate=0;
while(r.BaseStream.Position+8<=r.BaseStream.Length) {
string id=new string(r.ReadChars(4)); int size=r.ReadInt32(); long end=r.BaseStream.Position+size;
if(id=="fmt ") { int encoding=r.ReadInt16(),channels=r.ReadInt16(); rate=r.ReadInt32(); r.ReadInt32(); r.ReadInt16(); format=encoding==1 && channels==1 && r.ReadInt16()==16 && rate>0; }
if(id=="data") {
if(!format) throw new Exception("Mono PCM16 required");
var data=new short[size/2]; for(int i=0;i<data.Length;i++) data[i]=r.ReadInt16();
if(rate==24000) return data;
var resampled=new short[(int)((long)data.Length*24000/rate)];
for(int i=0;i<resampled.Length;i++) { double index=(double)i*rate/24000; int left=(int)index; double fraction=index-left; resampled[i]=(short)(data[left]*(1-fraction)+data[Math.Min(left+1,data.Length-1)]*fraction); }
return resampled;
}
r.BaseStream.Position=end+(size%2);
}
}
throw new Exception("WAV data missing");
}
public static double[] Compare(string original,string captured) {
var source=Read(original); var output=Read(captured); int length=Math.Min(4096,source.Length);
double bestEnergy=-1; int anchor=0;
for(int offset=0;offset+length<=source.Length;offset+=512) {
double energy=0; for(int j=0;j<length;j++) energy+=(double)source[offset+j]*source[offset+j];
if(energy>bestEnergy) { bestEnergy=energy; anchor=offset; }
}
if(bestEnergy<=0 || output.Length<source.Length) throw new Exception("Non-silent complete reference/capture required");
double sourceAnchorEnergy=0; for(int j=0;j<length;j+=8) sourceAnchorEnergy+=(double)source[anchor+j]*source[anchor+j];
double best=-1; int bestLag=0;
for(int lag=0;lag+source.Length<=output.Length;lag++) {
double dot=0, energy=0;
for(int j=0;j<length;j+=8) { double value=output[lag+anchor+j]; dot+=source[anchor+j]*value; energy+=value*value; }
double correlation=energy>0 ? dot/Math.Sqrt(sourceAnchorEnergy*energy) : 0;
if(correlation>best) { best=correlation; bestLag=lag; }
}
double fullDot=0,sourceEnergy=0,outputEnergy=0; int nonzero=0;
for(int j=0;j<source.Length;j++) { double s=source[j],o=output[bestLag+j]; fullDot+=s*o; sourceEnergy+=s*s; outputEnergy+=o*o; if(Math.Abs(o)>32) nonzero++; }
double fullCorrelation=fullDot/Math.Sqrt(sourceEnergy*outputEnergy);
return new[]{fullCorrelation,bestLag/24000.0,source.Length/24000.0,output.Length/24000.0,Math.Sqrt(outputEnergy/source.Length),fullDot/sourceEnergy,(double)nonzero};
}
}
'@
$values=[AndroidPcmComparison]::Compare((Resolve-Path -LiteralPath $ReferencePath).Path,(Resolve-Path -LiteralPath $CapturedPath).Path)
$passed=$values[0] -ge 0.95 -and $values[4] -gt 32 -and $values[6] -gt 100
$record=[ordered]@{ reference=$ReferencePath; referenceSha256=(Get-FileHash -LiteralPath $ReferencePath -Algorithm SHA256).Hash;
captured=$CapturedPath; capturedSha256=(Get-FileHash -LiteralPath $CapturedPath -Algorithm SHA256).Hash;
normalizedCorrelation=$values[0]; onsetSeconds=$values[1]; referenceDurationSeconds=$values[2]; capturedDurationSeconds=$values[3]; rmsPcm=$values[4]; gain=$values[5]; nonSilentSamples=$values[6]; thresholdCorrelation=0.95; passed=$passed; auditoryReview=$false }
$record | ConvertTo-Json | Set-Content -LiteralPath $OutputPath -Encoding utf8
Write-Host "실제 PCM 대조: 상관 $($values[0]), 비무음 샘플 $($values[6]), 통과 $passed"
if (!$passed) { exit 1 }