Загрузка данных


# 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
```