# name
Вывод USB-Audio, S/PDIF / Toslink для ГУ Changan CS55 Plus / UNI-S

# content
Это будет рассказ о том, как я наконец получил полноценный цифровой источник аудио сигнала c USB порта штатного ГУ Changan CS55 Plus / UNI-S

Для меня это важная веха, так как получить цифру для внешних аудиопроцессоров (DSP)/ процессорный усилителей я пытался с момента покупки этого автомобиля, но всё время упирался в стену. И это касается двух рассматриваемых мной вариантов:

Корректировка файлов конфигурации в штатном Andorid ГУ;

Поиск I2S шин для внедрения I2S -> S/PDIF-Toslink конвертера;

Первый вариант на протяжении долгого времени не давал результатов, какой бы я ни делал конфигурацию sink устройств в ГУ, пакеты на подключаемый USB ЦАП не шли, и почему будет понятно позже.

Второй вариант с шиной I2S был в принципе решаем, я разбирал и крутил плату ГУ несколько раз, но похоже всё сводилось к тому, что ЦАПом в ГУ выступает Fortemedia FM1388 (это многофункциональный цип), а даташита на него в открытом доступе нет, только для производителей, или выкупить на китайских форумах), а тыкать вслепую или и искать I2S анализатором времени и желания не было.

Новым импульсом для данной темы стало моё увлечение и работа с агентскими ИИ системами, чем я активно занимаюсь половину года, в том числе применение и в профессиональной деятельности. Я был уже достаточно готов запустить на ГУ по wireless ADB команду агентов для исследований, тестов, поиска решений, и дальнейшей разработки.

Это было сложный путь, где зачастую ИИ-агенты складывали руки, и говорили, что "вариантов больше нет, надо принять неизбежное, это очень кастомное устройство", но я не давал им сдаваться и заставлял искать и искать, пробовать, ошибаться, не верить в очевидные факты, так как за ними может скрываться истина и новые возможности! Мы жгли токены как сумасшедшие, но у меня были щедрые лимиты, и шаг за шагом продвигались, возвращались, двигались в другом направлении, потом опять возвращались, и меняли концепцию подхода.

В итоге я получил, что, что хотел, и даже больше!

Кому скучно читать, как устроенно аудио в ГУ Changan CS55 Plus / UNI-S, что удалось найти, попробовать, и как работает конечный результат, то можете сразу переходить внизу к установщику, и пробовать на своих USB устройствах.
Кому реально интересно, как оно получилось, и что дальше, я разложу в деталях.

Штатная медиасистема Changan CS55 Plus / UNI-S — это AAOS (Android Automotive) в качестве базы на платформе MediaTek со своим простым DSP и усилителем (TDA75616EP). Звук на выходе УНЧ ГУ неплох, особенно на прошивке версии 5.9, где китайцы забыли прикрутить корректировку АЧХ в DSP, и на выходе мы имеем ровную полку 20 Гц - 20 кГц. Конечно, всё зависит от преобразователя высокого уровня на конкретном целевом устройстве (процеусь / усилитель), что там: простой резистивный делитель, либо полноценный преобразователь на ОУ? Но в любом случае большинство владельцев сталкиваются с наличием так называемых "фиксиков", шумов и писков ввиду проблем экранирования сигнальных цепей от цифровых на плате ГУ, их очень хорошо слышно после установки новых твитеров, и избавиться можно только функцией Noise Gate в отдельный DSP (правда это не у всех).

У нас есть USB порт, и если в него вставить какое-нибудь USB аудио устройство, внешний USB-ЦАП, чтобы дальше подключить свой DSP, свой усилитель, свою акустику, то система Android его видит, но выводить на него звук полностью отказывается.

В чём вообще проблема с USB audio на ГУ?

Предлагаю разобрать, как устроен звуковой тракт штатного ГУ.

