B4J Question Convert an array of Bytes to an array of Ints

max123

Well-Known Member
Licensed User
Longtime User
Hi guys,

I've a code where MIDI input come from serial port and the received array of bytes need to be converted to an array of integers to be routed on internal system Midi loopback.

I do not like my piece of code I wrote, even it should execute as fast possible, it is important because thit is a Serial-Midi bridge where Midi latency is very important to be as small possible.
Here my code, is there a better way to do it ?
Thanks

B4X:
Sub AStream_NewData (Buffer() As Byte)
    If Midi.attachedOutputs.Length > 0 Then SendMidiMessage(Buffer)
End Sub

Sub SendMidiMessage(Buffer() As Byte)  ' <<< RECEIVE AN ARRAY OF BYTES
    Dim debug As Boolean = ckDebug.Checked
  
    If debug Then
        Dim sb As StringBuilder
        sb.Initialize
    End If
  
    Dim bf(Buffer.Length) As Int
  
    For i = 0 To bf.Length -1
        bf(i) = Buffer(i)
        If debug Then
            If bf(i) >= 0 Then
                sb.Append(bf(i))
            Else
                sb.Append(bf(i)+256)
            End If
            If i < bf.Length -1 Then sb.Append(", ")
        End If  
    Next
  
    If debug Then LogMessage("Send Midi Message: " & bf.Length & " Bytes [" & sb.ToString & "]")
    Midi.SendMessage(bf)  ' <<< SEND AN ARRAY OF INTEGERS
End Sub
 
Last edited:

emexes

Expert
Licensed User
You'd like to convert an array of signed Bytes toa comma+space separated string of unsigned decimal numbers?

Eg three bytes 0xFF 0x7F 0x30 having unsigned decimal values 255 127 48 becomes string "255, 127, 48" ?

In which case, your code looks close-enough.

You could use Bit.And(bf(i), 0xFF) to convert from signed Byte to unsigned Int, might be a speed gain from eliminating an array lookup.

And I'd probably always add the ", " each time, and then knock two characters off just before the .ToString.

No, I thought there was a .Unappend / .Remove method, but apparently not. So I'd do with an If, like you, but test i <> 0 rather than a length comparison.

And move all the debug stuff into a single If, rather than repeat each time around the loop.
 
Last edited:
Upvote 0

emexes

Expert
Licensed User
Ugh, I just realised the string thing is separate to the array of Ints. How about:

B4X:
Sub SendMidiMessage(Buffer() As Byte)  ' <<< RECEIVE AN ARRAY OF BYTES

    Dim bf(Buffer.Length) As Int
 
    For i = 0 To bf.Length - 1
        bf(i) = Bit.And(Buffer(i), 0xFF)
    Next
   
    If ckDebug.Checked Then
        Dim sb As StringBuilder
        sb.Initialize

        For i = 0 to bf.Length - 1
            If i <> 0 Then
                sb.Append(", ")
            end if
            sb.Append(bf(i))
        Next
   
        LogMessage("Send Midi Message: " & bf.Length & " Bytes [" & sb.ToString & "]")
    End If
   
    Midi.SendMessage(bf)  ' <<< SEND AN ARRAY OF INTEGERS

End Sub
 
Upvote 0

max123

Well-Known Member
Licensed User
Longtime User
Thanks for reply @emexes this seem better than my code because do not mix strings with numbers, but is Bit.And slow ?

The problem here is that I want to avoid declare an Int array and assign it in a loop one by one, for sure this slow down.
Is there a way maybe to use 'As' keyword to cast it, but not one by one by index ?

Thanks
 

Attachments

  • Screen Shot 12-27-22 at 03.29 PM.PNG
    Screen Shot 12-27-22 at 03.29 PM.PNG
    16.5 KB · Views: 46
Upvote 0

emexes

Expert
Licensed User
I like this even better:

B4X:
Sub SendMidiMessage(Buffer() As Byte)  ' <<< RECEIVE AN ARRAY OF BYTES

    Dim bf(Buffer.Length) As Int
 
    For i = 0 To bf.Length - 1
        bf(i) = Bit.And(Buffer(i), 0xFF)
    Next
  
    If ckDebug.Checked Then
        LogMessage("Send Midi Message: " & bf.Length & " Bytes [" & IntsToString(bf, ", ") & "]")
    End If
  
    Midi.SendMessage(bf)  ' <<< SEND AN ARRAY OF INTEGERS

