День добрый, уважаемые читатели!
С недавних пор занимаюсь разработкой приложений под 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. если вы нашли грамматические ошибки в тексте, то лучше напишите в пм.
Автор: Suvitruf