읽는데 9 분 정도 걸려요
2022년 4월 1일 오후 6:57에 작성됨.

2000년대 중후반부터 PC를 통한 음악 감상이 일반화되고, 2010년대에 접어들어서는 기존의 Hi-fi 시장이 급격하게 PC과 모바일 기반으로 이동하기 시작했다. 이런 추세에 맞추어 USB DAC같은 하드웨어나 OS가 오디오 구조를 바꿔나가는 등의 변화가 있었다.

오늘날 많은 사람들이 PC를 통한 Hi-fi 시스템을 구축하고 사용하지만 그들 대부분은 윈도우를 사용하며, 일부는 macOS를 사용한다. 그래서 리눅스에서의 Hi-fi에 대한 이야기가 많이 없기에 대충 정리해보고자 한다.

OS의 오디오 출력 구조

일단은 우리가 USB 오디오를 사용한다고 가정하자. 우리가 소리를 듣기 위해서는 최종적으로 USB DAC에 신호가 전달되어야 한다. 장치 제어와 같은 일은 OS의 커널이 해야 하는 일이고, “무슨 신호를 어떻게” 장치에 전달할지를 정의하는 것은 드라이버의 역할이다. 그래서 커널의 USB를 제어하는 부분을 반드시 거치게 된다. 이 부분에 대해서는 많은 것들이 표준화되어 있어 어떤 제품이 들어가도 원하는 대로 동작하게 할 수 있다. 하지만 OS가 표준을 어디까지 구현하는지는 강제된 것이 아니기 때문에 문제가 생길 수 있는데, 윈도우 10 초창기까지는 USB Audio class 2에 대한 드라이버가 내장되지 않았기 때문에 24비트/192khz 출력, 비동기 오디오 스트리밍과 같은 몇몇 주요 기능들을 사용할 수 없었다. 하지만 현재는 우리가 보통 쓰는 OS들은 다 이정도 표준은 구현하고 있으니 신경을 안 써도 될 것이다. 하지만 제조사가 별도의 드라이버를 제공하는 경우가 있는데, 이것은 USB 오디오 표준 이외의 기능을 구현하기 위해서이다. 이를테면 하드웨어 볼륨 컨트롤을 좀 더 세세하게 나누거나, PC에서 디지털 필터를 조정한다거나, DAC의 설정을 바꾼다거나, ASIO같은 것을 구현한다거나 하면 USB 오디오 표준 외적인 내용이기 때문에 장치 제조사가 알아서 구현해야 한다.

근데 USB를 직접 제어한다면 소프트웨어 개발 난이도가 지옥으로 향하게 된다. DOS시절에는 당연하게 그래야 했지만 사용자가 어떤 장치를 연결하느냐에 따라서 소프트웨어를 각자 짜야 한다면 파편화가 심해질 것이기 때문에 OS는 어떤 하드웨어를 사용하더라도 같은 방법으로 접근할 수 있게 추상화 계층을 제공한다. 이를테면 ‘USB 장치 A에 데이터 “XXX”를 전송한다’란 것보다는 ‘오디오 장치 A에 24비트, 48khz로 구성된 오디오 스트림 “XXX”를 재생한다’가 더 편할 것이다. 윈도우에서는 WASAPI의 일부 부분, 리눅스에서는 ALSA가 이 역할을 수행하고 있다. (꼭 그렇지는 않지만) 여기 부분까지로 가면 하드웨어와 1:1로 소통할 수 있어 데이터에 대한 OS의 변경을 최소화할 수 있다. 쉽게 말해서 비트 퍼펙트가 가능하다는 뜻이다. 그래서 많은 Hi-fi 가이드는 OS의 기타 기능들을 우회하고 이 수준에서 오디오를 출력하게 설정한다.

물론 PC를 음악 재생기로만 쓴다면 이렇게 해도 큰 문제가 없다. 하지만 생각할 수 있는 몇 가지 문제가 있는데, 만약 재생하고자 하는 오디오 스트림을 해당 오디오 장치가 지원하지 않는다면 문제가 생긴다. 이를테면 많은 USB DAC는 PCM 형식의 오디오 스트림을 재생 가능하지만 돌비 디지털이나 AAC 형식의 스트림을 재생하지는 못한다. 많은 음악 형식이 44.1khz의 샘플링 레이트를 가지고 있지만 삼성 갤럭시의 많은 기기들은 44.1khz를 지원하지 않기 때문에 재생할 수 없다. 애플 USB C DAC를 사용한다면 24비트 96khz를 비롯한 고해상도 음원을 재생할 수 없다.

