SoundFlow 음성 손상·Zigging 원인 분석
핵심 결론: 다섯 WAV에서 관찰된 현상은 하나의 EQ 문제로 묶이지 않는다.
zigging.wav는 Kardome 처리 전MIC2입력이 이미 포화된 문제이고,중간에 씹힘2.wav는sndflow.ko가 4 ms의 디지털 무음을 삽입한 정황이 명확하다.중간에 씹힘.wav는 완전한 무음 없이 시간이 생략된 형태이며, SoundFlow 입력 큐가 overflow 때 최대 320 ms를 한꺼번에 삭제하는 현재 정책이 가장 중요한 원인 후보다. 추가 파일 중ring은 높은 배경 noise,기계음은 강한 주기성·대역 불균형이 핵심이며, 두 파일의 캡처 지점은 현재 WAV만으로 확정할 수 없다.
이 문서는 prob/의 다섯 WAV와 A1 UM vendor overlay의 현재 SoundFlow
구현을 대조해 원인 경계를 정리한다.
분석 파일:
| 파일 | 형식 | 길이 | 관찰된 핵심 현상 |
|---|---|---|---|
zigging.wav |
16 kHz, Float32, 4채널 | 74.752초 | MIC2 단독 포화와 전체 채널 zero 구간 |
중간에 씹힘.wav |
16 kHz, PCM16, 모노 | 6.58초 | 긴 zero 구간 없이 음성 일부가 생략된 청감 |
중간에 씹힘2.wav |
16 kHz, PCM16, 모노 | 8.28초 | 2.368초에 정확히 64샘플의 완전한 zero |
ring_소리와_홤꼐 너무 많은노이즈 들어옴.wav |
16 kHz, PCM16, 모노 | 5.32초 | 무음부 noise 상승과 고역 에너지 과다 |
기계음처럼 소리 뭉계짐.wav |
16 kHz, PCM16, 모노 | 5.22초 | 강한 주기성, 저주파 편중과 중고역 약화 |
마지막 두 파일은 PCM16 모노라는 사실만 확인된다. 원본 4채널 Float dump와 형식이 다르지만, 이것만으로 Kardome 전처리 전/후를 판정할 수는 없다. 파일 안에 capture tap이나 처리 설정을 식별할 metadata도 없으므로 아래 분석은 신호 증상 분석이며 전처리 인과 판정이 아니다.
1. 실제 SoundFlow 처리 경로
사용자가 처음 확인한
airbot/hardware/interfaces/soundflowservice/1.0/default는 HIDL
인터페이스 기본 스켈레톤이며, start(), stop() 등은 TODO 상태다.
실제 제품 서비스 모듈은 다음 경로다.
Service module:
airbot/external/sndflow/service
Kernel loopback:
airbot/external/sndflow/kmodule/sndflow.c
음성 경로를 단순화하면 다음과 같다.
물리 MIC 4채널 + reference
-> ALSA card 0 / device 0 capture
-> SoundFlowInput 입력 큐
-> 24-bit PCM을 Float32로 변환
-> Kardome AFE + KWS
-> ALSA card 4 / device 1 playback
-> sndflow.ko loopback
-> ALSA card 4 / device 0 capture
-> Android Audio HAL / STT
이 경로를 기준으로 Kardome 전, Kardome 처리, sndflow.ko 출력 전달
문제를 분리해야 한다.
2. Zigging: MIC2 입력 자체가 포화
2.1 정량 결과
zigging.wav는 16 kHz Float32 4채널 파일이다. 이상은 약
28.800초부터 시작하며 31.15~31.18초에 가장 심하다.
| 지표 | 결과 | 의미 |
|---|---|---|
| 이상 채널 | MIC2, 배열 index 1 |
네 마이크 중 특정 입력 경로에 집중 |
| Float 최댓값 | +1.0 |
디지털 양의 최대치 |
| Float 최솟값 | -1.0000001 |
24-bit 최소 코드 변환 결과 |
| 16 ms RMS 비율 | 다른 MIC 중앙값의 최대 약 999배 | 약 +60 dB, 감도 차이로 설명 불가 |
| 24-bit 원본 코드 | +8,388,607 / -8,388,608 도달 |
Float 저장 전 PCM부터 포화 |
| 채널 상관도 | MIC2와 나머지 MIC가 거의 0 | 공통 외부음보다 MIC2 단독 이상에 가까움 |
Float 값은 다음 변환과 정확히 일치한다.
Float sample = signed 24-bit PCM / 8,388,607
따라서 Float WAV 저장 과정에서 값이 깨진 것이 아니다. SoundFlow가 물리 PCM을 Float로 바꾸기 전부터 MIC2가 이미 최대·최소 코드에 도달했다.
Kardome의 krdmAfeRun() 입력은 const float*이고, SoundFlow MIC
덤프는 같은 입력 버퍼를 기록한다. 그러므로 이 현상을 EQ, AGC,
Kardome 출력이 새로 만들었다고 볼 수 없다.
2.2 추가로 확인된 전체 채널 zero
MIC2 포화와 별개로 네 마이크가 동시에 정확히 0이 되는 구간도 있다.
| 시작 시점 | 길이 |
|---|---|
| 8.975초 | 약 21.4 ms |
| 23.087초 부근 | 합계 약 28.5 ms |
| 51.876초 | 약 32.3 ms |
| 70.276초 | 약 32.4 ms |
조용한 공간에서도 마이크 자체 noise가 있으므로 모든 채널이 수십 ms 동안 정확히 0인 것은 정상 정적이 아니다. 물리 capture, DSP, DMA 또는 driver 계층의 zero-fill/공급 중단을 별도로 확인해야 한다.
2.3 원인 경계와 확인 방법
현재 WAV로 확정할 수 있는 경계는 Kardome 이전 입력 문제까지다.
세부 원인은 다음 교차시험으로 나눈다.
| 시험 | 결과 | 판정 |
|---|---|---|
| MIC2와 다른 MIC의 물리 연결 교환 | 이상이 마이크를 따라 이동 | MIC 소자, 배선, 전원, 접지 |
| MIC 연결 교환 후에도 동일 slot 이상 | 계속 MIC2 채널에서 발생 | codec, PDM/TDM slot, LPASS/DSP, DMA |
| card0/device0 원본 5채널에도 zero | SoundFlow 이전부터 zero | capture driver/DSP/transport |
| card0 원본은 정상, SoundFlow MIC dump만 이상 | 변환/큐/dump 경로 문제 | SoundFlow userspace 재검토 |
3. 4 ms 씹힘: sndflow.ko의 시간 snapshot 불일치
3.1 WAV에서 확정된 사실
중간에 씹힘2.wav의 2.368초에는 정확히 64샘플이 모두 0이다.
64 samples / 16,000 samples per second
= 0.004 second
= 4 ms
앞뒤에는 정상 발화 에너지가 존재한다.
| 구간 | 레벨 |
|---|---|
| 직전 4 ms | 약 -14.8 dBFS |
| 문제 4 ms | 완전한 디지털 zero |
| 직후 4 ms | 약 -15.3 dBFS |
이는 자연스러운 무음이 아니라 말소리 사이에 삽입된 디지털 무음 블록이다. 4 ms가 모음에 걸리면 짧은 click/끊김으로 들릴 수 있고, 파열음이나 짧은 자음에 걸리면 자음이 약해진 것처럼 들릴 수 있다.
3.2 왜 발생하는가
원본 ALSA aloop.c는 현재 시간을 한 번만 읽고 같은 cur_jiffies를
재생과 캡처 계산에 사용한다.
cur_jiffies = jiffies
play delta <- cur_jiffies
capture delta <- cur_jiffies
현재 sndflow.c는 공통 snapshot을 제거하고 jiffies를 각각 읽는다.
play delta <- 첫 번째 jiffies
capture delta <- 두 번째 jiffies
두 읽기 사이에 kernel tick이 증가하면 실제 재생·캡처가 정상이어도 캡처가 한 tick 앞선 것처럼 계산된다.
play read: 100
kernel tick: 100 -> 101
capture read: 101
잘못된 판단: capture가 1 tick 앞섬
현재 동작: 차이만큼 capture buffer를 zero로 채움
실기 커널이 HZ=250이면 한 tick은 1/250초 = 4 ms다. WAV의
64샘플 zero와 정확히 같은 크기다.
중요한 점은 4 ms마다 계속 무음을 넣는다는 뜻이 아니라, 두 시간 읽기가 tick 경계에 걸린 순간에 4 ms 크기의 무음이 간헐적으로 삽입될 수 있다는 뜻이다.
3.3 수정 방향
원본 aloop.c처럼 loopback update 시작 시 jiffies를 한 번만 읽고
재생·캡처 delta에 같은 값을 사용해야 한다. 이 수정 후 다음 조건을
확인한다.
- 30분 이상 연속 녹음에서 32샘플 이상의 hard-zero run이 0회
- 음성 active 구간의 64샘플 zero가 0회
- playback/capture pointer drift와 XRUN이 0회
- KWS/STT 발화 경계의 click과 자음 누락이 재현되지 않음
4. 최대 320 ms 씹힘: 입력 큐 전체 삭제
4.1 현재 구현
SoundFlow 입력은 1,024샘플 단위로 들어온다.
1 buffer = 1,024 samples / 16,000 Hz = 64 ms
PCM_OVERFLOW_BUFFER_COUNT = 4
현재 SoundFlowInput.cpp는 새 버퍼를 queue에 넣은 뒤 queue 크기가
4를 넘으면 mPcmBufferQue.clear()를 실행한다. queue에 4개가 쌓인
상태에서 5번째가 들어오면 최신 버퍼까지 포함해 5개를 모두 지운다.
64 ms x 5 buffers = 320 ms
이때 무음을 쓰는 것이 아니라 320 ms의 원본 시간이 파일과 처리 경로에서 사라진다.
원본: [앞 음절][중간 320 ms][뒤 음절]
삭제: [앞 음절] [뒤 음절]
따라서 WAV에는 긴 zero가 보이지 않아도 음절이나 짧은 단어가 통째로
생략될 수 있다. 중간에 씹힘.wav가 이 형태와 맞지만, overflow
로그가 현재 주석 처리돼 있어 해당 파일의 특정 순간을 queue clear와
직접 연결하는 단계는 남아 있다.
4.2 정상 처리 방식인가
이 정책은 지연이 무한히 누적되는 것을 피하고 최신 시점으로 빠르게 복귀하기 위한 비상 처리로 추정된다. 그러나 KWS/STT의 정상 처리 정책으로는 부적절하다.
| 항목 | 현재 동작 | KWS/STT 영향 |
|---|---|---|
| overflow 처리 | queue 전체 삭제 | 최대 320 ms 손실 |
| 새 입력 보존 | 최신 5번째 버퍼도 삭제 | 복구 시작점까지 손실 |
| 진단 | overflow 로그 주석 처리 | WAV 손상과 원인 연결 불가 |
| 시간 연속성 | 보장하지 않음 | 음절/단어/호출어 누락 |
일반적으로 짧은 자음은 수십 ms, 음절은 약 100~300 ms 범위다. 320 ms 삭제는 음절 하나 또는 짧은 단어, KWS 호출어 일부를 통째로 없앨 수 있는 크기다.
4.3 권장 처리 정책
근본 목표는 버퍼를 버리는 것이 아니라 한 버퍼를 다음 입력 deadline인 64 ms 안에 처리하는 것이다.
권장 목표:
average / P99 processing time < 64 ms
운영 목표 < 40~50 ms
normal queue depth 0~1
overflow / dropped frame 0
순간적인 지연이 불가피할 때도 queue 전체 clear()보다 다음 순서를
사용한다.
- 입력 capture는 계속 읽어 ALSA overrun을 방지한다.
- 고정 크기 ring buffer와 buffer pool을 사용해 매 주기
malloc을 피한다. - consumer는 polling보다 condition variable로 즉시 깨운다.
- overflow가 불가피하면 가장 오래된 버퍼 한 개만 제거해 손실을 320 ms에서 64 ms로 제한한다.
- sequence number와 timestamp로 손실 구간을 명시한다.
- 지속적인 overflow면 AFE state reset 또는 controlled restart를 수행하고 원인을 로그로 남긴다.
오래된 버퍼 하나를 버리는 것도 정상 목표는 아니다. 이는 전체 삭제보다 피해가 작은 비상 fallback이며, 최종 통과 기준은 drop 0회다. 단순히 queue를 4개에서 8개로 키우는 것은 순간 CPU spike에는 도움이 되지만, 지속적으로 처리시간이 64 ms를 넘으면 지연만 늘고 결국 다시 overflow가 난다.
5. 추가 음질 손상: 높은 noise와 기계음
추가 두 파일은 긴 zero, rail clipping, 64/1,024샘플 단위 유실이
주요 증상이 아니다. 따라서 앞의 sndflow.ko zero-fill이나 queue
전체 삭제와 같은 계열로 바로 분류하면 안 된다.
비교 기준은 같은 16 kHz PCM16 모노인 중간에 씹힘.wav의 상대적으로
깨끗한 구간이다. 발화 내용과 녹음 조건이 완전히 같은 paired sample은
아니므로, 아래 수치는 절대적인 A/B 성능 차이가 아니라 이상 신호를
찾기 위한 진단 지표다.
5.1 ring: 배경 noise가 음성을 따라 올라옴
| 지표 | ring |
비교 파일 | 차이 |
|---|---|---|---|
| 무음부 RMS | -37.83 dBFS |
-53.64 dBFS |
+15.81 dB |
| 음성부 RMS | -21.35 dBFS |
-21.49 dBFS |
+0.15 dB |
| 음성/무음 RMS 차이 | 16.48 dB |
32.14 dB |
-15.66 dB |
| 음성부 4~7.8 kHz RMS | -28.87 dBFS |
-41.61 dBFS |
+12.74 dB |
| peak | -7.92 dBFS |
-8.88 dBFS |
clipping 없음 |
음성 크기는 비교 파일과 거의 같지만 무음부가 약 16 dB 높다. 즉 마이크 전체 gain 부족이 아니라 noise가 음성과 함께 커진 형태다. 음성부 고역도 약 13 dB 강해 바람, 마찰, 전기성 광대역 noise 또는 noise suppression의 고역 잔류가 말소리와 함께 두드러질 수 있다. 무음부에는 약 1.18 kHz에서 주변 대역보다 12.9 dB 높은 국부 peak도 관찰되지만, 단일 짧은 파일만으로 ring의 물리 주파수나 발생원을 확정하지 않는다.
KWS/STT 관점에서는 음성/배경 대비가 약 15.7 dB 줄어 VAD 경계와
무성 자음 검출이 불안정해지고 false accept/reject 가능성이 커진다.
특히 고역 noise가 /s/, /f/, /h/ 계열 자음과 겹치면 STT 음소
혼동이 증가할 수 있다.
5.2 기계음: clipping보다 과도한 톤성·대역 불균형
| 지표 | 기계음 |
비교 파일 | 해석 |
|---|---|---|---|
| 무음부 RMS | -54.09 dBFS |
-53.64 dBFS |
배경 noise는 유사 |
| 음성/무음 RMS 차이 | 33.50 dB |
32.14 dB |
단순 SNR 저하는 아님 |
| 음성부 spectral flatness 중앙값 | 0.00191 |
0.01297 |
약 6.8배 낮아 톤성 강함 |
| 가장 강한 국부 peak | 250 Hz, 주변 대비 18.8 dB |
최대 9.2 dB |
저주파 주기 성분 우세 |
| 2~4 kHz / 80 Hz~2 kHz 상대 레벨 | -25.12 dB |
-19.78 dB |
중고역이 추가로 5.34 dB 약함 |
| peak | -8.98 dBFS |
-8.88 dBFS |
clipping 없음 |
이 파일은 무음 noise가 높은 것이 아니다. 음성 구간에서 250 Hz 부근과 고조파가 과도하게 지배하고, 2~4 kHz의 자음·명료도 대역이 상대적으로 약하다. 그래서 파형 peak나 RMS는 충분해도 음색이 로봇·기계처럼 뭉쳐 들릴 수 있다.
가능한 후보는 다음과 같지만 현재 단계에서는 순위를 확정할 수 없다.
- AFE noise suppression이 음성의 일부 성분만 남기는 musical-noise 또는 tonal artifact
- beamforming/채널 정렬 오류에 따른 주파수별 상쇄
- AGC/limiter가 저주파 주기 성분을 과도하게 강조
- 입력 자체의 모터·구조진동이 음성 기본주파수와 겹친 경우
- 녹음 tap 이후의 codec, resampling 또는 대역 제한
KWS/STT에는 에너지 부족보다 음소 특징 손상이 더 큰 위험이다. 호출어 전체 RMS가 정상이어도 2~4 kHz 자음 정보가 약해지면 유사 호출어 구분과 일반 STT의 자음 인식률이 나빠질 수 있다.
5.3 전처리 전/후를 판정하는 최소 시험
현재 파일명이나 WAV header만으로 처리 위치를 정할 수 없다. 동일 발화를 한 번 수행하면서 아래 tap을 같은 monotonic timestamp로 동시에 저장해야 한다.
A. card0/device0 물리 입력 원본
B. SoundFlow Float 변환 직후, krdmAfeRun() 이전
C. krdmAfeRun() 출력
D. card4/device0 또는 Android Audio HAL 최종 입력
| 최초 이상 지점 | 판정 |
|---|---|
| A부터 동일한 noise/기계음 | MIC, 모터, 바람, 구조진동, codec/DSP 입력 문제 |
| A 정상, B부터 이상 | 채널 mapping 또는 PCM 변환·입력 queue 문제 |
| B 정상, C부터 이상 | Kardome AFE/beamforming/NR/AGC 설정 영향 |
| C 정상, D부터 이상 | loopback, resampling, Audio HAL 또는 후단 처리 문제 |
Kardome bypass와 현재 설정을 번갈아 녹음하되 gain을 맞춘 뒤, 동일 발화·동일 거리에서 무음 RMS, 250 Hz tone prominence, 2~4 kHz 상대 에너지, KWS FRR/FAR, STT CER/WER를 비교한다.
6. 필수 계측
현재는 가장 중요한 overflow 로그가 주석 처리돼 있다. 다음 측정값을 한 buffer 단위로 남겨야 WAV와 runtime 원인을 연결할 수 있다.
| 계측값 | 목적 |
|---|---|
| input sequence number | 누락된 buffer 번호 확인 |
| capture timestamp | 물리 입력 시간축 확인 |
| queue depth / high-water mark | overflow 직전 적체 확인 |
| dropped buffer/sample count | 실제 손실량 계산 |
krdmAfeRun() 시간 |
AFE deadline 초과 확인 |
| KWS 처리시간 | KWS가 적체에 기여하는지 확인 |
pcm_write() 시간 |
output blocking 확인 |
| dump write 시간/short write | debug 저장 영향 분리 |
| ALSA XRUN / pointer drift | kernel output 전달 이상 확인 |
clear_capture_buf() bytes/count |
4 ms zero 삽입 직접 증명 |
예시:
seq=1042 queue=4 afe=52ms kws=7ms pcm_write=11ms total=73ms
overflow=true dropped_buffers=1 dropped_samples=1024
sndflow_loopback zero_fill_bytes=128 frames=64 duration_ms=4
7. 수정 우선순위와 재시험 Gate
| 우선순위 | 변경 | 이유 | 통과 기준 |
|---|---|---|---|
| 1 | sndflow.c에서 공통 cur_jiffies 복원 |
4 ms zero와 코드 크기가 정확히 일치 | active 구간 hard-zero 0회 |
| 2 | queue overflow 계측 활성화 | 첫 번째 씹힘과 queue clear 연결 | sequence gap과 WAV 시점 추적 가능 |
| 3 | queue 전체 clear() 제거 |
최대 320 ms 삭제 방지 | dropped frame 0, fallback도 64 ms 이하 |
| 4 | AFE/KWS/PCM write deadline 분석 | overflow 근본 원인 제거 | P99 64 ms 미만 |
| 5 | MIC2 교차 연결 및 card0 원본 동시 수집 | Zigging의 물리/slot 원인 분리 | 원인 계층 확정 |
| 6 | A~D 동시 tap 및 Kardome bypass A/B | ring·기계음의 최초 발생 지점 분리 |
동일 입력 paired 비교 확보 |
재시험은 동일 발화를 반복하고 아래 자료를 같은 시간축으로 수집한다.
1. card0/device0 원본 5채널 PCM
2. SoundFlow 4채널 MIC Float dump
3. Kardome mono output
4. card4/device0 최종 capture
5. queue/AFE/KWS/pcm_write/sndflow.ko 계측 로그
8. 확정 사실과 남은 확인
확정
zigging.wav의 MIC2는 Float 저장 전 24-bit PCM부터 포화돼 있다.중간에 씹힘2.wav에는 발화 중간 정확히 64샘플, 4 ms의 zero가 있다.- 현재
sndflow.c는 원본aloop.c와 달리 공통cur_jiffies를 사용하지 않는다. - capture가 playback보다 앞선 것으로 계산되면 kernel code가 그 차이를 zero로 채운다.
- 현재 입력 queue는 5번째 buffer가 들어왔을 때 queue 전체를 삭제할 수 있다.
ring파일은 비교 파일보다 무음 RMS가 15.81 dB 높고 음성/무음 RMS 차이가 15.66 dB 작다.기계음파일은 clipping 없이 spectral flatness가 비교 파일의 약 1/6.8이고 2~4 kHz가 저주파 대역 대비 5.34 dB 더 약하다.
추가 확인 필요
- 실기 kernel의
CONFIG_HZ와 4 ms zero 발생 시점의clear_capture_buf()직접 로그 중간에 씹힘.wav시점의 실제 queue overflow/sequence gap- MIC2 문제가 마이크 소자를 따라가는지 PDM/TDM slot에 남는지
- debug dump 활성화가 처리 deadline에 미치는 영향
- 추가 두 파일의 정확한 capture tap과 Kardome 전처리 적용 여부
- 같은 발화·환경의 bypass/처리 paired sample에서 KWS FRR/FAR와 STT CER/WER가 실제로 변하는 정도
소스 근거
실제 서비스 빌드:
airbot/external/sndflow/service/Android.mk
입력 queue와 전체 clear:
airbot/external/sndflow/service/SoundFlowInput.cpp
airbot/external/sndflow/service/PcmCfg.h
PCM 변환, Kardome, output write:
airbot/external/sndflow/service/SoundFlowProcessor.cpp
WAV dump:
airbot/external/sndflow/service/WaveFile.cpp
SoundFlow loopback:
airbot/external/sndflow/kmodule/sndflow.c
비교 기준:
kernel/msm-5.4/sound/drivers/aloop.c