Android Tutorial Wrapping the jFugue Midi Programming jar (B4a & B4j)

Having just posted the jFugue jar file and examples of accessing it, I thought it would be a good opportunity to show how I would go about creating a wrapper in B4x for this.

This is only one way of doing it and a lot of it is down to how you prefer to work.

When designing the wrapper, you need to consider how the methods you create will affect the programmers work flow.

The wrapper I am going to use for this was created in B4j, simply because I prefer to use it when working with non gui code as there is no external device or emulator to worry about. As the classes we are concerned with are the same for B4j and B4a, it won’t make any difference which environment you decide to work in.

Before you start coding anything, you need to read through any examples of the API's use that are available so you can get a feel for how it all works. It is not always necessary to wrap every class that appears in the documentation. Some are used internally from other classes and we will never need to call methods on them directly, some may be used very infrequently and we may decide not to wrap them at this point. There is always the option to access these directly using JavaObject.

Before you start

Download the jFuguePlayer.bas module attached to this post. This is a modified version that will unwrap any wrapped PatternProducer objects that you may create, which include Pattern, ChordProgression Rhythm etc.. But don’t do anything with it yet.

Create a Project

Create a new project and call it something like jFugueWrapper and save it. We then need to add the jFuguePlayer class from the original example project (not the one you just downloaded) using the Project/Add Existing Modules menu item. We will also need to select the JavaObject library in the Libraries Manager tab.

We need to add the following lines to the top of the Main class so that the jar files can be found (they should already be in your additional libraries folder if you have downloaded and run the example project):

B4j:
#AdditionalJar: jfugue-5.0.4

B4a:
#AdditionalJar: jfugue-android
#AdditionalJar: javaxmidi

It doesn’t matter if this is right at the top, or in the Project Attributes region, it will work the same (example from B4j).

B4X:
#Region  Project Attributes
#MainFormWidth: 600
#MainFormHeight: 400
#AdditionalJar: jfugue-5.0.4
#End Region

Creating a Class

As the Pattern class is probably going to be the most commonly used class in this library, we will look at wrapping that one.

Create a new class module and give it a name. So that it is not likely to conflict with any other modules I have I tend to give wrapper modules a prefix, so I’ll call it jfPattern.

So open the documentation for the Pattern class from the jfugue website here: http://www.jfugue.org/doc/org/jfugue/pattern/Pattern.html

The good thing about Javadocs is that they are always pretty consistent in their layout. We need to find the full class name for Pattern which will be very near the top. In this case it is: org.jfugue.pattern.Pattern.

Go to your jfPattern class module and create a new String variable:

B4X:
Dim ClassName As String = "org.jfugue.pattern.Pattern"

When we Intialize our JavaObject this will tell java where to find the class.

Creating an object - Constructors

The first thing we need to do is check the classes constructors. These define what is required to create an object for the class. For the class Pattern there are 4.

Pattern()
will create an empty Pattern object and requires no parameters.

Pattern(PatternProducer... producers)
will create a Pattern object from an array of objects of type PatternProducer or one of its subclasses

Pattern(String... strings)
will create a Pattern object from an array of Strings

Pattern(String string)
will create a Pattern object from a single String

We can’t reproduce this behaviour in B4x because we can’t use overloaded methods (Multiple methods with the same name but requiring different parameters). So our first decision is how to let the users of this wrapper construct or initialize a Pattern object.

Looking ahead into the Method Summary section of the documentation, there are two methods with the name add(). These will take as parameters either an array of objects of type PatternProducer, or a string. We really don’t need to provide methods for all of the constructors as we can add to the patterns at any time.

In B4x, we must call the initialize sub on (most) new objects, although the initialize method in the B4x Class could be empty, it does some work behind the scenes to integrate the object into the B4x environment. So for this class it seems that the easiest way would be to create an empty pattern when the initialize sub is called. Then provide the add methods to actually populate the resulting Pattern object.

Will this require more code for the programmer using the wrapper? Not really, if we implemented each of the constructors we would still have to call the initialize method resulting in code similar to this:

B4X:
Dim P1 As Pattern
P1.Initialize
P1.Construct1(“A B C D E”)

And we would also need to provide methods for Construct2 and 3.

Whereas if we create an empty Pattern in the Initialize methods the resultant code would be something like this:
B4X:
Dim P1 As Pattern
P1.Initialize
P1.AddString(“A B C D E”)

Or

B4X:
Dim P1 As Pattern
P1.Initialize
P1.AddProducer(P2)

Job done!.

So back in our jfPattern class, we need to create a Variable in the Class_Globals sub to hold the object when it is created, which will be of type JavaObject:

B4X:
Dim JO As JavaObject

The in the Initialize sub we can create the Pattern Object.

B4X:
Public Sub Initialize
    ‘Create a new pattern object by calling the no parameter constructor
    JO.InitializeNewInstance(ClassName,Null)
End Sub

To test this is working we can Dim and Initialize a new pattern in the Main module, so in the Appstart sub (B4j) or Activity_Create in B4a (Not really the best place to do this, but fine for our testing purposes) add the code:

B4X:
Dim P1 As Pattern
P1.Initialize

And run it.

If there are no errors (check the log tab) then we have successfully created a Pattern Object.

To prove this further we can add a Log statement to the code:

B4X:
Dim P1 As Pattern
P1.Initialize
Log(P1)

The output is different on B4j and B4a but somewhere in the log you should see:

classname=org.jfugue.pattern.Pattern

If so it worked.

Creating Some Methods

Now we can create our add Methods

In the jfPattern module add a new sub:
B4X:
Sub AddString(StringToAdd As String)
    JO.RunMethod("add",Array(StringToAdd))
End Sub

And in the main module after the p1.initialize add the line:
B4X:
P1.AddString("A B C D E")

And run it. Check the logs for errors. If there are none, then again it worked.

Now we need to hear the results.

Change the code in the Main Module to look like this:

B4X:
    Dim Player As jFuguePlayer
    Player.Initialize

    Dim P1 As jfPattern
    P1.Initialize
    P1.AddString("A B C D E")

    Player.Play(P1)

And run it. Check the logs (it should fail)

Why doesn’t this work?

Because the Player method requires an object of type:
org.jfugue.pattern.Pattern but we have passed it our wrapper class.

With each wrapper class we need to provide a method to get the underlying object.

In the jfPattern class add the following Sub:
B4X:
Sub GetObjectJO As JavaObject
    Return JO
End Sub

And change the Player.Play call in Main to
B4X:
Player.Play(P1.GetObjectJO)

Now run it and Check the logs. It should now play the pattern.

Now I’d like you to replace the jFuguePlayer module with the one you downloaded from this post, you can copy and paste the code from it, or copy the file to the project directory (after closing the project) then reopen the project. You could also use the Project/ Add new module option, but you will have to remove and delete the existing module first. The new jfPlayer will unwrap any Wrappers for the PatternProducer classes you may create. It will also deal appropriately with Arrays or objects passed. I don’t want to go into the details of that here, but if you have any questions about it, start a new thread and I will respond. It’s the same module for B4a and B4j.

Also, for that to work, we need to create a second GetObject Sub in the jfPatternWrapper (and any other wrapper you create for PatternProducer classes that are going to be played like ChordProgression or Rhythm):

B4X:
Sub GetObject As Object
    Return JO
End Sub

And change the Player.Play call in Main to
B4X:
Player.Play(P1)

Hopefully it will now play the pattern.

We need the second GetObject sub so that we don’t get an object that is wrapped in a JavaObject. It is useful to have both.

Our jfPattern Class should now look like this:
B4X:
'Class module
Sub Class_Globals
    Private fx As JFX
    Dim ClassName As String = "org.jfugue.pattern.Pattern"
    Dim JO As JavaObject
End Sub

'Initializes the object. You can add parameters to this method if needed.
Public Sub Initialize
    JO.InitializeNewInstance(ClassName,Null)
End Sub

Sub AddString(StringToAdd As String)
    JO.RunMethod("add",Array(StringToAdd))
End Sub

'Return an unwrapped object as JavaObject
Sub GetObjectJO As JavaObject
    Return JO
End Sub

'Return an unwrapped object as Object
Sub GetObject As Object
    Return JO
End Sub

We will now create some more add Methods.

Firstly looking at the documentation the add methods are for a single string, or for an array of producer.

A single string is fine as we could always concatenate multiple strings before calling the sub, or call the sub multiple times, so we’ll leave it as it is..

The Array of producers gives us a bit of a problem however, as the array has to be the same type as it’s contents.

We can't just pass an object array unfortunately. And it won’t accept a single producer.

This is possibly an exception rather than a rule. Most of the wrappers I have created have not needed to be passed a typed array, there are usually other methods that you can use to avoid having to do this. But it does serve as an example for this run through.

The necessary code is already in the jFuguePlayer Class, we should create a Utils code module and put it in there, but for expediency we will write it in the AddProducer and AddProducers Subs.

Firstly we need to be able to get the wrapped object regardless of what type of object it is. Our jfPattern or another producer Class (if you write some more). Using a little JavaObject Magic, we can.

The sub looks like this:
B4X:
Sub GetWrappedObject(Pattern As JavaObject) As JavaObject
    #if Debug
    Return Pattern.RunMethod("_getobject",Array(Pattern))
    #end if
    #if Release
    Return Pattern.RunMethod("_getobject",Null)
    #End if
End Sub

As you can see the passed object is cast to a JavaObject, then we can use it’s RunMethod method to call the GetObject sub we’ve created in the Class. It is further complicated in that the sub has different arguments depending on whether we are running in Debug or Release modes, which we don’t normally see. B4x takes care of that for us. But because we are poking around inside, we need to be aware of it.

