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