Midi controllers with orx-midi

The orx-midi library provides a simple interface to interact with MIDI controllers.

Prerequisites

Assuming you are working on an openrndr-template based project, all you have to do is enable orx-midi in the orxFeatures set in build.gradle.kts and reimport the gradle project.

Listing MIDI controllers

To connect to a MIDI controller you will need its device name which can be discovered by calling the listMidiDevices() function.

fun main() = application {
    program {
        listMidiDevices().forEach {
            println("name: '${it.name}', vendor: '${it.vendor}', receiver:${it.receive}, transmitter:${it.transmit}")
        }
    }
}

Connecting to a MIDI controller

Once you have the controller name you can use openMidiDevice to connect to the MIDI controller. For example to use a Behringer BCR2000 controller on a Ubuntu system we can use the following.

fun main() = application {
    program {
        val controller = openMidiDevice("BCR2000")
    }
}

Tip: request BCR2000 instead of BCR2000 [hw:2,0,0] so your program continues to work after plugging your controller into a different USB port. Caveat: do specify the full name if connecting multiple controllers of the same brand and model.

Listening to the controller

Once connected to a controller we can start listening to the MIDI events it sends out. The orx-midi library supports six types of MIDI events.

fun main() = application {
    program {
        val controller = openMidiDevice("BCR2000 [hw:2,0,0]")
        
        controller.controlChanged.listen {
            println("[control change] channel: ${it.channel}, control: ${it.control}, value: ${it.value}")
        }
        controller.noteOn.listen {
            println("[note on] channel: ${it.channel}, key: ${it.note}, velocity: ${it.velocity}")
        }
        controller.noteOff.listen {
            println("[note off] channel: ${it.channel}, key: ${it.note},")
        }
        controller.channelPressure.listen {
            println("[channel pressure] channel: ${it.channel}, pressure: ${it.pressure}")
        }
        controller.pitchBend.listen {
            println("[pitch bend] channel: ${it.channel}, pitch: ${it.pitchBend}")
        }
        controller.programChanged.listen {
            println("[program change] channel: ${it.channel}, program: ${it.program}")
        }
    }
}

Talking to the controller

MIDI controllers can often react to data received from software. A common use case with MIDI controllers with endless rotary encoders is setting up initial values for the encoders when the program launches. Those values are then reflected in LED lights or in a display in the controller.

fun main() = application {
    program {
        val controller = openMidiDevice("BCR2000")
        
        // send a control change
        controller.controlChange(channel = 1, control = 3, value = 42)
        
        // send a program change
        controller.programChange(channel = 2, program = 5)
        
        // send a note on event
        controller.noteOn(channel = 3, key = 60, velocity = 100)
        
        // send a note off event
        controller.noteOn(channel = 3, key = 60, velocity = 0)
        
        // send a pitch bend event
        controller.pitchBend(channel = 4, 50)
        
        // send a channel pressure event
        controller.channelPressure(channel = 4, 100)
    }
}

MIDI console

For debugging purposes one can visualize all the MIDI events by using the MidiConsole.

fun main() = application {
    program {
        val controller = openMidiDevice("Launchpad [hw:4,0,0]")
        
        extend(MidiConsole()) {
            register(controller)
        }
    }
}

Variable binding

One can easily bind MIDI controller inputs like knobs and sliders to program variables. In the following example 7 inputs control the radius, position and color of a circle.

Note that ColorParameter binds four consecutive inputs (red, green, blue and alpha).

More about Parameter Annotations.

fun main() = application {
    program {
        val controller = openMidiDevice("Launchpad [hw:4,0,0]")
        
        val settings = object {
            @DoubleParameter("radius", 0.0, 100.0)
            var radius = 0.0
            
            @DoubleParameter("x", -100.0, 100.0)
            var x = 0.0
            
            @DoubleParameter("y", -100.0, 100.0)
            var y = 0.0
            
            @ColorParameter("fill")
            var color = ColorRGBa.WHITE
        }
        
        bindMidiControl(settings::radius, controller, channel = 0, control = 1)
        bindMidiControl(settings::x, controller, 0, 2)
        bindMidiControl(settings::y, controller, 0, 3)
        bindMidiControl(settings::color, controller, 0, 4)
        
        extend {
            drawer.fill = settings.color
            drawer.circle(drawer.bounds.center + Vector2(settings.x, settings.y), settings.radius)
        }
    }
}

edit on GitHub