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;i32)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"