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

День добрый, уважаемые Хабраюзеры!

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



Введение


Про настройку среды писать много не стоит, как мне кажется. Независимо от того, в какой среде вы разрабатываете (Eclipse, IntelliJ IDEA и т.д.), настройка довольно простая.
  1. Сам Android NDK.
  2. Для сборки под WIn понадобится Cygwin .
  3. Плагины, для того же Eclipse: CDT.

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

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

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


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


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



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

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

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

Необходимо определить методы для экспорта, который мы будем вызывать из Java. Как пример, при запуске приложения мы будем грузить музыку в OpenAL. Для этого определим метод:
JNIEXPORT void JNICALL Java_ru_suvitruf_androidndk_tutorial4_MainActivity_loadAudio(JNIEnv *pEnv, jobject pThis, jobject pNativeCallListener, jobject assetManager);


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

Затем нам необходимо будет его реализовать, но об этом немного позже.
Немного про наименование
Стоит немного сказать про наименование методов. 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.

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

Прочитать 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, ¤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);
}

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

Заключение


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

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

Исходники

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

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

Похожие публикации

Комментарии 13
    0
    Наконец-то про NDK написали
      0
      OpenAL не лучший выбор, слышал задержки на андроиде у него просто чудовищные. Но как пример использования NDK вполне сойдет.
        0
        Насколько я понимаю (я с NDK никогда не работал, хотя планирую освоить) использование OpenAL наиболее оправданно для кроссплатформенного кода для Android/Bb/iOS, о чём вы в статье и указали. Но в документации по разработке гуглеры большей частью указывают на спеки Хроноса по OpenSL ES. Работали с ним? Много ли подводных камней, и стоит ли по его поводу заморачиваться или можно жить с OpenAL и не париться?
          0
          Я сам с ndk не так давно начал работать. Сейчас вот планируем игру в маркет выпустить. Весь звук у нас на OpenAL. Никаких проблем ни на одном устройстве не встретили. Так что, как мне кажется, можно с OpenAL работать. Естественно, это касается только звука.

          Большие музыкальные файлы в Java/Objective C надо выносить.
          +1
          Если используется Android Developer Tools Bundle, то Cygwin не нужен, достаточно скачать NDK и прописать путь к нему в настройках ADT-шного Eclipse. Будет писать предупреждение об отсутствии Cygwin во время компиляции, но на него можно забить. Не пробовал с обычным Eclipse.
            0
            Да, начиная с версии NDK R7 есть ndk-build.cmd
              0
              Да можно и F7 (не помню точно) в Eclipse нажать.
            0
            NDK, безусловно, очень интересная вещь. И если бы была только одна аппаратная платформа, то (ИМХО) все бы только на С/С++ и кодили. Но сила Android-а в его всеядности по отношению к железу, за счёт DalvikVM прослойки. Т.е. написав только на Java, программа должна (теоретически) запускаться на любом железе с подходящей версией API. Если писать нативный код, то, по-хорошему, нужно поддерживать версии для всего актуального железа, на сейчас это (пока) только ARM, x86 и начинается MIPS. И если даже само количество платформ вряд-ли увеличится, то текущие точно будут развиваться. Фрагментация — зло, очень тяжело поддерживать весь этот зоопарк. В Google это очень хорошо понимают и без самой крайней необходимости использовать не советуют. И не просто не советуют, а в каждых новых версиях открывают всё больше низкоуровнего API, для которого раньше без NDK никак. Тем не менее NDK они тоже продолжают развивать, и даже подтягивают туда Clang!

            Сам сейчас стараюсь все делать на Java. Да, иногда сильно медленней, но такова идеология Android-а.

            По поводу примера — он хорош, но область наверное не самая показательная, т.к. никакого преимущества в скорости работы это не даст, одни только лаги. Нужно стараться использовать родные форматы, в идеале — аппаратно ускоряемые (mp3,aac).

            В нескольких проектах я использовал NDK, для того, что бы прикрутить библиотеку MuPDF для рендера PDF-ок в битмапу. Так как ничего родного (как в iOS) не нашёл, а Java-реализации ну совсем не радовали скоростью.
              0
              Соглашусь с тем, что без надобности лучше не использовать ndk. В моём случае движок игровой был на C++ изначально, поэтому выхода особого то и не было.

              По поводу же фрагментации. Зачастую ndk используют из-за того, что уже есть много C++ наработок (готовых библиотек и т.д.). Как я могу наблюдать, многие сообщества Open Source проекты постепенно допиливают, чтобы помимо x86 нормально и под ARM запускалосm (тот же Vorbis, OpenCV и другие).

              По поводу примера — он хорош, но область наверное не самая показательная, т.к. никакого преимущества в скорости работы это не даст, одни только лаги.
              Если смотреть так, то да. Мне же удачный билд OpenAL на Anroid позволил использовать полностью весь тот код по работе со звуком, который был написан для работы на iOS.
              0
              Тема хорошая, но скомкано вышло…
              Если уж в заголовке «Основы...», то хотелось бы чуть подробнее и более связно.
                0
                Я подумал, что если только основы в статье рассмотреть, то это не уровень хабра. Если же и основы, и OpenAL, то слишком много.

                В итоге всего по чуть-чуть =/
                +1
                Сперва думал, что статья хорошая, а потом почитав, понял, что не совсем:(

                developer.android.com/intl/ru/guide/appendix/media-formats.html
                -Форматы WAV и OGG и так поддерживаются Андроидом, зачем использовать такой ужас лишний в виде NDK с OpenAL? Лучше тогда уж OpenCV — там много полезных вещей для Image Processing-а.
                -Для новичка конечно кое-что есть полезное в плане связки кода С++/Java, но во-первых Cygwin как уже отметили, давно не нужен, ибо просто юзаем ndk-build и во-вторых зачем постоянно извиняться, что статья большая, когда вы же ее составляли? Напишите структуру, а потом уже материал… а то грустно читать порой Хабр. Толковых статей стало меньше.

                Понимаю, что вы осилили NDK, вы молодец и все такое, но пожалуйста продумывайте лучше статью и досконально перед ее написанием изучите предмет…

                А так все хорошо.
                  0
                  Если вы статью и комментарии читали, то заметили, что у нас изначально движок игры на C++. Поэтому, чтобы не переписывать полностью всю работу со звуком, как раз и используется OpenAL.

                  Это конечно замечательно, что Android поддерживает эти форматы. Вопрос лишь в том, поддерживается ли это в ndk?

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое