habrahabr

Как я осовременивал двадцатилетний домашний кинотеатр, или как упасть в глазах аудиофилов

  • среда, 4 декабря 2024 г. в 00:00:09
https://habr.com/ru/articles/861748/

Привет, Хабр! Мне вот сказали, что было бы интересно почитать тут статью о моих приключениях с доставшимся мне по наследству Philips LX8300SA, как я его пытался осовременить для использования с моим Mac. Поэтому я и решил попробовать себя в написании своей первой тут статьи :)

Главный герой этой статьи, Philips LX8300SA
Главный герой этой статьи, Philips LX8300SA

Как все начиналось

Отрыл у нас в квартире эту вундервафлю, попытался подключить - динамики показались вполне неплохими. Быстренько собрал полную конфигурацию у себя на столе, звук понравился больше, чем у недавно проданной тогда Яндекс Станции 2. К сожалению, фото уже не найду, но расстановка 5.1 там была нарушена полностью. Сделав AirPlay через ShairPort-Sync, слушал на нем треки из Apple Music. А потом мне захотелось большего - наконец понять, правда ли треки в Dolby Atmos так хороши.

Скрытый текст

Я прекрасно знаю, что 5.1(.0) система недостаточна для Dolby Atmos. Но, даже в документации Dolby по конфигурациям аудиосистем, есть отступление, что в системах без потолочных/up-firing динамиков можно использовать "виртуальный" Atmos.

Up-firing динамики - это колонны, так же играющие звук в потолок под определенным углом. Ими можно отчасти заменить потолочные динамики в конфигурациях Dolby Atmos.

Расстановка динамиков

Как я уже написал выше, изначально все это добро просто стояло на столе. Никакой схемы расстановки, upmix делал Dolby Pro Logic II, встроенный в DSP домашнего кинотеатра. В общем, дикий колхоз и 0 смысла от 5.1 конфигурации. Ниже показана авторская репрезентация того, как это было.

Мама, я - графический дизайнер!
Мама, я - графический дизайнер!

После того, как я захотел испытать на слух нормальный 5.1, я сделал нормальную расстановку по гайдам Dolby. Задние динамики разнесены по краям комнаты (RL - с края дивана, RR - на коробке от штатива у стенки), стол стоит по центру комнаты (стены комнаты), напротив дивана, на его краях FL и FR динамики, а по центру стола - сабвуфер (LFE). Фото задних динамиков не будет, простите. Мне слишком стыдно показывать колхоз, на котором они держатся. Передние динамики немного повернуты, чтобы быть более направленными в сторону слушателя.

Расстановка динамиков на столе
Расстановка динамиков на столе

Передача 6-канального звука на двадцатилетний домашний кинотеатр с современной машины

Самая интересная часть - как же передавать многоканальный звук на данный AV ресивер с современного железа? Для начала - аналоговых входов на каждый канал у него нет.

Порты LX8300SA
Порты LX8300SA

Да и ALC662 на борту моего Linux сервера - довольно посредственный кодек. Поэтому вариант с передачей аналогового звука я отбросил сразу же. Остается только коаксиальный S/PDIF. Какие же кодеки мы имеем на борту?

  • PCM 2.0 (заявлены 48kHz/24bit, но, если верить ALSA, спокойно принимает и 96kHz/32bit)

  • Dolby Digital (AC-3) 5.1 до 640 Kbps

  • DTS 5.1 до 768 Kbps

Скрытый текст

Потолок у Dolby AC-3 по битрейту - 640 Kbps даже на бумаге, так что тут с моими опытами все ясно.

Что же не так с DTS? По умолчанию, DTS на DVD шел с битрейтом в 1536 Kbps. Это позволяло получить довольно приличное качество для 6-канального lossy кодека. Пропатчив dcaenc (прозрачный энкодер DTS для ALSA), опытным путем выяснил, что максимальный битрейт без артефактов - 768Kbps. Дело, скорее всего, не в подключении. Битрейт того же PCM 2.0 48kHz / 24 bit уже равен 48 kHz * 24 bit * 2 ch = 2304 Kbps. И он работает нормально.

