0

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.

TomV
  • 1,157
  • 1
  • 13
  • 25

2 Answers2

1

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...

Renetik
  • 5,887
  • 1
  • 47
  • 66
0

I believe this is actually good example but needs some refactoring:

 private fun sendMidi1Immediate(msg: ByteArray, offset: Int, count: Int) {
    var off = offset
    var c = count
    var runningStatus = 0
    while (c > 0) {
        var stat = msg[off].toUnsigned()
        if (stat < 0x80) {
            stat = runningStatus
        } else {
            off++
            c--
        }
        runningStatus = stat
        val ch = stat and 0x0F
        when (stat and 0xF0) {
            0x80 -> syn.noteOff(ch, msg[off].toUnsigned())
            0x90 -> {
                if (msg[off + 1].toInt() == 0)
                    syn.noteOff(ch, msg[off].toUnsigned())
                else
                    syn.noteOn(ch, msg[off].toUnsigned(), msg[off + 1].toUnsigned())
            }
            0xA0 -> {
                // No PAf in fluidsynth?
            }
            0xB0 -> syn.cc(ch, msg[off].toUnsigned(), msg[off + 1].toUnsigned())
            0xC0 -> syn.programChange(ch, msg[off].toUnsigned())
            0xD0 -> syn.channelPressure(ch, msg[off].toUnsigned())
            0xE0 -> syn.pitchBend(ch, msg[off].toUnsigned() + msg[off + 1].toUnsigned() * 0x80)
            0xF0 -> {
                if (stat == 0xF0) { // sysex
                    val idx = msg.drop(off).indexOf(0xF7.toByte())
                    val sysex = msg.copyOfRange(off, off + idx)
                    syn.sysex(sysex, null)
                    if (sysex[0] == 0x7E.toByte() && sysex[1] == 0x7F.toByte() && sysex[2] == 0x0D.toByte() &&
                        sysex[3] == 0x12.toByte() && sysex[4] == 1.toByte()) {
                        // we don't check the rest (Source MUID / Destination MUID / Authority Level)
                        // as it is obvious that this MIDI receiver is the ultimate destination.
                        midiProtocol = (sysex[14] and 3).toUnsigned()
                    }
                }
            }
        }
        when (stat and 0xF0) {
            0xC0,0xD0 -> {
                off++
                c--
            }
            0xF0 -> {
                off += c - 1
                c = 0
            }
            else -> {
                off += 2
                c -= 2
            }
        }
    }
}

I have stolen it from https://github.com/atsushieno/fluidsynth-midi-service-j/blob/main/app/src/main/java/dev/atsushieno/fluidsynthmidideviceservicej/FluidsynthMidiReceiver.kt if that still exists at time of reading, could be better to check it there. I would never write code like this but looks like some guys can write like this and it still work...

Renetik
  • 5,887
  • 1
  • 47
  • 66