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.
17 comments
Join the conversationMaxV - April 20, 2017
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
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
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
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
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
Thanks Ali,
Have to go do some learnin’ up!
Paul McClernan - April 27, 2017
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
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
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
Thanks for the heads up Ali. I’ll wait and keep track of your progress for this.
Peter Reid - September 14, 2017
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
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
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
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
to
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