The Jerusalem PostCaught between primal urges and aspirationsZDF heuteAktuelle Pressemitteilungen des ZDFDeadlineRichard Lloyd Dies: Guitarist With Influential Punk Pioneers Television Was 75ESPNRahm leads by 1, McIlroy 2 back at Abu Dhabi ChampionshipTagesschauFAQ: Was ist über die Schüsse in Krakau bekannt?BBC News'Dana, please take the job!' - Trump asks UFC boss to be White House press secretaryالشرقالرئيس الإسرائيلي يكلف جادي آيزينكوت بتشكيل الحكومةCBS NewsMore races called as Democratic majority comes into focusGuardian SportSão Paulo Formula One Grand Prix qualifying in Brazil – live again after a red flagRai NewsPistorius: "Non possiamo più mandare altri Patriot a Kiev". Tajani nel mirino del dark webCollider’Saturday Night Live’s Famous Formula Has 1 Unfixable ProblemSudinfo« S’il te plaît, accepte ce boulot » : Donald Trump propose le poste de porte-parole de la Maison Blanche à Dana White, patron de la fédération de MMA
The Daily Newsstand · Free, Always
Saturday, November 7, 2026

SoundCloud для Mac, которого у SoundCloud нет

Translate
Полноэкранный плеер Nimbus

Полноэкранный плеер Nimbus

Я слушаю SoundCloud каждый день, а на Mac у него есть только сайт. Поэтому я написал Nimbus — нативный клиент на SwiftUI и AppKit. Треки в нём сменяют друг друга без паузы, работают медиаклавиши и Now Playing, на волне висят комментарии, музыку можно отправить на Chromecast или AirPlay, а полноэкранный режим превращает трек в пластинку, которую можно поскретчить.

Бесплатно, код открыт под MIT. Поставить можно за минуту:

brew install --cask dearonski/tap/nimbus

Нужны macOS 26 и Apple Silicon. Подробности установки — в конце статьи. Дальше — что получилось и что пришлось раскопать по дороге: куда уходят полторы секунды между треками, почему SwiftUI пришлось посадить на AppKit и как подружить Mac с Chromecast, для которого у Google нет SDK под macOS.

Почему не сайт

Сайт SoundCloud хороший. Плохо то, что это вкладка:

  • она теряется среди тридцати других, и чтобы переключить трек, её приходится искать;

  • медиаклавиши достаются тому, кто последним издал звук, — то SoundCloud, то видео в соседней вкладке;

  • между треками слышна пауза — для сета, сведённого встык, это заметно;

  • браузер держит целый движок ради одного плеера.

Официального десктопного приложения у SoundCloud нет. Мне хотелось, чтобы он вёл себя как Музыка на Mac: жил в Dock, слушался клавиатуры и не тормозил на длинных лентах.

Что получилось

Без паузы между треками

Следующий трек готовится за 20 секунд до конца текущего, так что смена занимает 20–34 мс. Как это устроено — ниже, в разделе «Под капотом».

Волна с комментариями

Страница трека: волна с комментариями

Страница трека: волна с комментариями

Лица сидят на той секунде, где оставлен комментарий, как на сайте. Под курсором аватар вырастает, а текст появляется одной строкой внутри волны — ничего не перекрывает.

Пластинка

⇧⌘F открывает трек на весь экран: обложка из плеера улетает в конверт, из него выезжает пластинка. Она крутится на 33⅓ оборота, умноженных на скорость воспроизведения, на паузе плавно тормозит, а комментарии всплывают над названием, когда трек до них доходит. Пластинку можно схватить и покрутить: один оборот — десять секунд трека.

Фон за пластинкой собран из цветов самой обложки и медленно переливается, а при смене трека перетекает в новые цвета. Плеер внизу окна тоже слегка подкрашивается под обложку.

Скорость с режимом Vinyl

От 0,5× до 2×. В обычном режиме тон сохраняется, в режиме Vinyl едет вместе с темпом — так звучат slowed- и sped-up-версии, которыми SoundCloud завален. Теперь их можно делать из любого трека на лету.

