libogg + libopus
posheДля решения этой задачи нам потребуется не только `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
```