Architecture Components / Kotlin / MidiPad

MidiPad – Discovering MIDI Devices

Musical Instrument Digital Interface (MIDI) has been around since the early 1980’s and the basic specification has changed little since. It is a standard by which electronic musical instruments and other devices can communicate with each other. In Marshmallow (V6.0 – API 23) Android actually got some good MIDI support, and in this series of articles we’ll take a look at how we can create a MIDI controller app. For the non-musicians and those who have no interest in MIDI, do not despair there will be some custom controls we create along the way which may still be of interest. In this article we’ll take a look at how we can discover MIDI devices.

Previously we looked at a couple of tricks with Kotlin and the Architecture Components to facilitate some of the things that we’ll need later on, and before we dive in to the MIDI stuff we’ll look at one more little trick.

Once we have a list of discovered MIDI devices, we’ll hold them in a LiveData object and update that as we detect the availability of devices changing after the initial discovery. In other words, if a new device becomes available then we’ll add it to the list. However this isn’t quite as straightforward as it sounds. While LiveData will notify any observers if the value of the LiveData object changes, if we have a LiveData> instance, this will only notify observers if we call the setValue() method to change the List itself, but not if the content of the existing List changes. However, there is a useful trick that we can use to implement a LiveData>> object which will notify its observers when the contents of the List changes:

internal open class MutableListLiveData(private val list: MutableList = mutableListOf()) :
        MutableList by list,
        LiveData>() {

    override fun add(element: T): Boolean =
            element.actionAndUpdate { list.add(it) }

    override fun add(index: Int, element: T) =
            list.add(index, element).also { updateValue() }

    override fun addAll(elements: Collection): Boolean =
            elements.actionAndUpdate { list.addAll(elements) }

    override fun addAll(index: Int, elements: Collection): Boolean =
            elements.actionAndUpdate { list.addAll(index, it) }

    override fun remove(element: T): Boolean =
            element.actionAndUpdate { list.remove(it) }

    override fun removeAt(index: Int): T =
            list.removeAt(index).also { updateValue() }

    override fun removeAll(elements: Collection): Boolean =
            elements.actionAndUpdate { list.removeAll(it) }

    override fun retainAll(elements: Collection): Boolean =
            elements.actionAndUpdate { list.retainAll(it) }

    override fun clear() =
            list.clear().also { updateValue() }

    override fun set(index: Int, element: T): T =
            list.set(index, element).also{ updateValue() }

    private fun  T.actionAndUpdate(action: (item: T) -> Boolean): Boolean =
            action(this).applyIfTrue { updateValue() }

    private fun Boolean.applyIfTrue(action: () -> Unit): Boolean {
        takeIf { it }?.run { action() }
        return this
    }

    private fun updateValue() {
        value = list
    }
}

This implements two separate behaviours: a MutableList so we can add and remove items just as we can with any MutableList; and a LiveData so it can be observed. Much of the MutableList behaviour is delegated to the list field with the exception of any methods which would alter the contents of the list such as add() and remove() methods. For these, we still delegate the actual list manipulation down to the list field, but we check if it has actually altered the content and, if so, then we set the value of the LiveData to trigger a notification to any observers. That really is all there is to it, but what that provides us is a mutable List implementation which is also a LiveData object which will notify any observers when the content of the List changes.

Now that we have all the building blocks we can start connecting things up. The class responsible for monitoring and maintaining the list of available MIDI devices is MidiDeviceMonitor:

class MidiDeviceMonitor internal constructor(
        context: Context,
        private val midiManager: MidiManager,
        private val handler: Handler = Handler(context.mainLooper),
        private val data: MutableListLiveData<MidiDeviceInfo> = MutableListLiveData()
) : MediatorLiveData<List<MidiDeviceInfo>>() {

    override fun onActive() {
        super.onActive()
        midiManager.registerDeviceCallback(deviceCallback, handler)
        data.addAll(midiManager.devices)
    }

    override fun onInactive() {
        midiManager.unregisterDeviceCallback(deviceCallback)
        data.clear()
        super.onInactive()
    }

    override fun observe(owner: LifecycleOwner?, observer: Observer<List<MidiDeviceInfo>>?) {
        super.observe(owner, observer)
        addSource(data, observer)
    }

    override fun removeObserver(observer: Observer<List<MidiDeviceInfo>>?) {
        super.removeObserver(observer)
        if (!hasObservers()) {
            removeSource(data)
        }
    }

    private val deviceCallback = object : DeviceCallback() {
        override fun onDeviceAdded(device: MidiDeviceInfo?) {
            super.onDeviceAdded(device)
            device?.also {
                data.add(it)
            }
        }

        override fun onDeviceRemoved(device: MidiDeviceInfo?) {
            super.onDeviceRemoved(device)
            device?.also {
                data.remove(it)
            }
        }
    }
}

The MidiManager class is the how we interact with the Midi APIs. Midi device discovery is really quite easy. In the onActive() method is called when the number of active observers and onInactive() methods are called when the number of active observers changes from zero to one, and the onInactive() method is called whenever the number of active observers falls to zero again. So we register and unregister for device callbacks in these methods. In onActive() we also get a list of currently known devices and use this to initialise the list of devices.

The DeviceCallback callback methods will be called whenever a device is added or removed, and we update the list accordingly which, because of MutableListLiveData will result in notifications to any observers.

The only other things worth mentioning is the use of MediatorLiveData. This enables us to act as a proxy to another LiveData object. The reasoning for using this rather than extending MutableListLiveData directly is for separation of concerns and to ensure that any consumer of MidiDeviceMonitor is completely agnostic of MutableListLiveData. To implement this we need to add and remove sources which will add and remove the observer from the LiveData object that we’re proxying.

In the previous article we saw the MidiController class which was only a stub implementation. But now that we have these other components we can begin implementing this class:

class MidiController(
        context: Context,
        private val midiManager: MidiManager = context.getSystemService(Context.MIDI_SERVICE) as MidiManager,
        private val midiDeviceMonitor: MidiDeviceMonitor = MidiDeviceMonitor(context, midiManager)
) : AndroidViewModel(context.applicationContext as Application) {

    fun observeDevices(lifecycleOwner: LifecycleOwner, observer: Observer<List<MidiDeviceInfo>>) =
        midiDeviceMonitor.observe(lifecycleOwner, observer)

    fun open(@Suppress("UNUSED_PARAMETER") midiDeviceInfo: MidiDeviceInfo?) {
        //NO-OP yet
    }

    fun closeAll() {
        //NO-OP yet
    }

    fun removeObserver(observer: Observer<List<MidiDeviceInfo>>) =
            midiDeviceMonitor.removeObserver(observer)
}

All of the building blocks that we created have made this implementation really quite easy. The MidiController is the ViewModel which will persist across configuration changes, and MidiDeviceMonitor will listen for changes and make notify the observer. The observer in this case is the SpinnerAdapter that we looked at in the previous article, so this will now correctly display the a list of available Midi devices in the Spinner, and update that list whenever devices appear and disappear.

In the next article we’ll turn our attention to the UI and look at how we display the pad controls.

The source code for this article is available here.

© 2017, Mark Allison. All rights reserved.

Copyright © 2017 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.