touch MAX

получи максимум — личный блог "тыж программиста"

Auro-3D: как в обычном PCM прячут высотные каналы

Пока все спорят «Atmos — маркетинг или нет», рядом тихо живёт другой 3D-звук — Auro-3D. Без «объектов вокруг головы», зато с честными высотными каналами звука и очень хитрой упаковкой в обычный многоканальный PCM.

Я давно интересуюсь звуком, писал обзоры по железу, писал про Dolby Atmos. В том числе интересовался Auro 3D, который был представлен бельгийской компанией Galaxy Studios в 2006 году и назывался Octopus. В 2011 стал доступен в потребительских устройствах, а в 2019 был представлен новый формат Auro-Cx, который кроме канального кодирования может использовать и объектное, как и Dolby Atmos и DTS:X

Микрофоны для записи auro 3d

Скачав несколько демо роликов и образов дисков, я обнаружил лишь звуковые дорожки в формате DTS HD MA (или многоканальный Flac) классической раскладки 5.1\7.1, а популярная утилита MediaInfo ничего не показывала про высотные каналы, тогда как железный ресивер (с нужным декодером) эти дорожки показывал как Auro 9.1 или 13.1 и, если приложить ухо, высотные каналы звучали только на правильных верхних колонках и не звучали внизу. Ресиверы без декодера Auro озвучивал все каналы внизу.

Тогда я начал искать официальную документацию, но большинство ссылок были устаревшими, а новые вели лишь на маркетинговое описание формата. Но webarchive и другие файлпомойки мне помогли и я все таки нашел старое описание декореда. Ссылка на доки в конце статьи.

Оттуда я узнал что Auro 3D встраивает информацию в нижние биты 24 битного PCM сигнала. Нейронки в 2025 мало что знали о формате или только информацию из wiki. Тогда я декодировал с помощью ffmpeg звук до многоканального PCM 24bit, быстренько накидал на python скрипт, который из 24 битного PCM извлекал фиксированные верхние 4 бита и тогда я воочию увидел метаданные Auro, вот пример тишины, но содержащие магические биты информации:

Эти блоки повторялись (внутри были и постоянные данным и меняющиеся), а потом и смешивались с реальными звуковыми сигналами (их паттерн все еще можно было разглядеть в потоке двоичного кода). Центральный канал и LFE могли не содержать эту мету. Угадать, что за что какой бит отвечает, было нереально.

Потом начал искать декодер, приложения и официальный декодер платный, а все что удалось скачать — зашифровано\обфусцировано. Те прошивки ресиверов которые удавалось расковырять, декодер auro был в формате для специализированных DSP. И наконец мне улыбнулась удача и я нашел декодер в ELF, да и еще с сохранёнными debug символами. И началась работа над реверс инжинирингом, сначала над детектором layout, а потом и до полноценного декодера.

Я долго ковырял этот формат изнутри (примерно год) и таки раскопал + помогли LLM:

  1. где и как лежит мета;
  2. какой битрейт у меты;
  3. как высоты вытаскивают обратно из нижнего слоя и теряется ли качество после dematrix.

Сначала немного теории

Auro-3D — канальный иммерсивный формат: нижний bed (как 5.1/7.1) плюс слой высоты (HL, HR, HLS, HRS, Top/HC). Типичные потребительские раскладки:

  • 9.1 = 5.1 + 4 высоты
  • 11.1 = 7.1 + 4 высоты
  • 13.1 = 7.1 + 5 высот + Top

Есть специфический Auro 2D, в котором в стерео (в контейнере он 2.1) смикширован звук в 5.1 (и по тестам, декодер очень качественно их восстанавливает). Так же есть редкие 4.0.4H или 5.1.2H.

Профессиональное оборудование поддерживает до 27 каналов — AuroMax® 26.1 и выглядит устрашающе:

Но по HDMI можно отдать только 8 дискретных каналов без сжатия, а для, например, 9.1 уже нужно 10. Ответ от Auro:

закодировать высоту внутрь обычного PCM 5.1/7.1 так, чтобы без декодера файл звучал как нормальный surround, а с декодером — разворачивался в полный Auro с высотой.