Самый простой вариант применить эти кодеки, который я нашел - использовать Linux сервер для прозрачного энкодинга PCM 5.1 в Dolby AC-3 / DTS. Я использую Fedora, установка плагина ALSA для AC-3 была довольно простой:

sudo dnf install alsa-plugins-a52

Далее я поднял битрейт в стандартном конфиге A52 по пути /etc/alsa/conf.d/60-a52-encoder.conf:

...
@args.BITRATE {
  type integer
  default 640
}
...

Качество звука при проигрывании тестовых файлов через mpv меня устраивало. С DTS оно было хуже, поэтому я остановился на Dolby AC-3. Но теперь возник вопрос: как передать 5.1 звук с Mac на Linux, причем так, чтобы Mac видел это безобразие как 5.1 звуковое устройство, чтобы декодер Atmos в Apple Music правильно раскидывал звук на динамики?

Стриминг 5.1 звука по сети с Mac на Linux

С первого взгляда нетривиальная задача. Давайте посмотрим найденных мною кандидатов, а именно способы стриминга между платформами в целом:

  • NetJACK2. Хороший вариант, если бы сервер был у меня в LAN. К сожалению, весь трафик роутера идет через сервер, он стоит в WAN, поэтому мультикаст добраться до локальной сети не сможет.

  • ROC: Новый способ передачи звука по сети, имеет удобные утилиты и низкую задержку. Но не поддерживает 5.1 звук (есть незакрытый Issue с полупустым чеклистом на момент написания статьи).

  • AirPlay: Не поддерживает Dolby Atmos стриминг (хоть и есть движения в этом направлении).

Задуманные для этого добра методы нам не подходят. Итак, пришло время делать костыли - за протокол был взят PulseAudio, который умеет прозрачно передавать звук по TCP. Для включения этого протокола на PipeWire требуется создать файл ~/.config/pipewire/pipewire-pulse.conf.d/network.conf :

pulse.properties = {
    server.address = [
        "unix:native"
        "tcp:192.168.128.1:4656"
    ]
}

192.168.128.1 здесь - локальный IP самого сервера.

Теперь поговорим о стороне клиента. Устанавливаем PulseAudio на macOS (да, я сам был удивлен, что это - кроссплатформа):

brew install pulseaudio

Но тут возникает проблема. PulseAudio на macOS не умеет работать с CoreAudio (подсистемой аудио на Mac), в результате чего устройств из PA мы не видим. Поэтому я пошел пересобирать BlackHole (Open-Source loopback драйвер под Mac). Устанавливаем Xcode Command Line Tools, скачиваем репозиторий и собираем с изменненой конфигурацией (названия изменены для отсутствия конфликтов с обычным BlackHole) без codesign:

xcodebuild CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO \
  -project BlackHole.xcodeproj \
  GCC_PREPROCESSOR_DEFINITIONS='$GCC_PREPROCESSOR_DEFINITIONS
  kLatency_Frame_Size=4096 kNumber_Of_Channels=6 kSampleRates=44100,48000
  kDevice_IsHidden=false kDevice_HasInput=true kDevice_HasOutput=false
  kDevice_Name="PhilipsCapture"
  kDevice2_Name="Philips5.1"
  kDevice2_IsHidden=false kDevice2_HasInput=false kDevice2_HasOutput=true'

Далее подписываем драйвер для использования локально на вашем Mac, чтобы Gatekeeper не делал нам мозг:

sudo codesign --force --deep --sign - build/Release/BlackHole.driver

Следующий вопрос - чем писать звук в pipe? Самым стабильным вариантом у меня оказался sox. Устанавливаем:

brew install sox

После этого я решил на скорую руку сделать скрипт для быстрого включения всего этого безобразия. Пройдемся по основным частям.

Аргументы запуска. Из основных для себя настроек я выделил звуковую схему (5.1 / 2.0) и тип моего расположения в данный момент (диван/стол). О расположении поясню далее.

#!/bin/bash

# Sound stage mode:
# 5.1 / 2.0
MODE="$1"
[ -z "$MODE" ] && MODE="5.1"