Chromecast и AirPlay

Кнопка Play On в плеере показывает Chromecast-устройства и колонки AirPlay в сети. На Chromecast трек играет сам телевизор или колонка, а Nimbus остаётся пультом: очередь, громкость и скорость управляют устройством.

Очередь, плейлисты и всё остальное

Лайки сеткой и открытая очередь

Лайки сеткой и открытая очередь

  • Очередь с перетаскиванием, которая переживает перезапуск вместе с треком и страницей.

  • Свои плейлисты: создать, переименовать, переставить треки перетаскиванием, добавить любой трек из его меню.

  • Колокольчик Activity в тулбаре: кто подписался, кто лайкнул, репостнул или прокомментировал ваши треки, с кнопкой Follow Back.

  • Назад и вперёд свайпом, как в Finder; меню и шорткаты — как в Музыке, разделы на ⌘1–⌘8.

  • Мгновенный поиск по всему, что вы уже видели, пока грузится выдача SoundCloud; на пустом поиске — свежие чарты по жанрам.

  • Лайки, репосты, подписки и комментарии работают на запись. Прослушивания попадают в вашу историю SoundCloud, как с сайта.

  • Ссылки soundcloud.com из описаний открываются прямо в приложении.

Под капотом

Это HLS-клиент, а не «скачай mp3»

Первое, что выясняется: SoundCloud отдаёт звук как AAC в HLS. Плейлист .m3u8, сегменты в fMP4. Старый прогрессивный MP3 ещё жив, но до звука от него проходит 3570 мс против 1477 мс у HLS — я оставил его только запасным вариантом.

Вторая новость: подписанные ссылки на плейлист живут около пяти минут. Для трека на три минуты это неважно, а вот часовой DJ-сет, которыми SoundCloud знаменит, без мер замолкал бы на шестой минуте. Поэтому длинные треки переполучают плейлист прямо во время игры.

Как 1477 мс стали 20

Первая версия делала паузу между треками примерно в полторы секунды. Я разложил её по шагам и выяснил, что сам плеер ни при чём: от «готов» до «играет» у AVPlayer проходило 0,3 мс. Остальное — сеть: получить ссылку на поток, скачать плейлист, дождаться первых сегментов. Значит, всё это нужно делать заранее.

Подвох в том, что AVPlayerItem, созданный впрок, не грузит ничего, пока не попадёт внутрь плеера. Поэтому в Nimbus две деки — два AVPlayer, как два проигрывателя у диджея. Следующий трек кладётся в резервную деку за 20 секунд до конца текущего и успевает набрать буфер, а смена трека — это просто переключение дек. Само переключение занимает 0,1 мс, до звука — 19,7, 34,4 и 20,9 мс на трёх замерах.

Почему не AVQueuePlayer, который умеет предзагрузку из коробки? У него один аудиовыход, и кроссфейд на нём не сделать никогда. Две деки оставляют под него задел: это просто встречные кривые громкости.

Были и грабли. Флаг automaticallyWaitsToMinimizeStalling = false выглядел как очевидное ускорение старта, а на деле треки перестали включаться совсем — без звука и без ошибки. Между плеером и CDN у меня свой загрузчик, данные приходят не мгновенно, и плеер уходил в бесконечное ожидание. Флаг вернул на место, а заодно выяснилось, что на HLS он и не лечил ничего.

46 миллисекунд, которые я решил оставить

Настоящий sample-accurate gapless так и не случился, и вот почему. AAC-кодер добавляет в начало каждого трека 2048 сэмплов тишины на «прогрев» — это ≈ 46 мс. Обычно контейнер сообщает плееру, сколько отрезать, но у SoundCloud эта запись (elst в fMP4) обнулена: так её затирает муксер FFmpeg, я воспроизвёл это локально. Конец трека SoundCloud при этом подрезает корректно, так что формат это выражать умеет.

Лечится патчем нескольких байтов в init-сегменте каждого трека. Я решил пока этого не делать: 46 мс слышны только на материале, сведённом встык, а полуторасекундная дыра уже закрыта.