Весь звук системы — медиа, голос навигатора, downlink телефонного звонка
(hands-free) — сводится в Audio HAL в один единственный поток: 8 каналов, формат S16_LE, 48 кГц, упакованные в TDM. Этот поток HAL пишет в одно ALSA-устройство /dev/snd/pcmC0D3p, откуда он уходит дальше на штатный DSP и штатный усилитель. То есть штатно каждая «дорожка» — это просто свой канал в общем 8-канальном кадре.

USB-host порт на голове — отдельная история. Во-первых, он физически управляется контроллером MCU.
Во-вторых, и это главное: даже если ЦАП в системе виден, CarAudioService наотрез отказывается маршрутизировать на него звук. В этом ГУ маршрутизация жёстко привязана к bus-устройствам, которые описаны под встроенный DSP, и USB-выход туда просто не вписан. Никакими штатными настройками это окно не открыть, там сплошная стена.

❗ Важная вводная, которая определила весь дальнейший подход: в железо не лезем, модули ядра не собираем (такие попытки у меня тоже были, но словил панику ядра на простейшем модуле, и понял, что закончится всё плохо).
На этом ГУ единственный путь восстановления — это UART, и то, если удастся загрузиться, и если что-то пойдёт не так, то ГУ уйдёт в вечный bootloop ещё до того, как мы успеем что-либо сделать. Поэтому всё решение - строго в userland.

А можно ли вообще получить звук на USB?

Первый вопрос был простой: USB-ЦАП в принципе способен заиграть на этом ГУ или порт мёртв полностью? Оказалось — способен. USB ЦАП в системе появляется, ALSA её видит, можно открыть PCM, с помощью tinyalsa можно отправить тестовый WAV и даже услышать звук. Значит, проблема не в железе, не в ограниченном USB порте, а в маршрутизации. А раз так, то цифровой аудио поток звук надо перехватить где-то внутри и «руками»
доставить на ЦАП.

И вот тут начались забавные грабли.

Первая реализация: захват в ядре (петля D29)

Логика была такая: раз весь звук всё равно сходится в AFE (Audio Front-End
MediaTek), а почему бы не снять готовый микс прямо там — на внутренних loopback-устройствах захвата. У MTK для этого есть служебные memif-устройства, в листинге было 35 штук, но в ходе тестирования реально с медиа выходом оказались всего три: D16, D29, D35.
Поигравшись, самым лучшим претендентом оказался D29, на котором действительно слышен весь выходной микс.
Был собран небольшой бинарник, который захватывал пакеты с D29, отправлял с tinyalsa на USB.

Звук пошёл. Казалось бы — вот она победа. Но 😁

После первого же ухода ГУ в сон (заглушили и закрыли автомобиль) и пробуждения, в захваченный поток начинал подмешиваться сигнал с салонных микрофонов. Получалась классическая положительная обратная связь: динамик -> микрофон -> захват -> снова динамик ->
самовозбуждение, эхо. Отделить чистый микс воспроизведения от микрофона на этом уровне нельзя — петля так устроена. При этом после полной перезагрузки ГУ (кнопкой минус на руле) всё отлично, но после захода в сон - микрофон на выходе. Это оказался мертвый вариант.

Затем начался поиск причин того, почему микрфон там вообще появляется, что меняется в AFE после сна, какие регистры меняются, нашли, а можно ли из переопределить, переписать регистры после сна, почему нельзя, а зачем там микрофон вообще, если он там есть, значит это нужно... Десяток итераций.

Рабочее решение

К рабочему варианту пришлось идти очень долго, я сжег много сессий Opus 4.8 на Max в Claude Code, заставлял его не отчаиваться (он это из раза в раз делал), требовал искать, грызть, не верить тому, что проверял до этого, думать нестандартно, творчески, я подкидывал ему идеи, которые для него казались малоперспективными. Но я настаивал, и в итоге они выходили рабочими направлениями.
Команда из двух Deepseek V4 Flash и Deepseek V4 Pro в Pi Coding Agent выполняли много монотонной работы, активно подкидывали идеи и помогали Opus не копаться в тупиковых вариантах. Это была интересная командная работа, где каждый внес свой вклад. В итоге решение было найдено, и это был праздник, так как качество результата было топ, и в дальнейшем его удалось поэтапно проапгрейдить!

В чем принцип решения:

Если поток воспроизведения, который HAL пишет на DSP, — это вызов ioctl с буфером кадра TDM, то перехватить его можно прямо внутри процесса Audio HAL, не трогая ни ядро, ни сам звук на DSP.

Как это работает по шагам:

Внедрение. Небольшой загрузчик через ptrace механизм заставляет процесс Audio HAL загрузить нашу библиотеку libtap.so (обычный dlopen изнутри процесса). Это позволяет обойти AT_SECURE, из-за которого обычный LD_PRELOADна HAL не срабатывает.

1. Перехват. libtap ставит GOT-hook на ioctl во всех модулях HAL и на каждом вызове WRITEI (запись кадра воспроизведения) делает копию буфера в общую память (SPSC ring-буфер, 1 МиБ). Подчеркну: это именно tee, копия, штатный звук на встроенный DSP идёт ровно как шёл, мы в него не вмешиваемся. И поскольку снимаем мы поток воспроизведения, фидбека микрофона тут нет в принципе.

2. Сведение и доставка. Отдельный процесс ringbridge (свой бинарник) читает кольцо, разбирает 8 каналов по карте источников и сводит все активные в стерео для USB ЦАПа.
Незадействованные каналы в кадре — ровно нули, поэтому можно просто суммировать все включённые источники, каждый со своей громкостью. На практике активен обычно один источник, так что переключение «само собой» оказывается контекстным.

медиа — каналы 1 и 2 (стерео);

звонок / hands-free — канал 5 (моно);

навигация — канал 0 (моно).

3. Громкость и Mute с руля. Кнопки громкости на руле при нажатии меняют индекс CarVolumeGroup, который мы вычитываем через dumpsys car_service с опросом раз в 500 мс и применяем как плавную (без щелчков) регулировку уже в ringbridge. Аппаратный Mute с руля (копка "звёздочка") живёт ниже Android — на уровне DSP, но мы его мы тоже отлавливаем и применяем mute в ringbridge.

4. Окончательны выход на USB-ЦАП - через tinyalsa.

Технические особенности моста и почему имеем достойный качественный цифровой поток на выходе.

Формат и битность.
На входе у нас S16 bit, 8 каналов, 48 кГц — это подтверждено на
уровне ядра, никакого скрытого 24/32 бита там нет. Все USB-ЦАПы / конверторы, какой бы высокой битности не были, умеют нативно принимать стандартный 16 bit / 48 кГц. Для любителей 24 bit / 96 кГц и выше тут вариантов нет, ГУ больше не отдаст (хотя зачем это в автомобиле, но не об этом).

Обработка громкости и фундамент под эквализацию и прочие фильтры.
Все мы знаем, что обычная софтовая регулировка громкости снижает битность исходного потока при применении коэффициента громкости вниз, и это приводит как минимум к потере детальности, а на низких уровнях вообще к росту гармонических искажений, нечетных гармоник, что мне довелось поймать на синусе 1 кГц при тестировании наших первых методик регулировки громкости. Это конечно не вариант вообще. Но в то же время отказываться от регулировки громкости кнопками на руле — для меня так же неприемлемый подход.

Поэтому было принято решение:
Внутри бинарника ringbridge всё считается в 32-bit float:
16-битный вход -> float -> умножение на коэффициент громкости (и в будущем фильтры EQ/тонкомпенсации) -> один единственный дизеринг (TPDF) -> конверсия обратно в 16 bit.
Один дизеринг на самом выходе — это и есть правильный путь: никаких
промежуточных округлений, никакого накопления шума. На максимальной громкости 20/20 тракт вообще переходит режим bit-perfect passthrough — байт в байт.

