← Docs hub

PCM WebSocket Recorder PoC

이 문서는 ~/work/20.PoC에 만든 Android PCM 녹음 PoC와 Cloudflare WebSocket relay를 실기기에서 검증하는 방법을 정리한다.

목표는 우리 디바이스에서 system/priv-app으로 동작하는 백그라운드 마이크 녹음 서비스가 16 kHz mono PCM16 chunk를 WebSocket으로 송신하고, 웹페이지에서 수신 상태를 확인하며, 웹페이지에서 보낸 TTS WAV 파일을 기기가 수신해 재생하는 것이다.

현재 앱은 서비스앱 기준으로 정리되어 있다. Launcher Activity는 Manifest에서 제거했고, 녹음 시작 트리거는 BOOT_COMPLETED 또는 명시적 foreground service start다.

PCM WebSocket Recorder Architecture

1. 현재 구현 위치

영역 경로 설명
Android PoC app /home/silogood/work/20.PoC/app foreground service 기반 PCM 녹음/송신 + WAV 수신/재생 앱
Cloudflare relay /home/silogood/work/20.PoC/cloudflare-pcm-relay Worker + Durable Object 양방향 WebSocket relay
PoC README /home/silogood/work/20.PoC/README.md 로컬 명령/업링크/다운링크 포맷 요약

주요 Android 파일:

파일 역할
BootCompletedReceiver.java 부팅 완료 후 기본 WebSocket URL로 녹음 서비스 자동 시작
PcmRecorderService.java foreground service, AudioRecord, PCM chunk 송신, WAV downlink 저장/재생
PcmWebSocketStreamer.java OkHttp WebSocket client, binary/text downlink callback
PcmRecorderConfig.java 기본 Worker WebSocket URL과 URL 유효성 판단
PcmFrameHeader.java PCM1 binary frame header 생성
PcmStreamConfig.java 16 kHz / mono / PCM16 / chunk size 설정
PcmChunkRingBuffer.java 녹음 thread와 WebSocket sender thread 분리용 bounded queue
WavPayload.java RIFF/WAVE header 판별
AndroidManifest.xml RECORD_AUDIO, INTERNET, RECEIVE_BOOT_COMPLETED, FOREGROUND_SERVICE_MICROPHONE 선언

주요 Cloudflare 파일:

파일 역할
src/worker.js /, /upload, /listen, Durable Object session relay
public/index.html 웹 모니터 페이지, waveform/frame/bytes 표시, WAV 파일 전송 UI
wrangler.jsonc Worker, Durable Object binding, migration 설정

2. Cloudflare 포트 제약과 선택 구조

Cloudflare Pages/Workers는 일반 TCP 서버처럼 임의 포트를 열어 raw socket을 받는 구조가 아니다. Worker가 받을 수 있는 기본 경로는 HTTPS/WebSocket이고, 실제 외부 포트는 일반적으로 443이다.

따라서 이 PoC는 아래 방식으로 설계한다.

요구 구현 방식
앱에서 PCM을 실시간 전송 wss://{worker-host}/upload?session=demo WebSocket
웹페이지에서 들어오는 PCM 확인 https://{worker-host}/ 접속 후 /listen?session=demo WebSocket
웹페이지에서 기기로 WAV 전송 같은 /listen?session=demo WebSocket에서 raw RIFF/WAVE binary 송신
session별 송신자/수신자 매칭 Cloudflare Durable Object
waveform/재생 확인 브라우저 JavaScript에서 PCM16 frame 파싱
웹 실시간 재생 80ms jitter buffer 기반 WebAudio scheduling
기기 TTS WAV 수신 확인 Android가 WAV를 cache file로 저장 후 MediaPlayer 재생

임의 TCP port 수신이 반드시 필요하면 Cloudflare Spectrum 또는 별도 TCP 서버가 필요하다. 현재 요구인 “웹페이지에서 받아보고 앱에서 쏘는 PoC”에는 Worker WebSocket relay가 더 단순하다.

3. Android -> Web PCM 업링크 포맷

Android 앱은 AudioRecord를 아래 설정으로 연다.

