Содержание статьи +
- Кратко
- Зачем это нужно
- Одна мысль: кодек работает только с целыми блоками данных
- От сэмплов к миллисекундам: арифметика, с которой вы будете постоянно сталкиваться
- Гранулы: лишний слой внутри MP3 (и почему вы встретите это слово)
- Размеры фреймов, которые вы реально встретите, рядом
- Как аудиопакет выглядит в сети
- Почему возникает задержка: фрейм – это единица ожидания
- Ловушка, которую стоит запомнить: фреймы не совпадают с вашими сегментами
- Где здесь Фора Софт
- Главное
- Что читать дальше
Кратко
Сжатое аудио – это никогда не одна длинная непрерывная дорожка звука: оно разбито на небольшие фрагменты фиксированного размера, называемые фреймами. Каждый фрейм содержит короткий отрезок времени – обычно от 20 до 25 миллисекунд. Фрейм – это минимальная единица, которую кодек может закодировать или декодировать самостоятельно: AAC работает с 1024 сэмплами за раз, MP3 – с 1152 сэмплами, разделёнными на две гранулы по 576, а Opus – с фреймами, длительность которых можно задать от 2,5 до 60 миллисекунд. Именно такая дискретизация позволяет реализовывать стриминг, перемотку, восстановление после потерь и голосовые вызовы в реальном времени – но она же и порождает скрытую задержку, поскольку каждый этап обработки – от микрофона до динамика – вынужден ждать завершения целого фрейма, прежде чем начать работу. Эта статья объясняет, что такое фрейм, гранула и RTP-пакет, почему число 20 миллисекунд так часто встречается в аудиотехнологиях и как эти микроскопические задержки накапливаются, формируя ощутимую задержку для пользователя.
Зачем это нужно
Если вы создаёте стриминговый сервис, инструмент видеосвязи или любой продукт, который записывает или воспроизводит звук, фрейм – это единица, на которой держится весь ваш аудиоконвейер, и почти никто за пределами команды кодека не обращает на него внимания. А он объясняет те баги, с которыми вы сталкиваетесь: почему в звонке есть фиксированная задержка, которую невозможно уменьшить; почему перемотка в подкасте сдвигается на долю секунды; почему потерянный сетевой пакет обрывает ровно один короткий отрезок, а не весь поток; и почему две аудиодорожки, которые должны совпадать, начинают расходиться. Эта статья даёт продакт-менеджеру или разработчику базовый словарь для обсуждения фреймов, пакетов и задержки, которую они создают – достаточно, чтобы разобраться в настройках кодека, поговорить с инженером и понять, какой параметр действительно влияет на задержку.
Одна мысль: кодек работает только с целыми блоками данных
Начнём с факта, который объясняет всё остальное. Современный аудиокодек не может сжимать звук по одному сэмплу. Ему нужен блок сэмплов, чтобы анализировать их совместно, потому что основной приём, используемый каждым кодеком – преобразование волны в набор частот и отбрасывание тех, что ваше ухо не различает, – работает только на участке сигнала, а не на отдельной точке. Этот участок и есть фрейм – наименьшая единица аудио, которую кодек может закодировать или декодировать независимо, без опоры на другие фреймы.
Бытовая аналогия – кино. Фильм – это не одно непрерывное изображение, а последовательность отдельных кадров, показанных достаточно быстро, чтобы создать иллюзию движения. Звук – та же идея, только во времени. Микрофон выдаёт непрерывный поток измерений – 48 000 значений в секунду для видеозвука (см. частоту дискретизации) – а кодек берёт их фиксированными порциями, сжимает каждую порцию в небольшой пакет байтов и отправляет их один за другим. Проиграйте пакеты по порядку – и непрерывный звук восстановится.
Две вещи, которые стоит запомнить из этого раздела. Первая: фрейм – это единица времени, хотя и измеряется в сэмплах: 1024 сэмпла при частоте дискретизации 48 000 Гц – это временной отрезок. Вторая: фрейм – атом всего конвейера; нельзя декодировать половину фрейма, нельзя перемотать на середину и нельзя передать по сети меньше одного. Все последующие цифры – следствие именно этого.
От сэмплов к миллисекундам: арифметика, с которой вы будете постоянно сталкиваться
Фрейм задаётся в сэмплах, но важно вам, сколько миллисекунд звука он держит, потому что именно миллисекунды пользователь чувствует как задержку. Перевод – это одно деление, и его стоит один раз посчитать вслух, чтобы числа перестали быть загадкой.
Возьмём 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 кГц | Внутренняя подъединица | Типичное применение |
|---|---|---|---|---|
| Opus | 120–2880 (вы выбираете) | 2.5 / 5 / 10 / 20 / 40 / 60 мс | нет | WebRTC-звонки, современный стриминг |
| AAC-LC | 1024 | 21,3 мс | 8×128 коротких блоков на транзиентах | MP4-видео, OTT, вещание |
| MP3 | 1152 | 24,0 мс | 2 гранулы × 576 | легаси-файлы, подкасты |
| AC-3 (Dolby Digital) | 1536 | 32,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.
Почему возникает задержка: фрейм – это единица ожидания
Вот часть, которая важнее всего для любого, кто выпускает живой продукт, – ради неё и написана статья. Нарезка даёт стриминг и восстановление после потерь, но у неё есть цена: каждый этап конвейера должен дождаться полного фрейма, прежде чем приступить к работе. Нельзя закодировать фрейм, который ещё не собран целиком, и нельзя декодировать тот, что ещё не пришёл полностью. Эти ожидания накапливаются, и их сумма – это половина задержки, которую нельзя уменьшить, не меняя размер фрейма.
Пройдём путь одного 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-пакет передаёт один фрейм, помеченный так, чтобы система могла справиться с потерей.
- Задержка накапливается, потому что каждый этап ждёт завершения целого фрейма; размер фрейма определяет нижнюю границу задержки.
- Аудиофреймы не всегда точно вписываются в сегменты потока – из-за этого возникает дрейф и щелчки при склейке.
Что читать дальше
- Аудио в контейнерах: как MP4, MKV, fMP4 и MPEG-TS переносят звук – где хранятся аудиофреймы после упаковки.
- Opus: открытый кодек, который захватил WebRTC – кодек, размер фрейма в котором вы можете выбирать самостоятельно.
- Jitter buffer: NetEQ, мозг аудио в WebRTC – как приёмник преобразует неравномерные пакеты в стабильное воспроизведение.