diff --git a/ktmidi/src/commonMain/kotlin/dev/atsushieno/ktmidi/ci/CIFactory.kt b/ktmidi/src/commonMain/kotlin/dev/atsushieno/ktmidi/ci/CIFactory.kt index 58da03e30..471154358 100644 --- a/ktmidi/src/commonMain/kotlin/dev/atsushieno/ktmidi/ci/CIFactory.kt +++ b/ktmidi/src/commonMain/kotlin/dev/atsushieno/ktmidi/ci/CIFactory.kt @@ -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 @@ -88,9 +91,9 @@ object CIFactory { dst: MutableList, 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) @@ -158,14 +161,14 @@ object CIFactory { fun midiCIEndpointMessage(dst: MutableList, versionAndFormat: Byte, sourceMUID: Int, destinationMUID: Int, status: Byte ) : List { - 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, versionAndFormat: Byte, sourceMUID: Int, destinationMUID: Int, status: Byte, informationData: List ) : List { - 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) @@ -175,7 +178,7 @@ object CIFactory { dst: MutableList, versionAndFormat: Byte, sourceMUID: Int, targetMUID: Int ) : List { - 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) } @@ -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() @@ -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) } diff --git a/ktmidi/src/commonMain/kotlin/dev/atsushieno/ktmidi/ci/CIRetrieval.kt b/ktmidi/src/commonMain/kotlin/dev/atsushieno/ktmidi/ci/CIRetrieval.kt index 926038131..37fe98afa 100644 --- a/ktmidi/src/commonMain/kotlin/dev/atsushieno/ktmidi/ci/CIRetrieval.kt +++ b/ktmidi/src/commonMain/kotlin/dev/atsushieno/ktmidi/ci/CIRetrieval.kt @@ -29,6 +29,12 @@ object CIRetrieval { fun midiCIGetDestinationMUID(sysex: List) = 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) = + 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) = MidiCIProtocolTypeInfo(sysex[0], sysex[1], sysex[2], 0, 0) diff --git a/ktmidi/src/commonMain/kotlin/dev/atsushieno/ktmidi/ci/MidiCIConnection.kt b/ktmidi/src/commonMain/kotlin/dev/atsushieno/ktmidi/ci/MidiCIConnection.kt index 081e12777..8f09f825b 100644 --- a/ktmidi/src/commonMain/kotlin/dev/atsushieno/ktmidi/ci/MidiCIConnection.kt +++ b/ktmidi/src/commonMain/kotlin/dev/atsushieno/ktmidi/ci/MidiCIConnection.kt @@ -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 @@ -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) @@ -100,7 +108,7 @@ class MidiCIInitiator(private val sendOutput: (data: List) -> 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 @@ -127,6 +135,36 @@ class MidiCIInitiator(private val sendOutput: (data: List) -> 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, messageLength: UShort, messageText: List) -> Unit = { _,_,_,_,_,_,_,_ -> } + var onNak: (sourceMUID: Int, destinationMUID: Int, originalTransactionSubId: Byte, nakStatusCode: Byte, nakStatusData: Byte, nakDetailsForEachSubIdClassification: List, messageLength: UShort, messageText: List) -> Unit = { _,_,_,_,_,_,_,_ -> } + private fun defaultProcessAckNak(isNak: Boolean, sourceMUID: Int, destinationMUID: Int, originalTransactionSubId: Byte, statusCode: Byte, statusData: Byte, detailsForEachSubIdClassification: List, messageLength: UShort, messageText: List) { + 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, messageLength: UShort, messageText: List -> + 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, messageLength: UShort, messageText: List -> + defaultProcessAckNak(true, sourceMUID, destinationMUID, originalTransactionSubId, statusCode, statusData, detailsForEachSubIdClassification, messageLength, messageText) + } + var processNak = defaultProcessNak + + private val defaultProcessEndpointMessageResponse = { sourceMUID: Int, destinationMUID: Int, status: Byte, data: List -> + 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. @@ -185,8 +223,6 @@ class MidiCIInitiator(private val sendOutput: (data: List) -> Unit, fun sendEndpointMessage(targetMuid: Int, status: Byte) { val buf = MutableList(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) } @@ -271,19 +307,46 @@ class MidiCIInitiator(private val sendOutput: (data: List) -> 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 -> { @@ -343,6 +406,7 @@ class MidiCIResponder(private val sendOutput: (data: List) -> Unit, var supportedProtocols: List = MidiCIConstants.Midi2ThenMidi1Protocols var profileSet: MutableList> = mutableListOf() var functionBlock: Byte = MidiCIConstants.NO_FUNCTION_BLOCK + var productInstanceId: String? = null var midiCIBufferSize = 128 @@ -353,7 +417,6 @@ class MidiCIResponder(private val sendOutput: (data: List) -> 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 -> @@ -367,6 +430,16 @@ class MidiCIResponder(private val sendOutput: (data: List) -> Unit, } var processDiscovery = defaultProcessDiscovery + private val defaultProcessEndpointMessage: (initiatorMUID: Int, destinationMUID: Int, status: Byte) -> Unit = { initiatorMUID, destinationMUID, status -> + val dst = MutableList(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, initiatorMUID: Int) -> List = // we don't listen to initiator :p { _, _ -> supportedProtocols } @@ -425,7 +498,6 @@ class MidiCIResponder(private val sendOutput: (data: List) -> 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) { if (data[0] != 0x7E.toByte() || data[2] != 0xD.toByte()) return // not MIDI-CI sysex @@ -439,6 +511,14 @@ class MidiCIResponder(private val sendOutput: (data: List) -> 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 -> {