libogg + libopus

libogg + libopus

poshe

https://peerpad.net/#/r/markdown/9tuFErEJkwD3HVhDa4HbP2s8zPmoh2qQyKV7KE6fQZGK/4XTTM6K18iCBWiCnHRFsVRdijzwng6Kn7JBu3D8hnaurz4FG1

Для решения этой задачи нам потребуется не только `libogg` (которая отвечает только за контейнер/упаковку данных), но и `libopus` (которая отвечает непосредственно за декодирование аудио из сжатых пакетов в PCM семплы). `libogg` сама по себе не умеет декодировать звук, она только "достает" пакеты байт из файла.


Ниже представлен полный пример кода.


### Зависимости

Вам понадобятся установленные библиотеки:

* `libogg`

* `libopus`


### Код (main.cpp)


```cpp

#include <iostream>

#include <vector>

#include <cstring>

#include <cstdio>


// Заголовочный файл Ogg (контейнер)

#include <ogg/ogg.h>

// Заголовочный файл Opus (декодер)

#include <opus/opus.h>


// Определим тип для блока семплов

using AudioBlock = std::vector<int16_t>;


// Вспомогательная функция для чтения uint32 из массива байт (little endian, как в спецификации Ogg)

uint32_t read_le32(const unsigned char* data) {

return data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24);

}


// Вспомогательная функция для чтения uint16

uint16_t read_le16(const unsigned char* data) {

return data[0] | (data[1] << 8);

}


/**

* Основная функция чтения

* @param filename Путь к файлу

* @param _data Указатель на вектор, который будет заполнен векторами семплов

* @param _samplerate Указатель для записи частоты дискретизации

* @return true, если успешно, false при ошибках или несоответствии формата

*/

bool func(const char* filename, std::vector<AudioBlock>* _data, uint32_t* _samplerate) {

if (!_data || !_samplerate) return false;

_data->clear();


FILE* fin = fopen(filename, "rb");

if (!fin) return false;


// --- Инициализация Ogg Sync ---

// ogg_sync_state используется для буферизации данных, считанных из файла,

// и поиска границ страниц Ogg.

ogg_sync_state oy;

ogg_sync_init(&oy);


// --- Состояния потока ---

// В Ogg файле может быть несколько потоков. Нам нужно найти тот, который Opus.

ogg_stream_state os;

bool stream_init = false;

int opus_serialno = -1;


// --- Состояния декодера ---

OpusDecoder* decoder = nullptr;

bool headers_processed = false; // Флаг: прошли ли мы OpusHead и OpusTags


// --- Буфер для чтения страниц ---

ogg_page og;

ogg_packet op;


bool success = false; // Итоговый флаг успеха

// Частота декодирования Opus всегда 48kHz (стандарт),

// хотя в заголовке может быть написано другое число (исходная частота).

// Мы будем использовать 48000 для декодирования.

const int DECODE_RATE = 48000;


while (true) {

// 1. Читаем данные из файла в буфер Ogg Sync

// Альтернатива: можно читать весь файл в память сразу, но буферизация экономит память

// на больших файлах.

const int buffer_size = 4096;

char* buffer = ogg_sync_buffer(&oy, buffer_size);

int bytes = fread(buffer, 1, buffer_size, fin);

ogg_sync_wrote(&oy, bytes);


// 2. Пытаемся достать страницы Ogg из буфера

// ogg_sync_pageout возвращает 1, если страница найдена, 0 если нужно больше данных,

// -1 если рассинхронизация (ошибка потока, но можно пропустить).

while (ogg_sync_pageout(&oy, &og) == 1) {

// Если мы еще не инициализировали наш поток (ищем начало)

if (!stream_init) {

// Проверяем, является ли эта страница началом потока (BOS - Beginning Of Stream)

if (ogg_page_bos(&og)) {

// Создаем временный stream state для проверки заголовка

ogg_stream_state test_os;

ogg_stream_init(&test_os, ogg_page_serialno(&og));

ogg_stream_pagein(&test_os, &og);

// Достаем первый пакет

if (ogg_stream_packetout(&test_os, &op) == 1) {

// Проверяем сигнатуру "OpusHead" (первые 8 байт)

if (op.bytes >= 19 && memcmp(op.packet, "OpusHead", 8) == 0) {

// НАШЛИ OPUS ПОТОК!

// Парсинг заголовка (RFC 7845)

// Byte 8: Version (must be 1)

// Byte 9: Channel Count

int channels = op.packet[9];

if (channels != 1) {

// Требование ТЗ: если не моно, выходим

ogg_stream_clear(&test_os);

goto cleanup;

}


// Byte 12-15: Input Sample Rate (информационное поле)

// Примечание: Opus всегда декодируется в 48/24/16/12/8 kHz.

// Значение в хедере - это частота исходника до кодирования.

// Для ТЗ вернем то, что в хедере, или реальную частоту декодирования?

// Обычно удобнее знать частоту полученных семплов (48000).

// Но прочитаем из хедера для справки.

// uint32_t header_rate = read_le32(op.packet + 12);

// Мы будем декодировать в 48000, так как это native rate для Opus.

*_samplerate = DECODE_RATE;


// Инициализируем настоящий поток

opus_serialno = ogg_page_serialno(&og);

ogg_stream_init(&os, opus_serialno);

stream_init = true;


// Инициализируем декодер libopus

int error;

decoder = opus_decoder_create(DECODE_RATE, channels, &error);

if (error != OPUS_OK) {

ogg_stream_clear(&test_os);

goto cleanup;

}

// Нужно переподать страницу в основной stream state,

// так как test_os "съел" пакет. Но так как мы уже обработали Header,

// в 'os' эту страницу можно не подавать или подать, но учесть что пакет вынут.

// Проще: просто скопируем структуры, так как test_os уже настроен.

// Но libogg не имеет функции копирования.

// Правильный путь: инициализируем 'os' и делаем pagein снова.

ogg_stream_clear(&test_os); // удаляем тест

ogg_stream_init(&os, opus_serialno); // создаем реальный

ogg_stream_pagein(&os, &og); // скармливаем страницу с OpusHead

// Вычитываем OpusHead пакет из os, чтобы сдвинуть внутренний указатель

ogg_stream_packetout(&os, &op);

} else {

// Не Opus, очищаем тест

ogg_stream_clear(&test_os);

}

} else {

ogg_stream_clear(&test_os);

}

}

} else {

// Если поток уже найден, проверяем, принадлежит ли страница ему

if (ogg_page_serialno(&og) == opus_serialno) {

ogg_stream_pagein(&os, &og);


// 3. Достаем пакеты из потока

while (ogg_stream_packetout(&os, &op) == 1) {

// Opus поток имеет структуру:

// Пакет 1: OpusHead (мы его уже прочитали выше или только что)

// Пакет 2: OpusTags (комментарии)

// Пакет 3+: Аудио данные

if (op.bytes >= 8 && memcmp(op.packet, "OpusHead", 8) == 0) {

// Это хедер (если попался снова или при первой инициализации)

continue;

}

if (op.bytes >= 8 && memcmp(op.packet, "OpusTags", 8) == 0) {

// Это теги, пропускаем

headers_processed = true;

continue;

}


// Если дошли сюда - это аудио (или теги, если headers_processed еще не true,

// но проверка выше должна поймать OpusTags).

// Максимальный размер фрейма Opus (120ms @ 48kHz = 5760 семплов)

const int MAX_FRAME_SIZE = 5760;

int16_t out_buf[MAX_FRAME_SIZE];


// Декодируем

// Аргумент 4 (frame_size): макс. емкость буфера на канал.

// Аргумент 5 (decode_fec): 0 (без коррекции ошибок, так как читаем файл).

int samples_decoded = opus_decode(decoder,

op.packet, op.bytes,

out_buf, MAX_FRAME_SIZE, 0);


if (samples_decoded > 0) {

// Создаем блок данных и копируем в вектор

// Альтернатива: можно иметь один огромный вектор, но ТЗ просит блоки.

AudioBlock block;

block.reserve(samples_decoded); // Оптимизация аллокации

// Копируем данные

block.assign(out_buf, out_buf + samples_decoded);

// Добавляем блок в общий массив

_data->push_back(std::move(block));

}

}

}

}

}


if (bytes == 0) break; // Конец файла

}


if (stream_init) {

success = true;

}


cleanup:

// Очистка ресурсов

if (decoder) opus_decoder_destroy(decoder);

if (stream_init) ogg_stream_clear(&os);

ogg_sync_clear(&oy);

fclose(fin);


return success;

}


// Пример использования

int main() {

const char* file = "test.ogg";

// Убедитесь, что у вас есть одноканальный opus файл с таким именем

// Например, можно создать ffmpeg -i input.mp3 -c:a libopus -ac 1 test.ogg


std::vector<std::vector<int16_t>> audio_data;

uint32_t rate = 0;


if (func(file, &audio_data, &rate)) {

std::cout << "Success! Found Opus stream." << std::endl;

std::cout << "Sample Rate: " << rate << " Hz" << std::endl;

std::cout << "Total blocks: " << audio_data.size() << std::endl;

size_t total_samples = 0;

for(const auto& block : audio_data) total_samples += block.size();

std::cout << "Total samples: " << total_samples << std::endl;

double duration = (double)total_samples / rate;

std::cout << "Duration: " << duration << " sec" << std::endl;

} else {

std::cout << "Failed to open or invalid format (must be 1ch Opus Ogg)." << std::endl;

}


return 0;

}

```