게다가 게임을 하면서 보이스챗을 하거나, 음악 재생 중에 알림이 오거나 하는 일이 생긴다면 이런 “저수준” API로는 하드웨어를 물고 있어야 하기 때문에 우선권을 다른 기기들에게 전달할 수가 없다. (물론 ALSA를 잘 설정한다면 가능하긴 하지만 그다지 좋은 사용법은 아니다.) 따라서 샘플링레이트 컨버터, 오디오 믹서, 세션 매니저와 같은 역할을 시키기 위한 또 하나의 추상화 계층이 필요하며, 우리는 이걸 ‘오디오 서버’라고 부른다.

Pulseaudio

흔히 그저 Pulse라고 부르는 Pulseaudio는 오랜 시간 동안 리눅스의 사실상 표준 오디오 서버로 사용되어 왔다. 현 시점에서 우분투나 데비안와 같은 배포판을 깔게 되면 애플리케이션 입장에서의 오디오 출력은 Pulseaudio에게 하게 된다. Pulseaudio는 위에 말한 믹서, 샘플링레이트 컨버터, 세션 매니저의 역할을 그저 잘 수행하기 때문에 그냥 사용한다면 별 문제가 없다. (이건 윈도우의 WASAPI도 마찬가지이다.) 하지만 기본 설정에 만족하지 못하는 사람들은 최대한 높은 퀄리티를 구현하기 위해 몇 가지 설정을 바꾸게 된다.

우선 Pulseaudio의 기본 샘플링레이트 변환기는 Speex코덱의 샘플링레이트 변환기를 사용하게 설정되어 있다. 기본 설정으로는 음질보다는 변환 속도와 최적화에 신경쓰고 있기 때문에(PC뿐만이 아니라 Arm보드와 같은 저사양 기기에서도 돌아가야 하기 때문이다) 대충 아래와 같은 느낌으로 출력하게 된다.

src.infinitewave.ca 를 참조한 Speex (Q1)의 Sweep 파형
src.infinitewave.ca 를 참조한 Speex (Q1)의 주파수 응답

단순히 주파수를 스윽 올리기만 해도 노이즈가 좀 많이 생기는 것을 볼 수 있다. 일단 고주파 대역을 깎아먹고, 저정도로 큰 노이즈는 충분히 청음가능하다. 이 상태로는 WASAPI에 비해 “좋다”고 보기 힘들다. 따라서 사람들은 대신 Sox 리샘플러를 사용하곤 한다.

src.infinitewave.ca 를 참조한 Sox (VHQ)의 Sweep 파형
Bottom Image
src.infinitewave.ca 를 참조한 Sox (VHQ)의 주파수 응답

Sox를 사용하게 되면 거의 완벽한 결과물을 보여 준다. 이 정도라면 리샘플링 손실이 거의 없다고 봐도 될 정도이다. 그래서 이 정도 설정으로만 해 놓고 써도 별 문제는 없지만 최대한 손실을 없게 하고 싶은 사람들은 avoid-resampling 옵션을 켜게 된다.

Pulseaudio 11버전부터 추가된 이 기능은 “메인 샘플링 레이트”와 “대체 샘플링 레이트”를 설정한 다음 대체 샘플링 레이트로 설정하는 게 더 이득인 상황에는 대체 샘플링 레이트로 재생하게 된다. 이를테면 메인 샘플링 레이트가 48khz로 설정되어 있지만 44.1khz의 음원을 재생할 때에는 샘플링 레이트 변환을 수행하지 않는 것이 더 이득이기 때문에 샘플링 레이트를 44.1khz로 설정한다. 하지만 48khz로 뭔가를 재생하고 있을 때에는 44.1khz 신호가 입력되더라도 “이미 장치가 물린 다음”이기 때문에 44.1khz 신호를 48khz로 리샘플링해서 믹서로 보낸다.

