Search code examples
androidmidi

How to decode Midi data Android


I'm writing an Android application. A MIDI piano keyboard is connected physically by a cable to an Android device. I have been following the official Android Midi documentation here https://developer.android.com/reference/android/media/midi/package-summary, but I am stuck with decoding the raw Midi data which I am receiving.

@RequiresApi(api = Build.VERSION_CODES.M)
class MidiFramer extends MidiReceiver {
    public void onSend(byte[] data, int offset,
                       int count, long timestamp) throws IOException {
        // parse MIDI or whatever
        // How to convert data to something readable? Below doesn't make any sense.
        Log.v(LOG_TAG, "onSend strData:" + data +" length:"+data.length);

        StringBuffer sb = new StringBuffer();
        for (int i=0; i<data.length; i++){
            String hex = new String (data, StandardCharsets.UTF_8);
            sb.append(hex);
        }
        Log.v(LOG_TAG, "onSend sb:" + sb.toString());


    }
}

Essentially from the raw Midi data which is being received, I want to know what note is being played (e.g. D4 / C#5) on the physical piano keyboard. Any help would be appreciated.


Solution

  • This is my refactored solution using inspiration from multiple sources already tested with various devices and connections:

    @WorkerThread
    private fun onSend(data: ByteArray, startOffset: Int, count: Int) {
        var offset = startOffset
        fun param(index: Int) = data[offset + index].unsignedInt
        while (offset < count) {
            val command = data[offset] and STATUS_COMMAND_MASK
            val channel: Int = data[offset] and STATUS_CHANNEL_MASK
            when (command) {
                STATUS_NOTE_ON -> {
                    val velocity = param(2)
                    if (velocity == 0) noteOff(channel, note = param(1))
                    else noteOn(channel, note = param(1), velocity = param(2))
                    offset += 3
                }
                STATUS_NOTE_OFF -> {
                    noteOff(channel, note = param(1), velocity = param(2))
                    offset += 3
                }
                STATUS_CONTROL_CHANGE -> {
                    cc(channel, command = param(1), value = param(2))
                    offset += 3
                }
                STATUS_PITCH_BEND -> {
                    pitchBend(channel, bend = (param(2) shl 7) + data[offset + 1])
                    offset += 3
                }
                STATUS_PROGRAM_CHANGE -> {
                    programChange(channel, program = param(1))
                    offset += 2
                }
                STATUS_CHANNEL_PRESSURE -> {
                    channelPressure(channel, program = param(1))
                    offset += 2
                }
                STATUS_START -> {
                    start()
                    offset += 1
                }
                STATUS_CONTINUE -> {
                    midiContinue()
                    offset += 1
                }
                STATUS_STOP -> {
                    stop()
                    offset += 1
                }
                STATUS_CLOCK -> {
                    timing()
                    offset += 1
                }
                else -> {
                    logWarn {
                        "Unhandled MIDI data.size:${data.size} offset:$offset," +
                            " count:$count, command:$command}"
                    }
                    offset += 1
                }
            }
        }
    }
    

    That picth band part I am not sure about, but everywhere it is done differently, so will be seen if I ever get bug report on that, or will able to test it out.

    Posting some additional code so you can get it working easyly..

    object MidiConstants {
        internal const val TAG = "MidiTools"
        const val STATUS_COMMAND_MASK = 0xF0
        const val STATUS_CHANNEL_MASK = 0x0F
    
        // Channel voice messages.
        const val STATUS_NOTE_OFF = 0x80
        const val STATUS_NOTE_ON = 0x90
        const val STATUS_POLYPHONIC_AFTERTOUCH = 0xA0
        const val STATUS_CONTROL_CHANGE = 0xB0
        const val STATUS_PROGRAM_CHANGE = 0xC0
        const val STATUS_CHANNEL_PRESSURE = 0xD0
        const val STATUS_PITCH_BEND = 0xE0
    
        // System Common Messages.
        const val STATUS_SYSEX_START = 0xF0
        const val STATUS_MIDI_TIME_CODE = 0xF1
        const val STATUS_SONG_POSITION = 0xF2
        const val STATUS_SONG_SELECT = 0xF3
        const val STATUS_TUNE_REQUEST = 0xF6
        const val STATUS_SYSEX_END = 0xF7
        const val STATUS_RESET = 0xFF //followed by 0xFF and 0x00
    
        // System Real-Time Messages, single byte
        const val STATUS_CLOCK = 0xF8 //248
        const val STATUS_START = 0xFA //250
        const val STATUS_CONTINUE = 0xFB //251
        const val STATUS_STOP = 0xFC //252
        const val STATUS_ACTIVE_SENSING = 0xFE  //254
    }
    
    infix fun Byte.and(that: Int): Int = this.toInt().and(that)
    

    Hope I didnt forget anythin...