### Пояснения к реализации (шаг за шагом)


1. **Выбор библиотек**:

* Я использовал `<ogg/ogg.h>` для разбора страниц, как вы и просили.

* Я добавил `<opus/opus.h>`, потому что без него невозможно получить *реальные семплы*. Opus — это формат с сильным сжатием (как MP3), семплы там не лежат в открытом виде, их нужно математически восстанавливать.

* *Альтернатива*: Использовать `libopusfile`. Это библиотека высокого уровня, которая делает всё вышеописанное сама (открывает Ogg, ищет потоки, декодирует). Но так как задание требовало использования `libogg` и "ручной" структуры с блоками, подход с разделением (`libogg` + `libopus`) является наиболее точным ответом на требование "как это работает изнутри".


2. **Структура `AudioBlock`**:

* `std::vector<int16_t>`: Opus обычно декодируется либо в `float`, либо в `int16_t`. Вы просили 16-битные семплы.

* `_data`: Вектор векторов. Это удобно, потому что `opus_decode` выдает данные небольшими пачками (обычно по 20мс или 960 семплов). Складывать их в отдельные векторы в памяти проще, чем постоянно делать `resize` одного гигантского вектора (что вызывало бы частые копирования памяти).


3. **Логика поиска потока (`ogg_sync` -> `ogg_page` -> `ogg_stream`)**:

