diff --git a/README.md b/README.md index 937c9047..de17c291 100644 --- a/README.md +++ b/README.md @@ -13,12 +13,13 @@ A growing library of assorted data structures, algorithms and utilities. - `orx-jumpflood`, a filter/shader based implementation of the jump flood algorithm for finding fast approximate (directional) distance fields - `orx-kdtree`, a kd-tree implementation for fast nearest point searches - [`orx-mesh-generators`](orx-mesh-generators/README.md), triangular mesh generators +- [`orx-midi`](orx-midi/README.md), midi controller interface - [`orx-noise`](orx-noise/README.md), library for random number generation and noise - [`orx-no-clear`](orx-no-clear/README.md), a simple extension that provides drawing without clearing the background - [`orx-obj-loader`](orx-obj-loader/README.md), simple Wavefront .obj mesh loader ## Usage -ORX 0.0.23 is built against OPENRNDR 0.3.33-rc1, make sure you use this version in your project. Because OPENRNDR's API is pre 1.0 it tends to change from time to time. +ORX 0.0.24 is built against OPENRNDR 0.3.33-rc2, make sure you use this version in your project. Because OPENRNDR's API is pre 1.0 it tends to change from time to time. The easiest way to add ORX to your project is through the use of Jitpack. [Jitpack](http://jitpack.io) is a service that pulls Gradle based libraries from Github, builds them and serves the jar files. @@ -32,13 +33,13 @@ repositories { You can then add any of the ORX artifacts to your `dependencies {}`: ``` dependencies { - compile 'com.github.openrndr.orx::v0.0.23' + compile 'com.github.openrndr.orx::v0.0.24' } ``` For example if you want to use the `orx-no-clear` artifact one would use: ``` dependencies { - compile 'com.github.openrndr.orx:orx-no-clear:v0.0.23' + compile 'com.github.openrndr.orx:orx-no-clear:v0.0.24' } ``` \ No newline at end of file diff --git a/build.gradle b/build.gradle index ce8f9e16..9abe104b 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ plugins { allprojects { group 'org.openrndr.extra' - version '0.0.23' + version '0.0.24' } repositories { @@ -13,7 +13,7 @@ repositories { } ext { - openrndrVersion = "0.3.33-rc1" + openrndrVersion = "0.3.33-rc2" } subprojects { diff --git a/orx-midi/README.md b/orx-midi/README.md new file mode 100644 index 00000000..5df1b32a --- /dev/null +++ b/orx-midi/README.md @@ -0,0 +1,19 @@ +# orx-midi + +A minimal and limited library for Midi controllers. Orx-midi is a wrapper around javax.midi. + +## usage + +```kotlin + +// -- list all midi devices +MidiDeviceDescription.list().forEach { + println("${it.name}, ${it.vendor} r:${it.receive} t:${it.transmit}") +} + +// -- open a midi controller and listen for control changes +val dev = MidiTransceiver.fromDeviceVendor("BCR2000 [hw:2,0,0]", "ALSA (http://www.alsa-project.org)") +dev.controlChanged.listen { + println("${it.channel} ${it.control} ${it.value}") +} +``` \ No newline at end of file diff --git a/orx-midi/src/main/kotlin/MidiEvent.kt b/orx-midi/src/main/kotlin/MidiEvent.kt new file mode 100644 index 00000000..9e0348a2 --- /dev/null +++ b/orx-midi/src/main/kotlin/MidiEvent.kt @@ -0,0 +1,47 @@ +package org.openrndr.extra.midi + +enum class MidiEventType { + NOTE_ON, + NOTE_OFF, + CONTROL_CHANGED +} + + +class MidiEvent(val eventType: MidiEventType) { + var origin = Origin.DEVICE + var control: Int = 0 + var note: Int = 0 + var channel: Int = 0 + var value: Int = 0 + var velocity: Int = 0 + + enum class Origin { + DEVICE, + USER + } + + companion object { + fun noteOn(channel: Int, note: Int, velocity: Int): MidiEvent { + val midiEvent = MidiEvent(MidiEventType.NOTE_ON) + midiEvent.velocity = velocity + midiEvent.note = note + midiEvent.channel = channel + return midiEvent + } + + fun noteOff(channel: Int, note: Int): MidiEvent { + val midiEvent = MidiEvent(MidiEventType.NOTE_OFF) + midiEvent.note = note + midiEvent.channel = channel + return midiEvent + } + + fun controlChange(channel:Int, control: Int, value: Int): MidiEvent { + val midiEvent = MidiEvent(MidiEventType.CONTROL_CHANGED) + midiEvent.channel = channel + midiEvent.control = control + midiEvent.value = value + return midiEvent + } + } +} \ No newline at end of file diff --git a/orx-midi/src/main/kotlin/MidiTransceiver.kt b/orx-midi/src/main/kotlin/MidiTransceiver.kt new file mode 100644 index 00000000..7369bf39 --- /dev/null +++ b/orx-midi/src/main/kotlin/MidiTransceiver.kt @@ -0,0 +1,151 @@ +package org.openrndr.extra.midi + +import org.openrndr.events.Event +import javax.sound.midi.* + +data class MidiDeviceName(val name: String, val vendor: String) +class MidiDeviceCapabilities { + var receive: Boolean = false + var transmit: Boolean = false +} + +class MidiDeviceDescription(val name: String, val vendor: String, val receive: Boolean, val transmit: Boolean) { + companion object { + fun list(): List { + val caps = mutableMapOf() + + val infos = MidiSystem.getMidiDeviceInfo() + for (info in infos) { + val device = MidiSystem.getMidiDevice(info) + val name = MidiDeviceName(info.name, info.vendor) + val deviceCaps = caps.getOrPut(name) { MidiDeviceCapabilities() } + + if (device !is Sequencer && device !is Synthesizer) { + if (device.maxReceivers != 0 && device.maxTransmitters == 0) { + deviceCaps.receive = true + } + if (device.maxTransmitters != 0 && device.maxReceivers == 0) { + deviceCaps.transmit = true + } + } + } + return caps.map { + MidiDeviceDescription(it.key.name, it.key.vendor, it.value.receive, it.value.transmit) + } + } + } +} + +class MidiTransceiver(val receiverDevice: MidiDevice, val transmitterDevicer: MidiDevice) { + companion object { + fun fromDeviceVendor(name: String, vendor: String): MidiTransceiver { + val infos = MidiSystem.getMidiDeviceInfo() + + var receiverDevice: MidiDevice? = null + var transmitterDevice: MidiDevice? = null + + for (info in infos) { + try { + val device = MidiSystem.getMidiDevice(info) + if (device !is Sequencer && device !is Synthesizer) { + if (info.vendor == vendor && info.name == name) { + if (device.maxTransmitters != 0 && device.maxReceivers == 0) { + transmitterDevice = device + } + + if (device.maxReceivers != 0 && device.maxTransmitters == 0) { + receiverDevice = device + } + + } + } + } catch (e: MidiUnavailableException) { + throw IllegalStateException("no midi available") + } + } + + if (receiverDevice != null && transmitterDevice != null) { + receiverDevice.open() + transmitterDevice.open() + return MidiTransceiver(receiverDevice, transmitterDevice) + } else { + throw IllegalArgumentException("midi device not found ${name}:${vendor} ${receiverDevice} ${transmitterDevice}") + } + + + } + } + + private val receiver = receiverDevice.receiver + private val transmitter = transmitterDevicer.transmitter + + init { + transmitter.receiver = object : MidiDeviceReceiver { + override fun getMidiDevice(): MidiDevice? { + return null + } + + 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) { + ShortMessage.NOTE_ON -> noteOn.trigger(MidiEvent.noteOn(channel, cmd[1].toInt() and 0xff, cmd[2].toInt() and 0xff)) + ShortMessage.NOTE_OFF -> noteOff.trigger(MidiEvent.noteOff(channel, cmd[1].toInt() and 0xff)) + ShortMessage.CONTROL_CHANGE -> controlChanged.trigger(MidiEvent.controlChange(channel,cmd[1].toInt() and 0xff, cmd[2].toInt() and 0xff)) + } + } + + override fun close() { + + } + } + } + + val controlChanged = Event("midi-transceiver::controller-changed").signature(MidiEvent::class.java) + val noteOn = Event("midi-transceiver::note-on").signature(MidiEvent::class.java) + val noteOff = Event("midi-transceiver::note-off").signature(MidiEvent::class.java) + + fun controlChange(channel: Int, control: Int, value: Int) { + try { + val msg = ShortMessage(ShortMessage.CONTROL_CHANGE, channel, control, value) + if (receiverDevice != null) { + val tc = receiverDevice!!.microsecondPosition + receiver.send(msg, tc) + } + } catch (e: InvalidMidiDataException) { + // + } + + } + + fun noteOn(channel: Int, key: Int, velocity: Int) { + try { + val msg = ShortMessage(ShortMessage.NOTE_ON, channel, key, velocity) + if (receiverDevice != null) { + val tc = receiverDevice!!.microsecondPosition + receiver.send(msg, tc) + } + } catch (e: InvalidMidiDataException) { + // + } + } + + fun destroy() { + receiverDevice.close() + transmitterDevicer.close() + } +} + +fun main() { + MidiDeviceDescription.list().forEach { + println("> ${it.name}, ${it.vendor} r:${it.receive} t:${it.transmit}") + } + + val dev = MidiTransceiver.fromDeviceVendor("BCR2000 [hw:2,0,0]", "ALSA (http://www.alsa-project.org)") + dev.controlChanged.listen { + println("${it.channel} ${it.control} ${it.value}") + } + +// dev.destroy() +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 3df1238b..4905e364 100644 --- a/settings.gradle +++ b/settings.gradle @@ -9,10 +9,7 @@ include 'orx-camera', 'orx-jumpflood', 'orx-kdtree', 'orx-mesh-generators', + 'orx-midi', 'orx-no-clear', 'orx-noise', - 'orx-obj-loader' - - - - + 'orx-obj-loader' \ No newline at end of file