- PVSM.RU - https://www.pvsm.ru -

Основы Android NDK на примере работы с OpenAL

День добрый, уважаемые читатели!

С недавних пор занимаюсь разработкой приложений под Android, в частности разработкой игр. Так сложилось, что для одного проекта пришлось работать с Android ndk. Все трудности и нюансы работы с native рассмотреть в принципе невозможно в рамках одной статьи, решил в данной статье небольшое введение в ndk написать.
А чтобы статья была интересна не только новичкам, покажу как работать с OpenAL и форматами WAV, OGG.

Введение

Про настройку среды писать много не стоит, как мне кажется. Независимо от того, в какой среде вы разрабатываете (Eclipse, IntelliJ IDEA и т.д.), настройка довольно простая.

  1. Сам Android NDK [1].
  2. Для сборки под WIn понадобится Cygwin [2].
  3. Плагины, для того же Eclipse: CDT [3].

Естественно, у вас уже должны стоять ADT, JDK.

Зачем нужен NDK?

  • Работа с OpenGL ES. Думаю, большинство тех, кто использует NDK, используют его как раз для написания игр.
  • Использование кросс-платформенных игровых движков вроде Cocos2Dx
  • Самый очевидный случай – это когда вам надо использовать уже написанный на C++ код. За десятилетия на C++ уже куча всего написано. Да и не всё можно переписать, думаю тот же openCV бессмысленно бы было переписывать, в том время когда можно просто подключить готовые исходники.

Вызов C++ кода из Java

В целом всё довольно просто, основные шаги:

  1. Создание файлов с C++ и определение методов для экспорта.
  2. Создание .mk файлов.
  3. Генерация библиотеки.
  4. Подключение библиотеки в Java.

Про Makefiles(.mk) расписывать не буду. Можно почитать про него тут [4]. К тому же, на хабре есть хорошая статья по работе с .mk файлами [5] от BubaVV [6].

Про библиотеки из ndk можете почитать тут [7].

Создание C++ файлов

Необходимо определить методы для экспорта, который мы будем вызывать из Java. Как пример, при запуске приложения мы будем грузить музыку в OpenAL. Для этого определим метод:

JNIEXPORT void JNICALL Java_ru_suvitruf_androidndk_tutorial4_MainActivity_loadAudio(JNIEnv *pEnv, jobject pThis, jobject pNativeCallListener, jobject assetManager);

Я всё это ручками пишу, но есть удобная утилита для автоматической генерации javah [8].

Затем нам необходимо будет его реализовать, но об этом немного позже.

Немного про наименование

Стоит немного сказать про наименование методов. Java_ – обязательный префикс. ru_suvitruf_androidndk_tutorial4, так как у нас пакет ru.suvitruf.androidndk.tutorial4, ну а дальше наименование класса и метода на стороне Java. В каждой функции в качестве аргумента имеется JNIEnv* — интерфейс для работы с Java, при помощи него можно вызывать Java-методы, создавать Java-объекты. Второй обязательный параметр — jobject или jclass — в зависимости от того, является ли метод статическим. Если метод статический, то аргумент будет типа jclass (ссылка на класс объекта, в котором объявлен метод), если не статический — jobject — ссылка на объект, у которого был вызван метод.
Подключение библиотеки в Java

После генерации библиотека, необходимо её подключить в Java.

static {
		System.loadLibrary("AndroidNDK");
	}

И определить метод с тем же названием, как и в C++ коде:

//загрузка ресурсов
native public void loadAudio(NativeCalls nativeCallListener, AssetManager mng);

Вызывать так:

loadAudio(activity, activity.getResources().getAssets());

Вызов Java из C++

Немного посложнее, но не всё так страшно. Что нам надо:

  1. Определить у класса метод (в Java), который хотим вызвать.
  2. Получить дескриптор нужного класса (в C++).
  3. Описать сигнатуру метода.
  4. Получить идентификатор метода (ссылку).
  5. Вызвать метод у нужного объекта.

Конечно можно просто определить метод у класса, но лучше использовать интерфейсы. Тогда нам не придётся менять native код, если захотим работать с другим классом.

Как пример, создадим интерфейс всего с одним методом:

public interface NativeCalls {
	public void sendLog(String result);
}
CalledFromWrongThreadException и правильная реализация интерфейса

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

protected Handler handler = new Handler()
    {  
    	 @Override
         public void handleMessage(Message msg) {
    		 showResult(msg.getData().getString("result"));   		 
    	 }
    };
	
    
    public void showResult(String result){
    	((TextView) findViewById(R.id.log)).
    	setText(((TextView) findViewById(R.id.log)).getText()+result+"n");
    
    }
	
    //отобразить количество прочитаных байт
	@Override 
	public void sendLog(String result){
		Message msg = new Message();
		Bundle data = new Bundle();
		data.putString("result", result);
		msg.setData(data);
				
		handler.sendMessage(msg);
	}

Проблем с потоками может и не быть, но в нашем случае при запуске приложения будем создавать отдельный поток для загрузки ресурсов, поэтому для нас вопрос актуален.

Java интерфейсу в нативном коде на C++ будет соответствовать следующий класс:

NativeCallListener

class NativeCallListener {
public:

	NativeCallListener(JNIEnv* pJniEnv, jobject pWrapperInstance);
	NativeCallListener() {}
	//апуск таймера