End Sub

Sub IntsToString(Ints() As Int, Separator As String) As String
    
    Dim sb As StringBuilder
    sb.Initialize

    If Ints.Length <> 0 Then
        sb.Append(Ints(0))
        For i = 1 to Ints.Length - 1
            sb.Append(Separator)
            sb.Append(Ints(i))
        Next
    End If
    
    Return sb.ToString

End Sub
 
Upvote 0

emexes

Expert
Licensed User
The problem here is that I want to avoid declare an Int array and assign it in a loop one by one, for sure this slow down.

Isn't MIDI 4800 bps? I think you'll find that allocating a temporary array and converting element-by-element is thousands of times faster than that = not a bottleneck.

edit: I knew I should have checked first 🤣 MIDI is 31250 bps. I'm still happy to bet on an over-1000-times-faster speed ratio. 🏆
 
Upvote 0

max123

Well-Known Member
Licensed User
Longtime User
Yes thanks, but the problem here is not a string part, it only happen in Debug checkbox is checked.
The problem is convert a Byte array to an Int array without declare a new Int array, is that possible ?
 
Last edited:
Upvote 0

max123

Well-Known Member
Licensed User
Longtime User
Isn't MIDI 4800 bps? I think you'll find that allocating a temporary array and converting element-by-element is thousands of times faster than that = not a bottleneck.
Absolutely no, Midi is 31250 bps, but this is a Serial-Midi Bridge, by default I receive at 115200 but should work up 921600 bps, with ESP8266 and ESP32, Raspberry Pico and others, Arduino Uno and similar at 8Mhz, 16Mhz probably can max 115200. But ESP works up 240Mhz and based on usb-serial converter it mount can reach more higher speeds. Here latency in milliseconds is very important I will still searching a way to minimize..... Debug is another story, it only serves to debug, latency do no matter much.
 
Last edited:
Upvote 0

emexes

Expert
Licensed User
Absolutely no, Midi is 31250 bps, but this is a Serial-Midi Bridge, by default I receive at 115200 but should work up 921600 bps, with ESP8266 and ESP32, Raspberry Pico and others, Arduino Uno and similar at 8Mhz, 16Mhz probably can max 115200. But ESP works up 240Mhz. Here latency in milliseconds is very important I will still searching a way to minimize..... Debug is another story, it only serves to debug, latency do no matter much.

Assuming only one actual traditional MIDI bus cable, then the throughput is limited to 3125 bytes per second. I agree that the increased speed on the other side of the bridge reduces latency.

The doc I just read says that the high bit determines whether the byte is a status byte or a data byte, which is a nice match to Java/B4X signed Bytes, so I'm wondering if the conversion to unsigned values 0..255 is actually necessary. Sure, it's nice to make the numbers the same as in the documentation, but to the computer it's all just bits.

What is ESP? Is that as in Espressif, or is it a new MIDI protocol?

The latency caused by the debug decimal string could be eliminated by sending the MIDI array first ie immediately it's ready, and then building and Logging the debug string in the lull afterwards.
 
Upvote 0

max123

Well-Known Member
Licensed User
Longtime User
Yes ESP is Espressif chip ESP8266 or ESP32.

Yes the midi data are threated ad integers because java do not have unsigned byte, no unsigned int, for this reason because the status byte can be up 255 (8 bit number) need a cast.

I know it because I fully ported this midi library I used from Processing MidiBus library to B4J completely on java. It and in general javax.media.midi use integer, then cast to bytes when send to real hardware midi devices.

I always search a way to optimize, this code I wrote do not have a lots of things, just need to do Serial-Midi bridge with smaller latency possible, possibly should be around 1 ms.

So in your opinion there is not a way to cast a byte array to an int array without declare a new int array ?
 
Last edited:
Upvote 0

max123

