Основы 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     := 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. Код решил заливать на гитхаб. Так что исходники этого и предыдущих уроков можете скачать отсюда.