Это Auro-3D Octopus Codec (классический Auro-Codec или codec-v3).

Где прячется мета

Отдельного «мета-трека» нет. Всё живёт в младших битах 24-bit PCM.

Идея из white paper (ссылки на доку в конце статьи) простая:

  • для доставки звука 24 бита избыточны: реальный динамический диапазон системы/слуха ближе к 16–20 битам;
  • нижние 2–4 бита почти «шум»;
  • их и используют под служебные данные и residuals (ошибки предсказания).

Что именно лежит в LSB

Это не «пара флагов», а можно сказать полноценный side-channel (конечно после восстановления):

Глобальная ADOL-мета (layout/boot):

  • целевой layout (например 5.1+4H);
  • carrier layout (что физически в файле);
  • нужен ли mix3 (три выхода из одного carrier);
  • metadata_block (часто 1000 или 1024 сэмпла);
  • sync_sample — где впервые ловится синхрослово.

Per-frame данные по каналам:

  • какой выходной канал восстанавливаем;
  • от какого carrier-source предсказываем;
  • режим Extrapolate (1 / 2 / 3);
  • quant/scale;
  • параметры Golomb-Rice;
  • seeds предиктора;
  • упакованные residuals (не PCM высоты целиком!).

Синхронизация общая: декодер OR’ит LSB по живым carrier-каналам и ловит sync (типично 16 единиц подряд в младшем бите). Полезный битстрим с важным residuals при этом у каналов свой.

Внутри целая стейтмашина:
# ============================================================
# ADOL state machine (classic Auro-Codec, LSB side-channel)
# ============================================================

enum State:
  SEEK_SYNC
  PARSE_SYNC_HEADER
  CHECK_CRC
  PARSE_A3D_HEADER
  PARSE_ADOL_BLOCKS
  PARSE_ADOL_INSTR
  APPLY_SEMANTICS
  DONE
  FAIL

