ASAHI, 오디오
M1 Max 맥북 프로를 Asahi Linux(Omarchy)로 쓰면, 내장 스피커가 복잡한 음악에서 "칙칙" 끓고 순간순간 끊긴다. 적어도 이 장비에서는 하드웨어 고장이 아니라 세 겹의 소프트웨어 문제였다. 세 곳을 손보면 크게 줄지만, 완전히 사라지지는 않았다. 원인과 처방, 남은 문제, 그리고 macOS는 왜 멀쩡한지.
사인파 한 음을 재생하면 깨끗하다. 그런데 악기가 여럿 겹치는 음악이나 영상 소리를 틀면 "칙/틱" 하는 잡음이 섞이고, 이따금 소리가 순간(약 21밀리초) 뚝 끊긴다. 로그에는 아무 흔적도 없다. ALSA 언더런(xrun) 카운터는 0, CPU 부하도 한가하다.
흔적 없는 고장은 방향을 흐린다. 음원이 문제인지, 브라우저인지, 앰프인지, 커널인지. 하나씩 잘라내고 나니, 내 환경에서는 원인이 하드웨어 고장이 아니라 소프트웨어 오디오 경로에 있었다. 그것도 서로 다른 세 곳에.
맥북의 작은 내장 스피커는 그대로 울리면 얇고 저음이 없다. macOS든 Asahi든, 스피커로 나가기 전에 소리를 한 번 보정한다. 가상 오디오 장치 위에 FIR/임펄스 응답으로 주파수 특성(EQ)을 바로잡고, Bankstown 같은 DSP 플러그인으로 저역, 라우드니스까지 보정하는 체인을 건다. Asahi에서는 asahi-audio 패키지가 이 보정을 j314-convolver라는 필터 노드로 PipeWire 그래프에 끼워 넣는다.
즉 앱에서 스피커까지 소리는 여러 단계를 지난다. 아래 그림에서 각 단계를 눌러, 무슨 일을 하고 어디서 무엇이 깨지는지 확인한다. 갈색으로 표시된 단계가 이번에 문제가 났던 곳이다.
그림 1. 앱에서 스피커까지의 오디오 경로. 이 장비에서 크래클은 하드웨어가 아니라 중간 세 단계(스트림 전환, PipeWire 스케줄링, DSP 보정)에서 났다.
증상은 하나처럼 들렸지만 원인은 셋이었고, 각각 다른 곳을 고쳐야 했다.
표 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 -e나 cpu_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
같은 하드웨어인데 macOS에서는 이런 일이 없다. CoreAudio의 실시간 오디오 스레드가 오디오 워크그룹에 합류해, 커널과 성능 컨트롤러가 오디오의 마감(deadline)을 알고 CPU 배정, 주파수를 결정하기 때문이다. 그래서 같은 하드웨어에서도 마감 초과가 잘 드러나지 않는다. Asahi는 반대로 전력을 위해 오디오를 효율코어 쪽에 밀어 둔다. 그래서 리눅스에서는 오디오를 마감 중심으로 다루도록 손으로 재현해야 한다. 성능코어 고정과 rtkit 우선권이 그 자동 배정을 손으로 흉내 내는 방법이다.
"칙칙"은 주관적이라 고쳤는지 아닌지 귀로만 판단하기 어렵다. 그래서 보정을 거친 뒤의 소리를 그대로 녹음해, 파형에서 끊김을 기계적으로 찾았다.
# 보정(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로 올려도 여전 | 저음 아님 |
정리하면 시그니처는 이렇다. 사인음은 깨끗하고, 복잡한 음악에서만 깨지고, 로그엔 흔적이 없다. 이 조합이 "하드웨어, 용량이 아니라 실시간 스케줄링"을 가리켰다.
가장 값진 교훈은 정작 튜닝 과정에서 나왔다. 설정을 바꿔 볼 때마다 systemctl --user restart pipewire pipewire-pulse wireplumber를 짧은 간격으로 여러 번 눌렀는데, 이게 오디오를 통째로 죽였다.
systemd에는 짧은 시간 안의 재시작 횟수 상한이 있다(기본 10초에 5번, 수동 start에도 적용된다). 이걸 넘기면 systemd가 추가 기동을 거부해 wireplumber가 다시 뜨지 못하고, 그 결과 진짜 스피커 싱크가 만들어지지 않아 기본 출력이 Dummy Output만 남아 무음이 된다. 더 심하면 PipeWire 데몬까지 무응답으로 멈춘다.
불안정의 원인은 새 설정이 아니라 재시작 연타였다. 설정은 디스크에 적어 두고, 적용은 재부팅(또는 reset-failed 후 딱 한 번의 재시작)으로 한다. 그러면 같은 설정도 멀쩡하게 돌아간다.
소리가 안 나거나 출력이 Dummy로 잡히면, 재부팅 전에 낮은 단계부터 하나씩 시도한다. 핵심은 연속 재시작 금지다.
표 3. 단계별 복구.
| 단계 | 증상 | 조치 |
|---|---|---|
| 1. wireplumber만 죽음 | failed + Dummy | reset-failed 후 start(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으로 감싸 무한 대기를 막는다.
여기 미묘한 함정이 하나 더 있다. 새 v4.0 보정 파일은 패키지가 소유한 파일 위에 손으로 덮어쓴 것이다(2026-07-15 현재 내 저장소의 asahi-audio는 아직 3.4다). 그래서 패키지를 업데이트하면 다시 옛 3.4로 덮여 크래클이 재발한다. 저장소가 정식으로 4.0 이상을 담으면 이 문제는 사라진다.
그래서 v4.0 파일을 따로 보관하고, 업데이트 뒤 다시 적용하는 스크립트를 둔다. 더 나아가 패키지 설치 훅으로 자동화하면, 업데이트가 끝나는 즉시 보정을 되돌려 놓는다(패키지가 정식으로 4.0 이상이 되면 저절로 무력화된다). 성능코어 고정과 클럭 설정은 사용자 설정이라 업데이트와 무관하게 유지된다.
깔끔한 세 원인, 세 처방으로 정리했지만, 거기 닿기까지는 여러 번 헛짚었다. 되돌린 시도를 남기는 건, 같은 길을 다시 걷지 않기 위해서다.
표 4. 시도했다가 되돌린 것들.
| 시도 | 왜 해봤나 | 결과 |
|---|---|---|
음원, 브라우저 배제 (yt-dlp로 받아 mpv 로컬 재생) | 파일이나 브라우저가 원인인지 격리 | 로컬 재생도 동일 → 둘 다 무관. 배제엔 성공 |
| EasyEffects로 EQ 얹기 | 후처리로 덮어 보려 함 | 원인이 보정 체인 자체라 헛다리. 군더더기만 늘림 |
min-quantum=2048 / quantum 4096 강제 | 버퍼를 키우면 마감 여유가 생길까 | convolver와 충돌해 wireplumber 크래시. 금지로 분류 |
uclamp E-core 클램프 해제 (uclamp.max=1024) | 효율코어 편향을 직접 풀기 | 방향은 맞았지만, 결국 성능코어 하드 고정(affinity)이 더 확실했다 |
| rtkit를 뒤늦게 추가 | 실시간 우선권을 주려고 | 오히려 부팅 순서가 꼬여 wireplumber가 크래시, Dummy로. 근본 원인은 user@.service의 LimitRTPRIO=0였다 |
| 전부 되돌리고 stock 3.4로 롤백 | "우리가 얹은 게 원인인가"를 확정하려고 | 순정에서도 크래클 재현 → 원인이 내 변경이 아님을 확정. 되돌리기가 진단이었다 |
가장 크게 배운 건 마지막 줄이다. 손댄 걸 전부 걷어내고 순정으로 돌리자, "순정에서도 나는가?"라는 질문에 답이 나왔다. 그렇다는 답은 원인을 소프트웨어 스택 자체로 좁혔고, 이후엔 배제표를 보며 이미 확인한 길을 다시 걷지 않을 수 있었다.
캡처 자체도 한 번 속였다. 드롭아웃을 녹음하는데 파형이 거의 무음으로만 잡힌 적이 있었다(raw_peak < 0.02). 재생 소리가 그 모니터 노드에 닿지 않은 것이라, 분석 스크립트가 "near-silent capture"라며 건너뛰게 만들어 뒀다. 측정도 검증 대상이라는 걸, 측정이 거짓말을 하고 나서야 배웠다.
이 장비에서 맥북 스피커의 지지직은 결국 하드웨어 고장이 아니라, 리눅스가 애플 실리콘의 오디오를 macOS만큼 잘 챙기지 못해서 생긴 일이었다. 보정 프로파일을 최신으로, 오디오를 성능코어와 실시간 스케줄링으로, 스트림 전환을 부드럽게. 이 셋을 디스크에 적어 두고 재부팅으로 적용하면 스피커가 제 소리를 낸다. 그리고 튜닝하다 소리가 죽어도, 재시작을 연타하지 않고 단계별로 복구하면 대부분 재부팅 없이 돌아온다.
다만 완전히 사라지진 않았다. 평소에는 깨끗하고, 세 처방 전보다 훨씬 덜하지만, CPU가 크게 바쁠 때(무거운 앱이 성능코어를 함께 치는 상황) 여전히 이따금 "칙" 하는 소리가 섞인다. 실시간 우선권을 더 끌어올리는 여지가 남아 있고, 근본적으로는 배포판이 새 보정을 정식으로 담아 이 손질 전체가 필요 없어지는 게 가장 깔끔한 결말이다. 그때가 오면 손으로 덮어 둔 파일과 스크립트는 지우면 된다.