	//передать значение в Java метод
    void sendLog(jobject log);
    //очистка всех ресурсов
    void destroy();
	~NativeCallListener(){

	}
	void loadAudio();
	//void play();
	//void playOGG();
	ALCdevice* device;
	ALCcontext* context;
private:
	JNIEnv* getJniEnv();

    //ссылка на метод
	jmethodID sendLogID;
	//ссылка на объект
	jobject mObjectRef;
	JavaVM* mJVM;


	ALuint soundWAV;
	ALuint soundOGG;
	void load();
	void clean();

};

Теперь можно показать реализацию loadAudio метода, хэдер которого в первой части статьи был.

JNIEXPORT void JNICALL Java_ru_suvitruf_androidndk_tutorial4_MainActivity_loadAudio(JNIEnv *pEnv, jobject pThis, jobject pNativeCallListener, jobject assetManager) {
	listener = NativeCallListener(pEnv, pNativeCallListener);
	mgr = AAssetManager_fromJava(pEnv, assetManager);
	listener.loadAudio();
}

В конструкторе класса мы сохраняем дескриптор класса и получаем ссылку на его метод:

NativeCallListener::NativeCallListener(JNIEnv* pJniEnv, jobject pWrappedInstance) {
	pJniEnv->GetJavaVM(&mJVM);
	mObjectRef = pJniEnv->NewGlobalRef(pWrappedInstance);
	jclass cl = pJniEnv->GetObjectClass(pWrappedInstance);
        //тот самый, что определён в нашем интерфейсе в Java
	sendLogID = pJniEnv->GetMethodID(cl, "sendLog", "(Ljava/lang/String;)V");

}

Теперь мы может вызывать Java метод написав:

void NativeCallListener::sendLog(jobject log) {
	JNIEnv* jniEnv = getJniEnv();
	jniEnv->CallIntMethod(mObjectRef, sendLogID, log);

}

AAssetManager

Раньше использовалась open source библиотека libzip для работы с ресурсами приложения.
С 2.3 версии API в Android ndk появился замечательный класс для работы с директорией assets прямо из C++ кода.
Методы похожи на методы по работе с файлами из stdio.h. AAssetManager_open вместо fopen, AAsset_read вместо fread, AAsset_close вместо fclose.

Я для него небольшую обёртку написал. Код вставлять сюда не буду, так как в целом работа та же, что и с FILE обычным.

Работа с OpenAL

Статья уже довольная большая, а к самому интересному так и не приступил. Прошу меня простить за это…

Подготовка

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

Для звука я написал обёртки с необходимыми методами. Весь код тут приводить смысла нет, освещу самое интересное, а именно загрузку.

Прочитать WAV файл

Сначала необходимо описать структуру для хэдеров:

BasicWAVEHeader

typedef struct {
  char  riff[4];//'RIFF'
  unsigned int riffSize;
  char  wave[4];//'WAVE'
  char  fmt[4];//'fmt '
  unsigned int fmtSize;
  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;

Теперь читаем:

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

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);
	if(res){
		if (!(
			// Заголовки должны быть валидны.
			// Проблема в том, что не всегда так.
			// Многие конвертеры недобросовестные пихают в эти заголовки свои логотипы =/
			memcmp("RIFF",header->riff,4) ||
			memcmp("WAVE",header->wave,4) ||
			memcmp("fmt ",header->fmt,4)  ||
			memcmp("data",header->data,4)
		)){
			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. Так почему не работает, а на PC играет?
Непосредственно сами данные аудио хранятся после хэдера и их размер в dataSize. Если с этим полем что-то не так, то будут ошибки. Можно правда посчитать размер в лоб. Размер данных = размер файла — размер хэдера. Так что, думаю, плееры берут размер данных вычитая, а не из хэдера.

По работе с WAV вроде всё просто, так как формат не сжатый. При работе с .Ogg всё посложнее.

Прочитать Ogg файл

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

callbacks

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, &current_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);
}

sound у меня типа OALSound. Для работы с WAV и Ogg у меня классы, которые наследуются от него. Нам для них необходимо лишь написать реализацию загрузки переопределив метод базового класса virtual void load(AAssetManager *mgr, const char* filename)= 0;
Это позволяет унифицировать работу со звуков.

Заключение

Ещё раз извиняюсь, что статья вышла довольно объёмная, иначе не представляю как написать. С помощью представленной реализации можно работать со звуком независимо от платформы. Скажем, если вы пишите движок игры для iOS и Android.

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

Исходники

Проект написан на Eclipse. Исходники можно посмотреть на github [10].

P.S. жду критики и советов
P.P.S. если вы нашли грамматические ошибки в тексте, то лучше напишите в пм.

Автор: Suvitruf

Источник [11]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/android/32046

Ссылки в тексте:

[1] Android NDK: http://developer.android.com/sdk/ndk/index.html

[2] Cygwin : http://www.cygwin.com/

[3] CDT: http://download.eclipse.org/tools/cdt/releases/indigo/

[4] тут: http://mrbook.org/tutorials/make/

[5] хорошая статья по работе с .mk файлами: http://habrahabr.ru/post/155201/

[6] BubaVV: http://habrahabr.ru/users/bubavv/

[7] тут: http://mobilepearls.com/labs/native-android-api/

[8] javah: http://docs.oracle.com/javase/6/docs/technotes/tools/solaris/javah.html

[9] Tremor: http://svn.xiph.org/trunk/Tremor/

[10] github: https://github.com/Suvitruf/Android-ndk/tree/master/AndroidNDK-Tutorial4

[11] Источник: http://habrahabr.ru/post/176559/