그래서 일상적인 상황에서는 리샘플링을 최대한 피할 수 있지만 이것도 몇 가지 문제가 있다. 첫번째로 이것은 2022년 초 시점에서 작동하지 않는다. ALSA와 관련된 커널 버그 때문에 작동하지 않는다는 보고가 올라와 있고, 언젠가는 해결되겠지만 당분간은 이대로 사용하게 될 것이다. 두번째로는 대체 주파수를 하나만 지정할 수 있다는 것이다. 만약 96khz와 같은 고해상도 음원을 재생하게 된다면 이건 꼼짝없이 리샘플링을 하게 되거나, 다른 주파수를 포기해야 할 것이다.

Pipewire

Pipewire는 Pulseaudio를 대체하기 위한 오디오 서버이다. 시스템 어드민의 입장에서 보면 Pulse보다 가볍고 레이턴시가 적다는 특징이 있고, 좀 더 건드리는 부분이 적다. 하지만 오디오파일의 입장에서 보면 좀 재밌는 일을 할 수가 있다.

우선 Pipewire도 기본적인 아이디어는 비슷하다. 기본 샘플링 레이트와 대체 샘플링 레이트를 설정해서 가장 최적인 상황을 찾는데, Pipewire는 pulse와는 다르게 대체 샘플링 레이트가 다양하다는 특징이 있다. 예를 들면 기본 샘플링 레이트를 44.1khz로 설정하고, 대체 샘플링 레이트를 48khz, 96khz, 192khz로 설정하게 되면 사실상 현존하는 대부분의 PCM 음원은 리샘플링 없이 재생할 수 있다.

그리고 어떤 상황에서는 Pulse보다 좀 더 안정적이다. 이를테면 현재 Wine을 Pulseaudio를 경유해서 사용할 때, USB DAC를 사용한다면 레이턴시와 관련해서 오디오가 끊긴다. (이것도 역시 커널 단의 버그와 엮여 있다.) 하지만 Pipewire는 이런 문제가 없다.

또한 세션 매니저를 분리해서 사용하는데, Wireplumber와 같은 물건을 쓴다면 다양한 애플리케이션과 연동해서 Geek들이 좋아할 설정을 할 수 있다. 이를테면 Bluez와 연동해서 특정 블루투스 이어폰을 쓸 때는 AAC나 LDAC로 출력한다거나, 샘플링 레이트를 조정한다거나 하는 등의 일들을 할 수 있다. 특정 애플리케이션에서 들어오는 신호에게만 이펙트를 먹인다거나, 샘플링 레이트를 고정한다거나 하는 일들도 할 수 있다. (설정의 귀찮음을 감수한다면) 원하는 목적에 따라 유연하게 쓸 수 있다는 것이다.

Pipewire도 기본 리샘플러로 Speex코덱의 리샘플러를 사용하는데, 기본값은 퀄리티 4이고, 14까지 늘릴 수 있다.

Top Image
src.infinitewave.ca 를 참조한 Speex (Q10)의 Sweep 파형
Top Image
src.infinitewave.ca 를 참조한 Speex (Q10)의 주파수 응답
Bottom Image
src.infinitewave.ca 를 참조한 Speex (Q14)의 Sweep 파형
Bottom Image
src.infinitewave.ca 를 참조한 Speex (Q14)의 주파수 응답

Q10을 사용하다고 하면 사실상 청음하지 못할 정도의 노이즈가 발생하고 (보라색으로 보이는 영역은 -120db정도로의 크기로서 사실상 청음하기 힘들다.), Q14는 Sox보다는 못하지만 극히 적은 리샘플링 노이즈를 가지고 있다. 하지만 기본적으로는 리샘플링을 하지 않게 설정해서 쓰는 오디오파일들은 이런 리샘플링 노이즈 없이 사실상 비트퍼펙트에 가까운 퀄리티를 사용할 수 있을 것이다.

참고해야 할 점은 Speex 리샘플러의 Q14모드는 자원을 꽤나 많이 먹는다. (라이젠 2600 기준으로 5%가량 먹는다는 보고가 있다.)

Pipewire 사용하기

