Кодирование ogg-vorbis файла

Категория: Заметки | Скилл: C++ , encode/decode | Дата: 09.05.2020
Нижеизложенный текст является результатом разработки плагина кодека ogg для движка SkyXEngine, в тексте приведены источники из которых черпалась информация (официальная документация и репозиторий), а ссылка на полный исходный код плагина появится в ближайшее время.

Теория

Весь процесс можно кратко описать следующими последовательностями (пока все просто):

  • инициализация ogg/vorbis данных
  • подготовка данных
  • заливка данных
  • очистка того что уже не нужно
Процесс подготовки и заливки данных не совсем тривиальный. Сначала нужна инициализация семплов (PCM), после которой во вложенных циклах пойдет внутренее формирование данных и последовательная заливка. Звучит сложнее, но по сути все просто.

Цикличное формирование данных связано с тем, что:

  • ogg файл состоит из страниц (каждая из которых имеет заголовок и тело и не превышает обьем 64КБ (28 байт на заголовок, до 255 байт на список длин пакетов, и до 65025 байт на сжатые данные)),
  • тело страницы состоит из пакетов (однако, пакет не имеет минимальных и максимальных пределов, поэтому один пакет может быть на нескольких страницах),
  • а пакеты формируются при кодировании из некодированных блоков данных
То есть, процесс получения данных для заливки можно описать так:
  • формирование блоков из некодированных данных
  • отправка блоков на кодирование
  • формирование кодированных пакетов из блоков
  • формирование страниц из пакетов
Эти самые страницы и идут на запись в файл. Каждая страница принадлжеит логическому потоку, который помечается серийным номером (в примере ниже, (да и вообще на ресусрах проекта ogg/vorbis), используется rand). Это связано с тем, что один файл (физический поток) может состоять из нескольких логических потоков, которые могут быть расположены как последовательно, так и чередуясь страницами (для случая одновременного чтениях из нескольких логических потоков, чтобы не перемещать курсор далеко). Серийный номер позволяет отделить один логический поток от другого.

В документации есть более подробное описание контейнера ogg.

Здесь также важно понимать что сжатие данных происходит за счет манипуляции с битрейтом (частота подачи/поступления данных, немного теории по этому поводу здесь). Если очень кратко, то процесс кодирования это преобразование постоянного битрейта (constant bitrate) PCM данных в переменный (variable) или усредненный (average). Здесь есть немного информации о том, как производить настройку битрейта для vorbis.

Практика

Весь процесс кодирования, на примере нижеизложенного кода, можно описать так:

  • Обьявление ogg/vorbis данных
  • Первичная инициализация до кодирования, в том числе настройка битрейта
  • Запись (сброс) заголовков (первичных данных) в логический поток данных и запись в файл
  • Инициализация семплов в битовый поток ogg
  • Формирование данных из некодированных блоков в кодированные пакеты, из пакетов в страницы, и заливка (сброс) страниц в файл
  • Очистка данных

Примечание: В рассмотренном ниже коде подразумевается запись в m_pFile типа FILE*, но запись возможна и в область оперативной памяти. Так как vorbis кодек не накладывает ограничений на битность семпла, то в данном коде было использовано 2 байта на семпл (int16_t)

Обьявление ogg/vorbis данных:

//логический поток ogg
ogg_stream_state oOggStream;

//страница битового потока
ogg_page oOggPage;

//пакет сырых данных для декодирования
ogg_packet oOggPacket;

//статическая информация (настройки) битового потока
vorbis_info oVoInfo;

//комментарии
vorbis_comment oVoComment;

//состояние кодека https://xiph.org/vorbis/doc/libvorbis/vorbis_dsp_state.html
vorbis_dsp_state oVoMainState;

//блок аудиоданных https://xiph.org/vorbis/doc/libvorbis/vorbis_block.html
vorbis_block oVoDataBlock;

Первичная инициализация до кодирования:

//инициализация кодировщика (статических данных) с переменным битрейтом (variable bitrate)
vorbis_info_init(&oVoInfo);
iRetCode = vorbis_encode_init_vbr(&oVoInfo, pOutDesc->u8Channels, pOutDesc->uSampleRate, 0.1);

/* можно заюзать усредненный битрейт (average bitrate):
iRetCode = vorbis_encode_init(&vi,pOutDesc->u8Channels, pOutDesc->uSampleRate,-1,128000,-1);
 * но в данной реализации пусть кодировщик сам разбирается :)
*/

if(iRetCode)
	return false;

//инициализируем комментарий
vorbis_comment_init(&oVoComment);
vorbis_comment_add_tag(&oVoComment, "ENCODER", "SkyXEngine plugin [class 'codec_ogg']");

vorbis_analysis_init(&oVoMainState, &oVoInfo);
vorbis_block_init(&oVoMainState, &oVoDataBlock);

//инициализация битового потока и присвоение ему серийного номера
srand(time(NULL));
ogg_stream_init(&oOggStream,rand());

//создание и инициализация заголовков (идентификационного, комментариев, кода)
ogg_packet oHeaderId, oHeaderComment, oHeaderCode;
vorbis_analysis_headerout(&oVoMainState, &oVoComment, &oHeaderId, &oHeaderComment, &oHeaderCode);
ogg_stream_packetin(&oOggStream, &oHeaderId);
ogg_stream_packetin(&oOggStream, &oHeaderComment);
ogg_stream_packetin(&oOggStream, &oHeaderCode);

Запись (сброс) заголовков (первичных данных) в логический поток данных и запись в файл:

while(!iEndOfStream)
{
	iRetCode = ogg_stream_flush(&oOggStream, &oOggPage);
	if(iRetCode == 0)
		break;
	fwrite(oOggPage.header, 1, oOggPage.header_len, m_pFile);
	fwrite(oOggPage.body, 1, oOggPage.body_len, m_pFile);
}

