← nacyot / artifacts

ASAHI, 오디오

맥북 스피커가 지지직거린다: Asahi Linux에서 스피커 길들이기

M1 Max 맥북 프로를 Asahi Linux(Omarchy)로 쓰면, 내장 스피커가 복잡한 음악에서 "칙칙" 끓고 순간순간 끊긴다. 적어도 이 장비에서는 하드웨어 고장이 아니라 세 겹의 소프트웨어 문제였다. 세 곳을 손보면 크게 줄지만, 완전히 사라지지는 않았다. 원인과 처방, 남은 문제, 그리고 macOS는 왜 멀쩡한지.

M1 Max MacBook Pro 14"(코드명 J314), Asahi Arch Linux, Omarchy/Hyprland, PipeWire

1증상

사인파 한 음을 재생하면 깨끗하다. 그런데 악기가 여럿 겹치는 음악이나 영상 소리를 틀면 "칙/틱" 하는 잡음이 섞이고, 이따금 소리가 순간(약 21밀리초) 뚝 끊긴다. 로그에는 아무 흔적도 없다. ALSA 언더런(xrun) 카운터는 0, CPU 부하도 한가하다.

흔적 없는 고장은 방향을 흐린다. 음원이 문제인지, 브라우저인지, 앰프인지, 커널인지. 하나씩 잘라내고 나니, 내 환경에서는 원인이 하드웨어 고장이 아니라 소프트웨어 오디오 경로에 있었다. 그것도 서로 다른 세 곳에.

2왜 스피커에 보정(convolver)이 필요한가

맥북의 작은 내장 스피커는 그대로 울리면 얇고 저음이 없다. macOS든 Asahi든, 스피커로 나가기 전에 소리를 한 번 보정한다. 가상 오디오 장치 위에 FIR/임펄스 응답으로 주파수 특성(EQ)을 바로잡고, Bankstown 같은 DSP 플러그인으로 저역, 라우드니스까지 보정하는 체인을 건다. Asahi에서는 asahi-audio 패키지가 이 보정을 j314-convolver라는 필터 노드로 PipeWire 그래프에 끼워 넣는다.

즉 앱에서 스피커까지 소리는 여러 단계를 지난다. 아래 그림에서 각 단계를 눌러, 무슨 일을 하고 어디서 무엇이 깨지는지 확인한다. 갈색으로 표시된 단계가 이번에 문제가 났던 곳이다.

그림 1. 앱에서 스피커까지의 오디오 경로. 이 장비에서 크래클은 하드웨어가 아니라 중간 세 단계(스트림 전환, PipeWire 스케줄링, DSP 보정)에서 났다.

3세 갈래 원인과 처방

증상은 하나처럼 들렸지만 원인은 셋이었고, 각각 다른 곳을 고쳐야 했다.

표 1. 세 원인과 처방.

원인증상처방
① 낡은 DSP 프로파일메인 크래클("칙칙")asahi-audio v4.0의 새 J314 FIR + 완화된 저음 보정
② 효율코어 스케줄링연속 재생 중 약 21ms 끊김PipeWire를 성능코어(cpu2-9)에 고정 + rtkit 실시간 우선권
③ 스트림 전환 튐영상↔음악 전환 시 순간 팝convolver 유휴 정지 끄기 + 허용 클럭을 48kHz 하나로 단일화

왜 효율코어가 소리를 끊나

둘째 원인이 가장 뜻밖이었다. Asahi는 전력을 아끼려고 오디오 프로세스가 효율코어(E-core) 쪽에 머물도록 유도한다. 이건 코어를 못박는 고정(affinity)이 아니라, 스케줄러가 보는 최대 사용률 상한(uclamp.max)을 낮춰 두는 힌트다. 그런데 스피커 보정 convolver는 무거운 연산이라, 느린 효율코어에서는 실시간 마감을 놓쳐 한 주기(48kHz, 1024프레임 기준 약 21ms) 분량의 샘플 공급이 늦어진다. 버퍼 언더런이 아니라 마감 초과라서 로그에 흔적이 남지 않는다.