procedure ScanAdol(pcm_channels):
  meta = empty
  for ch in pcm_channels:
    samples = channel_pcm24(ch)
    state = SEEK_SYNC
    run_ones = 0
    s = 0

    while s < len(samples) and state != DONE:
      switch state:

        # --- 1) охота за sync: 16 единиц подряд в bit0 ---
        case SEEK_SYNC:
          if (samples[s] & 1) == 1:
            run_ones += 1
          else:
            run_ones = 0
          if run_ones >= 16:
            sync_start = s - 15
            state = PARSE_SYNC_HEADER
          else:
            s += 1

        # --- 2) разбор sync: m (число LSB), block_size ---
        case PARSE_SYNC_HEADER:
          sync = ParseSyncAt(samples, sync_start)
          if not sync.ok:
            run_ones = 0
            state = SEEK_SYNC
            s += 1
            continue
          if sync_start + sync.block_size > len(samples):
            state = FAIL
          else:
            state = CHECK_CRC

        # --- 3) CRC16-CCITT по блоку ---
        case CHECK_CRC:
          if CRC16(samples[sync_start .. +block_size]) != ExpectedCRC(...):
            run_ones = 0
            state = SEEK_SYNC
            s += 1
          else:
            state = PARSE_A3D_HEADER

        # --- 4) заголовок A3D-блока до ADOL ---
        case PARSE_A3D_HEADER:
          bits = MuxBitstream(samples, sync_start, m=sync.m)
          # первые 32 бита уже съедены sync'ом
          skip: u8, 4×bool, u4, 2×u8, 2×bool, u2, u4, u8
          adol_blocks = read_u8(bits)
          skip: u8
          # optional vector size зависит от count≠255 в 4 байтах
          count = count_non_255(read 4×u8)
          skip 4×u8
          vec_sz = {2→2, 3→5, else FAIL}
          skip vec_sz × u32
          state = PARSE_ADOL_BLOCKS
          b = 0
          instructions = []

        # --- 5) ADOL-блоки: каждый начинается с tag==1 ---
        case PARSE_ADOL_BLOCKS:
          if b >= adol_blocks:
            state = APPLY_SEMANTICS
            continue
          tag = read_u8(bits)
          if tag != 1:
            state = FAIL
            continue
          state = PARSE_ADOL_INSTR

        # --- 6) инструкции до opcode==0 (END) ---
        case PARSE_ADOL_INSTR:
          opcode = read_u8(bits)
          ins = {block:b, tag, opcode}

          switch opcode:
            case 0x00:                 # END
              # payload нет
            case 0x01, 0x03, 0x5A..0x62:
              ins.value = read_bits(16)
            case 0x02, 0x04, 0x1E, 0x1F, 0x41, 0x47, 0x50..0x58:
              ins.value = read_bits(8)
            case 0x0E, 0x46, 0x6E..0x76, 0x80..0x85, 0x8C..0x90:
              ins.value = read_bits(32)
            case 0x40:                 # primary downmix gain
              ins.value  = read_bits(8)   # channel
              ins.value2 = read_bits(8)   # scaler
            case 0x64..0x6C:           # encoder version и рядом
              ins.value = read_bits(24)
            default:
              state = FAIL
              continue

          instructions.append(ins)

          if opcode == 0x00:
            b += 1
            state = PARSE_ADOL_BLOCKS
          # иначе остаёмся в PARSE_ADOL_INSTR

        # --- 7) семантика ключевых opcode ---
        case APPLY_SEMANTICS:
          for ins in instructions:
            switch ins.opcode:
              case 0x1E:  # ChannelInputConfig
                meta.layout_id   = OriginalLayout(ins.value)
                meta.carrier_id  = CarrierLayout(ins.value)
              case 0x40:
                meta.primary_downmix = (ch=ins.value, scaler=ins.value2)
              case 0x41:
                meta.limit_simple = ins.value
              case 0x46:
                meta.secondary_downmix = ins.value
              case 0x47:
                meta.auromatic = (profile=value>>4, mode=value&0xF)
              case 0x64:
                meta.encoder_version = ins.value & 0xFFFFFF
              case 0x80..0x85:
                meta.loudness = ins.value
            meta.adol_instructions.append(ins)

          if meta.layout_id == 0:
            state = FAIL
          else:
            meta.found = true
            meta.sync_sample = sync_start
            meta.block_size  = sync.block_size
            meta.carrier_ch  = ch
            state = DONE

        case FAIL:
          run_ones = 0
          state = SEEK_SYNC
          s += 1

        case DONE:
          return meta

      # после успешного блока прыгаем за его конец
      if state == DONE:
        break
      if just_finished_good_block:
        s = sync_start + sync.block_size
        run_ones = 0
        # можно продолжить сканировать следующие sync,
        # но для boot достаточно первого валидного layout

  return meta


Число младших бит не константа на весь трек. Кодек смотрит уровень блоками (~1 мс @ 48 kHz) и, если сигнал тихий, забирает меньше LSB, чтобы не портить тишину. Если громко — можно забрать больше из полезного сигнала.

Примерная схема
SEEK_SYNC
│ 16× LSB=1

PARSE_SYNC → CHECK_CRC → PARSE_A3D_HEADER
│ │
│ fail ▼
└──────────────────── PARSE_ADOL_BLOCKS
│ tag=1

PARSE_ADOL_INSTR ⟲ (пока opcode≠0)
│ opcode=0, все блоки

APPLY_SEMANTICS → DONE

Любая нормализация/ресемпл/громкость «поверх» такого PCM легко убивает мету.

Какой итоговый битрейт

Тут важно не смешать три разных цифры.

1) Битрейт файла на диске / по HDMI

Carrier (физ каналы) остаётся обычным PCM или сжатым без потерь, например, в DTS HD MA. Для 5.1 24-bit @ 48 kHz:

6 × 24 × 48000 ≈ 6.9 Мбит/с

Для 7.1:

8 × 24 × 48000 ≈ 9.2 Мбит/с

После Auro-encode внешний битрейт тот же: это всё ещё 5.1/7.1 PCM. Высотные биты «бесплатны» с точки зрения числа каналов контейнера. Именно поэтому даже PCM Auro 13.1 влезает в HDMI/BD.

2) Сколько «места» у меты внутри

Мета — side-channel из нескольких LSB × сэмплы:

