Для порта игры пришлось работать с 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. Код решил заливать на гитхаб. Так что исходники этого и предыдущих уроков можете скачать отсюда.
Использование нативного кода, написанного на C++ — это тема, которую многие разработчики не затрагивают вовсе.
Уведомление: Android NDK: работа с OpenAL и постепенная подгрузка WAV | Suvitruf's Blog
Уведомление: Android NDK: работа с OpenSL ES | Suvitruf's Blog