성능코어(P-core)와 효율코어의 처리 능력 차이가 이 마감 초과를 설명한다.

그림 2. 코어 종류별 처리 능력(capacity). 효율코어는 성능코어의 절반 이하라, 무거운 보정 연산이 실시간 마감을 놓친다.

처방은 오디오 프로세스를 성능코어에 고정하는 것이다(아래 cpu2-9는 J314 10코어에서 cpu0-1이 효율코어, cpu2-9가 성능코어인 전제다. lscpu -ecpu_capacity로 확인한다).

# ~/.config/systemd/user/pipewire.service.d/cpuaffinity.conf (pipewire-pulse, wireplumber도 동일)
[Service]
CPUAffinity=2-9

여기에 rtkit를 켜면 PipeWire가 RTKit를 거쳐 오디오 처리 스레드에 실시간 스케줄링을 요청할 수 있다. 실제 정책, 우선순위는 배포판 설정과 RTKit 한도에 따라 달라지니 chrt, pw-top으로 확인한다(내 환경에서는 SCHED_RR, rtprio 20으로 관찰됐다).

sudo systemctl enable --now rtkit-daemon

4macOS는 왜 멀쩡한가

같은 하드웨어인데 macOS에서는 이런 일이 없다. CoreAudio의 실시간 오디오 스레드가 오디오 워크그룹에 합류해, 커널과 성능 컨트롤러가 오디오의 마감(deadline)을 알고 CPU 배정, 주파수를 결정하기 때문이다. 그래서 같은 하드웨어에서도 마감 초과가 잘 드러나지 않는다. Asahi는 반대로 전력을 위해 오디오를 효율코어 쪽에 밀어 둔다. 그래서 리눅스에서는 오디오를 마감 중심으로 다루도록 손으로 재현해야 한다. 성능코어 고정과 rtkit 우선권이 그 자동 배정을 손으로 흉내 내는 방법이다.

5귀 없이 잡기: 객관적 탐지

"칙칙"은 주관적이라 고쳤는지 아닌지 귀로만 판단하기 어렵다. 그래서 보정을 거친 뒤의 소리를 그대로 녹음해, 파형에서 끊김을 기계적으로 찾았다.

# 보정(DSP)을 거친 출력을 녹음하며 동시에 재생
pw-record --target alsa_output.platform-sound.RawSpeakers.monitor \
  --channels 2 --format s32 post.wav &
mpv --no-video song.webm
# → 약 21ms(한 주기) 동안 평평한 구간 = 진짜 끊김.
#   짧은 "click" 다수는 음악의 트랜지언트이지 고장이 아니다.

이 방법이 방향을 잡아 줬다. 무엇이 원인이 아닌지를 하나씩 배제하면서.

실제로 이렇게 잡힌다. 아래는 48.8초짜리 캡처 하나에서 찾아낸 드롭아웃 넷의 위치다. 각 막대에 마우스를 올리면 정확한 시각과 길이가 나온다. 넷 중 셋은 정확히 한 주기(1023프레임, 21.3ms)이고, 하나는 두 주기(2047프레임, 42.6ms)였다. 소리가 흐르는 내내 xrun은 0이었는데도, 파형에는 이렇게 평평한 구멍이 남는다.

그림 3. 실측 캡처(48.8초, 48kHz)에서 검출된 드롭아웃 4곳. 파형은 실측 통계(피크, 타임스탬프, 길이)로 재구성했다(원본 캡처 파일은 보존하지 않음). 평평한 붉은 구간이 소리가 빈 곳이다.

표 2. 배제표. 원인이 아니었던 것들.

가설확인판정
음원 파일 잡음파형 통계상 피크 −1.4dB, 평평 구간 0깨끗함
브라우저로컬 mpv 재생도 동일무관
버퍼 언더런, CPU 용량xrun 0, 부하 약 1.5/10스케줄링 문제(용량 아님)
앰프 보호(speakersafetyd)트립 0무관
저음 과다고역통과 250Hz로 올려도 여전저음 아님