ёмкость блока ≈ block_size × m бит

где m — сколько младших бит забирают (часто от 2–4, в реальных стримах бывает и больше).

Пример порядка величин: блок 1024, m=3 на один carrier-канал:

1024 × 3 ≈ 3072 бит ≈ 3 кбит на кадр

кадр ≈ 21 мс @ 48 kHz

→ порядка ~140+ кбит/с side-info на один carrier-канал

Каналов несколько, плюс общий header/sync/CRC.

Напомню в LSB лежит не «весь height PCM», а в основном:

  • параметры разделения;
  • сжатые residuals ошибки предсказания/разделения.

Энергия высоты уже запечена в carrier на этапе encode. Поэтому информации для восстановления хватает даже когда bed и height звучат «по-разному». А когда данные высотных каналов сильно коррелированы с bed, например, как на концертных записях, данных еще меньше и точность выше.

3) Сколько «сэкономили» относительно дискретных высот

Если бы 9.1 хранили дискретным PCM 24/48:

10 × 24 × 48000 ≈ 11.5 Мбит/с

Auro-9.1 в carrier 5.1:

≈ 6.9 Мбит/с PCM + высоты внутри LSB

Получается сжатие числа каналов примерно в 1.5–2+ раза (в white paper для Octopus пишут reduction от ~50% до сильно больше, в зависимости от режима), при этом совместимость с железом без Auro сохраняется.

Как восстанавливаются высотные каналы

Это не «угадай высоту из корреляции bed», как в старых matrix-кодеках. Это controlled mix → unmix.

На encode:

оригинал bed + height

↓ художественный mix (с gain’ами)

carrier 5.1/7.1 (уже содержит энергию высоты)

+

LSB: residuals + параметры разделения

На decode:

DelayLine(carrier)

→ Sync/Format detector

→ Parser (frame meta)

→ Golomb-Rice (распаковка residuals)

→ Extrapolate (unmix)

→ discrete bed + height

Режимы Extrapolate

Mode 2 (mix2) — из одного carrier + одного residual-потока собираются два канала. Классика для пар:

  • FL ↔ HL
  • FR ↔ HR
  • LS ↔ HLS
  • RS ↔ HRS

Mode 3 (mix3) — из одного carrier + двух residuals — уже три выхода. Нужен для Auro-2D и тяжёлых раскладок вроде 7.1 + 5H + T. Предиктор трёхфазный: первые сэмплы получаются из меты, дальше что-то в духе 4*last − 3*previous, residuals раскидываются по фазе.

Как высоты «вычитаются» из нижних

Без Auro обычная сумма необратима: S = A + B. С Auro в LSB есть информация, чтобы максимально приближённо восстановить A' и B' и при этом развести то, что было смешано с gain’ами.

Упрощённо для пары bed/height:

carrier ≈ g1·bed + g2·height (+ квантование/LSB-замещение)

decode:

height’ = f(carrier, residuals, predictor, scales)

bed’ = unmix(carrier, height’, gains…)

На практике OutputGenerator не «отнимает height из WAV линейно одной формулой на всё», а:

  1. берёт carrier-сэмплы из DelayLine;
  2. разворачивает residuals через Golomb-Rice;
  3. прогоняет Extrapolate — получает до 2–3 выходов;
  4. bed-слот, который участвовал в dematrix, помечается как восстановленный bed (иногда почти тихий, если в carrier почти вся энергия была «высотной» для identification-стримов);
  5. height уходит в native-выход.

Важный практический момент: после mix3 нельзя тупо скопировать сырые LS/RS из carrier обратно в bed. В channel-id тестах там как раз остаются «объявления» высотных каналов. Bed надо брать из dematrix/unmix, height — из native-реконструкции.

В декодере есть отдельный пайплайн, когда может запрашиваться большее количество каналов чем то что получилось после декодирования, тогда в дело вступает Auromatic — специальный апмиксер, он достраивает алгоритмически недсотающие каналы.

Без декодера вы слышите совместимый surround: высота уже «вшита» в bed-микс. С декодером этот микс разделяется обратно на слои.

Теряется ли качество после dematrix?

