В первой части я рассказал, как Kalinka выросла из эксперимента с Raspberry Pi, а затем подробно разобрал локальный поиск музыки по звучанию: CLAP, отдельную модель valence/arousal, бенчмарки и борьбу за память на Raspberry Pi 4.
Но за рамками статьи остался более фундаментальный вопрос: что такое Kalinka как продукт и почему она устроена именно так.
Музыкальных серверов уже достаточно. Есть MPD и moOde, Volumio и Roon, Navidrome и Jellyfin, Lyrion Music Server и Music Assistant. Поэтому фраза «я написал ещё один музыкальный сервер» почти неизбежно вызывает вопрос: зачем?
Ответ заключается не в одной уникальной функции. Kalinka занимает узкую область между несколькими классами систем:
сервер сам воспроизводит музыку и физически подключён к аудиосистеме;
локальная коллекция является основным источником, а не дополнением к стриминговому сервису;
поверх файлов строится отдельная библиотека с поиском и обогащёнными метаданными;
телефон, браузер и настольное приложение работают как пульты управления;
источники музыки и интеграции с устройствами подключаются в виде плагинов;
система работает на Linux-устройстве класса Raspberry Pi без обязательного аккаунта или подписки.
Главный принцип Kalinka можно сформулировать одной фразой:
Сервер является проигрывателем. Клиенты являются пультами управления.

