Android audio recording library using Java FFI

by Ali Lloyd on April 13, 2017 17 comments

In this example, we show you how to implement audio recording on Android using the new Java FFI capabilities in the latest DP of LiveCode 9.

The first step in figuring out how to create an Android audio recording library is to figure out what classes and methods are needed, which is tantamount to having a quick look at how it would be done in native code.

The API documentation for the MediaRecorder class has the following sample code:

MediaRecorder recorder = new MediaRecorder();
 recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
 recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
 recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
 recorder.setOutputFile(PATH_NAME);
 recorder.prepare();
 recorder.start();   // Recording is now started
 ...
 recorder.stop();
 recorder.reset();   // You can reuse the object by going back to setAudioSource() step
 recorder.release(); // Now the object cannot be reused

So in fact everything we need is included in the MediaRecorder class. We could wrap the entire class using an lcb-java specification and using the lc-compile-ffi-java tool, eg:

foreign package android.media

class MediaRecorder

constructor MediaRecorder()

final class method getAudioSourceMax() returns int
method getMaxAmplitude() returns int
method pause()
method prepare()
...

but this class is simple enough that we can just wrap the individual methods by hand.

This is going to be a library in the first instance, so we’ll start with a simple library shell:

library com.livecode.library.androidaudiorecorder

metadata version is "1.0.0"
metadata author is "LiveCode"
metadata title is "Android Audio Recorder"

-- It's safe to assume that any lcb module that uses java FFI bindings 
-- will need to use the following two modules:
use com.livecode.foreign
use com.livecode.java

end library

We wrap the MediaRecorder constructor using the binding string "java:android.media.MediaRecorder>new()". This uses new as the method name to indicate to the LCB VM that we want to call the constructor.

-- Define the foreign handler that binds to the MediaRecorder 
-- constructor
foreign handler _JNI_MediaRecorderConstructor() returns JObject binds to "java:android.media.MediaRecorder>new()"

We will need bindings for each of the methods we want to call on the MediaRecorder instance, namely

  • setAudioSource(source as int)
  • setOutputFormat(format as int)
  • setAudioEncoder(encoder as int)
  • setOutputFile(fileName as String)
  • prepare()
  • start()
  • stop()
  • reset()

All of these are instance methods of the MediaRecorder class with no return value. We wrap them as follows:

foreign handler _JNI_MediaRecorderSetRecordInput(in pRecorder as JObject, in pSource as JInt) returns nothing binds to "java:android.media.MediaRecorder>setAudioSource(I)V"
foreign handler _JNI_MediaRecorderSetRecordFormat(in pRecorder as JObject, in pFormat as JInt) returns nothing binds to "java:android.media.MediaRecorder>setOutputFormat(I)V"
foreign handler _JNI_MediaRecorderSetRecordEncoder(in pRecorder as JObject, in pEncoder as JInt) returns nothing binds to "java:android.media.MediaRecorder>setAudioEncoder(I)V"
foreign handler _JNI_MediaRecorderSetRecordOutputFile(in pRecorder as JObject, in pFile as JString) returns nothing binds to "java:android.media.MediaRecorder>setOutputFile(Ljava/lang/String;)V"
foreign handler _JNI_MediaRecorderPrepare(in pRecorder as JObject) returns nothing binds to "java:android.media.MediaRecorder>prepare()V"
foreign handler _JNI_MediaRecorderStart(in pRecorder as JObject) returns nothing binds to "java:android.media.MediaRecorder>start()V"
foreign handler _JNI_MediaRecorderStop(in pRecorder as JObject) returns nothing binds to "java:android.media.MediaRecorder>stop()V"
foreign handler _JNI_MediaRecorderReset(in pRecorder as JObject) returns nothing binds to "java:android.media.MediaRecorder>reset()V"

Because they are all instance methods of the class, the foreign handler takes JObject as its first argument – this is the instance of the class on which to call the given method. Wrapped in this way, calling _JNI_MediaRecorderStart(mRecorder) is just the same as recorder.start() in Java (albeit going through the JNI rather than calling directly).

There are a very limited selection of java types here, so the parameter codes in the binding strings are relatively straightforward

  • ()V is a method with no parameters and no return value
  • (I)V is a method with a (Java) int parameter and no return value
  • (Ljava/lang/String;)V is a method with a (Java) String parameter and no return value

