Фреймы, пакеты, гранулы: почему звук нарезается на куски

Автор: Николай СапуновОбновлено: август 202619 мин чтения
Содержание статьи +

Кратко

Сжатое аудио – это никогда не одна длинная лента звука; оно нарезано на маленькие куски фиксированного размера, которые называют фреймами, и каждый держит крошечный отрезок времени, обычно от 20 до 25 миллисекунд. Фрейм – это наименьшая единица, которую кодек может закодировать или декодировать сам по себе: AAC работает с 1024 сэмплами за раз, MP3 – с 1152 сэмплами, разбитыми на две гранулы по 576, а Opus – с фреймом, который вы задаёте от 2.5 до 60 миллисекунд. Именно эта нарезка делает возможными стриминг, перемотку, восстановление после потерь и звонки в реальном времени – но из неё же берётся скрытая задержка, потому что каждый этап от микрофона до динамика обязан дождаться целого фрейма, прежде чем что-то сделать. Эта статья показывает, что такое фрейм, гранула и RTP-пакет на самом деле, почему число 20 миллисекунд встречается повсюду и как эти маленькие ожидания складываются в задержку, которую чувствуют пользователи.

Зачем это нужно

Если вы строите стриминговый сервис, инструмент видеосвязи или любой продукт, который пишет или проигрывает звук, фрейм – это единица, на которой держится весь ваш аудиоконвейер, и почти никто за пределами команды кодека на него не смотрит. А он объясняет те баги, что вы видите: почему у звонка есть фиксированный пол задержки, который не подкрутить; почему перемотка в подкасте промахивается на долю секунды; почему потерянный сетевой пакет глушит ровно один маленький промежуток, а не весь поток; и почему две аудиодорожки, которые должны совпадать, расходятся. Эта статья даёт продакт-менеджеру или разработчику словарь, чтобы рассуждать о фреймах, пакетах и задержке, которую они создают, – достаточно, чтобы прочитать настройку кодека, поговорить с инженером и знать, какая ручка реально двигает задержку.

Одна мысль: кодек умеет работать только с целым куском за раз

Начнём с факта, который объясняет всё остальное. Современный аудиокодек не может сжимать звук по одному сэмплу. Ему нужен блок сэмплов, чтобы смотреть на них вместе, потому что приём, которым пользуется каждый кодек, – превратить волну в набор частот и выбросить те, что ваше ухо не заметит, – работает только на куске, а не на одной точке. Этот кусок и есть фрейм: наименьшая единица аудио, которую кодек может закодировать или декодировать самостоятельно, не опираясь на другие фреймы.

Бытовая аналогия – кино. Фильм – это не одно непрерывное изображение, а последовательность отдельных кадров, показанных достаточно быстро, чтобы выглядеть движением. Звук – та же идея, только во времени. Микрофон выдаёт ровный поток измерений – 48 000 штук каждую секунду для видеозвука (см. частоту дискретизации) – а кодек загребает их фиксированными горстями, сжимает каждую горсть в маленький пакет байтов и отправляет их один за другим. Проиграйте пакеты по порядку – и ровный звук вернётся.

Две вещи, которые стоит унести из этого раздела. Первая: фрейм – это единица времени, хотя и измеряется в сэмплах: 1024 сэмпла при 48 000 сэмплов в секунду – это отрезок времени. Вторая: фрейм – атом всего конвейера; нельзя декодировать половину фрейма, нельзя перемотать в его середину и нельзя отправить по сети меньше одного. Каждое число дальше – следствие этого.

Рисунок 1. Кодек загребает непрерывную волну фиксированными горстями. Каждый фрейм становится одним маленьким сжатым пакетом байтов.

От сэмплов к миллисекундам: арифметика, которую вы будете считать постоянно

Фрейм задаётся в сэмплах, но важно вам, сколько миллисекунд звука он держит, потому что именно миллисекунды пользователь чувствует как задержку. Перевод – это одно деление, и его стоит один раз посчитать вслух, чтобы числа перестали быть загадкой.

Возьмём AAC, самый частый кодек в видеофайлах, у которого фрейм – 1024 сэмпла. При стандартной для видео частоте 48 000 сэмплов в секунду:

длительность фрейма = сэмплов во фрейме / частота дискретизации
                    = 1024 / 48 000
                    = 0,02133 секунды
                    = 21,3 миллисекунды

Значит, один фрейм AAC – это около 21,3 миллисекунды звука. Прогоните то же деление для 1152-сэмплового фрейма MP3 – получите 24 миллисекунды; для фрейма Opus в 960 сэмплов – ровно 20 миллисекунд. Вот почему «примерно 20–25 миллисекунд» – это размер почти любого аудиофрейма, который вам встретится: это не совпадение и не прихоть комитета по стандартам, а то, во что превращается тысяча с небольшим сэмплов на частотах, которые использует видео. Меньше фрейм – меньше задержка, но хуже сжатие; больше фрейм – лучше сжатие, но больше задержка. Каждый кодек выбирает свою точку на этом компромиссе.