В этой статье я сначала обозначу границы продукта, а затем покажу, как они превратились в архитектуру из Python-сервера, плагинов, тонких клиентов и нативного аудиографа.
Что такое Kalinka
Kalinka — открытая headless-система воспроизведения музыки для 64-битного Linux.
Референсная конфигурация — Raspberry Pi, подключённая к ЦАП или цифровому входу усилителя. Однако сервер также работает на обычном Linux-компьютере, NUC или другом устройстве с архитектурой arm64 или amd64.
Система состоит из двух основных частей.
KalinkaPlayer — сервер. Его базовые компоненты отвечают за очередь, воспроизведение, REST- и WebSocket API, конфигурацию, обнаружение устройств в локальной сети через Zeroconf и жизненный цикл плагинов. Большая часть сервера написана на Python, а чувствительный к задержкам аудиотракт — на C++.
Kalinka AI — клиент на Flutter для Android и Linux. Он автоматически обнаруживает серверы в локальной сети и позволяет просматривать библиотеку, искать музыку, управлять очередью, избранным, плейлистами и настройками. Вместе с сервером также можно установить веб-клиент, который подключается по известному адресу и не использует сетевое обнаружение.
Телефон при этом не получает FLAC-файл и не передаёт звук по Bluetooth. Браузер не декодирует трек. Клиенты отправляют команды и получают актуальное состояние, а звук всегда выводит Linux-машина, подключённая к аудиосистеме.
Основным и наиболее развитым источником остаётся локальная библиотека. Дополнительно существуют плагины для Jamendo, экспериментальная интеграция с Qobuz и управление устройствами Yamaha MusicCast.
Сервер распространяется под лицензией GPL-3.0-or-later, клиент — под Apache 2.0.
Исходный код:
Границы продукта
Одинаковое выражение «музыкальный сервер» используется для систем с совершенно разной моделью. Поэтому сначала важно определить, что Kalinka делает, а что сознательно оставляет другим проектам.
Один сервер — один аудиовыход
Kalinka ближе к Volumio, moOde или самостоятельно собранному MPD-плееру, чем к обычному медиасерверу.
Компьютер с Kalinka физически подключён к ЦАП, ресиверу или активной акустике и сам воспроизводит музыку. Сейчас один экземпляр сервера управляет одним аудиовыходом.
Это не whole-home-система и не синхронный multi-room.
Локальная коллекция является основным источником
Kalinka создаётся вокруг файлов, которыми владеет пользователь. Потоковые источники могут находиться в той же очереди, но не определяют модель продукта.
Сервер индексирует коллекцию, извлекает обложки, пытается заполнить пробелы в метаданных и позволяет искать музыку как по названиям, так и по звучанию. Исходные файлы при этом не изменяются.
Клиент управляет, но не воспроизводит
Navidrome, Jellyfin и Subsonic-совместимые системы хорошо решают задачу доступа к коллекции с разных устройств и через интернет. В их основном сценарии сервер отдаёт файл или транскодированный поток клиенту, а клиент воспроизводит его локально.
Kalinka решает обратную задачу: клиент выбирает музыку, но воспроизводит её сервер, находящийся рядом с аудиосистемой.
Удалённый многопользовательский доступ через интернет не является целью проекта.
Это не универсальный аудиокомбайн
Kalinka пока не поддерживает десятки форматов, AirPlay, Spotify Connect, Bluetooth-вход, сложный DSP, room correction и синхронизацию нескольких зон.
Текущая поддержка аудиоформатов ограничена FLAC и MP3. Архитектура допускает подключение новых декодеров, но сейчас развитие локальной библиотеки, поиска и клиентского интерфейса имеет более высокий приоритет, чем максимальная широта форматов.
Это не закрытый коммерческий appliance
Установка и настройка постепенно упрощаются, а значительную часть параметров уже можно менять из приложения. Но система всё ещё рассчитана на пользователей, которые не боятся Linux и понимают, что такое ALSA-устройство.
В обмен на отсутствие подписки и vendor lock-in пользователь получает контроль над системой, но не круглосуточную поддержку и не гарантированную совместимость с любым экзотическим оборудованием.
Где Kalinka находится среди готовых решений
Сравнение здесь нужно не для того, чтобы объявить один проект лучше другого. Разные системы оптимизированы под разные сценарии.
Volumio и moOde значительно зрелее как универсальные Raspberry Pi-проигрыватели. Они поддерживают больше форматов, протоколов воспроизведения и DSP-сценариев. Kalinka делает ставку на собственную модель локальной библиотеки, расширение через плагины и единый интерфейс для локальных и потоковых источников.
Roon, Plexamp и Audirvana предлагают более отполированный пользовательский опыт, богатые каталоги метаданных и зрелые механизмы рекомендаций. Однако это закрытые продукты, которые обычно зависят от аккаунта, облачных компонентов или подписки.
Navidrome и Jellyfin лучше подходят для удалённого доступа к библиотеке и воспроизведения на множестве клиентских устройств. В Kalinka аудиовыходом владеет сам сервер. Теоретически Navidrome или другой медиасервер можно подключить к Kalinka как ещё один источник, не дублируя его функции внутри проекта.
MPD и Mopidy являются отличными строительными блоками. На их основе можно собрать гибкую систему, самостоятельно выбрав клиент, библиотеку, интеграции и связующую логику. Kalinka находится уровнем выше: это связный продукт с собственной моделью каталога, единым состоянием воспроизведения, официальным клиентом и SDK для плагинов.
Music Assistant ближе всего к Kalinka по общей идее: он также объединяет разные источники музыки и устройства воспроизведения. Однако Music Assistant ориентирован прежде всего на маршрутизацию музыки на широкий набор существующих проигрывателей и multi-room-сценарии. Kalinka пока решает более узкую задачу: один Linux-узел одновременно владеет локальной библиотекой, очередью и непосредственным ALSA-аудиотрактом.
В результате целевого пользователя Kalinka можно описать так:
У него есть локальная музыкальная коллекция, один хороший ЦАП или цифровой вход, небольшая Linux-машина рядом с аудиосистемой и желание получить интерфейс современного музыкального сервиса, не передавая коллекцию облачной платформе и не превращая телефон в источник звука.
Ниша не обязательно огромна. Но именно её границы объясняют архитектуру проекта.
Архитектура верхнего уровня
В работающей системе можно выделить четыре слоя:
клиенты;
Python-ядро сервера;
SDK и плагины;
нативный аудиотракт.