All that’s left to replicate the simple example in the Android API is to find out the actual (integer) values of the constants MediaRecorder.AudioSource.MIC, MediaRecorder.OutputFormat.THREE_GPP, and MediaRecorder.AudioEncoder.AMR_NB. These can be found on the AudioSource, OutputFormat and AudioEncoder API docs. In this case, these constants all map to 1.

Putting it all together gives us two handlers that we want to make available in LiveCode Script:

-- This will store an instance of a MediaRecorder
private variable mRecorder as optional JObject

public handler AndroidStartRecording(in pFileName as String)
	unsafe
		-- Don't create a new recorder object if there already is one
		if mRecorder is nothing then
			put _JNI_MediaRecorderConstructor() into mRecorder
		end if
		_JNI_MediaRecorderSetRecordInput(mRecorder, 1)
		_JNI_MediaRecorderSetRecordFormat(mRecorder, 1)
		_JNI_MediaRecorderSetRecordEncoder(mRecorder, 1)
		
		-- Convert the filename to a java string
		variable tFileName as JString
		put StringToJString(pFileName) into tFileName
		
		_JNI_MediaRecorderSetRecordOutputFile(mRecorder, tFileName)
		_JNI_MediaRecorderPrepare(mRecorder)
		_JNI_MediaRecorderStart(mRecorder)
	end unsafe
end handler

public handler AndroidStopRecording(in pFileName as String)
	-- Don't do anything if there is no recorder object
	if mRecorder is nothing then
		throw "recording has not been started!"
		return
	end if
	
	unsafe
		_JNI_MediaRecorderStop(mRecorder)
		_JNI_MediaRecorderReset(mRecorder)
	end unsafe
end handler

There is one final piece of the puzzle here, which is that recording audio on Android requires a permission in the manifest. We can add this as metadata to the library as follows:

metadata android.permissions is "RECORD_AUDIO"

Et voila, a simple Android audio recording library.

library com.livecode.library.androidaudiorecorder

metadata version is "1.0.0"
metadata author is "LiveCode"
metadata title is "Android Audio Recorder"

metadata android.permissions is "RECORD_AUDIO"

use com.livecode.foreign
use com.livecode.java

foreign handler _JNI_MediaRecorderConstructor() returns JObject binds to "java:android.media.MediaRecorder>new()"
foreign handler _JNI_MediaRecorderSetRecordInput(in pRecorder as JObject, in pSource as JInt) returns nothing binds to "java:android.media.MediaRecorder>setAudioSource(I)V"
foreign handler _JNI_MediaRecorderSetRecordFormat(in pRecorder as JObject, in pFormat as JInt) returns nothing binds to "java:android.media.MediaRecorder>setOutputFormat(I)V"
foreign handler _JNI_MediaRecorderSetRecordEncoder(in pRecorder as JObject, in pEncoder as JInt) returns nothing binds to "java:android.media.MediaRecorder>setAudioEncoder(I)V"
foreign handler _JNI_MediaRecorderSetRecordOutputFile(in pRecorder as JObject, in pFile as JString) returns nothing binds to "java:android.media.MediaRecorder>setOutputFile(Ljava/lang/String;)V"
foreign handler _JNI_MediaRecorderPrepare(in pRecorder as JObject) returns nothing binds to "java:android.media.MediaRecorder>prepare()V"
foreign handler _JNI_MediaRecorderStart(in pRecorder as JObject) returns nothing binds to "java:android.media.MediaRecorder>start()V"
foreign handler _JNI_MediaRecorderStop(in pRecorder as JObject) returns nothing binds to "java:android.media.MediaRecorder>stop()V"
foreign handler _JNI_MediaRecorderReset(in pRecorder as JObject) returns nothing binds to "java:android.media.MediaRecorder>reset()V"

-- This will store an instance of a MediaRecorder
private variable mRecorder as optional JObject

public handler AndroidStartRecording(in pFileName as String)
	unsafe
		-- Don't create a new recorder object if there already is one
		if mRecorder is nothing then
			put _JNI_MediaRecorderConstructor() into mRecorder
		end if
		_JNI_MediaRecorderSetRecordInput(mRecorder, 1)
		_JNI_MediaRecorderSetRecordFormat(mRecorder, 1)
		_JNI_MediaRecorderSetRecordEncoder(mRecorder, 1)
		
		-- Convert the filename to a java string
		variable tFileName as JString
		put StringToJString(pFileName) into tFileName
		
		_JNI_MediaRecorderSetRecordOutputFile(mRecorder, tFileName)
		_JNI_MediaRecorderPrepare(mRecorder)
		_JNI_MediaRecorderStart(mRecorder)
	end unsafe