Гранулы: лишний слой внутри MP3 (и почему вы встретите это слово)

MP3 добавляет одну тонкость, которую стоит знать, потому что термин всплывает в инструментах и путает людей. Фрейм MP3 в 1152 сэмпла внутри разбит на две половины по 576 сэмплов, и каждая половина называется гранулой – это подфрейм, который кодировщик может обрабатывать со своими настройками. Так у MP3 два уровня: фрейм – это единица, которая едет по сети и к которой вы перематываете, а гранула – единица, на которой реально считается сжатие, дважды на фрейм.

Гранулы не нужны, чтобы пользоваться MP3, но слово протекает в трёх местах: в логах кодировщика, в разговорах про gapless-воспроизведение (задержка кодировщика и хвостовое заполнение меряются в сэмплах, которые не делятся на гранулы ровно) и в коде перемотки, потому что перемотка может попасть только на границу фрейма, никогда на границу гранулы. Новые кодеки отказались от двухуровневой схемы – у AAC и Opus есть только фрейм, – и это одна из причин, по которой о них проще рассуждать. Увидев «granule» в логе, читайте это как «половина фрейма MP3» и идите дальше.

Размеры фреймов, которые вы реально встретите, рядом

Вот тот небольшой набор чисел, который стоит запомнить. Каждое значение – это собственное определение фрейма у кодека при частоте 48 кГц, которую использует видео; длительности округлены до одного знака.

КодекРазмер фрейма (сэмплы)Длительность при 48 кГцВнутренняя подъединицаТипичное применение
Opus120–2880 (вы выбираете)2.5 / 5 / 10 / 20 / 40 / 60 мснетWebRTC-звонки, современный стриминг
AAC-LC102421,3 мс8×128 коротких блоков на транзиентахMP4-видео, OTT, вещание
MP3115224,0 мс2 гранулы × 576легаси-файлы, подкасты
AC-3 (Dolby Digital)153632,0 мс6 аудиоблоков × 256вещание, Blu-ray
G.711 (телефония)1 (без преобразования)настраивается, часто пакеты по 20 мснетлегаси-VoIP, ТфОП

Особняком стоит Opus – кодек почти каждого браузерного звонка (см. кодек Opus). У него нет одного размера фрейма – вы его выбираете. Низколатентный режим CELT допускает фреймы 2.5, 5, 10 и 20 мс; речевой режим SILK – 10, 20, 40 и 60 мс; гибридный режим для полнополосного голоса – 10 и 20 мс. Спецификация IETF, RFC 6716, определяет все эти размеры. На практике почти все используют 20 мс, потому что это золотая середина между задержкой и эффективностью – достаточно коротко, чтобы разговор ощущался живым, и достаточно длинно, чтобы хорошо сжимать.

Как аудиопакет выглядит в сети

В файле фреймы просто идут один за другим. В живом звонке им надо пересечь интернет, и тут появляется пакет. Для аудио реального времени транспорт – это RTP (Real-time Transport Protocol, определён в RFC 3550), и правило простое: один RTP-пакет обычно несёт один аудиофрейм. Фрейм – это полезная нагрузка; RTP оборачивает вокруг него небольшой заголовок.

Этот заголовок и делает фрейм выживаемым в ненадёжной сети. Он несёт порядковый номер (sequence number), чтобы приёмник понял, пропал ли пакет или пришёл не по порядку, и временную метку (timestamp), которая говорит, куда именно на таймлайне ложится звук этого фрейма. Для Opus часы временной метки зафиксированы на 48 000 тиков в секунду независимо от реальной частоты аудио – правило из RFC 7587, формата RTP-нагрузки для Opus, – так что фрейм в 20 мс продвигает метку ровно на 960. Когда пакет теряется, приёмник видит разрыв в порядковых номерах и точно знает, какие 20 мс звука надо замаскировать, а не глючит весь поток. Это и есть выигрыш от нарезки в сети: потеря становится маленькой находимой дыркой, а не катастрофой. Как приёмник прячет эту дырку – отдельная тема, см. маскировку потерь пакетов; а как он сглаживает неровное время прихода – работа jitter buffer.

Рисунок 2. Один RTP-пакет несёт один аудиофрейм. Порядковый номер и 48-кГц метка позволяют приёмнику найти потерянный фрейм с точностью до 20 мс.

Почему задержка складывается: фрейм – это единица ожидания

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

