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

90 lines
5.7 KiB
PowerShell

param([Parameter(Mandatory)][string]$OutputPath, [int]$Seconds=15, [int]$Port=8554)
$ErrorActionPreference = 'Stop'
Add-Type -TypeDefinition @'
using System;
public static class AndroidPcmLevels {
public static double[] Measure(byte[] bytes) {
double sum=0, peak=0; int active=0;
for(int i=0;i+1<bytes.Length;i+=2) { int sample=(short)(bytes[i] | (bytes[i+1]<<8)); sum+=(double)sample*sample; peak=Math.Max(peak,Math.Abs(sample)); if(Math.Abs(sample)>32) active++; }
return new[]{peak, bytes.Length>0 ? Math.Sqrt(sum/(bytes.Length/2)) : 0, (double)active};
}
}
'@
# EmulatorController.streamAudio의 PCM을 직접 기록합니다. 제품 APK에는 포함되지 않습니다.
# wire schema: Android SDK emulator/lib/emulator_controller.proto.
[AppContext]::SetSwitch('System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport', $true)
$client = [Net.Http.HttpClient]::new()
$request = [Net.Http.HttpRequestMessage]::new([Net.Http.HttpMethod]::Post, "http://127.0.0.1:$Port/android.emulation.control.EmulatorController/streamAudio")
$request.Version = [Version]::new(2,0)
$request.VersionPolicy = [Net.Http.HttpVersionPolicy]::RequestVersionExact
$request.Headers.Add('TE','trailers')
$discovery = Get-ChildItem -LiteralPath (Join-Path $env:LOCALAPPDATA 'Temp/avd/running') -Filter 'pid_*.ini' | Sort-Object LastWriteTime -Descending | Select-Object -First 1
if (!$discovery) { throw 'AVD gRPC discovery 파일을 찾지 못했습니다.' }
$tokenLine = Get-Content -LiteralPath $discovery.FullName | Where-Object { $_ -match '^grpc.token=' } | Select-Object -First 1
if (!$tokenLine) { throw 'AVD를 -grpc-use-token으로 시작해야 합니다. 토큰을 로그에 기록하지 않습니다.' }
$request.Headers.Authorization = [Net.Http.Headers.AuthenticationHeaderValue]::new('Bearer', $tokenLine.Substring('grpc.token='.Length))
# AudioFormat: samplingRate=24000, channels=Mono(default), format=AUD_FMT_S16.
$request.Content = [Net.Http.ByteArrayContent]::new([byte[]]@(0,0,0,0,6,8,192,187,1,24,1))
$request.Content.Headers.ContentType = [Net.Http.Headers.MediaTypeHeaderValue]::new('application/grpc')
$cancel = [Threading.CancellationTokenSource]::new([TimeSpan]::FromSeconds($Seconds))
$pcm = [IO.MemoryStream]::new()
$packets = 0
$startedUtc=[DateTime]::UtcNow.ToString('o')
$firstPacketTimestamp=$null; $lastPacketTimestamp=$null
function Read-Exactly($Stream,[int]$Length) {
$bytes = [byte[]]::new($Length)
$offset=0
while ($offset -lt $Length) {
$n=$Stream.ReadAsync($bytes,$offset,$Length-$offset,$cancel.Token).GetAwaiter().GetResult()
if ($n -eq 0) { throw 'gRPC 오디오 스트림이 끝났습니다.' }
$offset += $n
}
return ,$bytes
}
function Read-Varint([byte[]]$Bytes,[ref]$Index) {
[long]$value=0; $shift=0
do { $b=$Bytes[$Index.Value]; $Index.Value++; $value=$value -bor (($b -band 127) -shl $shift); $shift+=7 } while ($b -band 128)
return $value
}
$status='failed'
try {
$response=$client.SendAsync($request,[Net.Http.HttpCompletionOption]::ResponseHeadersRead,$cancel.Token).GetAwaiter().GetResult()
$response.EnsureSuccessStatusCode() | Out-Null
$stream=$response.Content.ReadAsStream()
while (!$cancel.IsCancellationRequested) {
$header=Read-Exactly $stream 5
if ($header[0] -ne 0) { throw '압축 gRPC 프레임은 지원하지 않습니다.' }
$length=([int]$header[1] -shl 24) -bor ([int]$header[2] -shl 16) -bor ([int]$header[3] -shl 8) -bor [int]$header[4]
$message=Read-Exactly $stream $length
$i=0
while ($i -lt $message.Length) {
$tag=Read-Varint $message ([ref]$i)
switch ($tag -band 7) {
0 { $value=Read-Varint $message ([ref]$i); if (($tag -shr 3) -eq 2) { if ($null -eq $firstPacketTimestamp) { $firstPacketTimestamp=$value }; $lastPacketTimestamp=$value } }
2 {
$size=Read-Varint $message ([ref]$i)
if (($tag -shr 3) -eq 3) { $pcm.Write($message,$i,$size); $packets++ }
$i += $size
}
default { throw "예상하지 않은 protobuf wire type: $tag" }
}
}
}
} catch {
if (!$cancel.IsCancellationRequested) { throw }
} finally {
$data=$pcm.ToArray()
$levels=[AndroidPcmLevels]::Measure($data)
$file=[IO.File]::Create($OutputPath)
$writer=[IO.BinaryWriter]::new($file)
try {
$writer.Write([Text.Encoding]::ASCII.GetBytes('RIFF')); $writer.Write([int](36+$data.Length)); $writer.Write([Text.Encoding]::ASCII.GetBytes('WAVEfmt '));
$writer.Write([int]16); $writer.Write([int16]1); $writer.Write([int16]1); $writer.Write([int]24000); $writer.Write([int]48000); $writer.Write([int16]2); $writer.Write([int16]16);
$writer.Write([Text.Encoding]::ASCII.GetBytes('data')); $writer.Write([int]$data.Length); $writer.Write($data)
} finally { $writer.Dispose(); $file.Dispose(); $client.Dispose(); $cancel.Dispose(); $pcm.Dispose() }
[ordered]@{ method='EmulatorController.streamAudio'; endpoint="127.0.0.1:$Port"; sourceCommit=(& git rev-parse HEAD); sourceDirty=[bool](& git status --porcelain); startedUtc=$startedUtc; finishedUtc=[DateTime]::UtcNow.ToString('o'); firstDevicePacketTimestampUs=$firstPacketTimestamp; lastDevicePacketTimestampUs=$lastPacketTimestamp; packets=$packets; pcmBytes=$data.Length; sampleRate=24000; channels=1; bitsPerSample=16; peakPcm=$levels[0]; rmsPcm=$levels[1]; nonSilentSamples=$levels[2]; auditoryReview=$false } |
ConvertTo-Json | Set-Content -LiteralPath ($OutputPath + '.json') -Encoding utf8
}
if ($packets -eq 0) { throw '실제 오디오 패킷을 받지 못했습니다.' }
Write-Host "에뮬레이터 실제 PCM $packets 패킷, $($data.Length)바이트: $OutputPath"