Звук в Auro может быть сохранен внутри любого lossless контейнера поддерживающий многоканальный звук в 24bit это WAV, FLAC, DTS HD MA и даже Dolby TrueHD. Но железные плееры обычно работают только с DTS HD MA.

Сама компания Auro называет это virtually lossless:

  • это не perceptual coding как mp3/AAC/AC-3 (частоты не выкидывают «потому что мозг не услышит»);
  • residuals и quant дают маленькую ошибку;
  • в white paper артефакты часто ниже −85 dBFS, иногда около −90 dBFS;
  • спектр ошибки больше похож на низкоуровневый шум, чем на pre-echo/filter-bank кашу lossy-кодеков;
  • заявляется 100% channel separation после decode (слои реально разъезжаются, а не остаются «матричной кашей»).

Где качество всё же страдает:

  1. LSB substitution — младшие биты carrier больше не содержат «чистого» звука, а смешаны с метой. Напомню при прослушивании без Auro декодера обычно неслышно, но это уже не исходный 24-bit master as-is.
  2. Quant/scale — если side-budget жмут, энкодер сильнее квантует residuals (rate–distortion).
  3. Округления предиктора — Extrapolate работает в фиксированной арифметике, потом clamp в PCM24.
  4. Повторные бессмысленные encode/decode в целом заявлены устойчивыми, но любой «кривой» remux с громкостью/дизером поверх Auro-PCM ломает все.

Итого по качеству:

Что сравниваемРезультат
Исходные высотные каналы vs AuroВиртуальный lossless, не bit-perfect
Многоканал без декодера vs художественный downmixСовместимый surround, высоты уже внутри микса
Нижние каналы после dematrix vs исходныеВосстанавливается unmix’ом; ошибка на уровне шума кодека
«Матричный» Dolby Pro Logic-styleДругая природа: там корреляция, тут residuals + controlled mix

В моих тестовых файлах оказалось лишь два файлы в исходном многоканальном формате и закодированный в Auro и вот результаты сравнения:

orig5.1.flac vs декод auro2d.flac 96000 Hz, 6 ch

chcorrref_rmscand_rmsgain_db
FL0.9995270.0091540.009147-0.006
FR0.9996760.0082530.008248-0.005
C0.9996920.0087830.008778-0.005
LFEв дорожку auro2d не включили lfe канал0.0038580.000000nan
LS0.9994100.0088140.008806-0.008
RS0.9997830.0075840.007581-0.004

Сравнение других муз треков с WAV 7.1.4 → Auro 5.1+5H+T (без BL/BR):

chcorrref_rmscand_rmsgain_db
FL0.9808110.0633360.0676220.569
FR0.9742680.0623150.0666080.579
C1.0000000.0922180.086228-0.583
LFE1.0000000.0461640.043083-0.600
SL0.8614980.0239750.020202-1.487
SR0.8193900.0236860.018894-1.963
TFL0.9998490.0109210.0119930.813
TFR0.9998150.0121490.0131900.714
TBL0.8862410.0109210.0162093.430
TBR0.9228700.0121490.0173313.086
  • corr — корреляция Пирсона между каналом reference и candidate (после выравнивания по lag). 1.0 = формы волны совпадают, 0 = не связаны, отрицательное = в противофазе. nan = один из каналов тишина (нулевая энергия).
  • ref_rms — RMS reference-канала, нормированный на full-scale PCM24 (÷ 2²³). По сути относительная громкость оригинала (0…1).
  • cand_rms — то же для candidate (декод).
  • gain_db — разница уровней: 20·log10(cand_rms / ref_rms).
    0 ≈ одинаковая громкость, + = декод громче,  = тише. nan — если RMS одного из каналов ноль.

Декодер

По результатам года работы был написан декодер на основе реверс инжиниринга оригинального от Auro. Это бета версия! Точность восстановления не гарантирована, хотя по тестам выше декодирование вроде как работает правильно 🕺

Auromatic (при использовании параметра dsp-output-channels) нечем протестировать.

orua3d-decode — консольный декодер Auro-3D. Берёт файл с Auro-Codec (PCM/FLAC/MKV/DTS…) или AuroCX (MP4 a3ds), находит мету в LSB, разворачивает высоты/bed и пишет обычный многоканальный WAV/FLAC.