정리하면 시그니처는 이렇다. 사인음은 깨끗하고, 복잡한 음악에서만 깨지고, 로그엔 흔적이 없다. 이 조합이 "하드웨어, 용량이 아니라 실시간 스케줄링"을 가리켰다.

6진짜 함정: restart 연타가 오디오를 죽인다

가장 값진 교훈은 정작 튜닝 과정에서 나왔다. 설정을 바꿔 볼 때마다 systemctl --user restart pipewire pipewire-pulse wireplumber를 짧은 간격으로 여러 번 눌렀는데, 이게 오디오를 통째로 죽였다.

systemd에는 짧은 시간 안의 재시작 횟수 상한이 있다(기본 10초에 5번, 수동 start에도 적용된다). 이걸 넘기면 systemd가 추가 기동을 거부해 wireplumber가 다시 뜨지 못하고, 그 결과 진짜 스피커 싱크가 만들어지지 않아 기본 출력이 Dummy Output만 남아 무음이 된다. 더 심하면 PipeWire 데몬까지 무응답으로 멈춘다.

불안정의 원인은 새 설정이 아니라 재시작 연타였다. 설정은 디스크에 적어 두고, 적용은 재부팅(또는 reset-failed 후 딱 한 번의 재시작)으로 한다. 그러면 같은 설정도 멀쩡하게 돌아간다.

7복구 런북: 재부팅 전에

소리가 안 나거나 출력이 Dummy로 잡히면, 재부팅 전에 낮은 단계부터 하나씩 시도한다. 핵심은 연속 재시작 금지다.

표 3. 단계별 복구.

단계증상조치
1. wireplumber만 죽음failed + Dummyreset-failedstart(restart 아님)
2. 데몬 멈춤wpctl이 무응답정확한 이름으로 종료 후 소켓에서 재기동, 기본 싱크를 convolver로 지정
3. 디지털은 정상, 소리만 없음체인은 붙었는데 무음sudo systemctl restart speakersafetyd(스피커 보호 데몬, 앰프 상태 재초기화)
4. 그래도 무음커널/앰프가 멈춤재부팅(부끄러운 일 아니다)

2단계 명령은 이렇다.

systemctl --user reset-failed pipewire pipewire-pulse wireplumber
# ★ -x(정확한 이름)만. -f(명령줄 매칭)는 자기 셸까지 죽여 명령이 끊긴다.
pkill -x wireplumber; pkill -x pipewire-pulse; pkill -x pipewire
systemctl --user start pipewire pipewire-pulse wireplumber
# 기본 싱크가 Dummy면 convolver로 지정. 검증 명령은 timeout 으로 감싼다(데몬이 멈춰 있을 수 있다).
CID=$(timeout 6 pw-dump | jq -r '.[]|select(.info.props."node.name"=="audio_effect.j314-convolver")|.id' | head -1)
wpctl set-default "$CID"

두 가지 함정을 특히 조심한다. pkill은 반드시 -x(정확한 프로세스 이름)로 쓴다. -f는 전체 명령줄을 매칭해서, 그 문자열이 든 래퍼 셸이나 실행 중인 명령열까지 걸려 명령이 끊길 수 있다. 그리고 데몬이 멈춰 있을 수 있으니 진단, 검증 명령은 timeout으로 감싸 무한 대기를 막는다.

8영속성 함정: 업데이트가 되돌린다

여기 미묘한 함정이 하나 더 있다. 새 v4.0 보정 파일은 패키지가 소유한 파일 위에 손으로 덮어쓴 것이다(2026-07-15 현재 내 저장소의 asahi-audio는 아직 3.4다). 그래서 패키지를 업데이트하면 다시 옛 3.4로 덮여 크래클이 재발한다. 저장소가 정식으로 4.0 이상을 담으면 이 문제는 사라진다.