항목
Audio source MediaRecorder.AudioSource.VOICE_RECOGNITION
Sample rate 16000
Channel mono
Encoding PCM_16BIT
Chunk frames 320
Chunk bytes 640

WebSocket binary frame은 아래 구조다.

0..3   magic: "PCM1"
4..7   sequence: int32 little-endian
8..11  payloadBytes: int32 little-endian
12..15 flags/reserved: int32 little-endian
16..   PCM16 little-endian payload

웹페이지는 이 frame을 파싱해서 sequence, frame count, byte count, waveform을 표시한다. Enable audio playback을 누르면 브라우저에서 PCM을 실시간 재생한다.

실시간성을 위해 Android 송신은 녹음과 네트워크 송신을 분리한다.

AudioRecord.read()
  -> PcmChunkRingBuffer.offer()
  -> senderLoop()
  -> PcmWebSocketStreamer.sendPcm()
  -> Worker /upload
  -> Browser /listen
  -> WebAudio jitter playback

PcmChunkRingBuffer는 bounded queue다. 네트워크가 순간적으로 밀리면 오래된 chunk를 drop해서 “늦게라도 모두 재생”보다 “현재 음성에 가까운 실시간성”을 우선한다.

4. Web -> Android WAV 다운링크 포맷

웹페이지는 선택한 WAV 파일을 별도 wrapper 없이 raw RIFF/WAVE binary 그대로 전송한다.

Browser monitor
  -> /listen?session=demo WebSocket
  -> Durable Object
  -> current Android /upload sender
  -> Android PcmWebSocketStreamer.onMessage(ByteString)

Android는 수신 binary의 header를 아래 조건으로 확인한다.

0..3   "RIFF"
8..11  "WAVE"

조건을 만족하면 다음 순서로 처리한다.

WavPayload.isRiffWave(bytes)
  -> getCacheDir()/tts_downlink.wav 저장
  -> MediaPlayer.setDataSource(file)
  -> prepare()
  -> start()

현재 버전은 PoC 단계라 WAV 전체 파일을 한 번에 전송한다. 실제 TTS 스트리밍처럼 작은 조각 단위 재생이 필요하면 WAV1 chunk protocol 또는 raw PCM downlink protocol을 별도로 정의해야 한다.

5. Build

현재 시스템에는 별도 gradle 명령이 없으므로 기존 skmagic_ondeviceai_agent의 Gradle wrapper를 사용한다.

/home/silogood/work/2.A1_LLM_Aent/skmagic_ondeviceai_agent/gradlew -p /home/silogood/work/20.PoC testDebugUnitTest
/home/silogood/work/2.A1_LLM_Aent/skmagic_ondeviceai_agent/gradlew -p /home/silogood/work/20.PoC assembleDebug

APK 위치:

/home/silogood/work/20.PoC/app/build/outputs/apk/debug/app-debug.apk

현재 검증 결과:

검증 결과
testDebugUnitTest BUILD SUCCESSFUL
assembleDebug BUILD SUCCESSFUL
merged Manifest Launcher Activity 없음, BootCompletedReceiver + microphone foreground service만 선언
BOOT_COMPLETED simulation frames=512 bytes=335872 lastSeq=511

현재 Worker 배포 URL:

https://pcm-socket-recorder-relay.silogood.workers.dev/

6. Local relay 테스트

PC에서 Cloudflare Worker를 로컬로 띄운다.

cd /home/silogood/work/20.PoC/cloudflare-pcm-relay
npx wrangler dev --ip 0.0.0.0

브라우저에서 접속:

http://{PC_IP}:8787/

예:

http://192.168.10.20:8787/

웹페이지에서:

  1. session을 demo로 둔다.
  2. Connect monitor를 누른다.
  3. Android 앱에서 같은 session으로 송신한다.
  4. Frames, Bytes, Sequence, waveform이 증가하는지 확인한다.
  5. 필요한 경우 Enable audio playback을 눌러 브라우저에서 PCM 재생을 확인한다.
  6. TTS WAV file에서 WAV 파일을 선택하고 Send WAV to device를 누른다.
  7. 웹페이지에 WAV sent to Android가 표시되는지 확인한다.
  8. 기기에서 WAV가 재생되는지 확인한다.