Почему SwiftUI пришлось посадить на AppKit

Первые версии были SwiftUI сверху донизу: NavigationSplitView, страницы, длинные ленты в NSCollectionView со SwiftUI-ячейками. Работало, но профили то и дело показывали потери, которые не объяснялись кодом страницы: окно в покое ело десятки процентов CPU, прокрутка временами дёргалась, а каждая точечная правка давала пару процентов и не больше.

Я перестал лечить симптомы и собрал стенд: одна ячейка, которая обновляется 30 раз в секунду, в трёх разных обёртках.

Как устроено окно

CPU

Как было в Nimbus: SwiftUI-окно, внутри AppKit-список, внутри SwiftUI-ячейки

23,1%

AppKit-оболочка, SwiftUI-страница

13,7%

SwiftUI только в ячейке

7,3%

Причина — в том, как NSHostingView сообщает об изменениях: он перерисовывается в layout() и помечает себя как требующий раскладки. Каждый SwiftUI-хост выше по иерархии принимает это за смену размера и перекладывает своё поддерево. Одна тикающая ячейка раскачивает всё окно.

Отсюда правило, по которому Nimbus переписан: окно, сайдбар, тулбар и колонки — AppKit (NSSplitViewController, системный NSToolbar), SwiftUI — только в листьях, то есть в содержимом страниц и ячеек, а всё, что движется постоянно, — Core Animation. Волна, которая подсвечивает проигранную часть, стала слоем с маской: страница артиста с играющим треком — 53,6% CPU до и 3,3% после. Полноэкранная пластинка — 18,5% и 4,4%.

Две находки по пути, которые пригодятся любому, кто пишет под macOS 26:

  • Прозрачный тулбар и область перетаскивания. Лента лежит под тулбаром, и AppKit на каждом кадре прокрутки пересчитывает, за какие места можно таскать окно: обходит все кнопки и жесты на экране. В профиле это было до 19% главного потока. Помогло отключать перетаскивание окна на время прокрутки и убирать hit-testing у анимированных заглушек.

  • CoreGraphics и скругления. Сотни скруглённых прямоугольников одним путём заливаются в 15 раз медленнее, чем те же прямоугольники по одному: 10–90 мс против 0,6–1,5. Волна из столбиков именно такая, поэтому она рисуется по одному столбику в маску, в фоновом потоке и один раз на трек, а подсветка проигранной части — это уже просто слой под этой маской.

После переноса длинных списков на NSCollectionView время потерянных кадров на странице плейлиста упало с 349 до 60 мс на секунду прокрутки.

Пластинка и фон из обложки

Пластинка крутится на слоях Core Animation, а не в SwiftUI: постоянное вращение в SwiftUI — это пересчёт графа на каждом кадре. Самая дорогая мелочь оказалась в тени. Тень, привязанная к вращающемуся диску, перерисовывается каждый кадр, хотя от поворота круга не меняется. Её отбрасывает неподвижный круг под диском.

Фон из обложки устроен так: картинка усредняется по сетке 3 × 3, и девять цветов становятся опорными точками MeshGradient, которые медленно гуляют. Первая версия рисовала градиент в размер окна, и смена трека давала 137–159 мс потерянных кадров в секунду — по шкале Apple всё, что выше 10, уже видно глазом. Тот же градиент, нарисованный в 64 × 40 точек и растянутый, дал 5–62 мс/с и на глаз не отличается: градиент и так размыт.

Самый обидный баг был не в графике. При смене трека новый конверт ждёт свою обложку, но не дольше 0,4 секунды, чтобы не застрять на медленной сети. Я сделал это через withTaskGroup: одна задача ждёт картинку, другая — таймер, берём первую. Только группа задач в Swift не завершается, пока не закончатся все дочерние задачи, а загрузку картинки отменить изнутри было нельзя. На некэшированной обложке новый трек появлялся через 2,8 секунды. Помогла гонка на withCheckedContinuation: кто первым позовёт resume, тот и победил, второй вызов игнорируется. На 35 переключениях подряд самое долгое ожидание стало 415 мс.