end handler

public handler AndroidStopRecording(in pFileName as String)
	-- Don't do anything if there is no recorder object
	if mRecorder is nothing then
		throw "recording has not been started!"
		return
	end if
	
	unsafe
		_JNI_MediaRecorderStop(mRecorder)
		_JNI_MediaRecorderReset(mRecorder)
	end unsafe
end handler
end library

To provide more options on input sources, encoders and output formats you could use array literals for the mapping from sources to their defined values

constant kRecordInputs is { \
 "Default" : 0, \
 "Mic" : 1, \
 "VoiceUplink" : 2, \
 "VoiceDownlink" : 3, \
 "VoiceCall" : 4, \
 "Camcorder" : 5, \
 "VoiceRecognition" : 6, \
 "VoiceCommunication" : 7, \
 "RemoteSubmix" : 8, \
 "Unprocessed" : 9 \
}
constant kRecordCompressionTypes is { \
 "Default" : 0, \
 "AMR NB" : 1, \
 "AMR WB" : 2, \
 "AAC" : 3, \
 "HE AAC" : 4, \
 "AAC ELD" : 5, \
 "Vorbis" : 6 \
}
constant kRecordFormats is { \
 "ThreeGPP" : 1, \
 "MPEG-4" : 2, \
 "AMR WB" : 4, \
 "AAC ADTS" : 6, \
 "WebM" : 9 \
}

and handlers to allow these to be set, for example:

public handler AndroidSetRecordInput(in pInputSource as String)
	if pInputSource is not among the keys of kRecordInputs then
		throw "unrecognised source"
		return
	end if
	
	unsafe
		if mRecorder is nothing then 
			put _JNI_MediaRecorderConstructor() into mRecorder
		end if
	
		_JNI_MediaRecorderSetRecordInput(mRecorder, kRecordInputs[pInputSource])
	end unsafe
end handler

You may also want to create a LiveCode Script wrapper that uses the current values of the recordChannels, the recordRate and the recordSampleSize global properties, by adding a handler

foreign handler _JNI_MediaRecorderSetRecordChannels(in pRecorder as JObject, in pChannels as JInt) returns nothing binds to "java:android.media.MediaRecorder>setAudioChannels(I)V"
foreign handler _JNI_MediaRecorderSetRecordRate(in pRecorder as JObject, in pRate as JInt) returns nothing binds to "java:android.media.MediaRecorder>setAudioSamplingRate(I)V"
foreign handler _JNI_MediaRecorderSetRecordSampleSize(in pRecorder as JObject, in pRate as JInt) returns nothing binds to "java:android.media.MediaRecorder>setAudioEncodingBitRate(I)V"

public handler AndroidSetRecordConfig(in pChannels as Integer, in pRate as Real, in pSampleSize as Integer)	
	unsafe
		if mRecorder is nothing then 
			put _JNI_MediaRecorderConstructor() into mRecorder
		end if
		_JNI_MediaRecorderSetRecordChannels(mRecorder, pChannels) 
		_JNI_MediaRecorderSetRecordRate(mRecorder, pRate)
		_JNI_MediaRecorderSetRecordSampleSize(mRecorder, pSampleSize)
	end unsafe
end handler

to your LCB library, and calling

AndroidSetRecordConfig the recordChannels, the recordRate, the recordSampleSize

in the LCS wrapper.

Ali LloydAndroid audio recording library using Java FFI

Related Posts

Take a look at these posts

17 comments