* Ogg файл — это каша из страниц. Сначала мы загоняем байты в синхронизатор (`ogg_sync`).

* Он находит страницы (`ogg_page`).

* Мы проверяем флаг начала (`BOS`) и ищем сигнатуру "OpusHead". Это стандарт для Ogg Opus.

* Если нашли, запоминаем серийный номер (`serialno`). Теперь мы знаем, какие страницы принадлежат нашему аудио, а какие (возможно) обложке альбома или метаданным.


4. **Декодирование (`opus_decode`)**:

* В цикле `while(packetout)` мы отсеиваем первые два пакета ("OpusHead" и "OpusTags"). Это метаданные.

* Остальные пакеты скармливаем `opus_decode`.

* **Важно про частоту**: Opus внутри себя почти всегда работает на 48 кГц. Даже если вы закодируете файл с частотой 8 кГц, декодер выдаст вам 48 кГц (или можно попросить его даунсемплить, но нативное качество — 48k). Поэтому я возвращаю 48000 в `_samplerate`.


5. **Pre-skip (Нюанс)**:

* В кодеке Opus есть понятие "Pre-skip". В заголовке (байт 10-11) указано число семплов, которые декодер выдаст в самом начале, но которые являются техническими (для разогрева кодека) и их надо бы удалить, чтобы звук совпадал с оригиналом до миллисекунды.

* В данном примере я **не обрезал** pre-skip вручную ради упрощения кода (чтобы он влез в разумные рамки примера), но в продакшн-коде первые ~300-3000 семплов (значение из заголовка) из первого блока данных обычно выбрасываются.


### Компиляция

Пример команды для компиляции (Linux/GCC):

```bash

g++ main.cpp -o opus_reader -logg -lopus

```

Report Page