Skip to content

Commit

Permalink
MIDI-CI: add support for ACK and NAK in MidiCIConnection. Fix Profile…
Browse files Browse the repository at this point in the history
… CI ver.
  • Loading branch information
atsushieno committed Dec 10, 2023
1 parent a6028a4 commit 70be221
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 18 deletions.
17 changes: 10 additions & 7 deletions ktmidi/src/commonMain/kotlin/dev/atsushieno/ktmidi/ci/CIFactory.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ object CIFactory {

const val SUB_ID_2_DISCOVERY_INQUIRY: Byte = 0x70
const val SUB_ID_2_DISCOVERY_REPLY: Byte = 0x71
const val SUB_ID_2_ENDPOINT_MESSAGE_INQUIRY: Byte = 0x72
const val SUB_ID_2_ENDPOINT_MESSAGE_REPLY: Byte = 0x73
const val SUB_ID_2_ACK: Byte = 0x7D
const val SUB_ID_2_INVALIDATE_MUID: Byte = 0x7E
const val SUB_ID_2_NAK: Byte = 0x7F
const val SUB_ID_2_PROTOCOL_NEGOTIATION_INQUIRY: Byte = 0x10
Expand Down Expand Up @@ -88,9 +91,9 @@ object CIFactory {
dst: MutableList<Byte>,
deviceId: Byte, sysexSubId2: Byte, versionAndFormat: Byte, sourceMUID: Int, destinationMUID: Int
) {
dst[0] = 0x7E
dst[0] = MidiCIConstants.UNIVERSAL_SYSEX
dst[1] = deviceId
dst[2] = 0xD
dst[2] = MidiCIConstants.UNIVERSAL_SYSEX_SUB_ID_MIDI_CI
dst[3] = sysexSubId2
dst[4] = versionAndFormat
midiCiDirectUint32At(dst, 5, sourceMUID)
Expand Down Expand Up @@ -158,14 +161,14 @@ object CIFactory {

fun midiCIEndpointMessage(dst: MutableList<Byte>, versionAndFormat: Byte, sourceMUID: Int, destinationMUID: Int, status: Byte
) : List<Byte> {
midiCIMessageCommon(dst, 0x7F, 0x7E, versionAndFormat, sourceMUID, destinationMUID)
midiCIMessageCommon(dst, MidiCIConstants.FROM_FUNCTION_BLOCK, SUB_ID_2_ENDPOINT_MESSAGE_INQUIRY, versionAndFormat, sourceMUID, destinationMUID)
dst[13] = status
return dst.take(14)
}

fun midiCIEndpointMessageReply(dst: MutableList<Byte>, versionAndFormat: Byte, sourceMUID: Int, destinationMUID: Int, status: Byte, informationData: List<Byte>
) : List<Byte> {
midiCIMessageCommon(dst, 0x7F, 0x7E, versionAndFormat, sourceMUID, destinationMUID)
midiCIMessageCommon(dst, MidiCIConstants.FROM_FUNCTION_BLOCK, SUB_ID_2_ENDPOINT_MESSAGE_REPLY, versionAndFormat, sourceMUID, destinationMUID)
dst[13] = status
midiCiDirectUint16At(dst, 14, informationData.size.toUShort())
return dst.take(14)
Expand All @@ -175,7 +178,7 @@ object CIFactory {
dst: MutableList<Byte>,
versionAndFormat: Byte, sourceMUID: Int, targetMUID: Int
) : List<Byte> {
midiCIMessageCommon(dst, 0x7F, 0x7E, versionAndFormat, sourceMUID, 0x7F7F7F7F)
midiCIMessageCommon(dst, MidiCIConstants.FROM_FUNCTION_BLOCK, SUB_ID_2_INVALIDATE_MUID, versionAndFormat, sourceMUID, 0x7F7F7F7F)
midiCiDirectUint32At(dst, 13, targetMUID)
return dst.take(17)
}
Expand Down Expand Up @@ -298,7 +301,7 @@ object CIFactory {
midiCIMessageCommon(
dst, source,
SUB_ID_2_PROFILE_INQUIRY_REPLY,
1, sourceMUID, destinationMUID
MidiCIConstants.CI_VERSION_AND_FORMAT, sourceMUID, destinationMUID
)
dst[13] = (enabledProfiles.size and 0x7F).toByte()
dst[14] = ((enabledProfiles.size shr 7) and 0x7F).toByte()
Expand All @@ -322,7 +325,7 @@ object CIFactory {
midiCIMessageCommon(
dst, destination,
if (turnOn) SUB_ID_2_SET_PROFILE_ON else SUB_ID_2_SET_PROFILE_OFF,
1, sourceMUID, destinationMUID
MidiCIConstants.CI_VERSION_AND_FORMAT, sourceMUID, destinationMUID
)
midiCIProfile(dst, 13, profile)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ object CIRetrieval {
fun midiCIGetDestinationMUID(sysex: List<Byte>) =
sysex[9] + (sysex[10] shl 8) + (sysex[11] shl 16) + (sysex[12] shl 24)

/** retrieves source MUID from a MIDI-CI sysex7 chunk.
* The argument sysex bytestream is NOT specific to MIDI 1.0 bytestream and thus does NOT contain F0 and F7 (i.e. starts with 0xFE, xx, 0x0D...)
*/
fun midiCIGetMUIDToInvalidate(sysex: List<Byte>) =
sysex[13] + (sysex[14] shl 8) + (sysex[15] shl 16) + (sysex[16] shl 24)

/** retrieves a protocol type info from a MIDI-CI sysex7 chunk partial (from offset 0). */
private fun readSingleProtocol(sysex: List<Byte>) =
MidiCIProtocolTypeInfo(sysex[0], sysex[1], sysex[2], 0, 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package dev.atsushieno.ktmidi.ci

import dev.atsushieno.ktmidi.MidiCIProtocolType
import dev.atsushieno.ktmidi.MidiCIProtocolValue
import io.ktor.utils.io.charsets.*
import io.ktor.utils.io.core.*
import kotlin.random.Random
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
Expand Down Expand Up @@ -57,13 +59,19 @@ object MidiCISystem {
}

object MidiCIConstants {
const val UNIVERSAL_SYSEX: Byte = 0x7E
const val UNIVERSAL_SYSEX_SUB_ID_MIDI_CI: Byte = 0x0D

const val CI_VERSION_AND_FORMAT: Byte = 0x2

const val ENDPOINT_STATUS_PRODUCT_INSTANCE_ID: Byte = 0

const val DEFAULT_RECEIVABLE_MAX_SYSEX_SIZE = 4096

const val DEVICE_ID_MIDI_PORT: Byte = 0x7F

const val NO_FUNCTION_BLOCK: Byte = 0x7F
const val FROM_FUNCTION_BLOCK: Byte = 0x7F

val Midi1ProtocolTypeInfo = MidiCIProtocolTypeInfo(MidiCIProtocolType
.MIDI1.toByte(), MidiCIProtocolValue.MIDI1.toByte(), 0, 0, 0)
Expand Down Expand Up @@ -100,7 +108,7 @@ class MidiCIInitiator(private val sendOutput: (data: List<Byte>) -> Unit,
var device: DeviceDetails = DeviceDetails.empty
var midiCIBufferSize = 1024
var receivableMaxSysExSize = MidiCIConstants.DEFAULT_RECEIVABLE_MAX_SYSEX_SIZE
var productInstanceId: Byte = 0
var productInstanceId: String? = null

var state: MidiCIInitiatorState = MidiCIInitiatorState.Initial

Expand All @@ -127,6 +135,36 @@ class MidiCIInitiator(private val sendOutput: (data: List<Byte>) -> Unit,
}
var processDiscoveryResponse = defaultProcessDiscoveryResponse

private val defaultProcessInvalidateMUID = { sourceMUID: Int, destinationMUID: Int, muidToInvalidate: Int ->
if (muidToInvalidate == muid) {
state = MidiCIInitiatorState.Initial
}
}
var processInvalidateMUID = defaultProcessInvalidateMUID

var onAck: (sourceMUID: Int, destinationMUID: Int, originalTransactionSubId: Byte, nakStatusCode: Byte, nakStatusData: Byte, nakDetailsForEachSubIdClassification: List<Byte>, messageLength: UShort, messageText: List<Byte>) -> Unit = { _,_,_,_,_,_,_,_ -> }
var onNak: (sourceMUID: Int, destinationMUID: Int, originalTransactionSubId: Byte, nakStatusCode: Byte, nakStatusData: Byte, nakDetailsForEachSubIdClassification: List<Byte>, messageLength: UShort, messageText: List<Byte>) -> Unit = { _,_,_,_,_,_,_,_ -> }
private fun defaultProcessAckNak(isNak: Boolean, sourceMUID: Int, destinationMUID: Int, originalTransactionSubId: Byte, statusCode: Byte, statusData: Byte, detailsForEachSubIdClassification: List<Byte>, messageLength: UShort, messageText: List<Byte>) {
if (isNak)
onNak(sourceMUID, destinationMUID, originalTransactionSubId, statusCode, statusData, detailsForEachSubIdClassification, messageLength, messageText)
else
onAck(sourceMUID, destinationMUID, originalTransactionSubId, statusCode, statusData, detailsForEachSubIdClassification, messageLength, messageText)
}
private val defaultProcessAck = { sourceMUID: Int, destinationMUID: Int, originalTransactionSubId: Byte, statusCode: Byte, statusData: Byte, detailsForEachSubIdClassification: List<Byte>, messageLength: UShort, messageText: List<Byte> ->
defaultProcessAckNak(false, sourceMUID, destinationMUID, originalTransactionSubId, statusCode, statusData, detailsForEachSubIdClassification, messageLength, messageText)
}
var processAck = defaultProcessAck
private val defaultProcessNak = { sourceMUID: Int, destinationMUID: Int, originalTransactionSubId: Byte, statusCode: Byte, statusData: Byte, detailsForEachSubIdClassification: List<Byte>, messageLength: UShort, messageText: List<Byte> ->
defaultProcessAckNak(true, sourceMUID, destinationMUID, originalTransactionSubId, statusCode, statusData, detailsForEachSubIdClassification, messageLength, messageText)
}
var processNak = defaultProcessNak

private val defaultProcessEndpointMessageResponse = { sourceMUID: Int, destinationMUID: Int, status: Byte, data: List<Byte> ->
if (status == MidiCIConstants.ENDPOINT_STATUS_PRODUCT_INSTANCE_ID)
productInstanceId = data.toByteArray().decodeToString() // FIXME: verify that it is only ASCII chars?
}
var processEndpointMessageResponse = defaultProcessEndpointMessageResponse

/*
Protocol Negotiation is deprecated. We do not send any of those anymore.
Expand Down Expand Up @@ -185,8 +223,6 @@ class MidiCIInitiator(private val sendOutput: (data: List<Byte>) -> Unit,
fun sendEndpointMessage(targetMuid: Int, status: Byte) {
val buf = MutableList<Byte>(midiCIBufferSize) { 0 }
CIFactory.midiCIEndpointMessage(buf, MidiCIConstants.CI_VERSION_AND_FORMAT, muid, targetMuid, status)
// we set state before sending the MIDI data as it may process the rest of the events synchronously through the end...
state = MidiCIInitiatorState.DISCOVERY_SENT
sendOutput(buf)
}

Expand Down Expand Up @@ -271,19 +307,46 @@ class MidiCIInitiator(private val sendOutput: (data: List<Byte>) -> Unit,
CIRetrieval.midiCIGetSourceMUID(data),
CIRetrieval.midiCIGetDestinationMUID(data))
}
CIFactory.SUB_ID_2_ENDPOINT_MESSAGE_REPLY -> {
val sourceMUID = CIRetrieval.midiCIGetSourceMUID(data)
val destinationMUID = CIRetrieval.midiCIGetDestinationMUID(data)
val status = data[13]
val dataLength = data[14] + (data[15].toInt() shl 7)
val dataValue = data.drop(16).take(dataLength)
processEndpointMessageResponse(sourceMUID, destinationMUID, status, dataValue)
}
CIFactory.SUB_ID_2_INVALIDATE_MUID -> {
// Invalid MUID
processDiscoveryResponse(MidiCIDiscoveryResponseCode.InvalidateMUID,
CIRetrieval.midiCIGetDeviceDetails(data),
processInvalidateMUID(CIRetrieval.midiCIGetSourceMUID(data),
0x7F7F7F7F,
CIRetrieval.midiCIGetMUIDToInvalidate(data)
)
}
CIFactory.SUB_ID_2_ACK -> {
// ACK MIDI-CI
processAck(
CIRetrieval.midiCIGetSourceMUID(data),
0x7F7F7F7F)
CIRetrieval.midiCIGetDestinationMUID(data),
data[13],
data[14],
data[15],
data.drop(16).take(5),
(data[21] + (data[22].toInt() shl 7)).toUShort(),
data.drop(23)
)
}
CIFactory.SUB_ID_2_NAK -> {
// NAK MIDI-CI
processDiscoveryResponse(MidiCIDiscoveryResponseCode.NAK,
CIRetrieval.midiCIGetDeviceDetails(data),
processNak(
CIRetrieval.midiCIGetSourceMUID(data),
CIRetrieval.midiCIGetDestinationMUID(data))
CIRetrieval.midiCIGetDestinationMUID(data),
data[13],
data[14],
data[15],
data.drop(16).take(5),
(data[21] + (data[22].toInt() shl 7)).toUShort(),
data.drop(23)
)
}
// Profile Configuration
CIFactory.SUB_ID_2_PROFILE_INQUIRY_REPLY -> {
Expand Down Expand Up @@ -343,6 +406,7 @@ class MidiCIResponder(private val sendOutput: (data: List<Byte>) -> Unit,
var supportedProtocols: List<MidiCIProtocolTypeInfo> = MidiCIConstants.Midi2ThenMidi1Protocols
var profileSet: MutableList<Pair<MidiCIProfileId,Boolean>> = mutableListOf()
var functionBlock: Byte = MidiCIConstants.NO_FUNCTION_BLOCK
var productInstanceId: String? = null

var midiCIBufferSize = 128

Expand All @@ -353,7 +417,6 @@ class MidiCIResponder(private val sendOutput: (data: List<Byte>) -> Unit,
// FIXME: enable this when we start supporting Property Exchange.
//var establishedMaxSimulutaneousPropertyRequests: Byte? = null

@OptIn(ExperimentalTime::class)
private var protocolTestTimeout: Duration? = null

private val defaultProcessDiscovery: (deviceDetails: DeviceDetails?, initiatorMUID: Int, initiatorOutputPath: Byte) -> Unit = { deviceDetails, initiatorMUID, initiatorOutputPath ->
Expand All @@ -367,6 +430,16 @@ class MidiCIResponder(private val sendOutput: (data: List<Byte>) -> Unit,
}
var processDiscovery = defaultProcessDiscovery

private val defaultProcessEndpointMessage: (initiatorMUID: Int, destinationMUID: Int, status: Byte) -> Unit = { initiatorMUID, destinationMUID, status ->
val dst = MutableList<Byte>(midiCIBufferSize) { 0 }
val prodId = productInstanceId
sendOutput(CIFactory.midiCIEndpointMessageReply(
dst, MidiCIConstants.CI_VERSION_AND_FORMAT, muid, initiatorMUID, status,
if (status == MidiCIConstants.ENDPOINT_STATUS_PRODUCT_INSTANCE_ID && prodId != null) prodId.toByteArray(Charsets.ISO_8859_1).toList() else listOf() // FIXME: verify that it is only ASCII chars?
))
}
var processEndpointMessage = defaultProcessEndpointMessage

private val defaultProcessNegotiationInquiry: (supportedProtocols: List<MidiCIProtocolTypeInfo>, initiatorMUID: Int) -> List<MidiCIProtocolTypeInfo> =
// we don't listen to initiator :p
{ _, _ -> supportedProtocols }
Expand Down Expand Up @@ -425,7 +498,6 @@ class MidiCIResponder(private val sendOutput: (data: List<Byte>) -> Unit,
// private val defaultProcessGetMaxSimultaneousPropertyRequests: (destinationChannelOr7F: Byte, sourceMUID: Int, destinationMUID: Int, max: Byte) -> Byte = { _, _, _, max -> max }
//var processGetMaxSimultaneousPropertyRequests = defaultProcessGetMaxSimultaneousPropertyRequests

@OptIn(ExperimentalTime::class)
fun processInput(data: List<Byte>) {
if (data[0] != 0x7E.toByte() || data[2] != 0xD.toByte())
return // not MIDI-CI sysex
Expand All @@ -439,6 +511,14 @@ class MidiCIResponder(private val sendOutput: (data: List<Byte>) -> Unit,
processDiscovery(initiatorDevice, sourceMUID, initiatorOutputPath)
}

CIFactory.SUB_ID_2_ENDPOINT_MESSAGE_INQUIRY -> {
val sourceMUID = CIRetrieval.midiCIGetSourceMUID(data)
val destinationMUID = CIRetrieval.midiCIGetDestinationMUID(data)
// only available in MIDI-CI 1.2 or later.
val status = data[13]
processEndpointMessage(sourceMUID, destinationMUID, status)
}

/*
// Protocol Negotiation - is disabled
CIFactory.SUB_ID_2_PROTOCOL_NEGOTIATION_INQUIRY -> {
Expand Down

0 comments on commit 70be221

Please sign in to comment.