Нашёл я его не профайлером — тот показывал чистые кадры, ведь ничего не рисовалось, всё просто ждало. Нашли метки времени в системном логе на каждом шаге смены трека: по ним дыра видна с первого прогона.

Chromecast без SDK

Google Cast SDK есть для iOS, Android и Chrome, но не для macOS. Зато сам протокол несложный. Устройства объявляют себя по Bonjour (_googlecast._tcp), разговор идёт по TLS на порт 8009 с самоподписанным сертификатом устройства, а сообщения — это protobuf CastMessage с четырьмя байтами длины впереди и обязательным PING/PONG, иначе устройство рвёт соединение. На Network.framework это NWBrowser для поиска и NWConnection с TLS для канала.

Дальше вопрос, что запустить на телевизоре. Стандартный медиаресивер Google получил бы прямую ссылку на поток — а она, как мы помним, живёт пять минут, и часовой сет на ней не доиграть. У SoundCloud есть собственный ресивер для Chromecast. Ему достаточно сказать, какой трек играть: поток он получает сам, а прослушивание попадает в историю, как при касте с телефона.

Очередь при этом остаётся у Nimbus. Ресиверу уходит по одному треку, а следующий отправляется, когда текущий доиграл. Так продолжают действовать правила Nimbus для повтора, перемешивания и автоплея, а не логика ресивера, который любит дописывать в очередь свою станцию. Громкость и скорость Nimbus передаёт на устройство — с двумя поправками: громкость шлётся не чаще раза в 120 мс, а полторы секунды после движения ползунка Nimbus не верит громкости из ответов устройства — иначе ползунок прыгает назад от его же устаревших отчётов.

AirPlay проще: это системный AVRoutePickerView. А вот добавить Chromecast в системное меню вывода звука нельзя — для этого нужен закрытый entitlement, поэтому у Nimbus своё окно Play On с обоими списками.

Как я мерил

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

  • Плавность — только Instruments, шаблон Animation Hitches: сколько миллисекунд в секунду кадры опаздывают. xctrace позволяет записывать его из скрипта, так что любую правку можно прогнать тем же сценарием до и после.

  • Последовательность шагов — метки времени в системном логе (Logger.notice, а не .info: .info живёт только в памяти).

  • Анимации — покадровый разбор записи экрана. Превью SwiftUI показывает один кадр, а словами «дёргается вот тут» анимацию не опишешь.

Честно о минусах

  • Клиент неофициальный. Он ходит в тот же внутренний API, что и сайт, а входите вы в окне с обычной страницей входа SoundCloud. Если SoundCloud что-то поменяет, Nimbus может сломаться — обновления приходят прямо в приложении через Sparkle.

  • Только macOS 26 и Apple Silicon. Дизайн построен на Liquid Glass, а запасные варианты для старых систем я ни разу не проверил вживую, поэтому выпускать их не стал.

  • Без нотаризации Apple. Для неё нужен платный сертификат разработчика за $99 в год, поэтому при первом запуске macOS один раз попросит подтверждения. Первые пожертвования пойдут как раз на сертификат — ссылка в конце.

  • Интерфейс пока только на английском.

  • Треки Go+ и недоступные в вашем регионе помечены и пропускаются, как на сайте без подписки.

Попробовать

Через Homebrew:

brew install --cask dearonski/tap/nimbus

Или скачайте DMG со страницы релизов и перетащите в «Программы».

При первом запуске macOS скажет, что не может проверить приложение. Нажмите «Готово», откройте «Системные настройки → Конфиденциальность и безопасность» и нажмите «Всё равно открыть» напротив Nimbus.

Дальше всё как у обычного приложения, обновления включительно.

Код — на GitHub. Если клиент понравится, звезда на GitHub очень поможет. Если хотите, чтобы Nimbus открывался без лишнего шага в настройках, можно поддержать сбор на сертификат на Boosty. Если что-то сломается, пункт Help → Report an Issue… сам соберёт версию, систему и кусок лога.

А чего не хватает вам в SoundCloud? Напишите в комментариях — возможно, это будет в следующей версии.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.