Categories
Android NDK

OpenAL on Android

Although being slightly late with 3D Audio Support for Android 2.3 announced – this tutorial shows how to compile OpenAL for Android, so you can provide 3D Sound in your apps with 2.2 and below. The code has successfully been tested with the Nexus One (2.2) and the G1 (1.6).

Update: the resulting project for download as a single .zip file. To run the example create a directory called wav on your device’s SD card and put a sound file called lake.wav into it.

Update: some people reported latency issues. It can possibly be fixed in the OpenAL source. See last paragraph for a possible solution.

Update: if you are using a NativeActivity, and the app crashes on device = alcOpenDevice( NULL ); please take a look at Garen’s fix to the getEnv() method: http://pielot.org/2010/12/14/openal-on-android/#comment-1160

 

Preparation

Understand how to compile NDK resources

This tutorial requires working with the Android NDK. We will have to compile OpenAL from source into a native Shared Object and then build a Java Native Interface to work with it. The techniques I use to work with the NDK (on Windows) have been described in a previous tutorial. You might want to take a look to understand what exactly I am doing.

Remember to PRESS F5 after you COMPILED the SHARED OBJECT. Otherwise, Eclipse will not use the new .so.

Create HelloOpenAL Project

Create a normal Android SDK Project.

package org.pielot.helloopenal;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;

public class HelloOpenAL extends Activity {
 /** Called when the activity is first created. */
 @Override
 public void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.main);
 }

 private native int play(String filename);
}

Compile OpenAL for Android

The first step will be to compile OpenAL for Android. The goal will be to produce a Shared Object library libopenal.so that can be loaded into Android apps.

Download patched source of OpenAL

 

Thanks to Martins Mozeiko and Chris Robinson a version of OpenAL exists that has been adapted to the Android platform. Go to http://repo.or.cz/w/openal-soft/android.git and download latest version of the patched OpenAL sourcecode. For this tutorial I used version that can be downloaded here.

Extract into project folder. Rename top folder of downloaded source from ‘android’ to ‘openal’.

Create config.h

To compile OpenAL a file called config.h is needed.Copy it from <PROJECT_HOME>/openal/android/jni to <PROJECT_HOME>/openal/include.

Create Android.mk

To tell the NDK compiler what files to compile, we now need to create Android.mk in <PROJECT_HOME>/jni . The file should contain:

TARGET_PLATFORM := android-3
ROOT_PATH := $(call my-dir)

########################################################################################################
include $(CLEAR_VARS)

LOCAL_MODULE     := openal
LOCAL_ARM_MODE   := arm
LOCAL_PATH       := $(ROOT_PATH)
LOCAL_C_INCLUDES := $(LOCAL_PATH) $(LOCAL_PATH)/../openal/include $(LOCAL_PATH)/../openal/OpenAL32/Include
LOCAL_SRC_FILES  := ../openal/OpenAL32/alAuxEffectSlot.c \
 ../openal/OpenAL32/alBuffer.c        \
 ../openal/OpenAL32/alDatabuffer.c    \
 ../openal/OpenAL32/alEffect.c        \
 ../openal/OpenAL32/alError.c         \
 ../openal/OpenAL32/alExtension.c     \
 ../openal/OpenAL32/alFilter.c        \
 ../openal/OpenAL32/alListener.c      \
 ../openal/OpenAL32/alSource.c        \
 ../openal/OpenAL32/alState.c         \
 ../openal/OpenAL32/alThunk.c         \
 ../openal/Alc/ALc.c                  \
 ../openal/Alc/alcConfig.c            \
 ../openal/Alc/alcEcho.c              \
 ../openal/Alc/alcModulator.c         \
 ../openal/Alc/alcReverb.c            \
 ../openal/Alc/alcRing.c              \
 ../openal/Alc/alcThread.c            \
 ../openal/Alc/ALu.c                  \
 ../openal/Alc/android.c              \
 ../openal/Alc/bs2b.c                 \
 ../openal/Alc/null.c                 \

LOCAL_CFLAGS     := -DAL_BUILD_LIBRARY -DAL_ALEXT_PROTOTYPES
LOCAL_LDLIBS     := -llog -Wl,-s

include $(BUILD_SHARED_LIBRARY)

########################################################################################################

Compile OpenAL

Now compile the source code using the NDK. I used a technique described in another tutorial on Using cygwin with the Android NDK on Windows. I am creating a batch file make.bat in the projects directory containing:

@echo on

