Fix/midi transceiver fails on short/system messages (#347)
This commit is contained in:
@@ -28,6 +28,7 @@ ktor = "3.2.3"
|
|||||||
jgit = "7.3.0.202506031305-r"
|
jgit = "7.3.0.202506031305-r"
|
||||||
javaosc = "0.9"
|
javaosc = "0.9"
|
||||||
jsoup = "1.21.1"
|
jsoup = "1.21.1"
|
||||||
|
mockk = "1.13.11"
|
||||||
processing = "4.4.6"
|
processing = "4.4.6"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
@@ -49,7 +50,10 @@ kotest-assertions = { group = "io.kotest", name = "kotest-assertions-core", vers
|
|||||||
kotest-runner = { group = "io.kotest", name = "kotest-runner-junit5", version.ref = "kotest" }
|
kotest-runner = { group = "io.kotest", name = "kotest-runner-junit5", version.ref = "kotest" }
|
||||||
kotest-framework-engine = { group = "io.kotest", name = "kotest-framework-engine", version.ref = "kotest" }
|
kotest-framework-engine = { group = "io.kotest", name = "kotest-framework-engine", version.ref = "kotest" }
|
||||||
|
|
||||||
|
mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk"}
|
||||||
|
|
||||||
processing-core = { group = "org.processing", name = "core", version.ref = "processing"}
|
processing-core = { group = "org.processing", name = "core", version.ref = "processing"}
|
||||||
|
|
||||||
openrndr-application = { group = "org.openrndr", name = "openrndr-application", version.ref = "openrndr" }
|
openrndr-application = { group = "org.openrndr", name = "openrndr-application", version.ref = "openrndr" }
|
||||||
openrndr-extensions = { group = "org.openrndr", name = "openrndr-extensions", version.ref = "openrndr" }
|
openrndr-extensions = { group = "org.openrndr", name = "openrndr-extensions", version.ref = "openrndr" }
|
||||||
openrndr-math = { group = "org.openrndr", name = "openrndr-math", version.ref = "openrndr" }
|
openrndr-math = { group = "org.openrndr", name = "openrndr-math", version.ref = "openrndr" }
|
||||||
|
|||||||
@@ -9,4 +9,7 @@ dependencies {
|
|||||||
implementation(libs.kotlin.coroutines)
|
implementation(libs.kotlin.coroutines)
|
||||||
implementation(project(":orx-property-watchers"))
|
implementation(project(":orx-property-watchers"))
|
||||||
implementation(project(":orx-parameters"))
|
implementation(project(":orx-parameters"))
|
||||||
}
|
|
||||||
|
testImplementation(libs.mockk)
|
||||||
|
testImplementation(libs.kotest.assertions)
|
||||||
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ fun Program.bindMidiControl(
|
|||||||
val low = anno?.low ?: 0.0
|
val low = anno?.low ?: 0.0
|
||||||
val high = anno?.high ?: 1.0
|
val high = anno?.high ?: 1.0
|
||||||
transceiver.controlChanged.listen {
|
transceiver.controlChanged.listen {
|
||||||
if (it.eventType == MidiEventType.CONTROL_CHANGED && it.channel == channel && it.control == control) {
|
if (it.eventType == MidiEventType.CONTROL_CHANGE && it.channel == channel && it.control == control) {
|
||||||
val value = it.value.toDouble().map(0.0, 127.0, low, high, clamp = true)
|
val value = it.value.toDouble().map(0.0, 127.0, low, high, clamp = true)
|
||||||
property.set(value)
|
property.set(value)
|
||||||
}
|
}
|
||||||
@@ -83,7 +83,7 @@ fun Program.bindMidiControl(
|
|||||||
control: Int
|
control: Int
|
||||||
) {
|
) {
|
||||||
transceiver.controlChanged.listen {
|
transceiver.controlChanged.listen {
|
||||||
if (it.eventType == MidiEventType.CONTROL_CHANGED && it.channel == channel && it.control == control) {
|
if (it.eventType == MidiEventType.CONTROL_CHANGE && it.channel == channel && it.control == control) {
|
||||||
property.set(it.value >= 64)
|
property.set(it.value >= 64)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,12 +126,12 @@ fun Program.bindMidiControl(
|
|||||||
var y = v.y
|
var y = v.y
|
||||||
var changed = false
|
var changed = false
|
||||||
|
|
||||||
if (it.eventType == MidiEventType.CONTROL_CHANGED && it.channel == channelX && it.control == controlX) {
|
if (it.eventType == MidiEventType.CONTROL_CHANGE && it.channel == channelX && it.control == controlX) {
|
||||||
changed = true
|
changed = true
|
||||||
x = it.value.toDouble().map(0.0, 127.0, low, high, clamp = 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) {
|
if (it.eventType == MidiEventType.CONTROL_CHANGE && it.channel == channelY && it.control == controlY) {
|
||||||
changed = true
|
changed = true
|
||||||
y = it.value.toDouble().map(0.0, 127.0, low, high, clamp = true)
|
y = it.value.toDouble().map(0.0, 127.0, low, high, clamp = true)
|
||||||
}
|
}
|
||||||
@@ -187,17 +187,17 @@ fun Program.bindMidiControl(
|
|||||||
var z = v.z
|
var z = v.z
|
||||||
var changed = false
|
var changed = false
|
||||||
|
|
||||||
if (it.eventType == MidiEventType.CONTROL_CHANGED && it.channel == channelX && it.control == controlX) {
|
if (it.eventType == MidiEventType.CONTROL_CHANGE && it.channel == channelX && it.control == controlX) {
|
||||||
changed = true
|
changed = true
|
||||||
x = it.value.toDouble().map(0.0, 127.0, low, high, clamp = 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) {
|
if (it.eventType == MidiEventType.CONTROL_CHANGE && it.channel == channelY && it.control == controlY) {
|
||||||
changed = true
|
changed = true
|
||||||
y = it.value.toDouble().map(0.0, 127.0, low, high, clamp = 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) {
|
if (it.eventType == MidiEventType.CONTROL_CHANGE && it.channel == channelZ && it.control == controlZ) {
|
||||||
changed = true
|
changed = true
|
||||||
z = it.value.toDouble().map(0.0, 127.0, low, high, clamp = true)
|
z = it.value.toDouble().map(0.0, 127.0, low, high, clamp = true)
|
||||||
}
|
}
|
||||||
@@ -257,22 +257,22 @@ fun Program.bindMidiControl(
|
|||||||
var a = v.alpha
|
var a = v.alpha
|
||||||
var changed = false
|
var changed = false
|
||||||
|
|
||||||
if (it.eventType == MidiEventType.CONTROL_CHANGED && it.channel == channelR && it.control == controlR) {
|
if (it.eventType == MidiEventType.CONTROL_CHANGE && it.channel == channelR && it.control == controlR) {
|
||||||
changed = true
|
changed = true
|
||||||
r = it.value.toDouble().map(0.0, 127.0, low, high, clamp = 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) {
|
if (it.eventType == MidiEventType.CONTROL_CHANGE && it.channel == channelG && it.control == controlG) {
|
||||||
changed = true
|
changed = true
|
||||||
g = it.value.toDouble().map(0.0, 127.0, low, high, clamp = 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) {
|
if (it.eventType == MidiEventType.CONTROL_CHANGE && it.channel == channelB && it.control == controlB) {
|
||||||
changed = true
|
changed = true
|
||||||
b = it.value.toDouble().map(0.0, 127.0, low, high, clamp = 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) {
|
if (it.eventType == MidiEventType.CONTROL_CHANGE && it.channel == channelA && it.control == controlA) {
|
||||||
changed = true
|
changed = true
|
||||||
a = it.value.toDouble().map(0.0, 127.0, low, high, clamp = true)
|
a = it.value.toDouble().map(0.0, 127.0, low, high, clamp = true)
|
||||||
}
|
}
|
||||||
@@ -335,22 +335,22 @@ fun Program.bindMidiControl(
|
|||||||
var w = v.w
|
var w = v.w
|
||||||
var changed = false
|
var changed = false
|
||||||
|
|
||||||
if (it.eventType == MidiEventType.CONTROL_CHANGED && it.channel == channelX && it.control == controlX) {
|
if (it.eventType == MidiEventType.CONTROL_CHANGE && it.channel == channelX && it.control == controlX) {
|
||||||
changed = true
|
changed = true
|
||||||
x = it.value.toDouble().map(0.0, 127.0, low, high, clamp = 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) {
|
if (it.eventType == MidiEventType.CONTROL_CHANGE && it.channel == channelY && it.control == controlY) {
|
||||||
changed = true
|
changed = true
|
||||||
y = it.value.toDouble().map(0.0, 127.0, low, high, clamp = 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) {
|
if (it.eventType == MidiEventType.CONTROL_CHANGE && it.channel == channelZ && it.control == controlZ) {
|
||||||
changed = true
|
changed = true
|
||||||
z = it.value.toDouble().map(0.0, 127.0, low, high, clamp = true)
|
z = it.value.toDouble().map(0.0, 127.0, low, high, clamp = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (it.eventType == MidiEventType.CONTROL_CHANGED && it.channel == channelW && it.control == controlW) {
|
if (it.eventType == MidiEventType.CONTROL_CHANGE && it.channel == channelW && it.control == controlW) {
|
||||||
changed = true
|
changed = true
|
||||||
w = it.value.toDouble().map(0.0, 127.0, low, high, clamp = true)
|
w = it.value.toDouble().map(0.0, 127.0, low, high, clamp = true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,47 @@
|
|||||||
package org.openrndr.extra.midi
|
package org.openrndr.extra.midi
|
||||||
|
|
||||||
enum class MidiEventType {
|
import javax.sound.midi.MidiMessage
|
||||||
NOTE_ON,
|
import javax.sound.midi.ShortMessage
|
||||||
NOTE_OFF,
|
|
||||||
CONTROL_CHANGED,
|
enum class MidiEventType(val status: Int) {
|
||||||
PROGRAM_CHANGE,
|
|
||||||
CHANNEL_PRESSURE,
|
MIDI_TIME_CODE(ShortMessage.MIDI_TIME_CODE),
|
||||||
PITCH_BEND
|
SONG_POSITION_POINTER(ShortMessage.SONG_POSITION_POINTER),
|
||||||
|
SONG_SELECT(ShortMessage.SONG_SELECT),
|
||||||
|
TUNE_REQUEST(ShortMessage.TUNE_REQUEST),
|
||||||
|
END_OF_EXCLUSIVE(ShortMessage.END_OF_EXCLUSIVE),
|
||||||
|
TIMING_CLOCK(ShortMessage.TIMING_CLOCK),
|
||||||
|
START(ShortMessage.START),
|
||||||
|
CONTINUE(ShortMessage.CONTINUE),
|
||||||
|
STOP(ShortMessage.STOP),
|
||||||
|
ACTIVE_SENSING(ShortMessage.ACTIVE_SENSING),
|
||||||
|
SYSTEM_RESET(ShortMessage.SYSTEM_RESET),
|
||||||
|
NOTE_ON(ShortMessage.NOTE_ON),
|
||||||
|
NOTE_OFF(ShortMessage.NOTE_OFF),
|
||||||
|
CONTROL_CHANGE(ShortMessage.CONTROL_CHANGE),
|
||||||
|
PROGRAM_CHANGE(ShortMessage.PROGRAM_CHANGE),
|
||||||
|
CHANNEL_PRESSURE(ShortMessage.CHANNEL_PRESSURE),
|
||||||
|
PITCH_BEND(ShortMessage.PITCH_BEND);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private val statusMap: Map<Int, MidiEventType> =
|
||||||
|
entries.associateBy { it.status }
|
||||||
|
|
||||||
|
fun fromStatus(
|
||||||
|
status: Int
|
||||||
|
): MidiEventType = requireNotNull(
|
||||||
|
statusMap[if (status >= 0xf0) status else status and 0xf0]
|
||||||
|
) {
|
||||||
|
"Invalid MIDI status: $status"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val MidiMessage.eventType: MidiEventType get() = MidiEventType.fromStatus(status)
|
||||||
|
|
||||||
class MidiEvent(val eventType: MidiEventType) {
|
class MidiEvent(val eventType: MidiEventType) {
|
||||||
var origin = Origin.DEVICE
|
var origin = Origin.DEVICE
|
||||||
var control: Int = 0
|
var control: Int = 0
|
||||||
@@ -34,15 +67,16 @@ class MidiEvent(val eventType: MidiEventType) {
|
|||||||
return midiEvent
|
return midiEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
fun noteOff(channel: Int, note: Int): MidiEvent {
|
fun noteOff(channel: Int, note: Int, velocity: Int): MidiEvent {
|
||||||
val midiEvent = MidiEvent(MidiEventType.NOTE_OFF)
|
val midiEvent = MidiEvent(MidiEventType.NOTE_OFF)
|
||||||
midiEvent.note = note
|
midiEvent.note = note
|
||||||
midiEvent.channel = channel
|
midiEvent.channel = channel
|
||||||
|
midiEvent.velocity = velocity
|
||||||
return midiEvent
|
return midiEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
fun controlChange(channel: Int, control: Int, value: Int): MidiEvent {
|
fun controlChange(channel: Int, control: Int, value: Int): MidiEvent {
|
||||||
val midiEvent = MidiEvent(MidiEventType.CONTROL_CHANGED)
|
val midiEvent = MidiEvent(MidiEventType.CONTROL_CHANGE)
|
||||||
midiEvent.channel = channel
|
midiEvent.channel = channel
|
||||||
midiEvent.control = control
|
midiEvent.control = control
|
||||||
midiEvent.value = value
|
midiEvent.value = value
|
||||||
|
|||||||
@@ -118,80 +118,80 @@ class MidiTransceiver(program: Program, val receiverDevice: MidiDevice?, val tra
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun trigger(message: MidiMessage) {
|
||||||
|
val cmd = message.message
|
||||||
|
val channel = (cmd[0].toInt() and 0xff) and 0x0f
|
||||||
|
when (val eventType = message.eventType) {
|
||||||
|
|
||||||
|
MidiEventType.NOTE_ON -> {
|
||||||
|
val key = cmd[1].toInt() and 0xff
|
||||||
|
val velocity = cmd[2].toInt() and 0xff
|
||||||
|
if (velocity > 0) {
|
||||||
|
noteOn.trigger(MidiEvent.noteOn(channel, key, velocity))
|
||||||
|
} else {
|
||||||
|
noteOff.trigger(MidiEvent.noteOff(channel, key, velocity))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MidiEventType.NOTE_OFF -> noteOff.trigger(
|
||||||
|
MidiEvent.noteOff(
|
||||||
|
channel,
|
||||||
|
cmd[1].toInt() and 0xff,
|
||||||
|
cmd[2].toInt() and 0xff
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
MidiEventType.CONTROL_CHANGE -> controlChanged.trigger(
|
||||||
|
MidiEvent.controlChange(
|
||||||
|
channel,
|
||||||
|
cmd[1].toInt() and 0xff,
|
||||||
|
cmd[2].toInt() and 0xff
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
MidiEventType.PROGRAM_CHANGE -> programChanged.trigger(
|
||||||
|
MidiEvent.programChange(
|
||||||
|
channel,
|
||||||
|
cmd[1].toInt() and 0xff
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
MidiEventType.CHANNEL_PRESSURE -> channelPressure.trigger(
|
||||||
|
MidiEvent.channelPressure(
|
||||||
|
channel,
|
||||||
|
cmd[1].toInt() and 0xff
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// https://sites.uci.edu/camp2014/2014/04/30/managing-midi-pitchbend-messages/
|
||||||
|
// The next operation to combine two 7bit values
|
||||||
|
// was verified to give the same results as the Linux
|
||||||
|
// `midisnoop` program while using an `Alesis Vortex
|
||||||
|
// Wireless 2` device. This MIDI device does not provide a
|
||||||
|
// full range 14 bit pitch-bend resolution though, so
|
||||||
|
// a different device is needed to confirm the pitch bend
|
||||||
|
// values slide as expected from -8192 to +8191.
|
||||||
|
MidiEventType.PITCH_BEND -> pitchBend.trigger(
|
||||||
|
MidiEvent.pitchBend(
|
||||||
|
channel,
|
||||||
|
(cmd[2].toInt() shl 25 shr 18) + cmd[1].toInt()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
logger.trace { "Unsupported MIDI event type: $eventType" }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
transmitter?.receiver = object : MidiDeviceReceiver {
|
transmitter?.receiver = object : MidiDeviceReceiver {
|
||||||
override fun getMidiDevice(): MidiDevice? {
|
override fun getMidiDevice(): MidiDevice? {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun send(message: MidiMessage, timeStamp: Long) {
|
override fun send(message: MidiMessage, timeStamp: Long) {
|
||||||
val cmd = message.message
|
trigger(message)
|
||||||
val channel = (cmd[0].toInt() and 0xff) and 0x0f
|
|
||||||
val velocity = cmd[2].toInt() and 0xff
|
|
||||||
when ((cmd[0].toInt() and 0xff) and 0xf0) {
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ShortMessage.NOTE_ON -> if (velocity > 0) {
|
|
||||||
noteOn.trigger(
|
|
||||||
MidiEvent.noteOn(
|
|
||||||
channel,
|
|
||||||
cmd[1].toInt() and 0xff,
|
|
||||||
velocity
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
noteOff.trigger(
|
|
||||||
MidiEvent.noteOff(
|
|
||||||
channel,
|
|
||||||
cmd[1].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
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
ShortMessage.PROGRAM_CHANGE -> programChanged.trigger(
|
|
||||||
MidiEvent.programChange(
|
|
||||||
channel,
|
|
||||||
cmd[1].toInt() and 0xff
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
ShortMessage.CHANNEL_PRESSURE -> channelPressure.trigger(
|
|
||||||
MidiEvent.channelPressure(
|
|
||||||
channel,
|
|
||||||
cmd[1].toInt() and 0xff
|
|
||||||
)
|
|
||||||
)
|
|
||||||
// https://sites.uci.edu/camp2014/2014/04/30/managing-midi-pitchbend-messages/
|
|
||||||
// The next operation to combine two 7bit values
|
|
||||||
// was verified to give the same results as the Linux
|
|
||||||
// `midisnoop` program while using an `Alesis Vortex
|
|
||||||
// Wireless 2` device. This MIDI device does not provide a
|
|
||||||
// full range 14 bit pitch-bend resolution though, so
|
|
||||||
// a different device is needed to confirm the pitch bend
|
|
||||||
// values slide as expected from -8192 to +8191.
|
|
||||||
ShortMessage.PITCH_BEND -> pitchBend.trigger(
|
|
||||||
MidiEvent.pitchBend(
|
|
||||||
channel,
|
|
||||||
(cmd[2].toInt() shl 25 shr 18) + cmd[1].toInt()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
override fun close() {
|
override fun close() {
|
||||||
}
|
}
|
||||||
@@ -212,64 +212,45 @@ class MidiTransceiver(program: Program, val receiverDevice: MidiDevice?, val tra
|
|||||||
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) {
|
||||||
if (receiver != null && receiverDevice != null) {
|
send { ShortMessage(ShortMessage.CONTROL_CHANGE, channel, control, value) }
|
||||||
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) {
|
fun programChange(channel: Int, program: Int) {
|
||||||
if (receiver != null && receiverDevice != null) {
|
send { ShortMessage(ShortMessage.PROGRAM_CHANGE, channel, program) }
|
||||||
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) {
|
fun noteOn(channel: Int, key: Int, velocity: Int) {
|
||||||
if (receiver != null && receiverDevice != null) {
|
send { ShortMessage(ShortMessage.NOTE_ON, channel, key, velocity) }
|
||||||
try {
|
}
|
||||||
val msg = ShortMessage(ShortMessage.NOTE_ON, channel, key, velocity)
|
|
||||||
receiver.send(msg, receiverDevice.microsecondPosition)
|
fun noteOff(channel: Int, key: Int, velocity: Int) {
|
||||||
} catch (e: InvalidMidiDataException) {
|
send { ShortMessage(ShortMessage.NOTE_OFF, channel, key, velocity) }
|
||||||
logger.warn { e.message }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun channelPressure(channel: Int, value: Int) {
|
fun channelPressure(channel: Int, value: Int) {
|
||||||
if (receiver != null && receiverDevice != null) {
|
send { ShortMessage(ShortMessage.CHANNEL_PRESSURE, channel, value) }
|
||||||
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) {
|
fun pitchBend(channel: Int, value: Int) {
|
||||||
if (receiver != null && receiverDevice != null) {
|
send { ShortMessage(ShortMessage.PITCH_BEND, channel, value) }
|
||||||
try {
|
|
||||||
val msg = ShortMessage(ShortMessage.PITCH_BEND, channel, value)
|
|
||||||
receiver.send(msg, receiverDevice.microsecondPosition)
|
|
||||||
} catch (e: InvalidMidiDataException) {
|
|
||||||
logger.warn { e.message }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun destroy() {
|
fun destroy() {
|
||||||
receiverDevice?.close()
|
receiverDevice?.close()
|
||||||
transmitterDevicer?.close()
|
transmitterDevicer?.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun send(block: () -> MidiMessage) {
|
||||||
|
if (receiver != null && receiverDevice != null) {
|
||||||
|
try {
|
||||||
|
val msg = block()
|
||||||
|
receiver.send(msg, receiverDevice.microsecondPosition)
|
||||||
|
} catch (e: InvalidMidiDataException) {
|
||||||
|
logger.warn { e.message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
20
orx-jvm/orx-midi/src/test/kotlin/MidiTests.kt
Normal file
20
orx-jvm/orx-midi/src/test/kotlin/MidiTests.kt
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package org.openrndr.extra.midi
|
||||||
|
|
||||||
|
import javax.sound.midi.Receiver
|
||||||
|
import javax.sound.midi.Transmitter
|
||||||
|
|
||||||
|
class TestTransmitter : Transmitter {
|
||||||
|
|
||||||
|
private var receiver: Receiver? = null
|
||||||
|
|
||||||
|
override fun setReceiver(receiver: Receiver?) {
|
||||||
|
this.receiver = receiver
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getReceiver(): Receiver? = receiver
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
receiver?.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
118
orx-jvm/orx-midi/src/test/kotlin/MidiTransceiverTest.kt
Normal file
118
orx-jvm/orx-midi/src/test/kotlin/MidiTransceiverTest.kt
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
package org.openrndr.extra.midi
|
||||||
|
|
||||||
|
import io.kotest.matchers.should
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import io.kotest.matchers.types.beInstanceOf
|
||||||
|
import io.mockk.*
|
||||||
|
import org.openrndr.Program
|
||||||
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
import javax.sound.midi.MidiDevice
|
||||||
|
import javax.sound.midi.MidiMessage
|
||||||
|
import javax.sound.midi.Receiver
|
||||||
|
import javax.sound.midi.ShortMessage
|
||||||
|
import kotlin.test.Test
|
||||||
|
|
||||||
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
|
class MidiTransceiverTest {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val program = mockk<Program>(relaxed = true)
|
||||||
|
val receiver = mockk<Receiver>()
|
||||||
|
val receiverDevice = mockk<MidiDevice>(relaxed = true)
|
||||||
|
val messageSlot = slot<MidiMessage>()
|
||||||
|
|
||||||
|
val transmitter = TestTransmitter()
|
||||||
|
val transmitterDevice = mockk<MidiDevice>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
every { receiverDevice.receiver } returns receiver
|
||||||
|
every { receiver.send(capture(messageSlot), any()) } just runs
|
||||||
|
every { transmitterDevice.transmitter } returns transmitter
|
||||||
|
}
|
||||||
|
|
||||||
|
val transceiver = MidiTransceiver(
|
||||||
|
program,
|
||||||
|
receiverDevice,
|
||||||
|
transmitterDevice
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should send out NOTE_ON message`() {
|
||||||
|
// when
|
||||||
|
transceiver.noteOn(5, 10, 100)
|
||||||
|
|
||||||
|
// then
|
||||||
|
messageSlot.captured should beInstanceOf<ShortMessage>()
|
||||||
|
(messageSlot.captured as ShortMessage).apply {
|
||||||
|
command shouldBe ShortMessage.NOTE_ON
|
||||||
|
channel shouldBe 5
|
||||||
|
data1 shouldBe 10
|
||||||
|
data2 shouldBe 100
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should send out NOTE_OFF message`() {
|
||||||
|
// when
|
||||||
|
transceiver.noteOff(1, 10, 62)
|
||||||
|
|
||||||
|
// then
|
||||||
|
messageSlot.captured should beInstanceOf<ShortMessage>()
|
||||||
|
(messageSlot.captured as ShortMessage).apply {
|
||||||
|
command shouldBe ShortMessage.NOTE_OFF
|
||||||
|
channel shouldBe 1
|
||||||
|
data1 shouldBe 10
|
||||||
|
data2 shouldBe 62
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should receive NOTE_ON event on receiving NOTE_ON message`() {
|
||||||
|
// given
|
||||||
|
val eventSlot = AtomicReference<MidiEvent>()
|
||||||
|
transceiver.noteOn.listen {
|
||||||
|
eventSlot.set(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
transmitter.receiver!!.send(
|
||||||
|
ShortMessage(ShortMessage.NOTE_ON, 1, 2, 3), 1042
|
||||||
|
)
|
||||||
|
val noteOnEvent = eventSlot.get()
|
||||||
|
|
||||||
|
// then
|
||||||
|
noteOnEvent.apply {
|
||||||
|
eventType shouldBe MidiEventType.NOTE_ON
|
||||||
|
origin shouldBe MidiEvent.Origin.DEVICE
|
||||||
|
channel shouldBe 1
|
||||||
|
note shouldBe 2
|
||||||
|
velocity shouldBe 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should receive NOTE_OFF event on receiving NOTE_ON message with velocity 0`() {
|
||||||
|
// given
|
||||||
|
val eventSlot = AtomicReference<MidiEvent>()
|
||||||
|
transceiver.noteOff.listen {
|
||||||
|
eventSlot.set(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
transmitter.receiver!!.send(
|
||||||
|
ShortMessage(ShortMessage.NOTE_ON, 2, 3, 0), 1042
|
||||||
|
)
|
||||||
|
val noteOnEvent = eventSlot.get()
|
||||||
|
|
||||||
|
// then
|
||||||
|
noteOnEvent.apply {
|
||||||
|
eventType shouldBe MidiEventType.NOTE_OFF
|
||||||
|
origin shouldBe MidiEvent.Origin.DEVICE
|
||||||
|
channel shouldBe 2
|
||||||
|
note shouldBe 3
|
||||||
|
velocity shouldBe 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user