# 월패드 로비 SIP 무음 및 TVOUT 프레임·GC 문제 해결기
## 1. 문제 발생
월패드 운영 프로젝트에서 로비폰과 SIP 영상통화를 구현하는 과정에서 영상은 정상적으로 표시되고 SIP 연결도 정상적으로 이루어졌지만 통화 수락 후 서로 음성이 들리지 않는 문제가 발생했다.
기존 마스터 프로젝트에서는 동일한 장비에서 정상적으로 통화가 가능했기 때문에 하드웨어 자체의 문제보다는 운영 프로젝트의 오디오 처리 구조 차이를 의심했다.
당시 로비 통화에는 다음과 같이 DSP path 4와 volume 15를 적용하고 있었다.
AudioHardwareManager.setCallVolume(
path = 4,
volume = 15
)
AudioHardwareManager.applyAudioPath(
path = 4,
phase = AudioHardwareManager.RoutePhase.TALK,
context = context
)
하지만 SIP 통화 상태가 CONFIRMED이고 Audio Direction이 SendRecv이며 GPIO와 DSP path까지 정상적으로 적용됐음에도 음성이 나오지 않았다.
## 2. 처음 의심했던 부분
처음에는 다음 항목을 순서대로 의심했다.
- SIP SDP 협상 문제
- Audio Direction 설정 문제
- GPIO 설정 문제
- DSP path 번호 문제
- CRAM 파일 적용 문제
- 마이크 mute 상태 문제
- Android AudioManager 통화 모드 문제
로비는 하드웨어 설계상 path 4를 사용하는 것이 맞았기 때문에 path 번호 자체는 원인이 아니었다.
## 3. AudioTrack KeepAlive에서 발견한 힌트
테스트 중 DoorTalkKeepAlive를 실행했을 때 오디오 경로가 일부 활성화되는 현상이 있었다.
해당 함수에서는 Android AudioManager를 다음과 같이 설정하고 있었다.
AudioManager.MODE_IN_COMMUNICATION
speakerphone = false
microphoneMute = false
그리고 silent AudioTrack을 생성해 계속 PCM 데이터를 write하고 있었다.
이를 통해 DSP path와 GPIO를 설정하는 것만으로는 실제 Android Audio HAL 또는 PCM stream이 완전히 활성화되지 않을 수 있다는 점을 확인했다.
즉,
DSP/GPIO 설정
과
실제 PCM stream 활성화
는 별개의 문제였다.
## 4. SIP External Audio 구조 확인
현재 프로젝트는 SIP_EXTERNAL_AUDIO=true 구조를 사용하고 있었다.
이 방식에서는 SIP SDK가 마이크와 스피커를 자동으로 처리하는 것이 아니라 애플리케이션이 PCM 데이터를 직접 처리해야 한다.
따라서 정상적인 구조는 다음과 같아야 했다.
월패드 마이크
→ DSP
→ PCM Capture
→ SIP sendAudioFrame
→ RTP
→ 상대방
반대로 수신은
상대방
→ RTP
→ onAudioFrame
→ PCM Playback
→ DSP
→ 월패드 스피커
형태가 되어야 한다.
## 5. tinyalsa Capture / Playback 적용
마스터 프로젝트를 참고하여 tinyalsa card 1, device 0을 사용하도록 구성했다.
마이크 TX는 다음 구조로 구성했다.
card=1
device=0
capture=1
sampleRate=16000
channel=1
Capture PCM은 16kHz로 읽은 뒤 SIP에서 사용하는 8kHz PCM으로 변환하여 sendAudioFrame으로 전달했다.
RX도 처음에는 동일하게 tinyalsa playback을 열려고 했다.
## 6. 결정적인 로그
실제 테스트에서 다음 로그가 확인됐다.
START requested callId=1 card=1 device=0 rate=16000 ch=1
PLAYBACK OPEN FAILED card=1 device=0
반면 Capture는 정상적으로 열렸다.
CAPTURE OPEN OK
이 로그가 문제 해결의 핵심 단서였다.
SIP 협상이나 GPIO가 문제가 아니라 실제 출력용 tinyalsa playback 장치를 열 수 없는 상태였다.
즉 구조적으로
TX Capture = 정상
RX Playback = 실패
상태였다.
## 7. 최종 오디오 구조 변경
tinyalsa playback을 계속 억지로 열려고 하지 않고 TX와 RX를 서로 다른 방식으로 처리하도록 변경했다.
### TX : 월패드에서 로비폰
MIC
→ AK7755 DSP
→ DSP path 4
→ tinyalsa Capture
→ 16kHz PCM
→ 8kHz 변환
→ Software Gain
→ SIP sendAudioFrame
→ RTP
→ 로비폰
### RX : 로비폰에서 월패드
로비폰
→ SIP RTP
→ onAudioFrame
→ 8kHz PCM
→ 16kHz 변환
→ Software Gain
→ Android AudioTrack
→ Android Audio HAL
→ DSP path 4
→ Speaker
RX에서 tinyalsa Playback을 사용하지 않고 Android AudioTrack의 VOICE_COMMUNICATION 경로를 사용하도록 변경한 것이 핵심이었다.
## 8. 변경 후 Audio Bridge 정상 동작 확인
변경 후 다음 로그가 정상적으로 확인됐다.
START requested callId=1 TX=tinyalsa(card=1,dev=0,rate=16000) RX=AudioTrack(rate=16000)
CAPTURE OPEN OK
RX AUDIOTRACK OPEN OK
BRIDGE READY
이 상태에서 실제 SIP RX와 TX 프레임도 지속적으로 처리됐다.
## 9. PCM 데이터는 들어오는데 여전히 음량이 작은 문제
Audio Bridge를 정상적으로 연결한 이후 로그를 확인하니 SIP 오디오 프레임 자체는 정상적으로 송수신되고 있었다.
예를 들어 다음과 같은 로그가 발생했다.
RX #1 backend=AudioTrack sipSamples=160 outBytes=640 written=640 max=112
TX #1 samples=160 max=42
하지만 PCM amplitude가 지나치게 작았다.
대략적인 값은 다음과 같았다.
RX max = 80 ~ 120
TX max = 35 ~ 60
즉 RTP와 PCM 데이터는 정상적으로 흐르고 있지만 실제 오디오 신호가 거의 무음에 가까운 수준이었다.
## 10. Software Gain 적용
TX와 RX PCM 양쪽에 Software Gain을 적용했다.
단순하게 PCM 값을 곱할 경우 Short 범위를 넘어 overflow가 발생할 수 있기 때문에 반드시 clipping 처리를 적용했다.
처리 방법은 다음과 같다.
sample
→ gain 적용
→ Short.MIN_VALUE ~ Short.MAX_VALUE 범위 제한
→ PCM 출력
Software Gain 적용 후 실제 통화 음량이 정상적으로 확보됐다.
## 11. KeepAlive AudioTrack 중복 문제
초기 테스트에서는 DoorTalkKeepAlive AudioTrack을 이용해 DSP clock 및 Android Audio 경로를 활성화했다.
하지만 최종적으로 SipAudioBridge 자체에서 RX AudioTrack을 생성하기 때문에 KeepAlive AudioTrack까지 동시에 사용하면 두 개의 AudioTrack이 VOICE_COMMUNICATION 경로를 점유하게 된다.
따라서 최종 로비 SIP 통화에서는 별도의 DoorTalkKeepAlive를 제거했다.
최종적으로 로비 오디오 출력 스트림은 SipAudioBridge에서 단일 관리하도록 구성했다.
## 12. SIP 미리보기와 실제 통화 상태 분리
로비 수신 시 영상 미리보기 단계와 실제 통화 수락 단계를 구분했다.
미리보기 단계에서는
audio = RecvOnly
video = RecvOnly
183 Session Progress
를 사용했다.
사용자가 실제 통화 버튼을 누르면
audio = SendRecv
video = RecvOnly
200 OK
상태로 변경했다.
DSP, CRAM, GPIO 설정 직후에는 I2S 및 DSP가 안정화될 시간을 주기 위해 약간의 지연 후 실제 Audio Bridge를 시작했다.
# TVOUT 영상 프레임 문제
## 13. 간헐적인 buff noready 발생
USB TVOUT 영상 처리 중 간헐적으로 다음 로그가 발생했다.
buff noready frame_id=0
buff noready frame_id=1
bulkTransErr=255
ret=0
imageSize=691200
720 x 480 YUV422 한 프레임의 크기는
720 x 480 x 2 = 691200 bytes
이므로 imageSize 자체는 정상적인 값이었다.
문제는 프레임 크기 자체가 아니라 USB Consumer가 아직 준비되지 않은 상태에서 Producer가 프레임을 전달하는 타이밍이었다.
## 14. 초기 프레임 Drop
영상 스트림 초기화 직후에는 USB Consumer와 관련 버퍼가 완전히 안정화되지 않은 상태일 수 있다.
따라서 TVOUT 시작 직후 초기 프레임 일부를 의도적으로 버리도록 변경했다.
videoInit
→ 초기 프레임 discard
→ Consumer 안정화
→ 정상 프레임 송출
구조로 변경했다.
## 15. 프레임 주기 조정
영상이 25fps이면 한 프레임 간격은 약 40ms이다.
1000 / 25 = 40ms
Producer가 이보다 빠르게 데이터를 전달하면 Consumer가 처리하지 못한 데이터가 누적될 가능성이 있다.
따라서 TVOUT 송출 간격을 실제 영상 처리 속도에 맞게 조정했다.
# Garbage Collector / Memory 문제
## 16. 실시간 처리 루프의 객체 생성 문제
영상과 SIP 오디오는 매우 짧은 주기로 반복 처리된다.
20ms 단위 SIP Audio Frame만 계산해도 초당 약 50회 callback이 발생한다.
이 과정에서 매번
ByteArray
ShortArray
Pair
Triple
임시 객체
등을 생성하면 많은 Garbage가 발생한다.
영상 프레임 처리까지 합쳐지면 객체 생성량은 훨씬 증가한다.
## 17. GC가 실시간 통신에 미치는 영향
Garbage Collector가 자주 실행되면 일반 화면에서는 큰 문제가 없을 수 있지만 영상과 음성을 실시간으로 처리하는 월패드에서는 순간적인 Stop-the-world 또는 스케줄링 지연이 발생할 수 있다.
그 결과
영상 프레임 누락
Audio frame 처리 지연
USB Consumer 타이밍 지연
영상 끊김
음성 끊김
등의 문제가 발생할 수 있다.
## 18. 버퍼 재사용
이를 개선하기 위해 프레임마다 새로운 ByteArray 또는 ShortArray를 생성하지 않고 미리 할당된 버퍼를 반복해서 사용하도록 변경했다.
특히 SIP RX 변환 버퍼와 TVOUT 영상 프레임 버퍼를 재사용했다.
프레임 처리 hot path에서 사용되던 Pair, Triple 등의 임시 객체도 제거했다.
## 19. 강제 GC 사용 금지
메모리가 부족하다는 이유로 System.gc()를 직접 호출하지 않았다.
실시간 영상/음성 프로그램에서는 강제 GC가 오히려 프레임 처리 지연을 만들어 문제를 악화시킬 수 있다.
대신 객체 생성을 줄이고 버퍼의 생명주기를 명확하게 관리하는 방식으로 개선했다.
## 20. 메모리 압박 처리
Android에서 onTrimMemory 또는 onLowMemory가 호출될 때 사용하지 않는 대형 영상 버퍼를 정리할 수 있도록 처리했다.
단 현재 사용 중인 TVOUT 버퍼를 무조건 제거하면 오히려 프레임 오류가 발생할 수 있기 때문에 비활성 상태의 버퍼만 정리하도록 구성했다.
# 최종 결과
수정 후 다음 항목이 정상적으로 동작하는 것을 확인했다.
- 로비폰과 월패드 양방향 음성 통화 정상
- 월패드 마이크 TX 정상
- 로비폰 음성 RX 정상
- SIP 영상 통화 정상
- DSP path 4 정상
- tinyalsa Capture 정상
- AudioTrack RX 정상
- SIP Audio Frame TX/RX 정상
- PCM 음량 정상화
- USB TVOUT 프레임 안정화
- buff noready 발생 감소
- 불필요한 GC 감소
- 실시간 프레임 메모리 재사용
# 이번 문제에서 얻은 노하우
이번 문제의 핵심은 SIP 상태만 확인해서는 오디오 문제를 찾을 수 없다는 점이었다.
SIP가 CONFIRMED이고 SDP가 SendRecv 상태라고 하더라도 실제 음성 데이터가 정상적으로 스피커까지 전달된다는 의미는 아니다.
실제 통화 경로는 다음 단계까지 모두 확인해야 한다.
SIP Signaling
→ RTP
→ PCM callback
→ Capture / Playback
→ Android Audio HAL
→ DSP
→ GPIO
→ MIC / Speaker
특히 다음과 같은 로그를 단계별로 추가하는 것이 문제 분석에 매우 효과적이었다.
CAPTURE OPEN OK
PLAYBACK OPEN FAILED
RX AUDIOTRACK OPEN OK
BRIDGE READY
TX max
RX max
또한 PCM Frame이 존재한다고 해서 실제 음성이 존재한다고 판단하면 안 된다.
이번에도 TX/RX Frame은 계속 발생하고 있었지만 max amplitude가 30 ~ 120 수준으로 거의 무음에 가까웠다.
따라서 실시간 음성 문제를 분석할 때는 반드시 PCM amplitude까지 함께 확인하는 것이 좋다.
# 개발 후기
처음에는 로비 path 번호와 GPIO 또는 SIP 협상 문제라고 생각했다.
실제로는 SIP 이후 단계에서 발생한 tinyalsa Playback 장치 open 실패가 가장 큰 원인이었다.
PLAYBACK OPEN FAILED card=1 device=0
로그를 확인한 뒤 오디오 문제의 범위를 크게 좁힐 수 있었다.
결국
TX = tinyalsa Capture
RX = Android AudioTrack
으로 역할을 분리했고, PCM Software Gain까지 적용하면서 로비 양방향 통화가 정상화됐다.
영상 프레임 문제 역시 단순히 메모리 크기를 늘리는 방식이 아니라 Producer와 Consumer의 처리 속도를 맞추고 초기 불완전 프레임을 버리며 버퍼를 재사용하는 방식으로 해결했다.
이번 작업을 통해 Android 기반 임베디드 실시간 프로젝트에서는 SIP 자체뿐만 아니라 Audio HAL, DSP, tinyalsa, USB frame timing, GC와 메모리 할당 구조까지 함께 확인해야 안정적인 서비스를 만들 수 있다는 점을 다시 확인했다.