Клиенты общаются с сервером через REST и WebSocket.
REST используется для запросов каталога и выполнения команд. WebSocket доставляет изменения очереди, позиции воспроизведения, состояния проигрывателя и подключённого оборудования в реальном времени.
Python-ядро владеет:
API;
очередью;
текущим состоянием воспроизведения;
агрегацией каталога и поиска;
конфигурацией;
обнаружением сервера в локальной сети;
жизненным циклом плагинов.
Плагины предоставляют конкретные источники музыки и интеграции с устройствами.
Нативный аудиодвижок не знает, что такое Qobuz, Jamendo, MusicBrainz или локальная библиотека. Для него существуют только воспроизводимый ресурс, декодер и ALSA-выход.
Это разделение стало одним из самых важных архитектурных решений проекта. Каталог, поиск и интеграции можно развивать на Python, не смешивая их с аудиотрактом, который должен предсказуемо работать во время воспроизведения.
От нажатия Play до ALSA
Объект в очереди не содержит заранее сохранённый URL.
Вместо этого он хранит идентификатор сущности и источник. Когда трек становится текущим, сервер обращается к соответствующему модулю ввода — InputModule — и просит разрешить сущность в воспроизводимый ресурс.
Для локальной библиотеки результатом будет путь к файлу. Для потокового источника — HTTP URL. Такой URL может иметь ограниченный срок действия, поэтому его нужно получать непосредственно перед воспроизведением, а не в момент добавления трека в очередь.
После разрешения ресурс передаётся в C+±аудиограф:
FileInputNodeилиAudioGraphHttpStreamчитает данные;декодер преобразует FLAC или MP3 в PCM;
узлы обмениваются данными через ограниченные буферы;
ALSA sink выводит PCM на выбранное устройство.
Ограниченные буферы обеспечивают обратное давление: быстрый источник не может бесконечно накапливать данные, если следующий узел временно не успевает их обрабатывать.