# 5.1 mode:
# table / couch
MODE51="$2"
[ -z "$MODE51" ] && MODE51="couch"

# SSH target
TARGET="tdrk@supercomputer.local"

Адаптация громкости динамиков под моё расположение. Иногда я люблю слушать музыку за столом, сидя на кресле, а иногда сидя на диване. Понятное дело, что расстояния между мною и передними / задними динамиками при смене моей позиции сильно меняются. Это чинится с помощью громкости и задержки на ближайших к слушателю динамиках (так уж вышло, что скорость звука не такая и высокая). Задержку я так и не понял как реализовать на PipeWire, так что ее я пропустил. Подобрал вот такие значения опытным путем:

51prepare() {
    # 0 - 65535
    if [ "$MODE51" = "table" ]; then
        VOL_FS=53768
        VOL_FC=50368
        VOL_RS=65535
        VOL_LFE=65535
    elif [ "$MODE51" = "couch" ]; then
        VOL_FS=65535
        VOL_FC=65535
        VOL_RS=60535
        VOL_LFE=65535
    else
        echo "Unknown 5.1 mode - $MODE51"
        exit 1
    fi

    # FL FR RL RR FC LFE
    ssh "$TARGET" "pactl set-sink-volume $PADEVICE $VOL_FS $VOL_FS $VOL_RS $VOL_RS $VOL_FC $VOL_LFE" 
}

Подготовка клиента и сервера к проигрыванию. Необходимо выбрать нужный профиль S/PDIF и нужное звуковое устройство на Mac. Для этого надо скачать еще одну маленькую утилиту:

brew install switchaudio-osx

В коде все выглядит так:

setprofile() {
    ALSA_PCI="pci-0000_00_1b.0"

    ssh "$TARGET" "pactl set-card-profile alsa_card.$ALSA_PCI output:$1"
    export PADEVICE="alsa_output.$ALSA_PCI.$1"
    sleep 0.2 # Let AV receiver detect Dolby Digital / PCM output
}

prepare() {
    if [ "$MODE" = "2.0" ]; then
        setprofile "iec958-stereo"
        DEVICE="BlackHole 2ch"
    elif [ "$MODE" = "5.1" ]; then
        setprofile "iec958-ac3-surround-51"
        DEVICE="Philips5.1"
        51prepare # Adjust volumes depending on listener position
    else
        echo "Unknown sound stage $MODE"
        exit 1
    fi
    export PADEVICE
    SwitchAudioSource -s "$DEVICE"
}

Стриминг. Sox пишет данные из CoreAudio, отправляя их в pipe в формате PCM 5.1 (48kHz, S32LE). В аргументы функции пишем Loopback устройство на вход и маппинг каналов в формате PulseAudio.

run() {
    PASERVER="192.168.128.1:4656"
    CAP_DEVICE="$1"
    CHANNEL_MAP="$2"

    # Space acts as a +1
    CHANNELS=$(printf '%s ' `sed 's/[^,]//g' <<< "$CHANNEL_MAP"` | wc -c)
    sox \
        -r 48000 -b 32 -c $CHANNELS -e signed-integer \
        -t coreaudio "$CAP_DEVICE" \
        -p | paplay \
            -s "$PASERVER" -d "$PADEVICE" -v --raw \
            --rate 48000 --format s32le --channels $CHANNELS \
            --channel-map="$CHANNEL_MAP"
}

Далее делаем простую функцию для выбора этих аргументов на базе выбранной звуковой схемы:

pick() {
    case "$MODE" in
    "5.1") run \
        "PhilipsCapture" \
        "front-left,front-right,front-center,lfe,rear-left,rear-right"
    ;;
    "2.0") run \
        "BlackHole 2ch" \
        "front-left,front-right"
    ;;
    *) echo "Unknown sound stage $MODE"
       exit 1
    ;; 
    esac
}

Заходим в Audio MIDI Setup и выставляем каналы и частоту дискретизации как указаны ниже.

Скрытый текст

48000 Гц

1 - передний левый
2 - передний правый
3 - передний центральный
4 - сабвуфер
5 - задний левый
6 - задний правый