Well-Known Member
Licensed User
Longtime User
If you know java this is a relevant part, as tou see it threat internally data as integer, then cast as byte to match midi:
Java:
  /* Notifies all types of listeners of a new MIDI message from one of the MIDI input devices.
     *
     * Message:   The new inbound MidiMessage.
     * TimeStamp:   The timestamp.
    */
    void NotifyListeners(MidiMessage Message, long TimeStamp) {

        byte[] data = Message.getMessage();

        for(MidiListener listener : listeners) {

            /* -- RawMidiListener -- */     
            if(listener instanceof RawMidiListener) ((RawMidiListener)listener).rawMidiMessage(data);

            /* -- SimpleMidiListener -- */         
            if(listener instanceof SimpleMidiListener) {
                if((int)((byte)data[0] & 0xF0) == ShortMessage.NOTE_ON) {
                    ((SimpleMidiListener)listener).noteOn((int)(data[0] & 0x0F),(int)(data[1] & 0xFF),(int)(data[2] & 0xFF));
                } else if((int)((byte)data[0] & 0xF0) == ShortMessage.NOTE_OFF) {
                    ((SimpleMidiListener)listener).noteOff((int)(data[0] & 0x0F),(int)(data[1] & 0xFF),(int)(data[2] & 0xFF));
                } else if((int)((byte)data[0] & 0xF0) == ShortMessage.CONTROL_CHANGE) {
                    ((SimpleMidiListener)listener).controlChange((int)(data[0] & 0x0F),(int)(data[1] & 0xFF),(int)(data[2] & 0xFF));
                }
            }

            /* -- StandardMidiListener -- */     
            if(listener instanceof StandardMidiListener) ((StandardMidiListener)listener).midiMessage(Message, TimeStamp);

            /* -- ObjectMidiListener -- */
            if(listener instanceof ObjectMidiListener) {
                if((int)((byte)data[0] & 0xF0) == ShortMessage.NOTE_ON) {
                    ((ObjectMidiListener)listener).noteOn(new Note((int)(data[0] & 0x0F),(int)(data[1] & 0xFF),(int)(data[2] & 0xFF)));
                } else if((int)((byte)data[0] & 0xF0) == ShortMessage.NOTE_OFF) {
                    ((ObjectMidiListener)listener).noteOff(new Note((int)(data[0] & 0x0F),(int)(data[1] & 0xFF),(int)(data[2] & 0xFF)));
                } else if((int)((byte)data[0] & 0xF0) == ShortMessage.CONTROL_CHANGE) {
                    ((ObjectMidiListener)listener).controlChange(new ControlChange((int)(data[0] & 0x0F),(int)(data[1] & 0xFF),(int)(data[2] & 0xFF)));
                }
            }
        }
 
Upvote 0

max123

Well-Known Member
Licensed User
Longtime User
Thanks @OliverA, I've used it in other occasions but not here, I will look at. Thanks
 
Upvote 0

emexes

Expert
Licensed User
from Processing MidiBus library to B4J completely on java. It and in general javax.media.midi use integer, then cast to bytes when send to real hardware midi devices.

It looks like the Processing MidiBus library is already prepared to handle Java's #&&$ing signed Bytes:

Java:
public void sendMessage(byte[] data) {
        if ((int)((byte)data[0] & 0xFF) == MetaMessage.META) {

but if you've gone the clarity route of using Ints to represent Bytes as unsigned numbers, then hey, I don't think I've ever argued in favour of less clarity. 🍻
 
Upvote 0

OliverA

Expert
Licensed User
Longtime User
If you know java this is a relevant part, as tou see it threat internally data as integer
Unless I'm overlooking something, that code does not convert the byte array to an int array. What it does is take an individual byte from the array and convert it to int. Those are two totally different operations. What you are looking for is just
B4X:
Value as int = bit.and(data(0), 0xff)
But even that is unnecessary if you just code your NOTE_OFF, NOTE_ON and CONTROL_CHANGE as a byte value (I doubt they are larger than byte values, else the Java code would not work)
 
Upvote 0

max123

Well-Known Member
Licensed User
Longtime User
It looks like the Processing MidiBus library is already prepared to handle Java's #&&$ing signed Bytes:

Java:
public void sendMessage(byte[] data) {
        if ((int)((byte)data[0] & 0xFF) == MetaMessage.META) {

but if you've gone the clarity route of using Ints to represent Bytes as unsigned numbers, then hey, I don't think I've ever argued in favour of less clarity. 🍻
It even serves because on B4J side all midi data will be threated as integers
 
Upvote 0

emexes

Expert
Licensed User
So in your opinion there is not a way to cast a byte array to an int array without declare a new int array ?

Generally speaking, yes. Unless messages tend to be the same size, then you can reuse a single Int array, and only resize it when the number of MIDI bytes changes.

ByteConverter does have <Type>FromByte conversions, but I'm pretty sure that they don't do what you want, ie expand 8-bit Bytes into 32-bit Ints.
 
Last edited:
Upvote 0

max123

Well-Known Member
Licensed User
Longtime User
Unless I'm overlooking something, that code does not convert the byte array to an int array. What it does is take an individual byte from the array and convert it to int. Those are two totally different operations. What you are looking for is just
B4X:
Value as int = bit.and(data(0), 0xff)
But even that is unnecessary if you just code your NOTE_OFF, NOTE_ON and CONTROL_CHANGE as a byte value (I doubt they are larger than byte values, else the Java code would not work)
Yes oliver, I just confused, you are right, this function use interface listeners provided by javax midi class to receive midi input, it receive bytes then cast to int that is exactly the inverse of what I wrote :D, but the library internally use these as integers.

What I should post are the B4J calls, this is the code I wrote:
Java:
/**
     * Notifies any of the supported methods implemented inside the Parent of a new MIDI message from one of the MIDI input devices.
     *
     * Message:  The new inbound MidiMessage.
     * TimeStamp:   The timestamp
    */
    void NotifyParent(MidiMessage Message, long TimeStamp) {
        if (parent == null) return;
       
        byte[] data = Message.getMessage();  
       
        switch ( (int)((byte)data[0] & 0xF0) ) {
       
            case ShortMessage.NOTE_ON:   //////// NOTE ON ////////
                                                       
                LastNoteOnVelocity = Constrain((int)(data[2] & 0xFF), 0, 127);
               
                if ( parent.subExists(eventName + "_noteon") ){
//                    BA.Log("esiste la sub noteon");
                    try {      
                        parent.raiseEventFromDifferentThread(this, null, 0,  eventName + "_noteon", false, new Object[] { (int)(data[0] & 0x0F), (int)(data[1] & 0xFF), (int)(data[2] & 0xFF) });
                        //parent.raiseEvent(this, eventName + "_noteon", new Object[] { (int)(data[0] & 0x0F), (int)(data[1] & 0xFF), (int)(data[2] & 0xFF) });  //ORIGINALE********************
                    } catch (Exception e) {
                        //BA.Log("MidiBus Warning: Disabling NoteOn(Channel As Int, Note As Int, Velocity As Int) because an unkown exception was thrown and caught");
                        e.printStackTrace();  
                    }
                }
               
                if ( parent.subExists(eventName + "_noteon_timestamp_busname") ){          
                    try{
                        parent.raiseEventFromDifferentThread(this, null, 0, eventName + "_noteon_timestamp_busname", false, new Object[] { (int)(data[0] & 0x0F), (int)(data[1] & 0xFF), (int)(data[2] & 0xFF), TimeStamp/1000, bus_name });
                        //parent.raiseEvent(this, eventName + "_noteon_timestamp_busname", new Object[] { (int)(data[0] & 0x0F), (int)(data[1] & 0xFF), (int)(data[2] & 0xFF), TimeStamp/1000, bus_name });  //ORIGINALE********************
                    } catch (Exception e){
                        e.printStackTrace();
                    }
                }          
               
                if ( parent.subExists(eventName + "_noteon_note") ){
                    try {
                        parent.raiseEventFromDifferentThread(this, null, 0, eventName + "_noteon_note", false, new Object[] {new Note((int)(data[0] & 0x0F), (int)(data[1] & 0xFF), (int)(data[2] & 0xFF), TimeStamp/1000, bus_name)});
                        //parent.raiseEvent(this, eventName +  "_noteon_note", new Note((int)(data[0] & 0x0F), (int)(data[1] & 0xFF), (int)(data[2] & 0xFF), TimeStamp/1000, bus_name) );
                    } catch (Exception e){
                        e.printStackTrace();
                    }              
                }          
           
                break;
               
            case ShortMessage.NOTE_OFF:    //////// NOTE OFF ////////
               
                if ( parent.subExists(eventName + "_noteoff") ){
                    try {
                        parent.raiseEventFromDifferentThread(this, null, 0,  eventName + "_noteoff", false, new Object[] { (int)(data[0] & 0x0F), (int)(data[1] & 0xFF), (int)(data[2] & 0xFF) });
                        //parent.raiseEvent(this, eventName + "_noteoff", new Object[] { (int)(data[0] & 0x0F), (int)(data[1] & 0xFF), (int)(data[2] & 0xFF) });  //ORIGINALE********************
                    } catch(Exception e) {
                        e.printStackTrace();  
                    }
                }
               
                if ( parent.subExists(eventName + "_noteoff_timestamp_busname") ){
                    try {
                        parent.raiseEventFromDifferentThread(this, null, 0,  eventName + "_noteoff_timestamp_busname", false, new Object[] { (int)(data[0] & 0x0F), (int)(data[1] & 0xFF), (int)(data[2] & 0xFF), TimeStamp/1000, bus_name });
                        //parent.raiseEvent(this, eventName + "_noteoff_timestamp_busname", new Object[] { (int)(data[0] & 0x0F), (int)(data[1] & 0xFF), (int)(data[2] & 0xFF), TimeStamp/1000, bus_name });  //ORIGINALE********************
                    } catch(Exception e) {
                        //BA.Log("MidiBus Warning: Disabling NoteOff_TimeStamp_BusName(Channel As Int, Note As Int, Velocity As Int, TimeStamp As Long, BusName As String) because an unkown exception was thrown and caught");
                        e.printStackTrace();  
                    }
                }  
               
                if ( parent.subExists(eventName + "_noteoff_note") ){
                    try {    
                        parent.raiseEventFromDifferentThread(this, null, 0, eventName + "_noteoff_note", false, new Object[] {new Note((int)(data[0] & 0x0F), (int)(data[1] & 0xFF), (int)(data[2] & 0xFF), TimeStamp/1000, bus_name)});
                        //parent.raiseEvent(this, eventName + "_noteoff_note", new Note((int)(data[0] & 0x0F), (int)(data[1] & 0xFF), (int)(data[2] & 0xFF), TimeStamp/1000, bus_name) );
                    } catch(Exception e) {
                        e.printStackTrace();
                    }
                }  
               
                break;
               
            case ShortMessage.CONTROL_CHANGE:    //////// CONTROL CHANGE ////////
                   
                if ( parent.subExists(eventName + "_controlchange") ){
                    try {
                        parent.raiseEventFromDifferentThread(this, null, 0, eventName + "_controlchange", false, new Object[] { (int)(data[0] & 0x0F), (int)(data[1] & 0xFF), (int)(data[2] & 0xFF)});
                        //parent.raiseEvent(this, eventName + "_controlchange", new Object[] { (int)(data[0] & 0x0F), (int)(data[1] & 0xFF), (int)(data[2] & 0xFF)}); //ORIGINALE********************
                    } catch(Exception e) {
                        e.printStackTrace();
                    }
                }  
               
                if ( parent.subExists(eventName + "_controlchange_timestamp_busname") ){
                    try {
                        parent.raiseEventFromDifferentThread(this, null, 0, eventName + "_controlchange_timestamp_busname", false, new Object[] { (int)(data[0] & 0x0F), (int)(data[1] & 0xFF), (int)(data[2] & 0xFF), TimeStamp/1000, bus_name });
                        //parent.raiseEvent(this, eventName + "_controlchange_timestamp_busname", new Object[] { (int)(data[0] & 0x0F), (int)(data[1] & 0xFF), (int)(data[2] & 0xFF), TimeStamp/1000, bus_name }); //ORIGINALE********************
                    } catch(Exception e) {
                        e.printStackTrace();
                    }
                }  
               
                if ( parent.subExists(eventName + "_controlchange_control") ){
                    try {    
                        parent.raiseEventFromDifferentThread(this, null, 0, eventName + "_controlchange_control", false, new Object[] {new ControlChange((int)(data[0] & 0x0F), (int)(data[1] & 0xFF), (int)(data[2] & 0xFF), TimeStamp/1000, bus_name)});
                        //parent.raiseEvent(this, eventName + "_controlchange_control", new ControlChange((int)(data[0] & 0x0F), (int)(data[1] & 0xFF), (int)(data[2] & 0xFF), TimeStamp/1000, bus_name));
                    } catch(Exception e) {
                        e.printStackTrace();
                    }
                }
               
                break;
               
            default:
               
               break;
        }
       
        //////// RAW MIDI ////////      
       
        if ( parent.subExists(eventName + "_rawdata") ) {  // Always notify raw data input if callback sub exist
            try {
                int[] dataint = new int[data.length];              
                for(int i = 0; i < data.length; i++) dataint[i] = (int)(data[i] & 0xFF);

                parent.raiseEventFromDifferentThread(this, null, 0, eventName + "_rawdata", false, new Object[] { dataint });
//                parent.raiseEvent(this, eventName + "_rawdata", new Object[] { data });
                //parent.raiseEvent(this, eventName + "_rawdata", new Object[] { dataint }); //ORIGINALE********************
            } catch(Exception e) {
                e.printStackTrace();  
            }
        }
       
        if ( parent.subExists(eventName + "_rawdata_timestamp_busname") ) {  // Always notify raw data input if callback sub exist
            try {
                int[] dataint = new int[data.length];              
                for(int i = 0; i < data.length; i++)dataint[i] = (int)(data[i] & 0xFF);
               
                parent.raiseEventFromDifferentThread(this, null, 0, eventName + "_rawdata_timestamp_busname", false, new Object[] { dataint, TimeStamp/1000, bus_name });
                //parent.raiseEvent(this, eventName + "_rawdata_timestamp_busname", new Object[] { dataint, TimeStamp/1000, bus_name });  //ORIGINALE********************
            } catch(Exception e) {
                e.printStackTrace();  
            }
        }  

        //////// SHORT MESSAGE ////////  We return a ShortMessage class instead of MidiMessage because B4J do not know java MidiMessage and we need to wrap it inside a custom class
       
        if ( parent.subExists(eventName + "_shortmessage") ) {   // Always notify short messages if callback sub exist
            try {
                ShortMessage SM = new ShortMessage(data);
                parent.raiseEventFromDifferentThread(this, null, 0, eventName + "_shortmessage", false, new Object[] { SM });
            } catch(Exception e) {
                e.printStackTrace();  
            }
        }  
       
        if ( parent.subExists(eventName + "_shortmessage_timestamp_busname") ) {   // Always notify short messages if callback sub exist
            try {
                ShortMessage SM = new ShortMessage(data);
                parent.raiseEventFromDifferentThread(this, null, 0, eventName + "_shortmessage_timestamp_busname", false, new Object[] { SM, TimeStamp/1000, bus_name });
            } catch(Exception e) {
                e.printStackTrace();  
            }
        }
               
    }

And events, all use integers:
Java:
@Events(values = {"NoteOn(Channel As Int, Note As Int, Velocity As Int)",
                  "NoteOn_TimeStamp_BusName(Channel As Int, Note As Int, Velocity As Int, TimeStamp As Long, BusName As String)",
                  "NoteOn_Note(Note As Note)",
                        
                  "NoteOff(Channel As Int, Note As Int, Velocity As Int)",
                  "NoteOff_TimeStamp_BusName(Channel As Int, Note As Int, Velocity As Int, TimeStamp As Long, BusName As String)",
                  "NoteOff_Note(Note As Note)",
                      
                  "ControlChange(Channel As Int, Number As Int, Value As Int)",
                  "ControlChange_TimeStamp_BusName(Channel As Int, Number As Int, Value As Int, TimeStamp As Long, BusName As String)",
                  "ControlChange_Control(Change As ControlChange)",
                  
                  "ShortMessage(Msg As ShortMessage)",
                  "ShortMessage_TimeStamp_BusName(Msg As ShortMessage, TimeStamp As Long, BusName As String)",
                  
                  "RawData(Data() As Int)",
                  "RawData_TimeStamp_BusName(Data() As Int, TimeStamp As Long, BusName As String)"})
 
Last edited:
Upvote 0

emexes

Expert
Licensed User
Unless I'm overlooking something, that code does not convert the byte array to an int array. What it does is take an individual byte from the array and convert it to int. Those are two totally different operations.

I didn't say it converted a byte array to an int array. I chose one line that masked a signed byte to be an unsigned number, from many, many lines in that code module that also do that masking.

I did say: It looks like the Processing MidiBus library is already prepared to handle Java's #&&$ing signed Bytes
 
Upvote 0
Top