앱 입력 URL:

ws://{PC_IP}:8787/upload?session=demo

에뮬레이터에서는:

ws://10.0.2.2:8787/upload?session=demo

실기기에서는 PC와 기기가 같은 네트워크에 있어야 하고, PC 방화벽이 8787 접근을 막지 않아야 한다.

WSL/Windows portproxy 또는 방화벽 때문에 LAN 접근이 막히면 Quick Tunnel로 우회한다.

cd /home/silogood/work/20.PoC/cloudflare-pcm-relay
npx wrangler dev --ip 127.0.0.1 --port 8788 --tunnel --show-interactive-dev-session=false

현재 안정 배포 URL:

https://pcm-socket-recorder-relay.silogood.workers.dev/

Android 앱 기본 WebSocket URL:

wss://pcm-socket-recorder-relay.silogood.workers.dev/upload?session=demo

다운링크 로그:

adb logcat | grep -E "Received WAV downlink|WAV playback started|WAV playback error"

주의:

7. 실기기 system/priv-app 설치

우리 디바이스에서 서비스 환경으로 검증할 때는 일반 앱 설치보다 /system/priv-app push 설치를 기준으로 본다. 백그라운드 마이크 녹음을 안정적으로 유지하려면 system/priv-app 권한 경계에서 검증해야 하기 때문이다.

Priv-app Install Flow

기본 설치 절차:

adb root
adb remount
adb shell mkdir -p /system/priv-app/PcmSocketRecorder
adb push /home/silogood/work/20.PoC/app/build/outputs/apk/debug/app-debug.apk /system/priv-app/PcmSocketRecorder/PcmSocketRecorder.apk
adb reboot

부팅 후 설치 확인:

adb shell pm list packages | grep pcm
adb shell dumpsys package com.skmagic.poc.pcmstreamer | grep -i granted -A 40

명시적 서비스 시작:

adb shell am start-foreground-service \
  -n com.skmagic.poc.pcmstreamer/.PcmRecorderService \
  -a com.skmagic.poc.pcmstreamer.START \
  --es ws_url "wss://pcm-socket-recorder-relay.silogood.workers.dev/upload?session=demo"

명시적 서비스 종료:

adb shell am start-foreground-service \
  -n com.skmagic.poc.pcmstreamer/.PcmRecorderService \
  -a com.skmagic.poc.pcmstreamer.STOP

로그 확인:

adb logcat | grep -E "PcmRecorderService|PcmWebSocketStreamer|AudioRecord"

중요:

8. 권한과 백그라운드 마이크 판단

현재 Manifest 권한:

<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

서비스 선언:

<service
    android:name=".PcmRecorderService"
    android:exported="false"
    android:foregroundServiceType="microphone" />

<receiver
    android:name=".BootCompletedReceiver"
    android:enabled="true"
    android:exported="false">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
    </intent-filter>
</receiver>

판단:

9. Cloudflare deploy

아래 명령으로 Worker를 배포한다.

cd /home/silogood/work/20.PoC/cloudflare-pcm-relay
npx wrangler deploy

성공하면 앱 URL은 아래 형태다.

wss://pcm-socket-recorder-relay.silogood.workers.dev/upload?session=demo

웹 모니터:

https://pcm-socket-recorder-relay.{workers-dev-subdomain}.workers.dev/

Custom domain을 붙이면 앱 URL은 custom domain 기준으로 바꾼다.

10. Pass / Fail 기준

항목 Pass 기준
APK build assembleDebug 성공
Worker dry-run npx wrangler deploy --dry-run 성공
priv-app 설치 reboot 후 pm list packages에 package 표시
권한 RECORD_AUDIO, foreground service 관련 권한 granted 또는 system policy상 허용
service 실행 logcat에 PCM recording started 출력
WebSocket 연결 logcat에 WebSocket opened 출력
PCM 송신 웹페이지 Frames, Bytes, Sequence 증가
waveform/실시간 재생 브라우저 waveform이 음성에 반응하고 Enable audio playback 후 WebAudio로 재생
WAV 다운링크 웹페이지 WAV sent to Android 표시
WAV 재생 logcat에 Received WAV downlink, WAV playback started 출력
백그라운드 화면 off/background 후에도 일정 시간 frame 증가 유지