Заканчивается скрипт простым while true для автоматического перезапуска всей системы при обрывах соединения:

prepare
while true; do
pick
sleep 0.1
done

Полный текст скрипта:

Скрытый текст
#!/bin/bash

# Sound stage mode:
# 5.1 / 2.0
MODE="$1"
[ -z "$MODE" ] && MODE="5.1"

# 5.1 mode:
# table / couch
MODE51="$2"
[ -z "$MODE51" ] && MODE51="couch"

# SSH target
TARGET="tdrk@supercomputer.local"

51prepare() {
    # 0 - 65535
    if [ "$MODE51" = "table" ]; then
        VOL_FS=53768
        VOL_FC=50368
        VOL_RS=65535
        VOL_LFE=65535
    elif [ "$MODE51" = "couch" ]; then
        VOL_FS=65535
        VOL_FC=65535
        VOL_RS=60535
        VOL_LFE=65535
    else
        echo "Unknown 5.1 mode - $MODE51"
        exit 1
    fi

    # FL FR RL RR FC LFE
    ssh "$TARGET" "pactl set-sink-volume $PADEVICE $VOL_FS $VOL_FS $VOL_RS $VOL_RS $VOL_FC $VOL_LFE" 
}

setprofile() {
    ALSA_PCI="pci-0000_00_1b.0"

    ssh "$TARGET" "pactl set-card-profile alsa_card.$ALSA_PCI output:$1"
    export PADEVICE="alsa_output.$ALSA_PCI.$1"
    sleep 0.2 # Let AV receiver detect Dolby Digital / PCM output
}

prepare() {
    if [ "$MODE" = "2.0" ]; then
        setprofile "iec958-stereo"
        DEVICE="BlackHole 2ch"
    elif [ "$MODE" = "5.1" ]; then
        setprofile "iec958-ac3-surround-51"
        DEVICE="Philips5.1"
        51prepare # Adjust volumes depending on listener position
    else
        echo "Unknown sound stage $MODE"
        exit 1
    fi
    export PADEVICE
    SwitchAudioSource -s "$DEVICE"
}

run() {
    CAP_DEVICE="$1"
    CHANNEL_MAP="$2"

    # Space acts as a +1
    CHANNELS=$(printf '%s ' `sed 's/[^,]//g' <<< "$CHANNEL_MAP"` | wc -c)
    sox \
        -r 48000 -b 32 -c $CHANNELS -e signed-integer \
        -t coreaudio "$CAP_DEVICE" \
        -p | paplay \
            -s 192.168.128.1:4656 -d "$PADEVICE" -v --raw \
            --rate 48000 --format s32le --channels $CHANNELS \
            --channel-map="$CHANNEL_MAP"
}

pick() {
    case "$MODE" in
    "5.1") run \
        "PhilipsCapture" \
        "front-left,front-right,front-center,lfe,rear-left,rear-right"
    ;;
    "2.0") run \
        "BlackHole 2ch" \
        "front-left,front-right"
    ;;
    *) echo "Unknown sound stage $MODE"
       exit 1
    ;; 
    esac
}

prepare
while true; do
pick
sleep 0.1
done

./audio.sh 5.1 couch

И... Оно работает! Не без проблем, но работает. И, за свои 0 рублей (кинотеатр этот достался мне по наследству), звук просто шикарен. Что еще нужно для счастья?)

Заключение

Иногда соединение прерывается, иногда приходится делать systemctl --user restart pipewire pipewire-pulse (так и не разобрался, почему), но я доволен результатом такого колхоза и костылей, даже такой 5.1 звук имеет очень выраженный объем, многие треки из своей библиотеки я будто бы первый раз слушал.

Скрипт не идеален, потому-что писал в основном для себя. Мне будет приятно, если эта статья была вам чем-то полезна или просто принесла удовольствие при прочтении!)

Если таки найду деньги на покупку второго такого же кинотеатра и USB S/PDIF, попытаюсь выпустить вторую часть с доработкой данной 5.1 системы до 5.2.4 (уже Atmos-enabled система).

Спасибо за ваше внимание, буду рад услышать критику, так или иначе, моя первая статья тут :)