[orx-midi] Add property <-> midi control bindings

This commit is contained in:
Edwin Jakobs
2023-04-19 16:59:54 +02:00
parent bee77e7f64
commit a61edcbbf7
6 changed files with 344 additions and 56 deletions

View File

@@ -14,7 +14,7 @@ MidiDeviceDescription.list().forEach {
} }
// -- open a midi controller and listen for control changes // -- open a midi controller and listen for control changes
val dev = MidiTransceiver.fromDeviceVendor("BCR2000 [hw:2,0,0]", "ALSA (http://www.alsa-project.org)") val dev = MidiTransceiver.fromDeviceVendor(this, "BCR2000 [hw:2,0,0]", "ALSA (http://www.alsa-project.org)")
dev.controlChanged.listen { dev.controlChanged.listen {
println("${it.channel} ${it.control} ${it.value}") println("${it.channel} ${it.control} ${it.value}")
} }

View File

@@ -5,4 +5,8 @@ plugins {
dependencies { dependencies {
implementation(libs.openrndr.application) implementation(libs.openrndr.application)
implementation(libs.openrndr.math) implementation(libs.openrndr.math)
implementation(libs.kotlin.reflect)
implementation(libs.kotlin.coroutines)
implementation(project(":orx-property-watchers"))
implementation(project(":orx-parameters"))
} }

View File

@@ -0,0 +1,37 @@
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.extra.midi.MidiTransceiver
import org.openrndr.extra.midi.bindMidiControl
import org.openrndr.extra.parameters.ColorParameter
import org.openrndr.extra.parameters.DoubleParameter
import org.openrndr.math.Vector2
fun main() {
application {
program {
val midi = MidiTransceiver.fromDeviceVendor(this,"MIDI2x2 [hw:3,0,0]", "ALSA (http://www.alsa-project.org)")
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, midi, 0, 1)
bindMidiControl(settings::x, midi, 0, 2)
bindMidiControl(settings::y, midi, 0, 3)
bindMidiControl(settings::color, midi, 0, 4)
extend {
drawer.fill = settings.color
drawer.circle(drawer.bounds.center + Vector2(settings.x, settings.y), settings.radius)
}
}
}
}

View File

@@ -0,0 +1,17 @@
import org.openrndr.application
import org.openrndr.extra.midi.MidiDeviceDescription
import org.openrndr.extra.midi.MidiTransceiver
import org.openrndr.extra.midi.bindMidiControl
import org.openrndr.extra.parameters.DoubleParameter
fun main() {
application {
program {
//MidiDeviceDescription.list().forEach { println(it.toString()) }
val midi = MidiTransceiver.fromDeviceVendor(this,"MIDI2x2 [hw:3,0,0]", "ALSA (http://www.alsa-project.org)")
midi.controlChanged.listen {
println(it)
}
}
}
}

View File

