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

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

Кратко

Сжатое аудио – это никогда не одна длинная непрерывная дорожка звука: оно разбито на небольшие фрагменты фиксированного размера, называемые фреймами. Каждый фрейм содержит короткий отрезок времени – обычно от 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 мс – и захват, буфер джиттера и предварительная обработка сжимаются, снижая общую задержку, но ценой худшего сжатия и увеличения числа пакетов в секунду. Увеличьте до 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-пакет передаёт один фрейм, помеченный так, чтобы система могла справиться с потерей.
  • Задержка накапливается, потому что каждый этап ждёт завершения целого фрейма; размер фрейма определяет нижнюю границу задержки.
  • Аудиофреймы не всегда точно вписываются в сегменты потока – из-за этого возникает дрейф и щелчки при склейке.

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

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

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