@set BASHPATH="C:\cygwin\bin\bash"
@set PROJECTDIR="/cygdrive/d/dev/workspace-android/helloopenal"
@set NDKDIR="/cygdrive/d/dev/SDKs/android-ndk-r4b/ndk-build"

%BASHPATH% --login -c "cd %PROJECTDIR% && %NDKDIR%

@pause:

Save the file and execute it. If there is no error you have just compiled the OpenAL library into a Shared Object! You can find it in <PROJECT_HOME>/libs/armeabi. Now let’s see how we can make use of it.

The Native Interface

The next steps will be to create a Java Native Interface that allows us to access to OpenAL Shared Object.

Define Native Interface in Activity

Extend the HelloOpenAL Activity, so it looks like

package org.pielot.helloopenal;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;

public class HelloOpenAL extends Activity {
 /** Called when the activity is first created. */
 @Override
 public void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.main);

 System.loadLibrary("openal");
 System.loadLibrary("openaltest");
 int ret = play("/sdcard/wav/lake.wav");
 Log.i("HelloOpenAL", ""+ret);
 }

 private native int play(String filename);
}

Implement Native Interface

In <PROJECT_HOME> execute

javah.exe -classpath bin -d jni org.pielot.helloopenal.HelloOpenAL

to create the c header for the native function. Now <PROJECT_HOME>/jni should contain the file org_pielot_helloopenal_HelloOpenAL.h. Create org_pielot_helloopenal_HelloOpenAL.c in <PROJECT_HOME>/jni and fill it with

#include "org_pielot_helloopenal_HelloOpenAL.h"

JNIEXPORT jint JNICALL Java_org_pielot_helloopenal_HelloOpenAL_play
 (JNIEnv * env, jobject obj, jstring filename) {
 return 0;
}

Compile Native Interface

Add new library to Android.mk

########################################################################################################

include $(CLEAR_VARS)

LOCAL_MODULE     := openaltest
LOCAL_ARM_MODE   := arm
LOCAL_PATH       := $(ROOT_PATH)
LOCAL_C_INCLUDES := $(LOCAL_PATH)/../openal/include
LOCAL_SRC_FILES  := org_pielot_helloopenal_HelloOpenAL.c     \

LOCAL_LDLIBS     := -llog -Wl,-s

LOCAL_SHARED_LIBRARIES := libopenal

include $(BUILD_SHARED_LIBRARY)

########################################################################################################

Now compile again. The above created make.bat will do. You now should have two libraries in <PROJECT_HOME>/libs/armeabi/, namely libopenal.so and libopenalwrapper.so.

I you want you can run the app now. It should not crash and print ‘HelloOpenAL   0’ into the log.

Testing OpenAL

Now we have two libraries, one containing OpenAL and the other a native interface. We will now fill the latter with life to demonstrate the use of OpenAL.

Initialize and Release Audio Components

Therefore open org_pielot_helloopenal_HelloOpenAL.c and extend the existing code by:

#include "org_pielot_helloopenal_HelloOpenAL.h"

#include <stdio.h>
#include <stddef.h>
#include <string.h>
#include <AL/al.h>
#include <AL/alc.h>

JNIEXPORT jint JNICALL Java_org_pielot_helloopenal_HelloOpenAL_play
 (JNIEnv * env, jobject obj, jstring filename) {

 // Global Variables
 ALCdevice* device = 0;
 ALCcontext* context = 0;
 const ALint context_attribs[] = { ALC_FREQUENCY, 22050, 0 };

 // Initialization
 device = alcOpenDevice(0);
 context = alcCreateContext(device, context_attribs);
 alcMakeContextCurrent(context);

 // More code to come here ...

 // Cleaning up
 alcMakeContextCurrent(0);
 alcDestroyContext(context);
 alcCloseDevice(device);

 return 0;
}

This code will now acquire the audio resource and release them. You should be able to compile the code and execute the HelloOpenAL app. However, nothing will yet happen, as we still have to load and play sound.

Methods for Loading Audio Data

Now we need to load audio data. Unfortunately, OpenAL does not come with functions for loading audio data. There has been the very popular ALut toolkit, but this is not part of OpenAL anymore. We therefore need to provide our own methods to load .wav files.

The following code snippets have been posted by Gorax at www.gamedev.net. These are one struct and two methods methods to load .wav data and buffer it in the memory.

Add the following code to org_pielot_helloopenal_HelloOpenAL.c. Add the code above JNIEXPORT jint JNICALL Java_org_pielot_helloopenal_HelloOpenAL_play

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;