현재 실기기 검증 결과:

device: 192.168.123.108:5555
listener URL: wss://columns-compact-ooo-characterized.trycloudflare.com/listen
result: frames=342 bytes=224352 lastSeq=341

기기 로그:

PcmRecorderService: PCM recording started: 16kHz mono PCM16, chunkBytes=640
PcmWebSocketStreamer: WebSocket opened
PcmRecorderService: PCM chunks recorded: 350
PcmRecorderService: PCM chunks sent: 350
PcmWebSocketStreamer: WebSocket closed: 1000, client closing

Fail 시 먼저 볼 것:

증상 확인
앱에서 녹음 시작 안 됨 RECORD_AUDIO 권한, foregroundServiceType="microphone", logcat AudioRecord init failed
WebSocket 연결 실패 URL, PC/Cloudflare 네트워크, wrangler dev --ip 0.0.0.0, 방화벽
웹페이지 frame 증가 없음 session 이름 불일치, /upload/listen 경로 확인
priv-app 설치 후 앱 없음 push 경로, APK 파일명, reboot, package parse error
백그라운드에서 중단 system/priv-app 권한, foreground notification, doze/power policy

11. 후속 보강 필요 항목

현재 PoC는 “수동 실행 PoC”다. 서비스처럼 장시간 운용하려면 아래가 필요하다.

항목 이유
BOOT_COMPLETED receiver 부팅 후 자동 서비스 준비
설정 저장 WebSocket URL/session을 SharedPreferences 또는 device config로 유지
reconnect 네트워크 끊김/Worker 재배포 후 자동 복구
start/stop broadcast 또는 bound service UI 없이 외부 제어 가능
platform signing system/priv-app 권한 안정화
privapp permission allowlist privileged 권한 grant 명시
foreground notification 정책 서비스 장시간 유지
PCM file dump option 송신 문제와 녹음 문제 분리 디버깅
TLS/WSS 운영 실제 Cloudflare 배포 환경에서는 wss:// 사용
session auth 임의 사용자가 /upload로 보내는 것을 차단

11. 관련 명령 모음

Build:

/home/silogood/work/2.A1_LLM_Aent/skmagic_ondeviceai_agent/gradlew -p /home/silogood/work/20.PoC testDebugUnitTest
/home/silogood/work/2.A1_LLM_Aent/skmagic_ondeviceai_agent/gradlew -p /home/silogood/work/20.PoC assembleDebug

Local relay:

cd /home/silogood/work/20.PoC/cloudflare-pcm-relay
npx wrangler dev --ip 0.0.0.0

Priv-app push:

adb root
adb remount
adb shell mkdir -p /system/priv-app/PcmSocketRecorder
adb push /home/silogood/work/20.PoC/app/build/outputs/apk/debug/app-debug.apk /system/priv-app/PcmSocketRecorder/PcmSocketRecorder.apk
adb reboot

Log:

adb logcat | grep -E "PcmRecorderService|PcmWebSocketStreamer|AudioRecord"

Worker dry-run:

cd /home/silogood/work/20.PoC/cloudflare-pcm-relay
npx wrangler deploy --dry-run

Keyboard shortcuts

⌘K / Ctrl+KOpen command palette
/Focus search
g hGo to home
g pGo to projects
g sGo to sessions
j / kNext / prev row (tables)
?Show this help
EscClose dialogs

Structured queries

Mix key:value filters with free text in the palette:

type:sessionOnly session pages
project:llm-wikiFilter by project name (substring)
model:claudeFilter by model name (substring)
date:>2026-03-01Sessions after a date
date:<2026-04-01Sessions before a date
tags:rustPages mentioning a tag/topic
sort:dateSort results by date (newest first)

Example: type:session project:llm-wiki date:>2026-04 sort:date