Движок может заранее подготовить следующий трек и подключить его к аудиографу ещё до завершения текущего. Если параметры обоих потоков совместимы с уже открытым аудиотрактом, ALSA-устройство не приходится останавливать и запускать заново.
За переключение отвечает специальный узел AudioStreamSwitcher. Он объединяет несколько подготовленных аудиопотоков в один непрерывный поток и переключается между ними точно на границе треков. В отличие от остальных узлов графа, AudioStreamSwitcher не использует собственный рабочий поток (thread), а выполняет только роль переключателя.
Благодаря этому нативный плеер может одновременно держать открытыми несколько дорожек и начинать воспроизведение следующей без дополнительной задержки. За добавление и удаление этих дорожек в графе отвечает код, управляющий очередью воспроизведения.
За добавление этих дорожек в аудиограф отвечает код, управляющий очередью воспроизведения.
Почему аудиодвижок написан на C++
Большая часть Kalinka не требует нативного языка. API, индексирование, конфигурацию и интеграции удобно реализовывать на Python.
Но требования к аудиотракту отличаются:
прямой контроль над форматами ALSA;
отсутствие скрытого ресемплинга внутри приложения;
декодирование FLAC и MP3;
чтение временных HTTP URL;
ограниченные и предсказуемые буферы;
предварительная подготовка следующего трека;
воспроизведение без пауз между совместимыми композициями;
низкое потребление ресурсов.
Движок выводит звук непосредственно через ALSA, без PulseAudio или PipeWire между приложением и устройством. Он пытается открыть аудиоустройство с параметрами исходного потока.
Термин bit-perfect здесь требует аккуратности.
При отключённой программной регулировке громкости Kalinka не выполняет ресемплинг и не изменяет PCM после декодирования. Однако конечный результат зависит от конфигурации ALSA, драйвера и оборудования. Например, dmix или внешний DSP могут изменить сигнал уже за пределами приложения.
Поэтому корректнее говорить о бит-в-бит пути там, где его допускают настройки ALSA и оборудование.
Если программная регулировка громкости включена, амплитуда PCM изменяется и бит-в-бит путь ожидаемо перестаёт существовать.
Почему не MPD
На раннем этапе MPD был очевидным кандидатом. Это зрелый, компактный и хорошо проверенный проигрыватель с поддержкой ALSA.
Но по мере развития Kalinka потребовалась собственная модель состояния:
единая очередь для локальных и потоковых источников;
временные URL, получаемые непосредственно перед воспроизведением;
подробные уведомления клиентам;
собственная модель артистов, альбомов, плейлистов и треков;
поиск сразу по нескольким источникам;
управляемый переход между локальными файлами и HTTP-потоками;
конфигурационная схема, доступная клиентскому приложению;
расширение через общий SDK для плагинов.
Всё это можно было бы построить вокруг MPD. Но Python-сервер всё равно владел бы почти всей бизнес-логикой, а MPD оставался бы вторым компонентом со своей очередью, состоянием и правилами переходов.
Состояния двух систем пришлось бы постоянно синхронизировать.
Собственный аудиограф сделал низкоуровневую часть сложнее, но упростил систему целиком: очередь существует в одном месте, сервер точно знает текущее состояние, а нативный движок остаётся узким исполнителем команд.
Плагины вместо интеграций в ядре
Первоначально Kalinka была тесно связана с Qobuz. Со временем это стало выглядеть архитектурным тупиком: API сервиса мог измениться, а локальная коллекция и другие источники требовали совершенно другой логики.
Теперь ядро не содержит знаний о конкретных музыкальных сервисах.
Источники реализуют интерфейс InputModule, который включает операции для:
просмотра каталогов;
получения артистов, альбомов, плейлистов и треков;
поиска;
разрешения трека в локальный путь или URL;
работы с избранным;
управления плейлистами, принадлежащими конкретному источнику.
Отдельный интерфейс предназначен для управления внешним оборудованием.
Например, плагин MusicCast может включать ресивер, менять громкость и получать его состояние. Благодаря этому кнопки громкости телефона управляют моей Hi-Fi-системой, хотя сам аудиосигнал идёт отдельно: из ALSA через ЦАП или S/PDIF.
Плагины являются обычными Python-пакетами и обнаруживаются при запуске. Их можно распространять в отдельных Debian-пакетах, поэтому новый источник не требует изменения ядра сервера.
Для разработки существует cookiecutter-шаблон, который создаёт структуру пакета, интерфейсы, конфигурационную схему и тестовый каркас.
Конфигурация тоже является частью контракта плагина. Модуль публикует схему параметров, а клиент динамически строит по ней экран настроек. Поэтому добавление нового параметра обычно не требует изменений в Flutter-приложении.
Локальная библиотека как производная модель
Файлы пользователя остаются источником аудио, но не всегда являются хорошим источником структуры каталога.
В реальных коллекциях встречаются:
пустые или противоречивые теги;
разные варианты написания имени одного артиста;
отсутствующие номера дисков;
собственные оцифровки винила;
компиляции;
редкие издания;
директории с правильными именами и почти пустыми метаданными.
Kalinka не переписывает исходные файлы. Вместо этого плагин localfiles строит поверх них отдельную производную модель библиотеки.