Pipewire는 페도라와 같은 배포판에서는 기본으로 채용되었고, 다른 배포판들도 쉽게 적용할 수 있다. Pipewire는 기본적으로 Pulseaudio의 API와 호환되기 때문에 애플리케이션 부분에서는 따로 건드릴 필요가 없다는 점도 중요하게 작용한다.

배포판에 따라서 pipewire, pipewire-pulse 패키지를 깔게 되면 pulseaudio를 삭제하고 이것으로 대체될 것이다. 만약 기본 설정으로 쓴다면 건드릴 필요가 없겠지만, 우리가 원하는 결과를 얻기 위해서 몇 가지 설정을 바꿀 필요가 있다.

Pipewire는 유저별로 동작하기 때문에 ~/.config/pipewire 폴더에 설정파일들을 놓으면 되는데, 기본적으로는 Pipewire 위키를 참조하는 게 편하다. 여기서는 몇 가지 수정할 법한 부분들을 소개하겠다.

pipewire.conf

default.clock.rate          = 48000
## 기본 샘플링 레이트 설정이다. 48000hz가 기본값이다.
default.clock.allowed-rates = [ 44100 48000 96000 ]
## 대체 샘플링 레이트 설정이다. 기본적으로는 꺼져 있지만 설정을 바꿀 수 있는데, 자기가 사용하는 장치들의 공통분모를 뽑아내는 것을 권장한다. (기기별 설정을 이후에 바꿀 수는 있다.)
default.clock.quantum       = 1024
default.clock.min-quantum   = 16
default.clock.max-quantum   = 2048
default.clock.quantum-limit = 8192
## 레이턴시와 연관된 설정 값들이다. 기본적으로는 건드릴 필요가 없지만 소리가 끊기거나, 좀 더 적은 레이턴시를 원할 때 사용할 수 있다. 이것들은 기본적으로 몇 샘플들을 버퍼에 저장할지와 연관되어 있는데, 48000hz의 샘플링 레이트를 사용할 때 0.3ms에서 42ms까지 유동적으로 버퍼를 조정한다는 의미이다. 음악을 재생할 거라면 의미가 없지만 10ms정도의 레이턴시라면 아주 짧고, 5ms정도라면 실시간으로 연주하는 환경이라도 거의 감지할 수 없는 수준이다.

pipewire-pulse.conf

resample.quality      = 14
## 리샘플러 품질 설정이다. 기본값은 4로 설정되어 있고, 이게 문제가 있다고 생각한다면 10이나 14 정도로 올려서 쓰는 것도 좋다.

Wireplumber

Wireplumber는 pipewire를 위한 세션 관리자이다. Pipewire가 오디오 스트림을 관리한다면 이것은 장치를 관리하는데, 각각의 장치별로 pipewire의 설정을 조정할 수 있다. Lua기반 스크립팅을 사용하므로 간단한 설정 이외의 것을 하고자 한다면 Lua를 배울 필요가 있다.

이를테면 위에서 이야기했던 HUD100에 하드웨어 볼륨을 적용하고, 리샘플링 퀄리티를 높이고 샘플링 레이트를 조정하고자 한다면 udev를 수정하지 않고 alsa_monitor.rules을 아래와 같이 설정하면 된다.

  {
    matches = {
      {
        { "device.nick", "matches", "earstudio HUD100" },
      },
    },
    apply_properties = {
      ["device.profile-set"] = "radsone-hud100.conf",
      ["audio.allowed-rates"]    = "44100, 48000, 96000, 192000, 384000",
      ["audio.format"]           = "S32LE",
      ["api.alsa.use-ucm"]       = false,
      ["resample.quality"]       = 14,
    },
  },

모니터링하기

Pipewire는 몇 가지 툴을 제공하는데, 설정이 의도한 대로 잘 돌아가는지를 보고 싶으면 pw-cli ls를 사용해서 현재 설정값들을 볼 수 있다. 현재 재생 상태를 보고 싶으면 pw-top를 사용할 수 있다.

pw-top을 사용한 예

위 그림을 보면 파이어폭스에서 48000hz로 오디오를 출력하고 있고, 이것이 HUD100으로 나가고 있는 것을 볼 수 있다.