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

Опубликовано: 01.09.2018

Для порта игры пришлось работать с 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 := openal LOCAL_ARM_MODE := arm #LOCAL_PATH := $(ROOT_PATH) LOCAL_C_INCLUDES := $(LOCAL_PATH)/include $(LOCAL_PATH)/OpenAL32/Include LOCAL_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_PROTOTYPES LOCAL_LDLIBS := -llog -Wl,-s include $(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; else format = 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. Код решил заливать на гитхаб. Так что исходники этого и предыдущих уроков можете скачать отсюда .

IRC (Internet Relay Chat)
rss