//WARNING: This Doesn't Check To See If These Pointers Are Valid
char* readWAV(char* filename,BasicWAVEHeader* header){
 char* buffer = 0;
 FILE* file = f open(filename,"rb");
 if (!file) {
 return 0;
 }

 if (f read(header,sizeof(BasicWAVEHeader),1,file)){
 if (!(//these things *must* be valid with this basic header
 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,file)){
 f close(file);
 return buffer;
 }
 free(buffer);
 }
 }
 }
 f close(file);
 return 0;
}

ALuint createBufferFromWave(char* data,BasicWAVEHeader header){

 ALuint buffer = 0;
 ALuint format = 0;
 switch (header.bitsPerSample){
 case 8:
 format = (header.channels == 1) ? AL_FORMAT_MONO8 : AL_FORMAT_STEREO8;
 break;
 case 16:
 format = (header.channels == 1) ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16;
 break;
 default:
 return 0;
 }

 alGenBuffers(1,&buffer);
 alBufferData(buffer,format,data,header.dataSize,header.samplesPerSec);
 return buffer;
}

WARNING, I had to put spaces into the word f open, f read, and f close. Delete the spaces when you copy that piece of code. For some reason, WordPress does not accept the words without space in a post.

Load Audio Data into Buffer

In method JNIEXPORT jint JNICALL Java_org_pielot_helloopenal_HelloOpenAL_play located the comment

// TODO More Code comes here

and replace it by

// Create audio buffer
 ALuint buffer;
 const char* fnameptr = (*env)->GetStringUTFChars(env, filename, NULL);
 BasicWAVEHeader header;
 char* data = readWAV(fnameptr,&header);
 if (data){
 //Now We've Got A Wave In Memory, Time To Turn It Into A Usable Buffer
 buffer = createBufferFromWave(data,header);
 if (!buffer){
 free(data);
 return -1;
 }

 } else {
 return -1;
 }

 // TODO turn buffer into playing source

 // Release audio buffer
 alDeleteBuffers(1, &buffer);

This piece of code tries to load PCM .wav audio data from the passed filename. The audio data is loaded into an OpenAL buffer. The buffer itself is merely the cached audio data but cannot be played. It therefore has to be attached to a sound source.

Create a playing source from the buffer

In method JNIEXPORT jint JNICALL Java_org_pielot_helloopenal_HelloOpenAL_play located the comment

// TODO turn buffer into playing source

and replace it by

 // Create source from buffer and play it
 ALuint source = 0;
 alGenSources(1, &source );
 alSourcei(source, AL_BUFFER, buffer);

 // Play source
 alSourcePlay(source);

 int        sourceState = AL_PLAYING;
 do {
 alGetSourcei(source, AL_SOURCE_STATE, &sourceState);
 } while(sourceState == AL_PLAYING);

 // Release source
 alDeleteSources(1, &source);

This piece of code creates a sound source from the buffer and plays it once.

Test on the Device

Compile the native code again by using make.bat. It should compile without errors. If you are using Eclipse, select the project HelloOpenAL in the Package Explorer and press F5. Otherwise, Eclipse might not be aware that the Shared Objects were updated.

Next, go to your devices SD Card and add a .wav file. I created a folder called “wav” and put a mono .wav file called “lake.wav” into this folder. Make sure it matches the filename you passed play(String filename) in the HelloOpenAL activity.

Now it time for the big test! Once you start the app, the .wav file should be played once.

This has been tested on the Nexus One and the G1/HTC Dream.

Solving Latency Issues

Some people seem to have experienced a 0.5 sec lag between triggering the sound and the actual playback. In the comments aegisdino suggested the following solution:

In alcOpenDevice() of ALc.c source,
“device->NumUpdates” seems to apply the lag issue.
In normal cases, device->NumUpdates will be 4, then I can feel about 0.5sec lag.
But when I fix it to 1, the lag disappeared.

I did not test the solution, but as NumUpdates was 1 in my version of ALc.c it could be the solution.

Share this:
Share
Categories
Android NDK

Using cygwin with the Android NDK on Windows

This tutorial illustrates how to setup and use the Android NDK under Windows. It will use cygwin for compiling the native code. It has been tested on Windows XP and Windows 7.

This guide assumes that you have Eclipse with ADT and the Android SDK version 3 (1.5) up and running.

There are three important paths:

Eclipse Workspace       D:\Dev\workspace-android
NDK                     D:\Dev\SDKs\android-ndk-r4b
Cygwin                  C:\Cygwin

I am using these paths as they appear on my computer. Please adapt them to use system if necessary. Note that the paths MUST NOT CONTAIN SPACES.

The code for this tutorial is available here

Download NDK

Go to http://developer.android.com/sdk/ndk/index.html and download the Android NDK for Windows. At the time of writing, android android-ndk-r4b was the latest version. Copy the folder into D:\dev\SDKs\

Install Cygwin

Download setup.exe from http://cygwin.com/. The direct link is http://cygwin.com/setup.exe. Execute setup.exe and select a server to download the Cygwin files from. Then a huge list appears where you can select the components to download.

Add “Devel/make” and “Shells/bash”. Search for “make” and “shell” to find them. Press next to download. I installed all files to C:\cygwin.

Create Android Project

Create a new standard Android project. I called it HelloNDK and used the following code.

package org.pielot.hellondk;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;

public class HelloNDK extends Activity {
	/** Called when the activity is first created. */
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);

		System.loadLibrary("hellondk");
		int result = sayHello();
		Log.i("HelloNDK", "" + result);
	}

	private native int sayHello();
}

Make sure to compile it using the Android SDK. Javac.exe won’t work! Running this code should fail, as there is no native library yet.

Create Java Native Interface

Make sure your PATH contains your Java SDKs /bin directory. Open a terminal (cmd.exe) and enter javah. Receiving something like this means everything is alright.

Now, in the terminal, go to the root of your project in Eclipse’s workspace D:\Dev\workspace-android\HelloNDK. Enter

javah.exe -classpath bin/classes -d jni org.pielot.hellondk.HelloNDK

When no error occurs, the compilation was successful. Since some comments below report from difficulties with this step, you might want to check if HelloNDK.class is really located in bin/classes/org/pielot/hellondk/. If not, try changing bin/classes/… to bin/… or src/… . It appears to have worked for others.

Now you should find a new folder jni in your project, containing the file org_pielot_hellondk_HelloNDK.h

/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class org_pielot_hellondk_HelloNDK */

#ifndef _Included_org_pielot_hellondk_HelloNDK
#define _Included_org_pielot_hellondk_HelloNDK
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     org_pielot_hellondk_HelloNDK
 * Method:    sayHello
 * Signature: ()I
 */
JNIEXPORT jint JNICALL Java_org_pielot_hellondk_HelloNDK_sayHello
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

For my convenience, I usually create a .bat file including the above javah command and put it into the project folder.

Implement the Native Interface

Now we have to provide an implementation of the generated header file. Create org_pielot_hellondk_HelloNDK.c in the same folder as org_pielot_hellondk_HelloNDK.h and fill it with:

#include "org_pielot_hellondk_HelloNDK.h"

JNIEXPORT int JNICALL Java_org_pielot_hellondk_HelloNDK_sayHello
 (JNIEnv * env, jobject obj) {

 return 42;
}

Inform the Compiler what Files to Compile

Next, we have to inform the NDK compiler what files should be compiled. In the /jni folder we therefore create “Android.mk”:

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := hellondk
LOCAL_SRC_FILES := org_pielot_hellondk_HelloNDK.c

include $(BUILD_SHARED_LIBRARY)

Compiling the Native Code

In the HelloNDK project folder create a batch file named “make.bat”. Fill it with:

@echo on

@set BASHPATH="C:\cygwin\bin\bash"
@set PROJECTDIR="/cygdrive/d/dev/workspace-android/hellondk"
@set NDKDIR="/cygdrive/d/dev/SDKs/android-ndk-r4b/ndk-build"

%BASHPATH% --login -c "cd %PROJECTDIR% && %NDKDIR%

@pause:

This file will NOT WORK WHEN EXECUTED FROM WITHIN ECLIPSE. Thus, always find it with the Windows Explorer and execute it by double-clicking.

If successful it should look like

In the HelloNDK project directory you now should find libs/armeabi/libhellondk.so created.

Loading and Testing library

If you now run the HelloNDK Activity, you should see no exceptions. In LogCat, something like

12-05 13:42:45.311: INFO/HelloNDK(24329): 42

should appear. Voila! You have done it!

One last note: if you are just working on the C code, Eclipse will not realize that the native lib has been updated. To make sure that the latest version of the lib will be used, mark the HelloNDK project in Eclipse’s project explorer and hit F5.

Share this:
Share