С недавних пор занимаюсь разработкой приложений под Android, в частности разработкой игр. Так сложилось, что для одного проекта пришлось работать с Android ndk. Все трудности и нюансы работы с native рассмотреть в принципе невозможно в рамках одной статьи, решил в данной статье небольшое введение в ndk написать.
А чтобы статья была интересна не только новичкам, покажу как работать с OpenAL и форматами WAV, OGG.
Введение
Про настройку среды писать много не стоит, как мне кажется. Независимо от того, в какой среде вы разрабатываете (Eclipse, IntelliJ IDEA и т.д.), настройка довольно простая.
- Сам Android NDK.
- Для сборки под WIn понадобится Cygwin .
- Плагины, для того же Eclipse: CDT.
Естественно, у вас уже должны стоять ADT, JDK.
Зачем нужен NDK?
- Работа с OpenGL ES. Думаю, большинство тех, кто использует NDK, используют его как раз для написания игр.
- Использование кросс-платформенных игровых движков вроде Cocos2Dx
- Самый очевидный случай – это когда вам надо использовать уже написанный на C++ код. За десятилетия на C++ уже куча всего написано. Да и не всё можно переписать, думаю тот же openCV бессмысленно бы было переписывать, в том время когда можно просто подключить готовые исходники.
Вызов C++ кода из Java
В целом всё довольно просто, основные шаги:
- Создание файлов с C++ и определение методов для экспорта.
- Создание .mk файлов.
- Генерация библиотеки.
- Подключение библиотеки в 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
После генерации библиотека, необходимо её подключить в Java.
static { System.loadLibrary("AndroidNDK"); }
И определить метод с тем же названием, как и в C++ коде:
//загрузка ресурсов native public void loadAudio(NativeCalls nativeCallListener, AssetManager mng);
Вызывать так:
loadAudio(activity, activity.getResources().getAssets());
Вызов Java из C++
Немного посложнее, но не всё так страшно. Что нам надо:
- Определить у класса метод (в Java), который хотим вызвать.
- Получить дескриптор нужного класса (в C++).
- Описать сигнатуру метода.
- Получить идентификатор метода (ссылку).
- Вызвать метод у нужного объекта.
Конечно можно просто определить метод у класса, но лучше использовать интерфейсы. Тогда нам не придётся менять native код, если захотим работать с другим классом.
Как пример, создадим интерфейс всего с одним методом:
public interface NativeCalls { public void sendLog(String result); }
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++ будет соответствовать следующий класс:
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 файл
Сначала необходимо описать структуру для хэдеров:
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); }
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 методы по работе с данными:
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; }
Теперь необходимо прочитать:
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. если вы нашли грамматические ошибки в тексте, то лучше напишите в пм.
ссылка на оригинал статьи http://habrahabr.ru/post/176559/
Добавить комментарий