Важный момент: на громкости 20/20 поток идет максимальный, full-scale, 0 dBFs, это тот же уровень, который получаем с других подобныз устройство по оптике без регулировки громкости.

Борьба с clock drift (щелчки при работе USB устройств). Это тонкий момент, на котором ломается большинство простых схем таких «мостов». USB-ЦАП работает от своего тактового генератора (его endpoint — adaptive, от USB-клока SoC), а кольцо наполняется в темпе TDM/DSP. Два разных клока, они неизбежно «разъезжаются», рано или поздно буфер либо опустошается, либо переполняется, в итоге мы имеем рандомные щелчки при воспроизведении. Фиксированными «подушками» эти неприятные моменты только откладываются (перевтыкание ЦАПа просто заново подгоняет фазу, это проверенно лично на паре ЦАПов).
Решается это асинхронным семпл-рейт конвертером (ASRC): polyphase windowed-sinc, 32 отвода (Kaiser), 1024 фазы, плюс пропорциональный регулятор, который держит заполнение кольца у целевого значения. Коэффициент пересчёта плавно гуляет около 1.0 в пределах ~100 ppm — и щелчки уходят полностью. Проверено и на синусе, свипе (на этих сигналах щелчки синхронизации не могут спрятаться) на длинном прослушивании, и через сон/пробуждение, а также с перевтыканием ЦАПа: underrun = 0.

Громкая связь (Hands-free): Это отдельная длинная история.
При работе моста и разговоре по громкой связи была выявлена серьезная проблема - эхо у собеседника. Ему возвращалась в динамик телефона его же речь с задержкой. Был сломан механизм штатного эхоподавления.
За подавление эха отвечает не Android и не телефон, а встроенный эхоподавитель самой головы — MTK NREC (Noise Reduction & Echo Cancellation), живущий внутри аудио-HAL. Принцип любого эхоподавителя простой: он берёт копию того, что играет в динамиках, и вычитает её из сигнала микрофона. Чтобы вычитание сработало, копия должна быть точно выровнена по времени с реальным эхом «динамик -> салон -> микрофон».
И вот здесь мой мост всё сломал. Штатно звук от АЦП до микрофона возвращался примерно за 14 мс, и NREC был настроен ровно на эту задержку. А мост добавил в тракт целую цепочку: TDM-tap -> кольцо -> ASRC -> USB-ЦАП -> внешний DSP -> усилитель. В результате звук стал доходить до микрофона через ~101 мс спустя. NREC, заточенный под 14 мс, искал эхо не там, и просто его не видел.
Было несколько дней подбора параметров и поиска решений на слух, но затем перешли на объективный замер. HAL умеет дампить аудиопотоки захвата в файлы. Мы снимали два канала: сигнал микрофона (uplink) и эхо-референс (то, что играет), и считали их взаимную корреляцию на numpy в скользящем 3-секундном окне. Это сразу дало две вещи, которых не давали имперические тесты ухом:

точную задержку эха в миллисекундах (пик корреляции) — ~101 мс, стабильно;

динамику остаточного эха по ходу звонка — и вот тут вскрылся главный симптом: остаток сначала сходился, а потом, на длинном разговоре, начинал расходиться. NREC цеплялся за эхо, какое-то время держал его, но потом «терял» и плыл. Это и был эффект нарастания эха во время разговора.

Решением было укоротить буфер моста, и вернуть значение задержки в приемлемое для NREC диапазон. Тут возникла дилемма. Короткий буфер хорош для эхоподавления, но для музыки нужен наоборот глубокий буфер — иначе ASRC начинает щёлкать на клок-дрифте (вся борьба со щелчками, что описывал ранее). Один буфер не может быть и коротким, и длинным.
Решение — динамический режим звонка. Мост на лету определяет, идёт ли BT-звонок (по статусу BTCVSD playback PCM, опрос каждые 20 мс), и:

для медиа держит глубокий, комфортный буфер без щелчков и прочих артефактов клок-дрифта;

на время звонка резко переключается на короткий буфер (эхо ~41 мс), чтобы NREC справлялся;

после звонка возвращает глубокий буфер обратно.

Надёжность работы моста.
Мост поднимается init-сервисом с supervisor-циклом и авто-рестартом, переживает ежедневный сон/пробуждение ГУ (warm-safe: если библиотека уже внедрена — переиспользуется живое кольцо, повторно ничего не ломается).
Никаких правок ядра, фреймворка и железа. Штатные микрофоны и кнопки громкости продолжают работать как и раньше.

Какие устройства поддерживаются

На USB-ЦАП сейчас выводятся каналы: медиа, навигация и hands-free телефонного звонка. Канал голосового помощника еще не заводил, постараюсь позже его включить (я лично им не пользуюсь).

Форматы: вход S16 / 48 кГц; выход S16 / 48 кГц (с TPDF-дизерингом и
ASRC-ресемплингом под клок конкретного ЦАПа).

Проверенные USB-ЦАП:

Behringer UCA222 (Burr-Brown PCM2704) - это основной испытательных ЦАП;

FiiO E07K

безымянный USB→Toslink «USB AUDIO DAC» (S16, 32–44.1–48–96 кГц).

Все три — adaptive, все три заработали с ASRC. Тестовые пользователи также пробовали разные USB ЦАПы для наушников, USB аудиокарты, все работают.

DSP c USB-Audio входом:

Hellion HAM 8.10 DSP (JieLi (S16, только 48 кГц);

Hellion DHL-10

Hellion HAM 6.80 DSP

Hellion HAM 16.150DSP

По сути все процессоры / процеccорные усилители Hellion работают по USB, кабель USB-A -> USB-A, 5 метров, и цифровой поток пошел на процеусь, надо только выбрать USB вход в настройках.

Есть проблемы с этим режимом у Awave, популярных Awave DSP A10D / Awave DSP A8D.
Они используют у себя многосоставной hub, и при начальном запуске на USB у Awave появляется hub c устройствами, одно из которых идентифицирует себя как аудио-клиент, принимает поток, синхронизирует клоки, но звук не отдаёт никуда. Зато, после полной инициализации DSP и полной загрузки Awave, на USB у него появляется новое, уже реальное аудио-устройство, которое готово направлять звук в DSP на "USB-OTG" вход в софте. К сожалению, инициализировать его в ГУ можно только повторно переподключить его в USB порт. Я пробовал разные режимы сброса USB порта для переинициализации устройств, но все они приводят к полному отвалу USB портов на ГУ, с которым помогает только полная перезагрузка ГУ кнопкой "-" на руле. USB порт управляется MCU, и без его ведома он просто так не возвращается к жизни. Очень много было испытаний с бета-тестерами на Awave, Антоном и Стасом из нашей TG Changan группы.
Пока для Awave решение тоже, что и для многих остальных: конвертер USB-Toslink, и дальше оптику на процессор / процессорный усилитель.
Я протестировал успешно такой вариант, но подойдет по сути любой:

Есть еще и такой вариант:

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

На маркетплейсах есть масса вариантов, все они подойдут. Нет смысла брать что-то 24-bit, 96 kHz, выход в любом случае с ГУ ограничен 16 bit / 48 kHz, и эти устройства в любом случае подхватят поток и будут работать в режиме 16 bit / 48 kH, больше не взять. Можно конечно апсемплить, но в этом нет смысла.

Что дальше — а точнее, что уже готово.

Раз вся обработка идёт в 32-bit float, то добавить туда полноценный параметрический EQ с пресетами и динамическую тонкомпенсацию, привязанную к уровню громкости, оказалось делом техники — это всё уже внедрено, и об этом будет следующий пост.

Как всё установить и начать использовать:

Я подготовил специальный установщик моего моста, который при подключении к ГУ по ADB (USB и Wireless ADB) выполняет установки всех библиотек, бинарников, конфигурационных файлов, скриптов и APK приложения EQ/Тонкомпенсации.

Скачать установщики можно по следующим ссылкам (Версии для Windows x64 MacOs universal):

GD.USB.DAC.Audio.Bridge_1.1.1_x64-setup.exe

GD.USB.DAC.Audio.Bridge_1.1.1_x64-setup.exe.zip

8 MB

GD.USB.DAC.Audio.Bridge_1.1.1_universal.dmg

GD.USB.DAC.Audio.Bridge_1.1.1_universal.dmg.zip

24 MB

Оба установщика уже включают в себя ADB, поэтому отдельно устанавливать пакет ADB на ноутбук не требуется.

Порядок включения режима ADB на ГУ классический:

Заходим в приложение "Телефон" на ГУ и в номеронаберателе вводим "*#*#888", нажимаем кнопку вызова;

На появившейся цифровой панели набираем пароль "369875";

Через кнопку "settings" в инженерном меню переходим в меню настроек Android;

В пункт меню "ОБ УСТРОЙСТВЕ", нажимаем "номер сборки" 7 раз для получения прав разработчика;

Переходим к пункту меню "Для разработчиков", включаем "Отладка по USB"

Переходим повторно в инженерное меню согласно пунктов A. и B.;

Выбираем второй пункт бокового меню и нажимаем кнопку с надписью "USB" (вторая кнопка);

подключаем ГУ кабелем USB к ноуту, устанавливаем и запускаем мой установщик, выбираем тип подключения и обновляем список доступных устройств:

Выбираем наше найденное устройство и жмем кнопку далее:

Для тех, кто себе ставил Changan Tweaker, и у кого уже включено Wireless ADB в ГУ (Tweaker это включает по умолчанию, без этого не работает), то установку можно сделать и без включения режима ADB и подключения по USB. Можете подключиться к той же WiFi точке доступа, что и ГУ, на главной странице приложения Changan Tweaker узнать IP адрес своего ГУ, и внести его на вкладке "Wi-Fi (беспроводной)" установщика, нажать кнопку "Подключиться" и "Далее":

После подключения останется нажать кнопку установить и дождаться успешной установки:

После подтверждения установки нужно перезагрузить ГУ кнопкой "-" на руле (более 10 секунд). Это важно, нужна полная перезагрузка для запуска моста.

Дальше подключаем своё USB-устройство и получаем чистый цифровой поток на внешний процессор / процессорный усилитель.

Для владельцев усилителей без процессора, можете купить реальный USB ЦАП / мелкую USB аудиокарту и направить уже линейный сигнал на усилители. Вариантов дешевых и дорогих простой уйма.

Если надо удалить USB мост и вернуть всё обратно, то опять запускаем установщик, он подтвердит факт текущей установки моста, и кнопкой "Удалить" сносим все внесенные изменения и дополнительные файлы.
После удаления выполнить полную перезагрузку ГУ кнопкой "-" на руле (более 10 секунд).

Этот же принцип используем сейчас при обновлении сборки моста с бета-тестерами, полное удаление, перезагрузка, установка новой версии, перезагрузка.

В следующем посте расскажу про функционал приложения GD EQ, которое использует функции моего моста, и про дальнейшие планы доработок.

# preview.content
Вывод цифрового аудио сигнала / оптики (USB Audio / SPDIF/ Toslink из штатного ГУ Changan CS55 Plus / UNI-S. Полная история и готовое решение. 

# summary
Вывод цифрового аудио сигнала / оптики (USB Audio / SPDIF/ Toslink из штатного ГУ Changan CS55 Plus / UNI-S. Полная история и готовое решение. 