[orx-midi] Add property <-> midi control bindings
This commit is contained in:
@@ -14,7 +14,7 @@ MidiDeviceDescription.list().forEach {
|
||||
}
|
||||
|
||||
// -- 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 {
|
||||
println("${it.channel} ${it.control} ${it.value}")
|
||||
}
|
||||
|
||||
@@ -5,4 +5,8 @@ plugins {
|
||||
dependencies {
|
||||
implementation(libs.openrndr.application)
|
||||
implementation(libs.openrndr.math)
|
||||
implementation(libs.kotlin.reflect)
|
||||
implementation(libs.kotlin.coroutines)
|
||||
implementation(project(":orx-property-watchers"))
|
||||
implementation(project(":orx-parameters"))
|
||||
}
|
||||
37
orx-jvm/orx-midi/src/demo/kotlin/DemoMidiBinding01.kt
Normal file
37
orx-jvm/orx-midi/src/demo/kotlin/DemoMidiBinding01.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
orx-jvm/orx-midi/src/demo/kotlin/DemoMidiDevices.kt
Normal file
17
orx-jvm/orx-midi/src/demo/kotlin/DemoMidiDevices.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
222
orx-jvm/orx-midi/src/main/kotlin/MidiBindings.kt
Normal file
222
orx-jvm/orx-midi/src/main/kotlin/MidiBindings.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
package org.openrndr.extra.midi
|
||||
|
||||
import mu.KotlinLogging
|
||||
import org.openrndr.Program
|
||||
import org.openrndr.events.Event
|
||||
import javax.sound.midi.*
|
||||
|
||||
private val logger = KotlinLogging.logger { }
|
||||
|
||||
data class MidiDeviceName(val name: String, val vendor: String)
|
||||
class MidiDeviceCapabilities {
|
||||
var receive: Boolean = false
|
||||
@@ -50,18 +54,18 @@ data class MidiDeviceDescription(
|
||||
}
|
||||
}
|
||||
|
||||
fun open(): MidiTransceiver {
|
||||
fun open(program: Program): MidiTransceiver {
|
||||
require(receive && transmit) {
|
||||
"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 {
|
||||
fun fromDeviceVendor(name: String, vendor: String): MidiTransceiver {
|
||||
fun fromDeviceVendor(program: Program, name: String, vendor: String): MidiTransceiver {
|
||||
val infos = MidiSystem.getMidiDeviceInfo()
|
||||
|
||||
var receiverDevice: MidiDevice? = null
|
||||
@@ -72,11 +76,18 @@ class MidiTransceiver(val receiverDevice: MidiDevice, val transmitterDevicer: Mi
|
||||
val device = MidiSystem.getMidiDevice(info)
|
||||
if (device !is Sequencer && device !is Synthesizer) {
|
||||
if (info.vendor == vendor && info.name == name) {
|
||||
logger.info { "found matching device $name / $vendor" }
|
||||
if (device.maxTransmitters != 0 && device.maxReceivers == 0) {
|
||||
transmitterDevice = device
|
||||
logger.info {
|
||||
"found transmitter"
|
||||
}
|
||||
}
|
||||
if (device.maxReceivers != 0 && device.maxTransmitters == 0) {
|
||||
receiverDevice = device
|
||||
logger.info {
|
||||
"found receiver"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,15 +99,15 @@ class MidiTransceiver(val receiverDevice: MidiDevice, val transmitterDevicer: Mi
|
||||
if (receiverDevice != null && transmitterDevice != null) {
|
||||
receiverDevice.open()
|
||||
transmitterDevice.open()
|
||||
return MidiTransceiver(receiverDevice, transmitterDevice)
|
||||
return MidiTransceiver(program, receiverDevice, transmitterDevice)
|
||||
} 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 transmitter = transmitterDevicer.transmitter
|
||||
private val receiver = receiverDevice?.receiver
|
||||
private val transmitter = transmitterDevicer?.transmitter
|
||||
|
||||
private inner class Destroyer : Thread() {
|
||||
override fun run() {
|
||||
@@ -105,7 +116,7 @@ class MidiTransceiver(val receiverDevice: MidiDevice, val transmitterDevicer: Mi
|
||||
}
|
||||
|
||||
init {
|
||||
transmitter.receiver = object : MidiDeviceReceiver {
|
||||
transmitter?.receiver = object : MidiDeviceReceiver {
|
||||
override fun getMidiDevice(): MidiDevice? {
|
||||
return null
|
||||
}
|
||||
@@ -113,8 +124,7 @@ class MidiTransceiver(val receiverDevice: MidiDevice, val transmitterDevicer: Mi
|
||||
override fun send(message: MidiMessage, timeStamp: Long) {
|
||||
val cmd = message.message
|
||||
val channel = (cmd[0].toInt() and 0xff) and 0x0f
|
||||
val status = (cmd[0].toInt() and 0xff) and 0xf0
|
||||
when (status) {
|
||||
when ((cmd[0].toInt() and 0xff) and 0xf0) {
|
||||
ShortMessage.NOTE_ON -> noteOn.trigger(
|
||||
MidiEvent.noteOn(
|
||||
channel,
|
||||
@@ -171,8 +181,11 @@ class MidiTransceiver(val receiverDevice: MidiDevice, val transmitterDevicer: Mi
|
||||
}
|
||||
}
|
||||
|
||||
// shut down midi if user calls `exitProcess(0)`
|
||||
Runtime.getRuntime().addShutdownHook(Destroyer())
|
||||
val destroyer = Destroyer()
|
||||
program.ended.listen {
|
||||
destroyer.start()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
fun controlChange(channel: Int, control: Int, value: Int) {
|
||||
if (receiver != null && receiverDevice != null) {
|
||||
try {
|
||||
val msg = ShortMessage(ShortMessage.CONTROL_CHANGE, channel, control, value)
|
||||
receiver.send(msg, receiverDevice.microsecondPosition)
|
||||
} catch (e: InvalidMidiDataException) {
|
||||
//
|
||||
logger.warn { e.message }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun programChange(channel: Int, program: Int) {
|
||||
if (receiver != null && receiverDevice != null) {
|
||||
try {
|
||||
val msg = ShortMessage(ShortMessage.PROGRAM_CHANGE, channel, program)
|
||||
receiver.send(msg, receiverDevice.microsecondPosition)
|
||||
} catch (e: InvalidMidiDataException) {
|
||||
//
|
||||
logger.warn { e.message }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun noteOn(channel: Int, key: Int, velocity: Int) {
|
||||
if (receiver != null && receiverDevice != null) {
|
||||
try {
|
||||
val msg = ShortMessage(ShortMessage.NOTE_ON, channel, key, velocity)
|
||||
receiver.send(msg, receiverDevice.microsecondPosition)
|
||||
} catch (e: InvalidMidiDataException) {
|
||||
//
|
||||
logger.warn { e.message }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun channelPressure(channel: Int, value: Int) {
|
||||
if (receiver != null && receiverDevice != null) {
|
||||
try {
|
||||
val msg = ShortMessage(ShortMessage.CHANNEL_PRESSURE, channel, value)
|
||||
receiver.send(msg, receiverDevice.microsecondPosition)
|
||||
} catch (e: InvalidMidiDataException) {
|
||||
//
|
||||
logger.warn { e.message }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun pitchBend(channel: Int, value: Int) {
|
||||
if (receiver != null && receiverDevice != null) {
|
||||
try {
|
||||
val msg = ShortMessage(ShortMessage.PITCH_BEND, channel, value)
|
||||
receiver.send(msg, receiverDevice.microsecondPosition)
|
||||
} catch (e: InvalidMidiDataException) {
|
||||
//
|
||||
logger.warn { e.message }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
receiverDevice.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) }
|
||||
receiverDevice?.close()
|
||||
transmitterDevicer?.close()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user