Indexer отслеживает музыкальные директории и использует окно ожидания завершения загрузки: новый файл не обрабатывается, пока его размер продолжает меняться. Это защищает от индексирования частично скопированных FLAC-файлов.
Сначала индексатор читает встроенные теги, имя файла, путь и технические параметры аудио.
Затем он локально вычисляет Chromaprint-отпечаток. При наличии ключа отпечаток отправляется в AcoustID, чтобы попытаться определить запись.
Недостающие сведения дополняются через MusicBrainz, Wikidata и Deezer. Если внешние источники не помогают, используются файловые эвристики, чтобы библиотека оставалась пригодной для просмотра.
Отдельная логика агрегирует сборники с разными исполнителями, чтобы компиляции не распадались на несколько независимых альбомов.
Результат сохраняется в SQLite, а обложки кэшируются отдельно.
Такой подход не означает, что Kalinka всегда правильно определяет релиз. Fingerprint и внешние базы хорошо работают для распространённых записей, но собственные рипы, редкие издания и пользовательские сборники остаются сложными случаями.
Одна из текущих задач — перейти от независимого решения по каждому треку к согласованию на уровне альбома: учитывать общую директорию, последовательность треков, длительности, близость тегов и согласованность внешних результатов.
Однако даже текущая система обладает важным свойством: ошибка обогащения не повреждает исходные файлы. Производный индекс можно удалить и построить заново из настроек приложения.
Поиск по звучанию как часть общей системы
Локальный семантический поиск подробно разобран в первой статье, поэтому здесь я не буду повторять историю перехода от теггеров к CLAP, устройство valence/arousal-модели и результаты бенчмарков.
В общей архитектуре это опциональная ветка конвейера локальной библиотеки:
фоновый процесс читает аудио;
CLAP создаёт векторные представления;
векторы квантизуются и сохраняются;
текстовый запрос преобразуется в вектор;
система ищет ближайшие треки;
результат объединяется с лексическим поиском и сигналами из метаданных.
Тяжёлый аудиокодер требуется при индексировании, а текстовый кодер — при выполнении запросов. Это позволяет разделить жизненный цикл моделей и не держать их одновременно в памяти.
Важно, что семантический поиск не является отдельным экраном или специальным «AI-режимом». Пользователь вводит запрос в одно поле, а сервер сочетает несколько разных механизмов:
точные и нечёткие лексические совпадения;
поиск в каталогах подключённых источников;
распознавание каталожных запросов;
семантический поиск локальных треков по звучанию.
Например, запрос Most streamed on Qobuz направляется в соответствующий каталог Qobuz, а запрос вроде «что-нибудь меланхоличное на вечер» может использовать локальный поиск по звучанию.
Таким образом, AI-функции встроены в общую модель продукта, но не определяют всю архитектуру и не требуются для обычного воспроизведения.
Одна очередь воспроизведения для разных источников музыки
Плагинная модель позволяет локальным файлам и потоковым сервисам находиться в одной очереди.
Очередь хранит абстрактную ссылку на сущность. Когда приходит время воспроизведения, соответствующий плагин преобразует её в реальный ресурс.
Поэтому последовательность может выглядеть так:
локальный FLAC;
трек из Jamendo;
локальный MP3;
композиция из Qobuz.
Для аудиографа меняется только источник данных: локальный файл или HTTP-поток.