На входе нужен ffmpeg/ffprobe в PATH (для demux). Рядом с WAV пишется XML с картой каналов.

Минимум:

orua3d-decode -i input.flac 

Параметры

Вход / выход

ПараметрОписание
-i, --inputВходной файл: WAV, FLAC, MKV, MP4, M2TS, DTS, raw .s24le
-o, --outputКуда писать. По умолчанию <input>_decoded.wav (или _decoded_binaural.wav с --binaural)
--output-formatwav (без лимита каналов) или flac (макс. 8 каналов)
--output-bitsГлубина PCM на выходе: 16 или 24 (по умолчанию 24)
--rawВход — сырой interleaved s24le; нужны --rate и --channels
--rate / --channelsЧастота и число каналов для --raw

Диагностика (без декода в файл)

ПараметрОписание
--probeПоказать layout, carrier, sync, ADOL, маски — без записи PCM
--channel-diagramСхема «что из carrier → что на выходе» (dematrix / native / passthrough)
-v, --verboseПодробный лог

Ядро декода (лучше не трогать) 😅

ПараметрОписание
--block NРазмер внутреннего host-блока. По умолчанию подбирается так, чтобы кадры кодека не рвались; fallback 832 (как у native)
--dsp-output-channels NСколько каналов на выходе. 0 / не указано = авто из меты. Потолок native — 27. Для legacy PCM без меты: 6=5.1, 10=5.1.4, 12=7.1.4
--dsp-strength N«Сила» DSP/рендера, 0..15, по умолчанию 12
--dsp-headroom-db XЗапас по уровню на всём выходе, 0..24 дБ, по умолчанию 0

Порядок каналов и экспорт

ПараметрОписание
--wav-standardПереставить каналы в канонический WAVEFORMATEXTENSIBLE порядок и прописать dwChannelMask (удобно для плееров/DAW)
--clear-output-lsb [N]Обнулить младшие N бит на экспорте (1..8, по умолчанию 4). Срезает остатки sync/ADOL с passthrough-каналов — для «чистого» PCM
--mono-tracksДополнительно выгрузить каждый канал отдельным моно-файлом: … (FL).wav и т.п.

Наушники / виртуализация

ПараметрОписание
--binauralСвести декод в стерео через HRTF (принудительно 48 kHz)
--room-preset NПомещение: 0 HOME, 1 CONCERT, 2 LOUNGE, 3 CINEMA
--hrtf-preset NHRTF: 0 HPV2, 1..3 GENERIC_1..3
--virtualizer-mode N0 ENABLED, 1 DISABLED

Экспериментальное

ПараметрОписание
--restore-lfeЕсли LFE тихий/пустой — синтезировать из bed (mono sum + LPF 120 Hz, −10 dB). Не native-путь

Что декодер делает по шагам

  1. Demux через ffmpeg → PCM24
  2. Скан LSB → sync / ADOL → layout + carrier
  3. Codec-v3: DelayLine → Sync → Parser → Golomb-Rice → Extrapolate (unmix bed/height)
  4. (Опционально) binaural / restore-lfe / wav-standard / clear LSB
  5. WAV/FLAC + XML mapping

Ссылки

Вся официальная дока, которую только смог найти.

Мой 💾получившийся декодер, можете использовать бесплатно, не для коммерческого использования, с указанием автора @almirus.

Вирусов нет 😇

Поблагодарить автора:

Спасибо с российских карт

Мой публичный адрес для получения ETH 0x5AA8B619B4a1C598b48F370F48602E8E327c9E3D

Произведите оплату через Trust Wallet: https://link.trustwallet.com/send?coin=60&address=0x5AA8B619B4a1C598b48F370F48602E8E327c9E3D

Мой публичный адрес для получения USDT 0x5AA8B619B4a1C598b48F370F48602E8E327c9E3D

Произведите оплату через Trust Wallet: https://link.trustwallet.com/send?coin=10042221&address=0x5AA8B619B4a1C598b48F370F48602E8E327c9E3D&token_id=0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9

Мира!

, , , ,