atsushieno / ktmidi

Kotlin multiplatform library for MIDI access abstraction and data processing for MIDI 1.0, MIDI 2.0, SMF, SMF2 (MIDI Clip File), and MIDI-CI.
MIT License
70 stars 7 forks source link

Open input blocks and never completed #92

Open Daniel63656 opened 1 week ago

Daniel63656 commented 1 week ago

I try to receive messages from a connected usb MIDI device.

val midiAccess = AndroidMidiAccess(this)
val midiInput = runBlocking {
     midiAccess.openInput(midiAccess.inputs.first().id)
 }
midiInput.setMessageReceivedListener(this@MainActivity)

However, the app becomes unresponsive whenever calling openInput, indicating that no input can be established. This happens on google pixel 7 (android-version 14). The code only continues (setting listener) as soon as I unplug the Midi cable again. I know this is caused by runBlocking and this only helps to make clear that no input can be established. I noticed this did not happen on my older Samsung galaxy S9 (same code).

After some digging I found that the issue is this line in the MidiDevice.java file: FileDescriptor fd = mDeviceServer.openInputPort(token, portNumber); Somehow calling openInputPort on the IMidiDeviceServer blocks until the cable is removed again.

atsushieno commented 1 week ago

Opening a device (either via ktmidi or Android MIDI API directly) involves Service connection that most likely involves main thread (it might depend on how Android MIDI service is implemented, but anything that involves Service usually does), and you are most likely blocking the main thread. If you try to invoke openInput() on a background thread it may work.

Daniel63656 commented 1 week ago

If I do it in another thread then simply no connection is established until the cable is removed. I only switched to main thread to see what's going on. This is likely caused by a permission problem as it works on an older phone. Newer android versions need the user to permit USB connection for each device that is plugged in think.

atsushieno commented 1 week ago

If you are trying to connect to a MIDI device that needs permission, you are supposed to provide enough permission (manage permission requests and approvals).

When we just deal with MIDI inputs, there is no required permission in MIDI API itself, like this app does not need any: https://github.com/atsushieno/resident-midi-keyboard/blob/main/app/src/main/AndroidManifest.xml

Daniel63656 commented 1 week ago

This got changed in new android versions. You now need to grant permission via pop up for each USB device to initiate data transfer. Other apps like Midi connector ble also don't work on newer phones for that reason. I am trying to grant the permission and see if it works then

Atsushi Eno @.***> schrieb am Do., 31. Okt. 2024, 08:09:

If you are trying to connect to a MIDI device that needs permission, you are supposed to provide enough permission (manage permission requests and approvals).

When we just deal with MIDI inputs, there is no required permission in MIDI API itself, like this app does not need any: https://github.com/atsushieno/resident-midi-keyboard/blob/main/app/src/main/AndroidManifest.xml

— Reply to this email directly, view it on GitHub https://github.com/atsushieno/ktmidi/issues/92#issuecomment-2449172847, or unsubscribe https://github.com/notifications/unsubscribe-auth/AOYCLP3V72DRSYWK3ITG47LZ6HJQ3AVCNFSM6AAAAABQZ35UQ6VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDINBZGE3TEOBUG4 . You are receiving this because you authored the thread.Message ID: @.***>

atsushieno commented 1 week ago

Yes, please let me know if it works or not. I would make sure that you are not trying to block the main thread while you are trying to (indirectly) show the permission approval dialog, as (IF that's still the case) the permission dialog also needs the main thread.

Daniel63656 commented 1 week ago

Hm I tried everything, opening from CoroutineScope and only until permission is granted for the device like so

 val usbReceiver = object : BroadcastReceiver() {
            override fun onReceive(context: Context, intent: Intent) {
                Log.d("USB", "Received callback")
                if (ACTION_USB_PERMISSION == intent.action) {
                    synchronized(this) {
                        val device: UsbDevice? = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)
                        if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
                            Log.d("USB", "permission granted for device $device")
                            Toast.makeText(this@MainActivity, "CONNECTION ESTABLISHED!", Toast.LENGTH_SHORT).show()
                            CoroutineScope(Dispatchers.IO).launch {
                                val i = midiAccess.openInput("USB2.0-MIDI_2_0_1")
                                i.setMessageReceivedListener(this@MainActivity)
                            }
                        } else {
                            Log.d("USB", "permission denied for device $device")
                        }
                    }
                }
            }
        }

Still same behavior: Only if cable is removed, the receiver is connected.

atsushieno commented 1 week ago

Without any code I can examine, I can at best suggest some tips to debug your app issue: run it on Android Studio in debugging mode, then interrupt (break) while you are waiting for the openInput(), and check all those running threads on the debugger. There will be some blocking operation that is waiting for something that is waiting for some action that you think that should be already dispatched. MIDI connections are controlled by MidiManager, the client to the system internal MIDI Service, which usually requires main thread not blocked.