Одинаково названные объекты из разных источников не объединяются автоматически. У пользователя могут быть собственный виниловый рип, другое издание того же альбома и потоковая версия.
Источник объекта остаётся видимым в интерфейсе. Для локальных файлов отдельная атрибуция не показывается.
Поиск работает похожим образом: сервер параллельно отправляет запрос подходящим InputModule, получает результаты и агрегирует их, не теряя происхождение каждого объекта.
Доставка состояния клиентам
Когда сервер является проигрывателем, клиенту недостаточно получить успешный ответ на команду play.
Он должен быстро узнавать:
какой трек действительно начал играть;
текущую позицию;
новую очередь;
момент перехода на следующий трек;
состояние паузы и остановки;
громкость и состояние питания внешнего устройства;
прогресс индексирования библиотеки.
Команды и обычные запросы идут через REST. Изменения состояния публикуются через WebSocket-каналы.
Сервер остаётся единственным источником истины, а телефон, ноутбук и браузер получают одну и ту же модель.
Это особенно важно при управлении с нескольких устройств: действие на одном клиенте должно сразу появиться на всех остальных.
Развёртывание и разработка
Production-сборка состоит из отдельных Debian-пакетов для arm64 и amd64:
ядро сервера;
SDK для плагинов;
плагины, поддерживаемые самим проектом;
дополнительные необязательные компоненты.
При первом запуске bootstrap-скрипт создаёт приватное Python-окружение (venv) и устанавливает в него пакеты ядра и доступных плагинов.
Тяжёлые необязательные зависимости можно устанавливать только при включении соответствующего модуля. После изменения состава окружения служба перезапускается через systemd.
Для пользователя установка сведена к скрипту с сайта или готовым .deb-пакетам. После запуска основная настройка продолжается в клиентском приложении.
Для разработки существует режим запуска без root. Сервер, конфигурация, база данных, логи и тестовая музыкальная директория размещаются в пользовательском каталоге. Это позволяет запускать полный стек из исходников без установки systemd-службы.
Изменения Python-кода применяются после перезапуска приложения. После изменения C+±кода нативный модуль необходимо собрать заново.
Ограничения как часть архитектуры
Некоторые отсутствующие функции являются вопросом времени. Другие потребуют изменения самой модели системы.
Дополнительные форматы
ALAC, Opus и WAV можно добавить в виде новых декодеров.
DSD сложнее: потребуется определить, должна ли система поддерживать native DSD, DoP или конвертацию в PCM и какие гарантии при этом можно давать пользователю.
Multi-room
Синхронное воспроизведение нельзя добавить как ещё одну кнопку.
Понадобятся:
общие часы;
компенсация задержек;
дополнительная буферизация;
управление группами;
доставка аудио между узлами;
восстановление синхронизации после сетевых сбоев.
Текущая модель «один сервер — один выход» намеренно проще.
При этом индексирование локальной коллекции в будущем можно отделить от узла воспроизведения. Например, модуль библиотеки мог бы работать на NAS в Docker-контейнере, а проигрыватель получать от него каталог и воспроизводимые ресурсы.
DSP
Сейчас задача аудиотракта — вывести декодированный PCM без дополнительных преобразований.
Эквалайзер или room correction логично реализовать в виде дополнительных узлов аудиографа. Но тогда интерфейс должен явно показывать, что бит-в-бит путь отключён.
На данный момент единственным опциональным преобразованием является программная регулировка громкости. Она включается пользователем явно.
Удалённый доступ и многопользовательский режим
Это не просто авторизация поверх существующего API.
Понадобятся:
разграничение библиотек;
права на управление очередью;
безопасная публикация сервера в интернет;
возможно, транскодирование;
другая модель клиентских приложений.
Вероятно, Kalinka не должна превращаться в Navidrome. Подключить существующий медиасервер как источник может оказаться разумнее, чем дублировать его функции.
Что получилось
После нескольких итераций Kalinka стала не просто способом воспроизвести Qobuz через Raspberry Pi.
Сейчас это система с достаточно чёткими границами:
звук воспроизводится на стороне сервера через ALSA;
очередь и состояние существуют в одном месте;
локальная коллекция является главным источником;
библиотека строится отдельно и не изменяет оригинальные файлы;
источники и устройства подключаются через плагины;
разные источники используют общую очередь и общий поиск;
Android-, Linux- и веб-клиенты остаются тонкими пультами;
семантический поиск является необязательной локальной функцией;
сервер может работать на небольшом Linux-устройстве.
Kalinka не заменяет Roon, Volumio, moOde, Navidrome, Music Assistant или MPD во всех сценариях. Каждый из этих проектов значительно зрелее в своей области.
Смысл Kalinka не в максимальном количестве функций, а в конкретном сочетании свойств: локальная библиотека, сервер как физический проигрыватель, открытая модульная архитектура и удобная работа с коллекцией без обязательного облака или подписки.
Архитектура начинается не с AI-модели и не с красивого клиента. Она начинается с ответа на три вопроса:
где живёт музыка;
кто владеет очередью;
какое устройство в конечном счёте выдаёт звук.
Что дальше
Ближайшие технические направления:
согласование альбомов и обогащение метаданных на уровне коллекции;
поддержка новых аудиоформатов;
стабилизация API и SDK для плагинов;
дальнейшая работа над объединённым поиском;
тестирование с большим количеством ЦАП и ALSA-конфигураций;
упрощение первого запуска и настройки;
более точная документация архитектурных контрактов.
Kalinka открыта для тестировщиков и участников.
Полезны не только изменения в коде. Проекту нужны:
коллекции со сложными или противоречивыми тегами;
собственные рипы и редкие издания;
необычные ЦАП и аудиоконфигурации;
отчёты о несовместимостях;
идеи для новых источников и устройств.
Изменения, подготовленные с помощью AI-инструментов, тоже принимаются, если автор понимает код, может объяснить решение и добавляет необходимые тесты.
Сайт проекта: kalinkaplayer.com
Клиент: github.com/madenvel/KalinkaAI
