Загрузка данных
# AirPlay на колонке: установка, настройка и диагностика
## 1. Текущее состояние
- AirPlay играет с iPhone, переживает паузу, снятие с паузы и перезагрузку.
- Алиса продолжает работать параллельно (`dmix` микширует), отвечает голосом.
- Громкость управляется ползунком iPhone (`mixer_control_name` не задан; iPhone-громкость применяется в софте `shairport`), кнопками колонки и голосом.
- `dacp.sh` умеет `pause|play|next|prev` — управление айфоном с ПК/по SSH.
- Автозапуск: `/etc/init.d/S96airplay` (через 25 с после бута, ждёт `wlan0`).
Размер: `/data/local/airplay` ≈ 35 МБ, свободно на `/data` ≈ 35 МБ.
---
## 2. Установка
### 2.1 Runtime
Ядро `aarch64`, userspace 32-битный `armhf` (glibc 2.30) — поэтому взят Debian arm64 runtime с собственным загрузчиком:
```text
/data/local/airplay/runtime/
bin/shairport-sync (Debian trixie, 4.3.7)
bin/avahi-daemon (0.8)
bin/avahi-browse (для DACP, добавлен позже)
lib/ld-linux-aarch64.so.1
lib/*.so* (51 библиотека, NEEDED-замыкание)
```
Сборка: `build_airplay.py` (WSL), пакеты тянутся из `deb.debian.org`, кэш в `midi-kit/spotify/build/cache`, замыкание считается через `objdump -p`.
Опция `amd=AudioAccessory1,1` (патч «как HomePod») была **откачена** — см. 3.2.
### 2.2 Файлы на колонке
```text
/data/local/airplay/
runtime/ Debian arm64 + loader
etc/shairport-sync.conf конфиг приёмника
etc/avahi-daemon.conf mDNS
ctl.sh start|stop|restart|status
dacp.sh pause|play|next|prev|status
trim-log.sh обрезка лога раз в минуту
airplay.log лог shairport (-vv)
/etc/init.d/S96airplay автозапуск
/etc/dbus-1/system.d/avahi-dbus.conf политика D-Bus для avahi
/etc/passwd, /etc/group пользователь avahi (uid/gid 70)
```
### 2.3 Конфиг shairport (рабочий)
```ini
general = {
name = "HomePod";
output_backend = "pipe";
mdns_backend = "avahi";
port = 5000;
interpolation = "basic";
volume_range_db = 30;
default_airplay_volume = 0;
high_threshold_airplay_volume = 0;
resync_threshold_in_seconds = 0;
drift_tolerance_in_seconds = 0.5;
audio_backend_latency_offset_in_seconds = -1.5;
};
pipe = { name = "/run/airplay.fifo"; };
metadata = { enabled = "no"; };
```
### 2.4 Конфиг avahi (ключевой момент)
```ini
[server]
host-name=<СКРЫТОЕ_ИМЯ_ХОСТА>
use-ipv4=yes
use-ipv6=no
allow-interfaces=wlan0
disallow-other-stacks=no
enable-dbus=yes
[publish]
publish-addresses=no
publish-hinfo=no
publish-workstation=no
```
**Примечание:** имя хоста заменено на заглушку. Это не готовое значение для копирования в рабочий конфиг.
---
## 3. Что чинили и почему
### 3.1 mDNS-конфликт с квазаром (главная причина «видит, но не подключается»)
Симптом: iPhone видит устройство, подключиться не может; в логе:
```text
Received conflicting record [...ip6.arpa IN PTR <СКРЫТОЕ_ИМЯ_ХОСТА>.local]
Host name conflict, retrying with ...-7 / -8 / -9 ...
needs a rename: <СКРЫТЫЙ_MAC>@HomePod
```
Причина: avahi пытался публиковать своё имя и адресные записи, а квазар уже владеет именем и обратной PTR-записью для этого IP. Бесконечные переименования → сервис `_raop._tcp` не регистрировался корректно → SRV-цель не резолвилась.
Решение: `host-name` = **имя, которым владеет квазар**; `publish-addresses=no` (адреса публикует квазар).
### 3.2 Патч «как HomePod» ломал подключение
В бинарнике строка TXT `am=ShairportSync` была заменена на `am=AudioAccessory1,1`.
iOS начинал считать приёмник AirPlay 2 и пытался идти по протоколу AirPlay 2, которого у нас нет. Вернули `am=ShairportSync`.
### 3.3 Громкость: двойное ослабление
`shairport` и `Master` ослабляли сигнал дважды (-24 дБ по умолчанию + 40 % Master).
Исправлено: `default_airplay_volume = 0`, `volume_range_db = 30`, `high_threshold_airplay_volume = 0`.
Позже выяснилось: `ignore_volume_control = "yes"` убирает скачок после паузы, но **отключает ползунок iPhone** — от этого отказались, оставили управление с телефона.
Правильный формат значения — строка `"yes"`, не `1`: при `1` опция игнорируется (`Invalid ignore_volume_control option choice`).
### 3.4 `aplay` умирал после паузы (звук «то есть, то нет»)
При паузе FIFO доходит до EOF, `aplay` завершается, и следующая сессия остаётся без читателя.
Решение: `aplay` запускается в цикле:
```sh
while :; do
aplay -q -D speakers -f S16_LE -c 2 -r 44100 -t raw /run/airplay.fifo
sleep 0.3
done
```
### 3.5 Демоны умирали вместе с adb/ssh-сессией
Все процессы запускаются через `setsid`, иначе SIGHUP при закрытии сессии убивает их.
### 3.6 `ctl.sh` не убивал процессы
В `ps` они видны как `ld-linux-aarch64.so.1`, а не `shairport-sync`, поэтому `killall shairport-sync` не срабатывал.
Убийство — по полной командной строке (`ps | grep 'bin/shairport-sync'`).
### 3.7 Двойной запуск (два `aplay` на один FIFO)
`S96airplay` + ручной запуск давали два `aplay`, звук делился.
Добавлен лок ` /run/airplay.lock` (`mkdir`), второй запуск выходит с `busy`.
### 3.8 «Нет звука после рестарта» — мёртвые сегменты dmix
Взято из `librespot-micro-installer` (`service.sh`):
- `kill_sink` — убивает осиротевшие `aplay` по `/proc/*/cmdline`;
- `clean_shm` — удаляет мёртвые SysV-сегменты `dmix` (key 1024/1025, `nattch=0`, владелец мёртв) через `ipcrm`.
Проверено двумя перезапусками подряд: ноль ошибок `unable to create IPC shm instance`.
### 3.9 Лог
`-vv` нужен для строк `SETUP` с `DACP-ID`/`Active-Remote` (без них `dacp.sh` не работает), но пишет строку на каждый пакет.
Решение: `trim-log.sh` раз в минуту обрезает файл до 64 КБ, если он больше 256 КБ (через `tail -c` + `cat >`, чтобы не ломать append-fd).
### 3.10 DACP: управление айфоном
Проверено: AirPlay 1 не даёт приёмнику транспорта, но отправитель публикует `_dacp._tcp` (`iTunes_Ctrl_<DACP-ID>`, порт каждый раз новый: 59931, 59952, 59954…).
```http
GET http://<iphone>:<port>/ctrl-int/1/playpause
Active-Remote: <токен из SETUP>
DACP-ID: <DACP-ID>
```
Без `Active-Remote` — `HTTP 400`; с ним — `200`, пауза/снятие реально работают (проверено по `FLUSH`/`RECORD` и состоянию ALSA).
Порт 3689 на iPhone закрыт — искать надо именно через mDNS.
---
## 4. Что пробовали и почему не получилось
### 4.1 Родной ALSA-бэкенд («минус один процесс») — дважды провал
**Попытка 1:** `output_backend = "alsa"`, `output_device = "speakers"` (softvol квазара).
Итог: GStreamer перестал открывать `default`:
```text
[GST.alsa] Error -22 (Invalid argument) snd_pcm_open(..., SND_PCM_NONBLOCK)
error: Playback open error on device 'default'
```
Алиса замолчала: её `mediad` не мог открыть звук. Плюс кнопки громкости «перестали работать» — громкость квазара идёт через его аудиоклиент.
**Попытка 2:** отдельный `pcm.airplay` (`plug → dmix_spk`), `output_device = "airplay"`.
Итог: шум/визг — два клиента (44,1 и 48 кГц) в общем `dmix`; пришлось выдёргивать питание.
Вывод: `dmix` не уживается с GStreamer квазара в неблокирующем режиме. Pipe-схема с отдельным `aplay` — единственная стабильная.
### 4.2 Голосовое управление AirPlay («Алиса, пауза» → айфон)
- В логах `maind` **нет** `recognizedPhrase` ни при `error`, ни при `info` — только `onPhraseSpotterBegin` и `onStartVinsRequest`. Перехватывать нечего.
- `alice-bridge` (https://github.com/pharci/alice-bridge) читает внутренний IPC квазара `127.0.0.1:9875` (protobuf: `uint32 длина + тело`). Порт у нас открыт, кадры идут, но **распознанного текста в них нет**: структура сообщений отличается от другой модели устройства (у нас корневые поля 3 / 207 / 210, а парсер моста ждёт 253 / 252). В кадрах видны только `AliceCapability`, серийник и список возможностей.
- Итог: на прошивке `.341` голосом AirPlay не порулить. Остаётся `dacp.sh` вручную (ПК/SSH/iOS Shortcuts).
### 4.3 librespot-micro-installer
Это **Spotify Connect**, не AirPlay. Установщик проверяет `/proc/device-tree/model` и наличие `ntp8910a-amplifier` — у целевого устройства другая модель, а используется усилитель FS2105, поэтому установка откажется (и правильно).
Полезным из него взяли только `clean_shm`/`kill_sink` (см. 3.8).
### 4.4 Громкость «прыгает» и «кнопки не работают»
- Скачок после ответа Алисы: `Master` при замерах **не менялся** (40 % → 40 %), то есть это не скачок, а баланс: TTS нормализован громко, а AirPlay ослаблен ползунком iPhone (при 60 % приходит ≈ −20 дБ).
- Во время ALSA-эксперимента звук был мёртв, но нажатия «−» всё равно писали в энергонезависимый файл `last_vendor_volume` → после отката колонка восстановила **0** и была тихой. Исправлено вручную (`6`), копия: `/data/opt/backup/last_vendor_volume.pre-fix`.
- Вывод: громкость задавать кнопками/голосом (это обновляет состояние квазара), `amixer` — только для разовых проверок.
---
## 5. Что работает / что нет
| Возможность | Статус |
|---|---|
| AirPlay с iPhone (музыка) | работает |
| Пауза/снятие с айфона | работает |
| Переживает перезагрузку | да (автозапуск) |
| Алиса во время AirPlay | работает (`dmix`) |
| Громкость с iPhone | работает |
| `dacp.sh pause/play/next/prev` с ПК/SSH | работает (нужен активный сеанс) |
| Голосовое управление AirPlay | **невозможно** (нет текста фраз) |
| Родной ALSA-бэкенд | **несовместим** (GStreamer/шум) |
| Spotify Connect | не ставили (не нужен и несовместим по модели) |
---
## 6. Управление и откат
```sh
# состояние
sh /data/local/airplay/ctl.sh status
# перезапуск / остановка
sh /data/local/airplay/ctl.sh restart
sh /data/local/airplay/ctl.sh stop
# DACP (когда AirPlay уже играет)
sh /data/local/airplay/dacp.sh pause
sh /data/local/airplay/dacp.sh play
sh /data/local/airplay/dacp.sh next
sh /data/local/airplay/dacp.sh prev
sh /data/local/airplay/dacp.sh status
```