그래서 v4.0 파일을 따로 보관하고, 업데이트 뒤 다시 적용하는 스크립트를 둔다. 더 나아가 패키지 설치 훅으로 자동화하면, 업데이트가 끝나는 즉시 보정을 되돌려 놓는다(패키지가 정식으로 4.0 이상이 되면 저절로 무력화된다). 성능코어 고정과 클럭 설정은 사용자 설정이라 업데이트와 무관하게 유지된다.

9막다른 길들: 되돌린 시도

깔끔한 세 원인, 세 처방으로 정리했지만, 거기 닿기까지는 여러 번 헛짚었다. 되돌린 시도를 남기는 건, 같은 길을 다시 걷지 않기 위해서다.

표 4. 시도했다가 되돌린 것들.

시도왜 해봤나결과
음원, 브라우저 배제 (yt-dlp로 받아 mpv 로컬 재생)파일이나 브라우저가 원인인지 격리로컬 재생도 동일 → 둘 다 무관. 배제엔 성공
EasyEffects로 EQ 얹기후처리로 덮어 보려 함원인이 보정 체인 자체라 헛다리. 군더더기만 늘림
min-quantum=2048 / quantum 4096 강제버퍼를 키우면 마감 여유가 생길까convolver와 충돌해 wireplumber 크래시. 금지로 분류
uclamp E-core 클램프 해제 (uclamp.max=1024)효율코어 편향을 직접 풀기방향은 맞았지만, 결국 성능코어 하드 고정(affinity)이 더 확실했다
rtkit를 뒤늦게 추가실시간 우선권을 주려고오히려 부팅 순서가 꼬여 wireplumber가 크래시, Dummy로. 근본 원인은 user@.serviceLimitRTPRIO=0였다
전부 되돌리고 stock 3.4로 롤백"우리가 얹은 게 원인인가"를 확정하려고순정에서도 크래클 재현 → 원인이 내 변경이 아님을 확정. 되돌리기가 진단이었다

가장 크게 배운 건 마지막 줄이다. 손댄 걸 전부 걷어내고 순정으로 돌리자, "순정에서도 나는가?"라는 질문에 답이 나왔다. 그렇다는 답은 원인을 소프트웨어 스택 자체로 좁혔고, 이후엔 배제표를 보며 이미 확인한 길을 다시 걷지 않을 수 있었다.

캡처 자체도 한 번 속였다. 드롭아웃을 녹음하는데 파형이 거의 무음으로만 잡힌 적이 있었다(raw_peak < 0.02). 재생 소리가 그 모니터 노드에 닿지 않은 것이라, 분석 스크립트가 "near-silent capture"라며 건너뛰게 만들어 뒀다. 측정도 검증 대상이라는 걸, 측정이 거짓말을 하고 나서야 배웠다.

10마치며

이 장비에서 맥북 스피커의 지지직은 결국 하드웨어 고장이 아니라, 리눅스가 애플 실리콘의 오디오를 macOS만큼 잘 챙기지 못해서 생긴 일이었다. 보정 프로파일을 최신으로, 오디오를 성능코어와 실시간 스케줄링으로, 스트림 전환을 부드럽게. 이 셋을 디스크에 적어 두고 재부팅으로 적용하면 스피커가 제 소리를 낸다. 그리고 튜닝하다 소리가 죽어도, 재시작을 연타하지 않고 단계별로 복구하면 대부분 재부팅 없이 돌아온다.

다만 완전히 사라지진 않았다. 평소에는 깨끗하고, 세 처방 전보다 훨씬 덜하지만, CPU가 크게 바쁠 때(무거운 앱이 성능코어를 함께 치는 상황) 여전히 이따금 "칙" 하는 소리가 섞인다. 실시간 우선권을 더 끌어올리는 여지가 남아 있고, 근본적으로는 배포판이 새 보정을 정식으로 담아 이 손질 전체가 필요 없어지는 게 가장 깔끔한 결말이다. 그때가 오면 손으로 덮어 둔 파일과 스크립트는 지우면 된다.