← Docs hub

SoundFlow 음성 손상·Zigging 원인 분석

핵심 결론: 다섯 WAV에서 관찰된 현상은 하나의 EQ 문제로 묶이지 않는다. zigging.wav는 Kardome 처리 전 MIC2 입력이 이미 포화된 문제이고, 중간에 씹힘2.wavsndflow.ko가 4 ms의 디지털 무음을 삽입한 정황이 명확하다. 중간에 씹힘.wav는 완전한 무음 없이 시간이 생략된 형태이며, SoundFlow 입력 큐가 overflow 때 최대 320 ms를 한꺼번에 삭제하는 현재 정책이 가장 중요한 원인 후보다. 추가 파일 중 ring은 높은 배경 noise, 기계음은 강한 주기성·대역 불균형이 핵심이며, 두 파일의 캡처 지점은 현재 WAV만으로 확정할 수 없다.

SoundFlow 음성 손상 원인과 수정 우선순위

이 문서는 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에 같은 값을 사용해야 한다. 이 수정 후 다음 조건을 확인한다.

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()보다 다음 순서를 사용한다.

  1. 입력 capture는 계속 읽어 ALSA overrun을 방지한다.
  2. 고정 크기 ring buffer와 buffer pool을 사용해 매 주기 malloc을 피한다.
  3. consumer는 polling보다 condition variable로 즉시 깨운다.
  4. overflow가 불가피하면 가장 오래된 버퍼 한 개만 제거해 손실을 320 ms에서 64 ms로 제한한다.
  5. sequence number와 timestamp로 손실 구간을 명시한다.
  6. 지속적인 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는 충분해도 음색이 로봇·기계처럼 뭉쳐 들릴 수 있다.

가능한 후보는 다음과 같지만 현재 단계에서는 순위를 확정할 수 없다.

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. 확정 사실과 남은 확인

확정

추가 확인 필요

소스 근거

실제 서비스 빌드:
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

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