diff --git a/orx-jvm/orx-midi/README.md b/orx-jvm/orx-midi/README.md index 54a69b10..fe6c9aac 100644 --- a/orx-jvm/orx-midi/README.md +++ b/orx-jvm/orx-midi/README.md @@ -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}") } diff --git a/orx-jvm/orx-midi/build.gradle.kts b/orx-jvm/orx-midi/build.gradle.kts index 84b5623f..6102050a 100644 --- a/orx-jvm/orx-midi/build.gradle.kts +++ b/orx-jvm/orx-midi/build.gradle.kts @@ -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")) } \ No newline at end of file diff --git a/orx-jvm/orx-midi/src/demo/kotlin/DemoMidiBinding01.kt b/orx-jvm/orx-midi/src/demo/kotlin/DemoMidiBinding01.kt new file mode 100644 index 00000000..34f604ca --- /dev/null +++ b/orx-jvm/orx-midi/src/demo/kotlin/DemoMidiBinding01.kt @@ -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) + } + } + } +} \ No newline at end of file diff --git a/orx-jvm/orx-midi/src/demo/kotlin/DemoMidiDevices.kt b/orx-jvm/orx-midi/src/demo/kotlin/DemoMidiDevices.kt new file mode 100644 index 00000000..266710c1 --- /dev/null +++ b/orx-jvm/orx-midi/src/demo/kotlin/DemoMidiDevices.kt @@ -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) + } + } + } +} \ No newline at end of file diff --git a/orx-jvm/orx-midi/src/main/kotlin/MidiBindings.kt b/orx-jvm/orx-midi/src/main/kotlin/MidiBindings.kt new file mode 100644 index 00000000..95edb450 --- /dev/null +++ b/orx-jvm/orx-midi/src/main/kotlin/MidiBindings.kt @@ -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, 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, 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, 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, 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, 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() + } + } +} \ No newline at end of file diff --git a/orx-jvm/orx-midi/src/main/kotlin/MidiTransceiver.kt b/orx-jvm/orx-midi/src/main/kotlin/MidiTransceiver.kt index bcd293b6..f3bf85db 100644 --- a/orx-jvm/orx-midi/src/main/kotlin/MidiTransceiver.kt +++ b/orx-jvm/orx-midi/src/main/kotlin/MidiTransceiver.kt @@ -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("midi-transceiver::controller-changed") @@ -183,67 +196,62 @@ class MidiTransceiver(val receiverDevice: MidiDevice, val transmitterDevicer: Mi val pitchBend = Event("midi-transceiver::pitch-bend") fun controlChange(channel: Int, control: Int, value: Int) { - try { - val msg = ShortMessage(ShortMessage.CONTROL_CHANGE, channel, control, value) - receiver.send(msg, receiverDevice.microsecondPosition) - } catch (e: InvalidMidiDataException) { - // + 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) { - try { - val msg = ShortMessage(ShortMessage.PROGRAM_CHANGE, channel, program) - receiver.send(msg, receiverDevice.microsecondPosition) - } catch (e: InvalidMidiDataException) { - // + 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) { - try { - val msg = ShortMessage(ShortMessage.NOTE_ON, channel, key, velocity) - receiver.send(msg, receiverDevice.microsecondPosition) - } catch (e: InvalidMidiDataException) { - // + 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) { - try { - val msg = ShortMessage(ShortMessage.CHANNEL_PRESSURE, channel, value) - receiver.send(msg, receiverDevice.microsecondPosition) - } catch (e: InvalidMidiDataException) { - // + 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) { - try { - val msg = ShortMessage(ShortMessage.PITCH_BEND, channel, value) - receiver.send(msg, receiverDevice.microsecondPosition) - } catch (e: InvalidMidiDataException) { - // + 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() + 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) } - } -} \ No newline at end of file