@@ -0,0 +1,222 @@
package org.openrndr.extra.midi
import kotlinx.coroutines.yield
import org.openrndr.Program
import org.openrndr.color.ColorRGBa
import org.openrndr.extra.parameters.DoubleParameter
import org.openrndr.extra.parameters.Vector2Parameter
import org.openrndr.extra.parameters.Vector3Parameter
import org.openrndr.launch
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import org.openrndr.math.map
import kotlin.reflect.KMutableProperty0
import kotlin.reflect.full.findAnnotations
@JvmName("bindMidiControlDouble")
fun Program.bindMidiControl(property: KMutableProperty0<Double>, transceiver: MidiTransceiver, channel: Int, control: Int) {
val anno = property.findAnnotations(DoubleParameter::class).firstOrNull()
val low = anno?.low ?: 0.0
val high = anno?.high ?: 1.0
transceiver.controlChanged.listen {
if (it.eventType == MidiEventType.CONTROL_CHANGED && it.channel == channel && it.control == control) {
val value = it.value.toDouble().map(0.0, 127.0, low, high, clamp = true)
property.set(value)
}
}
launch {
var propertyValue = property.get()
while(true) {
val candidateValue = property.get()
if (candidateValue != propertyValue) {
propertyValue = candidateValue
val value = propertyValue.map(low, high, 0.0, 127.0, clamp = true).toInt()
transceiver.controlChange(channel, control, value)
}
yield()
}
}
}
@JvmName("bindMidiControlBoolean")
fun Program.bindMidiControl(property: KMutableProperty0<Boolean>, transceiver: MidiTransceiver, channel: Int, control: Int) {
transceiver.controlChanged.listen {
if (it.eventType == MidiEventType.CONTROL_CHANGED && it.channel == channel && it.control == control) {
property.set(it.value >= 64)
}
}
launch {
var propertyValue = property.get()
while(true) {
val candidateValue = property.get()
if (candidateValue != propertyValue) {
propertyValue = candidateValue
transceiver.controlChange(channel, control, if (propertyValue) 127 else 0)
}
yield()
}
}
}
@JvmName("bindMidiControlVector2")
fun Program.bindMidiControl(property: KMutableProperty0<Vector2>, transceiver: MidiTransceiver,
channelX: Int, controlX: Int,
channelY: Int = channelX, controlY: Int = controlX + 1) {
val anno = property.findAnnotations(Vector2Parameter::class).firstOrNull()
val low = anno?.min ?: 0.0
val high = anno?.max ?: 1.0
transceiver.controlChanged.listen {
val v = property.get()
var x = v.x
var y = v.y
var changed = false
if (it.eventType == MidiEventType.CONTROL_CHANGED && it.channel == channelX && it.control == controlX) {
changed = true
x = it.value.toDouble().map(0.0, 127.0, low, high, clamp = true)
}
if (it.eventType == MidiEventType.CONTROL_CHANGED && it.channel == channelY && it.control == controlY) {
changed = true
y = it.value.toDouble().map(0.0, 127.0, low, high, clamp = true)
}
if (changed) {
val nv = Vector2(x, y)
property.set(nv)
}
}
launch {
var propertyValue = property.get()
while(true) {
val candidateValue = property.get()
if (candidateValue != propertyValue) {
propertyValue = candidateValue
val valueX = propertyValue.x.map(low, high, 0.0, 127.0, clamp = true).toInt()
val valueY = propertyValue.y.map(low, high, 0.0, 127.0, clamp = true).toInt()
transceiver.controlChange(channelX, controlX, valueX)
transceiver.controlChange(channelY, controlY, valueY)
}
yield()
}
}
}
@JvmName("bindMidiControlVector3")
fun Program.bindMidiControl(property: KMutableProperty0<Vector3>, transceiver: MidiTransceiver,
channelX: Int, controlX: Int,
channelY: Int = channelX, controlY: Int = controlX + 1,
channelZ: Int = channelY, controlZ: Int = controlY + 1) {
val anno = property.findAnnotations(Vector3Parameter::class).firstOrNull()
val low = anno?.min ?: 0.0
val high = anno?.max ?: 1.0
transceiver.controlChanged.listen {
val v = property.get()
var x = v.x
var y = v.y
var z = v.z
var changed = false
if (it.eventType == MidiEventType.CONTROL_CHANGED && it.channel == channelX && it.control == controlX) {
changed = true
x = it.value.toDouble().map(0.0, 127.0, low, high, clamp = true)
}
if (it.eventType == MidiEventType.CONTROL_CHANGED && it.channel == channelY && it.control == controlY) {
changed = true
y = it.value.toDouble().map(0.0, 127.0, low, high, clamp = true)
}
if (it.eventType == MidiEventType.CONTROL_CHANGED && it.channel == channelZ && it.control == controlZ) {
changed = true
z = it.value.toDouble().map(0.0, 127.0, low, high, clamp = true)
}
if (changed) {
val nv = Vector3(x, y, z)
property.set(nv)
}
}
launch {
var propertyValue = property.get()
while(true) {
val candidateValue = property.get()
if (candidateValue != propertyValue) {
propertyValue = candidateValue
val valueX = propertyValue.x.map(low, high, 0.0, 127.0, clamp = true).toInt()
val valueY = propertyValue.y.map(low, high, 0.0, 127.0, clamp = true).toInt()
val valueZ = propertyValue.z.map(low, high, 0.0, 127.0, clamp = true).toInt()
transceiver.controlChange(channelX, controlX, valueX)
transceiver.controlChange(channelY, controlY, valueY)
transceiver.controlChange(channelZ, controlZ, valueZ)
}
yield()
}
}
}
@JvmName("bindMidiControlColorRGBa")
fun Program.bindMidiControl(property: KMutableProperty0<ColorRGBa>, transceiver: MidiTransceiver,
channelR: Int, controlR: Int,
channelG: Int = channelR, controlG: Int = controlR + 1,
channelB: Int = channelG, controlB: Int = controlG + 1,
channelA: Int = channelB, controlA: Int = controlB + 1,
) {
val low = 0.0
val high = 1.0
transceiver.controlChanged.listen {
val v = property.get()
var r = v.r
var g = v.g
var b = v.b
var a = v.alpha
var changed = false
if (it.eventType == MidiEventType.CONTROL_CHANGED && it.channel == channelR && it.control == controlR) {
changed = true
r = it.value.toDouble().map(0.0, 127.0, low, high, clamp = true)
}
if (it.eventType == MidiEventType.CONTROL_CHANGED && it.channel == channelG && it.control == controlG) {
changed = true
g = it.value.toDouble().map(0.0, 127.0, low, high, clamp = true)
}
if (it.eventType == MidiEventType.CONTROL_CHANGED && it.channel == channelB && it.control == controlB) {
changed = true
b = it.value.toDouble().map(0.0, 127.0, low, high, clamp = true)
}
if (it.eventType == MidiEventType.CONTROL_CHANGED && it.channel == channelA && it.control == controlA) {
changed = true
a = it.value.toDouble().map(0.0, 127.0, low, high, clamp = true)
}
if (changed) {
val nv = ColorRGBa(r, g, b, a)
property.set(nv)
}
}
launch {
var propertyValue = property.get()
while(true) {
val candidateValue = property.get()
if (candidateValue != propertyValue) {
propertyValue = candidateValue
val valueR = propertyValue.r.map(low, high, 0.0, 127.0, clamp = true).toInt()
val valueG = propertyValue.g.map(low, high, 0.0, 127.0, clamp = true).toInt()
val valueB = propertyValue.b.map(low, high, 0.0, 127.0, clamp = true).toInt()
val valueA = propertyValue.alpha.map(low, high, 0.0, 127.0, clamp = true).toInt()
transceiver.controlChange(channelR, controlR, valueR)
transceiver.controlChange(channelG, controlG, valueG)
transceiver.controlChange(channelB, controlB, valueB)
transceiver.controlChange(channelA, controlA, valueA)
}
yield()
}
}
}