Пройдём путь одного 20-мс фрейма ото рта одного человека до уха другого. Сначала микрофон должен захватить полные 20 мс, прежде чем кодировщик вообще увидит целый фрейм, – это 20 мс ушло ещё до начала работы. Затем кодировщику нужен весь фрейм плюс небольшой просмотр вперёд, чтобы его сжать. Пакет пересекает сеть, где ждёт в jitter buffer, который намеренно держит фрейм-другой, чтобы опоздавшие не дали разрывов. Наконец декодер восстанавливает фрейм, и звуковое устройство проигрывает его, тоже кусками. Добавим показательный набор чисел:

захват (надо набрать один фрейм 20 мс)   = 20 мс
кодировщик: фрейм + просмотр вперёд       ≈ 25 мс
сеть (в одну сторону, хороший путь)       ≈ 30 мс
jitter buffer (держит ~2 фрейма)          ≈ 40 мс
декодер + буфер устройства вывода         ≈ 20 мс
-------------------------------------------------
итого ото рта до уха                      ≈ 135 мс

Размер фрейма задаёт пол для трёх из этих пяти строк. Уполовиньте фрейм до 10 мс – и захват, jitter buffer и просмотр вперёд сжимаются, тянут сумму вниз – ценой худшего сжатия и большего числа пакетов в секунду. Удвойте до 40 мс – сожмёте лучше и пошлёте меньше пакетов, но только что добавили десятки миллисекунд неизбежной задержки. Это и есть центральное решение по настройке в аудио реального времени, и живёт оно целиком в размере фрейма. Полный бюджет, по этапу за раз, мы считаем в статье про WebRTC-конвейер аудио целиком.

Ловушка, которую стоит запомнить: фреймы не совпадают с вашими сегментами

Самый частый баг нарезки – рассогласование двух разных сеток. Аудио нарезано на фреймы нечётной длительности – 21,3 мс у AAC, – а стриминговые системы режут таймлайн на сегменты круглой длительности, вроде 2 или 6 секунд, а CMAF режет их ещё на более мелкие чанки. Беда в том, что 21,3-мс фреймы не делятся ровно на 2-секундный сегмент: 2000 / 21,3 – это около 93,75 фрейма, не целое число. Фрейм оказывается на границе.

Кодировщики справляются, округляя каждый сегмент до целого числа аудиофреймов, и значит границы аудиосегментов ложатся на пару миллисекунд в стороне от границ видеосегментов. Обычно плеер это терпит. Но именно поэтому аудио и видео в некоторых потоках медленно расходятся, поэтому вставка рекламы на границе сегмента может щёлкнуть, если склейка игнорирует сетку фреймов, и поэтому gapless-воспроизведению нужны явные метаданные заполнения, чтобы спрятать остаточный неполный фрейм кодировщика. Урок: когда синхронизация ломается у границы, подозревайте рассогласование сеток «фрейм против сегмента» раньше, чем кодек. Нарезка аудио и сегментирование контейнера – два разных ритма, и их приходится согласовывать намеренно, а не на удачу. Сторона контейнера этой истории – в статье про аудио в контейнерах.

Где здесь Фора Софт

Решения на уровне фрейма всплывают почти в каждом продукте реального времени и стриминга, который мы строим. В WebRTC-видеосвязи и телемедицине размер фрейма Opus – первая ручка, к которой мы тянемся, когда звонок ощущается тормозным: переход с большого фрейма на 20 мс или обмен небольшого сжатия на более короткое окно захвата часто возвращает ту отзывчивость, которую пользователи замечают. В OTT и e-learning-стриминге сетка «фрейм против сегмента» – ровно то место, откуда берутся дрейф аудио и щелчки на склейке рекламы, поэтому мы выравниваем длительности сегментов по целым аудиофреймам как обычную практику. В конвейерах записи и транскрипции знание, что атом – это фрейм, а не сэмпл, удерживает перемотки, обрезки и склейки там, где они должны быть. Во всех этих случаях отношение к фрейму как к полноправному понятию, а не как к детали реализации, и не даёт аудио стать тем, на что жалуются пользователи.

Главное

  • Сжатое аудио нарезано на фреймы – наименьшую единицу, которую кодек декодирует сам.
  • Фрейм – это отрезок времени: ~21 мс у AAC, 24 мс у MP3, ваш выбор у Opus.
  • Гранула MP3 – это половина фрейма; AAC и Opus убрали этот лишний слой.
  • В сети один RTP-пакет несёт один фрейм, помеченный так, чтобы пережить потерю.
  • Задержка складывается, ведь каждый этап ждёт целый фрейм; размер фрейма задаёт пол.
  • Аудиофреймы не делятся ровно на сегменты потока – отсюда дрейф и щелчки склейки.

Что читать дальше

Строите такую систему?

Подберём параметры кодирования под ваш контент и посчитаем стоимость доставки до старта разработки.