jvmMain.dev.atsushieno.ktmidi.JvmMidiAccess.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ktmidi-jvm Show documentation
Show all versions of ktmidi-jvm Show documentation
Kotlin Multiplatform library for MIDI 1.0 and MIDI 2.0
The newest version!
package dev.atsushieno.ktmidi
import javax.sound.midi.*
internal typealias JvmMidiMessage = javax.sound.midi.MidiMessage
class JvmMidiAccess : MidiAccess() {
override val name: String
get() = "JVM"
override val inputs: Iterable
get() = MidiSystem.getMidiDeviceInfo().map { i -> MidiSystem.getMidiDevice(i) }
.flatMap {
// make sure the device has an input port that can be retrieved
if (it.maxTransmitters == -1 || it.maxTransmitters > it.transmitters.count()) {
listOf(it to it.transmitter)
} else {
emptyList()
}
}
.mapIndexed { i, p -> JvmMidiTransmitterPortDetails(p.first, i, p.second) }
override val outputs: Iterable
get() = MidiSystem.getMidiDeviceInfo().map { i -> MidiSystem.getMidiDevice(i) }
.flatMap {
// make sure the device has an output port that can be retrieved
if (it.maxReceivers == -1 || it.maxReceivers > it.receivers.count()) {
listOf(it to it.receiver)
} else {
emptyList()
}
}
.mapIndexed { i, p -> JvmMidiReceiverPortDetails(p.first, i, p.second) }
override suspend fun openInput(portId: String): MidiInput {
val port = inputs.firstOrNull { i -> i.id == portId }
if (port == null || port !is JvmMidiTransmitterPortDetails)
throw IllegalArgumentException("Input port $portId was not found")
if (!port.device.isOpen)
port.device.open()
return JvmMidiInput(port)
}
override suspend fun openOutput(portId: String): MidiOutput {
val port = outputs.firstOrNull { i -> i.id == portId }
if (port == null || port !is JvmMidiReceiverPortDetails)
throw IllegalArgumentException("Output port $portId was not found")
if (!port.device.isOpen)
port.device.open()
return JvmMidiOutput(port)
}
override suspend fun createVirtualInputSender(context: PortCreatorContext): MidiOutput {
throw UnsupportedOperationException()
}
override suspend fun createVirtualOutputReceiver(context: PortCreatorContext): MidiInput {
throw UnsupportedOperationException()
}
}
internal abstract class JvmMidiPortDetails(override val id: String, info: MidiDevice.Info) : MidiPortDetails {
override val manufacturer: String? = info.vendor
override val name: String? = info.name
override val version: String? = info.version
override val midiTransportProtocol = 1
}
private class JvmMidiTransmitterPortDetails(val device: MidiDevice, portIndex: Int, val transmitter: Transmitter) :
JvmMidiPortDetails("InPort$portIndex", device.deviceInfo)
private class JvmMidiReceiverPortDetails(val device: MidiDevice, portIndex: Int, val receiver: Receiver) :
JvmMidiPortDetails("OutPort$portIndex", device.deviceInfo)
private fun toJvmMidiMessage(data: ByteArray, start: Int, length: Int): JvmMidiMessage {
if (length <= 0) throw IllegalArgumentException("non-positive length")
val arr = if (start == 0 && length == data.size) data else data.drop(start).take(length - start).toByteArray()
return when (arr[0]) {
0xF0.toByte(), 0xF7.toByte() -> SysexMessage(arr, length)
0xFF.toByte() -> MetaMessage(arr[1].toInt(), arr.drop(2).toByteArray(), length - 2)
else -> ShortMessage(
arr[0].toUByte().toInt(),
arr.getOrElse(1) { _ -> 0 }.toInt(),
arr.getOrElse(2) { _ -> 0 }.toInt()
)
}
}
private class JvmMidiInput(val port: JvmMidiTransmitterPortDetails) : MidiInput {
override val details: MidiPortDetails = port
private val state: MidiPortConnectionState = MidiPortConnectionState.OPEN
override val connectionState: MidiPortConnectionState
get() = state
override fun close() {
port.transmitter.close()
}
private var listener: OnMidiReceivedEventListener? = null
override fun setMessageReceivedListener(listener: OnMidiReceivedEventListener) {
this.listener = listener
}
init {
port.transmitter.receiver = object : Receiver {
override fun close() {}
override fun send(msg: JvmMidiMessage?, timestampInMicroseconds: Long) {
if (msg == null)
return
var start = 0
var length = msg.length
// Message begins with 0xF7 is an additional sysex message
if (msg.message[0] == 0xF7.toByte()) {
start = 1
length--
}
listener?.onEventReceived(msg.message, start, length, timestampInMicroseconds * 1000)
}
}
}
}
private class JvmMidiOutput(val port: JvmMidiReceiverPortDetails) : MidiOutput {
override val details: MidiPortDetails
get() = port
private val state: MidiPortConnectionState = MidiPortConnectionState.OPEN
override val connectionState: MidiPortConnectionState
get() = state
override fun close() {
port.receiver.close()
}
private var multiPacketSysex = false
override fun send(mevent: ByteArray, offset: Int, length: Int, timestampInNanoseconds: Long) {
val msg: JvmMidiMessage
if (multiPacketSysex) {
// If a multi-packet sysex message ends with 0xF7, it means that it is the last packet.
if (mevent[offset + length - 1] == 0xF7.toByte())
multiPacketSysex = false
// JVM requires that an additional sysex message must begin with 0xF7.
val buffer = ByteArray(length + 1)
buffer[0] = 0xF7.toByte()
mevent.copyInto(buffer, 1, offset, length)
msg = toJvmMidiMessage(buffer, 0, length + 1)
} else {
// If a sysex doesn't end with 0xF7, it is a multi-packet sysex message.
if (mevent[offset] == 0xF0.toByte() && mevent[offset + length - 1] != 0xF7.toByte())
multiPacketSysex = true
msg = toJvmMidiMessage(mevent, offset, length)
}
port.receiver.send(msg, timestampInNanoseconds)
}
}