On B4a, this won’t work in the Legacy Debugger as it needs the same arguments for release mode. If you want to work in the Legacy debugger you can just comment all the lines except:

B4X:
Return Pattern.RunMethod("_getobject",Null)

Or set you own flag and change the code accordingly. But it’s probably easier just to use the Rapid debugger or Release modes.

Then we need to wrap the producer object in an object array (as the add(PatternProducer… producers) method expects an array) and then convert the object array into an array of the correct type. JavaObject provides the InitilizeArray method which does exactly what we need for this.

The AddProducer sub then looks like this:

B4X:
Sub AddProducer(Producer As JavaObject)
    Try
        Dim P1 As JavaObject
        Dim Unwrapped As Object = GetWrappedObject(Producer)
        Dim ObjArr() As Object = Array(Unwrapped)
        P1.InitializeArray(GetType(Unwrapped),ObjArr)
        JO.RunMethod("add",Array(P1))
    Catch
        Log("Invalid type passed to AddProducer " & GetType(Producer))
        Return
    End Try
End Sub

We also need to enclose calls to the GetWrappedObject in a try..catch block as if the object is not one of our wrapped objects, there will probably not be a GetObject sub and it will fail.

Adding multiple producers at one time is only a little trickier and the sub looks like this:

B4X:
Sub AddProducers(Producers As Object)
    Dim P1 As JavaObject
    Try
        Dim ObjArr() As Object = Producers
        Dim ResultArr(ObjArr.Length) As Object
        For i = 0 To ObjArr.Length - 1
            ResultArr(i) = GetWrappedObject(ObjArr(i))
        Next
        P1.InitializeArray(GetType(ResultArr(0)),ResultArr)
        JO.RunMethod("add",Array(P1))
    Catch
        Log("Invalid type passed to AddProducer " & GetType(Producers))
        Return
    End Try
End Sub

We first have to unwrap all of the objects into a new Object array, then convert the array to the correct type using JavaObject.

Change your code in Main to something like this to test it:

B4X:
    Dim P1 As jfPattern
    P1.Initialize
    P1.AddString("C D E F G")
    Dim P2 As jfPattern
    P2.Initialize
    P2.AddProducers(Array(P1,P1))

    Player.Play(Array(P2))

The full jfPattern Class should now look like this:
B4X:
'Class module
Sub Class_Globals
    Dim ClassName As String = "org.jfugue.pattern.Pattern"
    Dim JO As JavaObject
End Sub

'Initializes the object. You can add parameters to this method if needed.
Public Sub Initialize
    JO.InitializeNewInstance(ClassName,Null)
End Sub

Sub AddString(StringToAdd As String)
    JO.RunMethod("add",Array(StringToAdd))
End Sub

Sub AddProducer(Producer As Object)
    Try
        Dim P1 As JavaObject
        Dim Unwrapped As Object = GetWrappedObject(Producer)
        Dim ObjArr() As Object = Array(Unwrapped)
        P1.InitializeArray(GetType(Unwrapped),ObjArr)
        JO.RunMethod("add",Array(P1))
    Catch
        Log("Invalid type passed to AddProducer " & GetType(Producer))
        Return
    End Try
End Sub

Sub AddProducers(Producers As Object)
    Dim P1 As JavaObject
    Try
        Dim ObjArr() As Object = Producers
        Dim ResultArr(ObjArr.Length) As Object
        For i = 0 To ObjArr.Length - 1
            ResultArr(i) = GetWrappedObject(ObjArr(i))
        Next
        P1.InitializeArray(GetType(ResultArr(0)),ResultArr)
        JO.RunMethod("add",Array(P1))
    Catch
        Log("Invalid type passed to AddProducer " & GetType(Producers))
        Return
    End Try
End Sub

'Return an unwrapped object as JavaObject
Sub GetObjectJO As JavaObject
    Return JO
End Sub

'Return an unwrapped object as Object
Sub GetObject As Object
    Return JO
End Sub

Private Sub GetWrappedObject(Pattern As JavaObject) As JavaObject
    #if Debug
    Return Pattern.RunMethod("_getobject",Array(Pattern))
    #end if
    #if Release
    Return Pattern.RunMethod("_getobject",Null)
    #End if
End Sub

I hope this introduction has given you enough information to go and wrap some more methods and classes and an insight into wrapping for a jar file. It is certainly worth while when it works as you want it too.

If you have any questions, please post them on the forum..
 

Attachments

  • jFuguePlayer.bas
    5.4 KB · Views: 230
Last edited:

Ydm

Active Member
Licensed User
Hello.
I need me one event.
Every note played will be triggered.

For example:

B4X:
JP.Play("A B C D E F G")

Sub MP_Play
  Log("This note:"+event.name)
End Sub
 
Top