Инициализация семплов в битовый поток ogg:

//количество блоков семплов (количество каналов * байт на семпл)
int iCountBlocks = uSize/ (pOutDesc->u8BlockAlign);

/* получаем выделенный массив (по количеству каналов) массивов (по количеству семплов)
  aaBuffer[iChannel][iSample]
*/
float **aaBuffer = vorbis_analysis_buffer(&oVoMainState, iCountBlocks);

//заполняем выделенный float массив нормализованными данными [-1.0, 1.0]
for(int i = 0; i < iCountBlocks ; ++i)
{
	for(int iChannels = 0; iChannels<pOutDesc->u8Channels; ++iChannels)
	{
		int16_t i16Sample = ( (int16_t*)pData )[i];
		aaBuffer[iChannels][i]=float(i16Sample)/32768.f;
	}
}

//сообщаем кодировщику что поступили данные для записи
vorbis_analysis_wrote(&oVoMainState, iCountBlocks);

pOutDesc->u8BlockAlign - размер блока семплов на каждый канал в байтах, если на 1 семпл выделяется 2 байта (int16_t), и звук имеет 2 канала тогда u8BlockAlign = 2 канала * 2 байта = 4 байта на 1 блок

UPDATE. Приведенный выше код успешно работал на 32 битной сборке, однако с тем же ogg файлом на 64 битной сборке программа падала с ошибкой переполнения стека. Проблема в том, что внутри vorbis_analysis_wrote выделяется память на стеке по общему количеству блоков, это может привести к переполнению стека если передать большое количество блоков. Решение проблемы - цикличный сборс данных пачками (например по 1024 блока):

//количество блоков семплов (количество каналов * байт на семпл)
int iCountBlocks = uSize/ (pOutDesc->u8BlockAlign);

//по сколько блоков записывать за один раз
int iPartBlocks = 1024;

//количество уже записанных блоков
int iCountReadedBlocks = 0;
	
	
//циклом (частями) передаем данные кодировщику
while (iCountReadedBlocks < iCountBlocks)
{
	//количество уже прочитанных сэмплов
	uint32_t uCountReadedSamples = (iCountReadedBlocks*pOutDesc->u8Channels);
		
	//количество блоков для текущей итерации
	int iCurrBlocks = iPartBlocks;
	if (iCountBlocks - iCountReadedBlocks < iPartBlocks)
		iCurrBlocks = iCountBlocks - iCountReadedBlocks;

	float **aaBuffer = vorbis_analysis_buffer(&oVoMainState, iCurrBlocks);

	for (int i = 0; i < iCurrBlocks; ++i)
	{
		for (int iChannels = 0; iChannels<pOutDesc->u8Channels; ++iChannels)
		{
			int16_t i16Sample = ( (int16_t*)pData)[uCountReadedSamples + i];
			aaBuffer[iChannels][i] = float(i16Sample) / 32768.f;
		}
	}

	vorbis_analysis_wrote(&oVoMainState, iCurrBlocks);
	iCountReadedBlocks += iCurrBlocks;
}

Формирование данных из некодированных блоков в кодированные пакеты, из пакетов в страницы, и заливка (сброс) страниц в файл:

//если еще не дошли до конца битового потока, тогда продолжаем запись
while(!iEndOfStream)
{
	/*разбивка несжатых данных на блоки, если не удалось, тогда сообщаем что данных больше не будет
		если не сообщить об этом, то не все данные будут записаны, не получится дойти до конца битового потока, потому что запись идет постраничная и последняя неполня страница не будет записана
	*/
	if(vorbis_analysis_blockout(&oVoMainState, &oVoDataBlock)!=1)
		vorbis_analysis_wrote(&oVoMainState, 0);

	//поиск режима кодирования и отправка блока на кодировку
	vorbis_analysis(&oVoDataBlock, NULL);
	vorbis_bitrate_addblock(&oVoDataBlock);

	//получение следующего доступного пакета
	while( vorbis_bitrate_flushpacket(&oVoMainState, &oOggPacket) )
	{
		//отправка пакета в битовый поток
		ogg_stream_packetin(&oOggStream, &oOggPacket);

		while(!iEndOfStream)
		{
			//формирование пакетов в страницы и отправка в битовый поток
			int result=ogg_stream_pageout(&oOggStream, &oOggPage);
			if(result==0)
				break;
			fwrite(oOggPage.header, 1, oOggPage.header_len, m_pFile);
			fwrite(oOggPage.body, 1, oOggPage.body_len, m_pFile);

			//если все записано (находимся в конце битового потока), сообщаем о завершении
			if(ogg_page_eos(&oOggPage))
				iEndOfStream=1;
		}
	}
}

Очистка данных:

ogg_stream_clear(&oOggStream);
vorbis_block_clear(&oVoDataBlock);
vorbis_dsp_clear(&oVoMainState);
vorbis_comment_clear(&oVoComment);
vorbis_info_clear(&oVoInfo);

Дополнительная информациия

Страница на официальном сайте проекта где крайне лаконично описан процесс vorbis кодирования PCM данных. А здесь по ссылкам черпал информацию по кодированию из первоисточника.

Пример кодирование на github, помог быстро понять что к чему, однако местами оказался сложнее чем есть на самом деле.

Документация по ogg

Есть еще Ogg media, но это уже совсем другой проект.

Я Виталий, ник в сети Byurrer.
Увлекаюсь программированием, веду интересные проекты, пишу здесь об интересующих меня вещах: о работе, проектах, увлечениях и проффесиональном развитии.

Скилы
php, c++, javascript, sql, hlsl, html, css
Проекты
SkyXEngine, PHP-API, S4G
Категории
В разработке :)
Популярное
В разработке :)