71 lines
5.7 KiB
PowerShell
71 lines
5.7 KiB
PowerShell
param([Parameter(Mandatory)][string]$OutputPath,[int]$Seconds=15,[string]$Serial='emulator-5554')
|
|
$ErrorActionPreference='Stop'
|
|
# 검사용 Android AudioRecord REMOTE_SUBMIX 원시 PCM. 서버/도구는 APK에 포함하지 않습니다.
|
|
$version='4.1';$zipSha='5b12172b3264b2889f4583ee64752ce832e29bc8b1089dca81093459697165db'
|
|
$source="https://github.com/Genymobile/scrcpy/releases/download/v$version/scrcpy-win64-v$version.zip"
|
|
$root=Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
|
$cache=Join-Path $root "artifacts/tools/scrcpy-v$version"
|
|
New-Item -ItemType Directory -Path $cache -Force | Out-Null
|
|
$zip=Join-Path $cache "scrcpy-win64-v$version.zip"
|
|
if (!(Test-Path -LiteralPath $zip)) { Invoke-WebRequest -Uri $source -OutFile $zip }
|
|
if ((Get-FileHash -LiteralPath $zip).Hash.ToLowerInvariant() -ne $zipSha) { throw 'scrcpy 공식 ZIP SHA 불일치' }
|
|
$toolDir=Join-Path $cache "scrcpy-win64-v$version";$exe=Join-Path $toolDir 'scrcpy.exe'
|
|
if (!(Test-Path -LiteralPath $exe)) { Expand-Archive -LiteralPath $zip -DestinationPath $cache }
|
|
$OutputPath=[IO.Path]::GetFullPath($OutputPath);$stereo=$OutputPath+'.stereo.wav'
|
|
$arguments=@("--serial=$Serial",'--no-window','--no-video','--no-control','--no-playback','--audio-codec=raw','--audio-source=output',"--record=$stereo","--time-limit=$Seconds",'--require-audio')
|
|
$record=[ordered]@{method='Android AudioRecord REMOTE_SUBMIX via scrcpy RAW';sourceCommit=(& git rev-parse HEAD);sourceDirty=[bool](& git status --porcelain);startedUtc=[DateTime]::UtcNow.ToString('o');serial=$Serial;toolVersion=$version;toolSource=$source;toolZipSha256=$zipSha;toolExecutableSha256=(Get-FileHash $exe).Hash;serverSha256=(Get-FileHash (Join-Path $toolDir 'scrcpy-server')).Hash;arguments=$arguments;auditoryReview=$false}
|
|
$oldAdb=$env:ADB
|
|
try {
|
|
$env:ADB=Join-Path $env:LOCALAPPDATA 'Android/Sdk/platform-tools/adb.exe'
|
|
$process=Start-Process $exe -ArgumentList ($arguments | ForEach-Object {'"'+$_+'"'}) -WindowStyle Hidden -RedirectStandardOutput ($OutputPath+'.stdout') -RedirectStandardError ($OutputPath+'.stderr') -PassThru
|
|
$record.recorderPid=$process.Id
|
|
$ready=$false
|
|
for ($attempt=0; $attempt -lt 100; $attempt++) {
|
|
$process.Refresh()
|
|
if ($process.HasExited) { break }
|
|
if ((Get-Content -LiteralPath ($OutputPath+'.stdout') -Raw -ErrorAction SilentlyContinue) -match 'Recording started to wav file:') {
|
|
$record.readyUtc=[DateTime]::UtcNow.ToString('o')
|
|
@{recorderPid=$process.Id;readyUtc=$record.readyUtc;method=$record.method} | ConvertTo-Json | Set-Content -LiteralPath ($OutputPath+'.ready.json') -Encoding utf8
|
|
$ready=$true; break
|
|
}
|
|
Start-Sleep -Milliseconds 100
|
|
}
|
|
if (!$ready) { if (!$process.HasExited) { $process.Kill(); $process.WaitForExit() }; throw 'Android PCM recorder did not report recording started' }
|
|
if (!$process.WaitForExit(($Seconds+20)*1000)) { $process.Kill();$process.WaitForExit();throw 'Android PCM 녹음 종료 시간 초과' }
|
|
$process.Refresh();$record.exitCode=$process.ExitCode
|
|
if ($process.ExitCode -ne 0) { throw "Android PCM recorder exit $($process.ExitCode)" }
|
|
Add-Type -TypeDefinition @'
|
|
using System;
|
|
using System.IO;
|
|
public static class AndroidNativePcm {
|
|
public static double[] Mono(string input,string output) {
|
|
short[] stereo=null;int rate=0;bool format=false;
|
|
using(var r=new BinaryReader(File.OpenRead(input))) {
|
|
if(new string(r.ReadChars(4))!="RIFF")throw new Exception("RIFF required");r.ReadInt32();if(new string(r.ReadChars(4))!="WAVE")throw new Exception("WAVE required");
|
|
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();int bits=r.ReadInt16();format=encoding==1&&channels==2&&bits==16&&rate==48000;}
|
|
if(id=="data") {if(!format||size%4!=0)throw new Exception("Actual stereo48k PCM16 required");stereo=new short[size/2];for(int i=0;i<stereo.Length;i++)stereo[i]=r.ReadInt16();break;}
|
|
r.BaseStream.Position=end+(size%2);
|
|
}
|
|
}
|
|
if(stereo==null||stereo.Length==0)throw new Exception("Actual PCM samples required");
|
|
double energy=0;int peak=0,active=0;int count=stereo.Length/2;
|
|
using(var w=new BinaryWriter(File.Create(output))) {
|
|
w.Write(System.Text.Encoding.ASCII.GetBytes("RIFF"));w.Write(36+count*2);w.Write(System.Text.Encoding.ASCII.GetBytes("WAVEfmt "));w.Write(16);w.Write((short)1);w.Write((short)1);w.Write(rate);w.Write(rate*2);w.Write((short)2);w.Write((short)16);w.Write(System.Text.Encoding.ASCII.GetBytes("data"));w.Write(count*2);
|
|
for(int i=0;i<stereo.Length;i+=2) {short value=(short)(((int)stereo[i]+stereo[i+1])/2);w.Write(value);energy+=(double)value*value;peak=Math.Max(peak,Math.Abs((int)value));if(Math.Abs((int)value)>32)active++;}
|
|
}
|
|
return new[]{rate,(double)count,peak,Math.Sqrt(energy/count),(double)active};
|
|
}
|
|
}
|
|
'@
|
|
$levels=[AndroidNativePcm]::Mono($stereo,$OutputPath)
|
|
$record.sampleRate=$levels[0];$record.channels=1;$record.bitsPerSample=16;$record.samples=$levels[1];$record.durationSeconds=$levels[1]/$levels[0];$record.peakPcm=$levels[2];$record.rmsPcm=$levels[3];$record.nonSilentSamples=$levels[4]
|
|
$record.stereoPath=$stereo;$record.stereoSha256=(Get-FileHash $stereo).Hash;$record.monoSha256=(Get-FileHash $OutputPath).Hash;$record.monoConversion='Each PCM16 stereo frame: (left+right)/2, integer division; original48k rate retained'
|
|
if ($levels[2] -ge 32767) { throw 'Android PCM clipping detected' }
|
|
} finally {
|
|
$env:ADB=$oldAdb;$record.finishedUtc=[DateTime]::UtcNow.ToString('o');$record.sourceCommitEnd=(& git rev-parse HEAD);$record.sourceDirtyEnd=[bool](& git status --porcelain)
|
|
$record | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath ($OutputPath+'.json') -Encoding utf8
|
|
}
|
|
Write-Host "Android 원시 PCM $($record.samples) 샘플: $OutputPath"
|