View File

@@ -1,8 +1,12 @@
package org.openrndr.extra.midi package org.openrndr.extra.midi
import mu.KotlinLogging
import org.openrndr.Program
import org.openrndr.events.Event import org.openrndr.events.Event
import javax.sound.midi.* import javax.sound.midi.*
private val logger = KotlinLogging.logger { }
data class MidiDeviceName(val name: String, val vendor: String) data class MidiDeviceName(val name: String, val vendor: String)
class MidiDeviceCapabilities { class MidiDeviceCapabilities {
var receive: Boolean = false var receive: Boolean = false
@@ -50,18 +54,18 @@ data class MidiDeviceDescription(
} }
} }
fun open(): MidiTransceiver { fun open(program: Program): MidiTransceiver {
require(receive && transmit) { require(receive && transmit) {
"device should be a receiver and transmitter" "device should be a receiver and transmitter"
} }
return MidiTransceiver.fromDeviceVendor(name, vendor) return MidiTransceiver.fromDeviceVendor(program, name, vendor)
} }
} }
class MidiTransceiver(val receiverDevice: MidiDevice, val transmitterDevicer: MidiDevice) { class MidiTransceiver(program: Program, val receiverDevice: MidiDevice?, val transmitterDevicer: MidiDevice?) {
companion object { companion object {
fun fromDeviceVendor(name: String, vendor: String): MidiTransceiver { fun fromDeviceVendor(program: Program, name: String, vendor: String): MidiTransceiver {
val infos = MidiSystem.getMidiDeviceInfo() val infos = MidiSystem.getMidiDeviceInfo()
var receiverDevice: MidiDevice? = null var receiverDevice: MidiDevice? = null
@@ -72,11 +76,18 @@ class MidiTransceiver(val receiverDevice: MidiDevice, val transmitterDevicer: Mi
val device = MidiSystem.getMidiDevice(info) val device = MidiSystem.getMidiDevice(info)
if (device !is Sequencer && device !is Synthesizer) { if (device !is Sequencer && device !is Synthesizer) {
if (info.vendor == vendor && info.name == name) { if (info.vendor == vendor && info.name == name) {
logger.info { "found matching device $name / $vendor" }
if (device.maxTransmitters != 0 && device.maxReceivers == 0) { if (device.maxTransmitters != 0 && device.maxReceivers == 0) {
transmitterDevice = device transmitterDevice = device
logger.info {
"found transmitter"
}
} }
if (device.maxReceivers != 0 && device.maxTransmitters == 0) { if (device.maxReceivers != 0 && device.maxTransmitters == 0) {
receiverDevice = device receiverDevice = device
logger.info {
"found receiver"
}
} }
} }
} }
@@ -88,15 +99,15 @@ class MidiTransceiver(val receiverDevice: MidiDevice, val transmitterDevicer: Mi
if (receiverDevice != null && transmitterDevice != null) { if (receiverDevice != null && transmitterDevice != null) {
receiverDevice.open() receiverDevice.open()
transmitterDevice.open() transmitterDevice.open()
return MidiTransceiver(receiverDevice, transmitterDevice) return MidiTransceiver(program, receiverDevice, transmitterDevice)
} else { } else {
throw IllegalArgumentException("midi device not found ${name}:${vendor} ${receiverDevice} ${transmitterDevice}") throw IllegalArgumentException("midi device not found ${name}:${vendor} $receiverDevice $transmitterDevice")
} }
} }
} }
private val receiver = receiverDevice.receiver private val receiver = receiverDevice?.receiver
private val transmitter = transmitterDevicer.transmitter private val transmitter = transmitterDevicer?.transmitter
private inner class Destroyer : Thread() { private inner class Destroyer : Thread() {
override fun run() { override fun run() {
@@ -105,7 +116,7 @@ class MidiTransceiver(val receiverDevice: MidiDevice, val transmitterDevicer: Mi
} }
init { init {
transmitter.receiver = object : MidiDeviceReceiver { transmitter?.receiver = object : MidiDeviceReceiver {
override fun getMidiDevice(): MidiDevice? { override fun getMidiDevice(): MidiDevice? {
return null return null
} }
@@ -113,8 +124,7 @@ class MidiTransceiver(val receiverDevice: MidiDevice, val transmitterDevicer: Mi
override fun send(message: MidiMessage, timeStamp: Long) { override fun send(message: MidiMessage, timeStamp: Long) {
val cmd = message.message val cmd = message.message
val channel = (cmd[0].toInt() and 0xff) and 0x0f val channel = (cmd[0].toInt() and 0xff) and 0x0f
val status = (cmd[0].toInt() and 0xff) and 0xf0 when ((cmd[0].toInt() and 0xff) and 0xf0) {
when (status) {
ShortMessage.NOTE_ON -> noteOn.trigger( ShortMessage.NOTE_ON -> noteOn.trigger(
MidiEvent.noteOn( MidiEvent.noteOn(
channel, channel,
@@ -171,8 +181,11 @@ class MidiTransceiver(val receiverDevice: MidiDevice, val transmitterDevicer: Mi
} }
} }
// shut down midi if user calls `exitProcess(0)` val destroyer = Destroyer()
Runtime.getRuntime().addShutdownHook(Destroyer()) program.ended.listen {
destroyer.start()
}
} }
val controlChanged = Event<MidiEvent>("midi-transceiver::controller-changed") val controlChanged = Event<MidiEvent>("midi-transceiver::controller-changed")
@@ -183,67 +196,62 @@ class MidiTransceiver(val receiverDevice: MidiDevice, val transmitterDevicer: Mi
val pitchBend = Event<MidiEvent>("midi-transceiver::pitch-bend") val pitchBend = Event<MidiEvent>("midi-transceiver::pitch-bend")
fun controlChange(channel: Int, control: Int, value: Int) { fun controlChange(channel: Int, control: Int, value: Int) {
try { if (receiver != null && receiverDevice != null) {
val msg = ShortMessage(ShortMessage.CONTROL_CHANGE, channel, control, value) try {
receiver.send(msg, receiverDevice.microsecondPosition) val msg = ShortMessage(ShortMessage.CONTROL_CHANGE, channel, control, value)
} catch (e: InvalidMidiDataException) { receiver.send(msg, receiverDevice.microsecondPosition)
// } catch (e: InvalidMidiDataException) {
logger.warn { e.message }
}
} }
} }
fun programChange(channel: Int, program: Int) { fun programChange(channel: Int, program: Int) {
try { if (receiver != null && receiverDevice != null) {
val msg = ShortMessage(ShortMessage.PROGRAM_CHANGE, channel, program) try {
receiver.send(msg, receiverDevice.microsecondPosition) val msg = ShortMessage(ShortMessage.PROGRAM_CHANGE, channel, program)
} catch (e: InvalidMidiDataException) { receiver.send(msg, receiverDevice.microsecondPosition)
// } catch (e: InvalidMidiDataException) {
logger.warn { e.message }
}
} }
} }
fun noteOn(channel: Int, key: Int, velocity: Int) { fun noteOn(channel: Int, key: Int, velocity: Int) {
try { if (receiver != null && receiverDevice != null) {
val msg = ShortMessage(ShortMessage.NOTE_ON, channel, key, velocity) try {
receiver.send(msg, receiverDevice.microsecondPosition) val msg = ShortMessage(ShortMessage.NOTE_ON, channel, key, velocity)
} catch (e: InvalidMidiDataException) { receiver.send(msg, receiverDevice.microsecondPosition)
// } catch (e: InvalidMidiDataException) {
logger.warn { e.message }
}
} }
} }
fun channelPressure(channel: Int, value: Int) { fun channelPressure(channel: Int, value: Int) {
try { if (receiver != null && receiverDevice != null) {
val msg = ShortMessage(ShortMessage.CHANNEL_PRESSURE, channel, value) try {
receiver.send(msg, receiverDevice.microsecondPosition) val msg = ShortMessage(ShortMessage.CHANNEL_PRESSURE, channel, value)
} catch (e: InvalidMidiDataException) { receiver.send(msg, receiverDevice.microsecondPosition)
// } catch (e: InvalidMidiDataException) {
logger.warn { e.message }
}
} }
} }
fun pitchBend(channel: Int, value: Int) { fun pitchBend(channel: Int, value: Int) {
try { if (receiver != null && receiverDevice != null) {
val msg = ShortMessage(ShortMessage.PITCH_BEND, channel, value) try {
receiver.send(msg, receiverDevice.microsecondPosition) val msg = ShortMessage(ShortMessage.PITCH_BEND, channel, value)
} catch (e: InvalidMidiDataException) { receiver.send(msg, receiverDevice.microsecondPosition)
// } catch (e: InvalidMidiDataException) {
logger.warn { e.message }
}
} }
} }
fun destroy() { fun destroy() {
receiverDevice.close() receiverDevice?.close()
transmitterDevicer.close() transmitterDevicer?.close()
} }
} }
fun main() {
val deviceName = "BCR2000"
MidiDeviceDescription.list().forEach(::println)
MidiDeviceDescription.list().firstOrNull { it.name.contains(deviceName) }
?.run {
val controller = MidiTransceiver.fromDeviceVendor(name, vendor)
controller.controlChanged.listen { println(it) }
controller.programChanged.listen { println(it) }
controller.noteOn.listen { println(it) }
controller.noteOff.listen { println(it) }
controller.channelPressure.listen { println(it) }
controller.pitchBend.listen { println(it) }
}
}