Join the conversation
  • MaxV - April 20, 2017 reply

    I’m glad to know it, but there is no documentation for widgets, so all widgets relate stuff is totally incomprehensible, untill you’ll write a real step by step guide. At the present even an experienced livecode developers can’t build a widget. Dictionary entries a too concise.

    Ali Lloyd - April 20, 2017 reply

    Hi Max, please check out the following resources and let us know what sort of documentation you would like to see!
    – The ‘Extending LiveCode guide on GitHub and in the Guides tab of the dictionary: https://github.com/livecode/livecode-ide/blob/develop/Documentation/guides/Extending%20LiveCode.md#create-your-own-simple-widget
    – Write a widget in 8 steps blog post: https://livecode.com/write-a-widget-in-8-steps/
    – LCB lessons: https://livecode.com/topic/introduction-6/ including creating a rotated text widget and a pie chart widget

    There are also Trevor DeVore’s blogs on creating a slider widget (http://www.bluemangolearning.com/livecode/2015/03/creating-a-slider-widget/) and a busy indicator (http://www.bluemangolearning.com/livecode/2015/04/creating-a-busy-indicator-in-livecode-builder/)

  • Paul McClernan - April 21, 2017 reply

    I could be wrong about this, but I’m pretty sure some apps, ones that use the Android media DB created by the media scanner and rescanned on every boot, will not be able to see your recorded files until you reboot your device. I’ve run into this problem in the past with a project of mine that creates MIDI files. What needs to happen is a wrapper for this method:

    void scanFile (Context context,
    String[] paths,
    String[] mimeTypes,
    MediaScannerConnection.OnScanCompletedListener callback)

    from here:
    https://developer.android.com/reference/android/media/MediaScannerConnection.html

    (the callback parameter can be empty/null from what I’ve read)
    Then you can pass a string path to your apps newly created media file and it’s metadata will be read into the Media DB.

    Paul McClernan - April 22, 2017 reply

    I’m trying to make a binding for the Convenience function I posted above
    foreign handler _JNI_MediaScannerConnection (pContext as JObject, pFileName as JString, pMimeTypes as JString, pCallBack as Any)
    returns nothing binds to “java:android.media.MediaScannerConnection>scanFile(?????;)”

    None of the examples show binding for a JObject param and the first param here would be a JObject containing the app context (if I’m understanding all of this correctly as I don’t know JAVA, I think I can get the hoop jumping bit from the battery code)

    scanFile (Context context,
    String[] paths,
    String[] mimeTypes,
    MediaScannerConnection.OnScanCompletedListener callback)

    my problems are where the question marks are below:
    scanFile(JObject???; Ljava/lang/String;Ljava/lang/String; null????;)V

    This is the sort of thing a more in depth or simplified explanation in the documentation I think would be helpful to people like me who don’t have any experience with lower level languages.

    Thanks

    Ali Lloyd - April 22, 2017 reply

    You need to look at the section on Java VM Type Signatures here: http://journals.ecs.soton.ac.uk/java/tutorial/native1.1/implementing/method.html -I’m sure I linked to this somewhere but can’t find it right now! So for a String array (`String[] `) you need `[Ljava/lang/String;`. In general for non-primitive types you need to find the fully qualified class name – so for `Context` that is here: https://developer.android.com/reference/android/content/Context.html – `android.content.Context`. So the type signature is `Landroid/content/Context;` . The callback’s signature is `Landroid/media/MediaScannerConnection.OnScanCompletedListener;` (I think). It is possible to use the `javap` tool to extract these signatures from compiled classes – indeed in a future iteration we intend to use the reflection API so that you don’t have to divine these signatures by hand!

    Your foreign handler should be
    `foreign handler _JNI_MediaScannerConnection (pContext as JObject, pFileName as JObject, pMimeTypes as JObject, pCallBack as optional JObject)` – that will allow you to pass `nothing` as the fourth parameter. Unfortunately creating a java array is currently a bit awkward – I think you would have to do it indirectly using the java.reflect.Array class…

    Paul McClernan - April 24, 2017 reply

    Thanks Ali,
    Have to go do some learnin’ up!

  • Paul McClernan - April 27, 2017 reply

    FYI, If you want to be able to record a phone call
    Capturing from VOICE_CALL source requires additional permission so you would need to add this line:
    metadata android.permissions is “CAPTURE_AUDIO_OUTPUT”

  • Ronald Bos - June 27, 2017 reply

    I am reading upon the Java FFI connector in Livecode 9. Does this work with desktop applications? I only see Android examples. Also as I read it you need a java .class file (compiled java library) to access it? Unfortunately the library of the Java Driver is source code only. So I need to compile this first, correct? The question then is how does the Java FFI know where to look for the correct classes? Also I assume you need to have the JDK installed on your development machine, will a standalone executable also need this?

    What I wish to accomplish is connecting to a NOSQL database directly. At the moment I’m using the Rest API of the NOSQL Database, which works fine, but I like to connect in a more direct manor, in the hope I get more speed when inserting data.

    Ali Lloyd - June 27, 2017 reply

    Hi Ronald,
    At the moment you cannot compile your own classes and use them on any platform – all that is currently available is the java standard library on mac & linux and the java standard library + android APIs on Android.

    One of the next things that we will be doing is allowing inclusion of compiled resources. The first stage is to do this on Android – that will be done for DP 8, but there is a little more to do to make it work on Desktop so I can’t guarantee it for the next release – however we will try.

    Once we have done this work, the idea will be that you put a .jar file with the compiled classes you need into a subfolder of the extension folder.

    You will indeed need the JDK installed on the development machine but only the JRE on machines running the standalone.

    Keep your eye on the blog and releases for when this is done- it should be very soon.

    Ronald Bos - June 29, 2017 reply

    Thanks for the heads up Ali. I’ll wait and keep track of your progress for this.

  • Peter Reid - September 14, 2017 reply

    I’ve just come across this and I’m trying to use this on an Amazon Fire 7 (2017 model) tablet. I seem to have installed it OK in LC 9.0.0-dp8 (on a Mac running 10.12.6) but I’m getting runtime errors from each call to a handler. For example, if I try the following:

    AndroidSetRecordInput “Mic”
    or
    put AndroidSetRecordInput(“Mic”) into tResult

    I get the following error message:

    Stack “test”: execution error at line 110 (LCB Error in file androidAudioRec.lcb at line 77: Java binding string does not match foreign handler signature or signature not supported)

    I get the same error message from the calls to the other handlers. I have checked that everything looks OK in the lcb file, but can’t find a reason for the error. Do you have a recent update to this or any other suggestions?

    Thanks

    Peter

    Peter Reid - September 15, 2017 reply

    I discovered that I had to remove the parameter “in pRecorder as JObject” from the binds in order to get the calls to work (apparently), i.e. the previous binding string error message disappeared. This seems to make sense as my understanding is that a private recorder object is created and used between the methods and so doesn’t need passing as a parameter.

    However, it’s still not working. In particular, when I try the following, the remote app in my Android tablet crashes out:

    AndroidStartRecording recFilePath

    I’ve checked that I can create a file for the specified path, so I don’t think it’s a file permission problem.

    By the way, I’m not sure whether the methods should be called as handlers or functions in my LiveCode scripts, both seem to work/fail equally, what should they be?

    Ali Lloyd - September 15, 2017 reply

    Hi Peter, unfortunately this blog has become slightly out of date – I will amend it. The pRecorder parameter is required to actually set the property of the recorder – these are non-static methods so they need to be called on an instance of the object (that is the recorder). The reason you got the binding error is that we now have java type aliases that should be used in the foreign handler declarations, so they should be (for example) `JInt` rather than `CInt`.

    I have tidied up the library, added documentation and a sample stack in this pull request, so feel free to use that code as a base: https://github.com/livecode/livecode/pull/5941/files

    Peter Reid - September 15, 2017 reply

    Hi Ali, thanks for looking at this. I’ve tried using your revised library but I’m getting the following error when I attempt the call : AndroidRecorderStartRecording gRecFile

    execution error at line 284 (LCB Error in file androidaudiorecorder.lcb at line 167: JNI exception thrown when calling native method) near “java”, char 1

    Like your example I don’t do any initialisation but rely on the defaults. My global gRecFile has the value /data/app/com.reidit.aphtrain-1/base.apk/material/wordRec.mp4, which is a valid location on the tablet.

    Any ideas?

    Ali Lloyd - September 15, 2017

    My version of the library is set up to take a filename relative to the public external storage folder, i.e. /storage/emulated/0/pFileName . You’ll have to tweak the library slightly if you want to record to a file within the app bundle. Try changing

    
        variable tFile as JString
        put GetAbsolutePathToPublicDocument(pFileName) into tFile
        _JNI_MediaRecorderSetRecordOutputFile(mRecorder, tFile)
    

    to

    
        _JNI_MediaRecorderSetRecordOutputFile(mRecorder, StringToJString(pFileName))
    

    Peter Reid - September 15, 2017

    Thanks Ali, I followed your lead and went with the storage you were using originally and it works! MANY thanks for this, it’s made a big difference to my current app.

    Peter Reid - September 17, 2017

    Sorry Ali but I meant to say that there was 1 typo in your revised lcb, line 114 should be:
    put GetOptionalProperty(mRecordCompressionType, “AMR NB”) into tRecordCompressionType

    instead of:
    put GetOptionalProperty(mRecordInput, “AMR NB”) into tRecordCompressionType

    Thanks again.

    Peter

Join the conversation

*