Основы Android NDK: работа с OpenAL и форматами WAV, OGG

Для порта игры пришлось работать с OpenAL. Можно конечно было выкинуть весь C++ код и переписать всю работу со звуков на Java, но это не интересно. Решил поделиться опытом и показать как на Android работать с OpenAL и форматами WAV, OGG.

Подготовка

В первую нужно собрать OpenAL. Для работы с WAV этого достаточно, но мы же ещё хотим и с OGG поработать. Для OGG нужен декодер Tremor.

Собрать библиотеки

Сам OpenAL скачать можно отсюда. По сути, вам необходимо просто папку в проект добавить и в .mk файле прописать всё для сборки. Не помню уже, в репозитарии есть ли .mk файл или нет. В любом случае, внизу статьи сможете скачать мой проект готовый.

Android.mk для OpenAL

LOCAL_PATH := $(call my-dir)include $(CLEAR_VARS)LOCAL_MODULE := openalLOCAL_ARM_MODE   := arm#LOCAL_PATH   := $(ROOT_PATH)LOCAL_C_INCLUDES := $(LOCAL_PATH)/include $(LOCAL_PATH)/OpenAL32/IncludeLOCAL_SRC_FILES  := \OpenAL32/alAuxEffectSlot.c \OpenAL32/alBuffer.c\OpenAL32/alDatabuffer.c\OpenAL32/alEffect.c\OpenAL32/alError.c \OpenAL32/alExtension.c \OpenAL32/alFilter.c\OpenAL32/alListener.c  \OpenAL32/alSource.c\OpenAL32/alState.c \OpenAL32/alThunk.c \Alc/ALc.c  \Alc/alcConfig.c\Alc/alcEcho.c  \Alc/alcModulator.c \Alc/alcReverb.c\Alc/alcRing.c  \Alc/alcThread.c\Alc/ALu.c  \Alc/android.c  \Alc/bs2b.c \Alc/null.c \LOCAL_CFLAGS := -DAL_BUILD_LIBRARY -DAL_ALEXT_PROTOTYPESLOCAL_LDLIBS := -llog -Wl,-sinclude $(BUILD_STATIC_LIBRARY)

У Tremor есть всё, что необходимо. Просто добавьте всю папку в проект и сможете работать с .Ogg. Остаётся только подключить библиотеки в Android.mk основного проекта, добавив пару строчек в него:

#пути к хэдерамLOCAL_C_INCLUDES :=  $(LOCAL_PATH)/../openal/ $(LOCAL_PATH)/../openal/include/AL $(LOCAL_PATH)/utils $(LOCAL_PATH)/../tremolo#подключение самих библиотек, собственноLOCAL_STATIC_LIBRARIES :=  openal tremolo

А так же включить библиотеки все в Application.mk:

APP_MODULES  := openal tremolo AndroidNDK 

Структура WAV

Немного теории не повредит. Waveform Audio File Format (WAVE, WAV) — формат хранения оцифрованных аудио данных. Данный формат поддерживает данные различной битности, с различной частотой выборки и числом каналов. Суть его в том, что данные не закодированы, оттого и огромный размер таких файлов.

Долго расписывать теорию преобразований звуковых сигналов не будет, кому надо, сам найдёт. Структуру файла можно разделить на две части: хэдер и сами данные. Для описания хэдера и считывания удобно создать свою структуру:

typedef struct {  char  riff[4];// 'RIFF'  unsigned int riffSize;// Размер чанка ‘RIFF’  char  wave[4];// 'WAVE'  char  fmt[4];// 'fmt '  unsigned int fmtSize;// размер fmt-чанка  unsigned short format; // Формат звуковых данных  unsigned short channels;  // Количество каналов  unsigned int samplesPerSec;// Частота дискретизации аудио сигнала  unsigned int bytesPerSec; // Количество байт передаваемых в секунду  unsigned short blockAlign;// Выравнивание данных в чанке данных  unsigned short bitsPerSample; // Количество бит на одну выборку сигнала  char  data[4];// 'data'  unsigned int dataSize;// размер блока с самими данными}BasicWAVEHeader;

Чтение WAV

Собственно, наша задача — считать хэдеры и данные, а потом на основе них заполнить OpenAL структуры. Метод для чтения WAV:

void OALWav::load(AAssetManager *mgr, const char* filename){this->filename = filename;this->data = 0;// читаем файлthis->data = this->readWAVFull(mgr, &header);// узнаём формат из хэдераgetFormat();// создаём буфер из данныхcreateBufferFromWave(data);source = 0;// создание сорса из буфера для последующего проигрыванияalGenSources(1, &source);alSourcei(source, AL_BUFFER, buffer);}

Само чтение ничего экстраординарного вроде не представляет.

Чтение WAV

char* OALWav::readWAVFull(AAssetManager *mgr, BasicWAVEHeader* header){char* buffer = 0;AAssetFile f = AAssetFile(mgr, filename);if (f.null()) {LOGE("no file %s in readWAV",filename);return 0;}int res = f.read(header,sizeof(BasicWAVEHeader),1);//LOGI("read %i bytes from %s", res,filename );if(res){//LOGI("AAsset_read %s,",filename);if (!(// Заголовки должны быть валидны.// Проблема в том, что не всегда так.// Многие конвертеры недобросовестные пихают в эти заголовки свои логотипы =/memcmp("RIFF",header->riff,4) ||memcmp("WAVE",header->wave,4) ||memcmp("fmt ",header->fmt,4)  ||memcmp("data",header->data,4))){//LOGI("data riff = %s", header->riff);//LOGI("data size = %u", header->dataSize);buffer = (char*)malloc(header->dataSize);if (buffer){if(f.read(buffer,header->dataSize,1)){f.close();return buffer;}free(buffer);}}}f.close();return 0;}

Ошибки в заголовках

Стоит сказать об WAV кое-что. Бывает такое, что файл на PC вроде прослушивается отлично, но в при работе в OpenAL с ним возникают ошибки. Это из-за косяков в заголовках файла. Я встречал много конвертеров, которые в хэдеры писал какую-то чушь (свой логотип как пример), как правило в dataSize.

Непосредственно сами данные аудио хранятся после хэдера и их размер в dataSize. Если с этим полем что-то не так, а вы будете читать данные в соответствии с этими данными, то будут ошибки. Можно правда посчитать размер в лоб. Размер данных = размер файла — размер хэдера. Так что, думаю, плееры берут размер данных вычитая, а не из хэдера.

Ogg

В чём особенность Ogg по сравнению с WAV? Это сжатый формат. Ogg является всего лишь контейнером. Музыка или видео сжимаются кодеками, а результат обработки хранится в подобных контейнерах. Контейнеры Ogg могут хранить потоки, закодированные несколькими кодеками. Так что, перед там как записать данные в буфер OpenAL, нам необходимо данные декодировать.Загвоздка в том, что по умолчанию Vorbis (а мы будем использовать именно этот кодек) читает из FILE, так что нам необходимо переопределить все callback методы по работе с данными.

Определение callbacks

unsigned int suiCurrPos = 0;unsigned int suiSize = 0;unsigned int Min( unsigned int agr1,  unsigned int agr2){return (agr1 <agr2) ?agr1 : agr2;}static size_t  read_func(void* ptr, size_t size, size_t nmemb, void* datasource){unsigned int uiBytes = Min(suiSize - suiCurrPos, (unsigned int)nmemb * (unsigned int)size);memcpy(ptr, (unsigned char*)datasource + suiCurrPos, uiBytes);suiCurrPos += uiBytes;return uiBytes;}static int seek_func(void* datasource, ogg_int64_t offset, int whence){if (whence == SEEK_SET)suiCurrPos = (unsigned int)offset;else if (whence == SEEK_CUR)suiCurrPos = suiCurrPos + (unsigned int)offset;else if (whence == SEEK_END)suiCurrPos = suiSize;return 0;}static int close_func(void* datasource){return 0;}static long tell_func(void* datasource){return (long)suiCurrPos;}

Теперь необходимо прочитать:

Само чтение Ogg файла

void OALOgg::getInfo(unsigned int uiOggSize, char* pvOggBuffer){// Заменяем колбэкиov_callbacks callbacks;callbacks.read_func = &read_func;callbacks.seek_func = &seek_func;callbacks.close_func = &close_func;callbacks.tell_func = &tell_func;suiCurrPos = 0;suiSize = uiOggSize;int iRet = ov_open_callbacks(pvOggBuffer, &vf, NULL, 0, callbacks);// Заголовкиvi = ov_info(&vf, -1);uiPCMSamples = (unsigned int)ov_pcm_total(&vf, -1);}void * OALOgg::ConvertOggToPCM(unsigned int uiOggSize, char* pvOggBuffer){if(suiSize == 0){getInfo( uiOggSize, pvOggBuffer);current_section = 0;iRead = 0;uiCurrPos = 0;}void* pvPCMBuffer = malloc(uiPCMSamples * vi->channels * sizeof(short));// Декодимdo{iRead = ov_read(&vf, (char*)pvPCMBuffer + uiCurrPos, 4096, ¤t_section);uiCurrPos += (unsigned int)iRead;}while (iRead != 0);return pvPCMBuffer;}void OALOgg::load(AAssetManager *mgr, const char* filename){this->filename = filename;char* buf = 0;AAssetFile f = AAssetFile(mgr, filename);if (f.null()) {LOGE("no file %s in readOgg",filename);return ;}buf = 0;buf = (char*)malloc(f.size());if (buf){if(f.read(buf,f.size(),1)){}else {free(buf);f.close();return;}}char * data = (char *)ConvertOggToPCM(f.size(),buf);f.close(); if (vi->channels == 1)format = AL_FORMAT_MONO16;  elseformat = AL_FORMAT_STEREO16;alGenBuffers(1,&buffer);alBufferData(buffer,format,data,uiPCMSamples * vi->channels * sizeof(short),vi->rate);source = 0;alGenSources(1, &source);alSourcei(source, AL_BUFFER, buffer);}

При запуске приложения вызываем C++ метод loadAudio, который вызывает load у NativeCallListener, который и грузит звуки:

void NativeCallListener:: load(){oalContext = new OALContext();//sound = new OALOgg();sound = new OALWav();char *  fileName = new char[64];strcpy(fileName, "audio/industrial_suspense1.wav");//strcpy(fileName, "audio/Katatonia - Deadhouse_(piano version).ogg");sound->load(mgr,fileName);}

Есть тут нюанс — аудио грузится целиком. Поэтому для звуков такое решение отличное, но для музыки нет. Представьте, сколько будет памяти потреблять распакованная .ogg песня. Поэтому, будет отлично, если кто-то на основе этого решения напишет проигрывание аудио со стримингом, а не полной загрузкой в буфер.

Теперь вы можете работать с OpenAL под Android. Код решил заливать на гитхаб. Так что исходники этого и предыдущих уроков можете скачать отсюда.

Основы Android NDK: работа с OpenAL и форматами WAV, OGG: 3 комментария

  1. Vitalik

    Использование нативного кода, написанного на C++ — это тема, которую многие разработчики не затрагивают вовсе.

  2. Уведомление: Android NDK: работа с OpenAL и постепенная подгрузка WAV | Suvitruf's Blog

  3. Уведомление: Android NDK: работа с OpenSL ES | Suvitruf's Blog

Добавить комментарий для Vitalik Отменить ответ

Ваш e-mail не будет опубликован. Обязательные поля помечены *