From 57a6e8004d1de171a7cd505edefb4dae59bb7973 Mon Sep 17 00:00:00 2001 From: Atsushi Eno Date: Tue, 13 Jun 2023 23:14:32 +0900 Subject: [PATCH] Add a bunch of new UMP messages in UmpFactory, with tests. They were added to the UMP specification as part of June 2023 updates. --- .../dev/atsushieno/ktmidi/RtMidiAccessTest.kt | 9 +- .../dev/atsushieno/ktmidi/RtMidiPlayerTest.kt | 11 +- ktmidi/api/ktmidi-api.txt | 197 +++++++++- .../dev/atsushieno/ktmidi/MidiConstants.kt | 120 ++++++- .../dev/atsushieno/ktmidi/UmpFactory.kt | 336 +++++++++++++++--- .../dev/atsushieno/ktmidi/UmpRetrieval.kt | 14 +- .../atsushieno/ktmidi/ci/MidiCIConnection.kt | 2 - .../dev/atsushieno/ktmidi/UmpFactoryTest.kt | 276 +++++++++++++- 8 files changed, 883 insertions(+), 82 deletions(-) diff --git a/ktmidi-jvm-desktop/src/test/java/dev/atsushieno/ktmidi/RtMidiAccessTest.kt b/ktmidi-jvm-desktop/src/test/java/dev/atsushieno/ktmidi/RtMidiAccessTest.kt index 20763ff3f..b6477a2c8 100644 --- a/ktmidi-jvm-desktop/src/test/java/dev/atsushieno/ktmidi/RtMidiAccessTest.kt +++ b/ktmidi-jvm-desktop/src/test/java/dev/atsushieno/ktmidi/RtMidiAccessTest.kt @@ -8,11 +8,16 @@ class RtMidiAccessTest { @Test fun rtMidiAccessInfo() { // skip running this test on GH Actions Linux VMs (it's kind of hacky check but would be mostly harmless...) - val homeRunnwrWork = File("/home/runner/work") - if (System.getProperty("os.name").lowercase().contains("linux") && homeRunnwrWork.exists()) { + val homeRunnerWork = File("/home/runner/work") + if (System.getProperty("os.name").lowercase().contains("linux") && homeRunnerWork.exists()) { println("Test rtMidiAccessInfo() is skipped on GitHub Actions as ALSA is unavailable.") return } + // it does not work on M1 mac and arm64 Linux either + if (System.getProperty("os.arch") == "aarch64") { + println("Test rtMidiAccessInfo() is skipped as rtmidi-jna aarch64 builds are not available.") + return + } val rtmidi = RtMidiAccess() diff --git a/ktmidi-jvm-desktop/src/test/java/dev/atsushieno/ktmidi/RtMidiPlayerTest.kt b/ktmidi-jvm-desktop/src/test/java/dev/atsushieno/ktmidi/RtMidiPlayerTest.kt index 979ad018d..f1ab13780 100644 --- a/ktmidi-jvm-desktop/src/test/java/dev/atsushieno/ktmidi/RtMidiPlayerTest.kt +++ b/ktmidi-jvm-desktop/src/test/java/dev/atsushieno/ktmidi/RtMidiPlayerTest.kt @@ -19,9 +19,14 @@ class RtMidiPlayerTest { @Test fun playSimple() { // skip running this test on GH Actions Linux VMs (it's kind of hacky check but would be mostly harmless...) - val homeRunnwrWork = File("/home/runner/work") - if (System.getProperty("os.name").lowercase().contains("linux") && homeRunnwrWork.exists()) { - println("Test rtMidiAccessInfo() is skipped on GitHub Actions as ALSA is unavailable.") + val homeRunnerWork = File("/home/runner/work") + if (System.getProperty("os.name").lowercase().contains("linux") && homeRunnerWork.exists()) { + println("Test playSimple() is skipped on GitHub Actions as ALSA is unavailable.") + return + } + // it does not work on M1 mac and arm64 Linux either + if (System.getProperty("os.arch") == "aarch64") { + println("Test playSimple() is skipped as rtmidi-jna aarch64 builds are not available.") return } diff --git a/ktmidi/api/ktmidi-api.txt b/ktmidi/api/ktmidi-api.txt index 358d44336..32fe3b839 100644 --- a/ktmidi/api/ktmidi-api.txt +++ b/ktmidi/api/ktmidi-api.txt @@ -12,6 +12,59 @@ package dev.atsushieno.ktmidi { property @NonNull public Iterable outputs; } + public final class ChordAlterationType { + field public static final byte ADD_DEGREE = 16; // 0x10 + field @NonNull public static final dev.atsushieno.ktmidi.ChordAlterationType INSTANCE; + field public static final byte LOWER_DEGREE = 64; // 0x40 + field public static final byte NO_ALTERATION = 0; // 0x0 + field public static final byte RAISE_DEGREE = 48; // 0x30 + field public static final byte SUBTRACT_DEGREE = 32; // 0x20 + } + + public final class ChordSharpFlatsField { + field public static final byte BASS_NOTE_AS_CHORD_TONIC_NOTE = -8; // 0xfffffff8 + field public static final byte DOUBLE_FLAT = -2; // 0xfffffffe + field public static final byte DOUBLE_SHARP = 2; // 0x2 + field public static final byte FLAT = -1; // 0xffffffff + field @NonNull public static final dev.atsushieno.ktmidi.ChordSharpFlatsField INSTANCE; + field public static final byte NATURAL = 0; // 0x0 + field public static final byte SHARP = 1; // 0x1 + } + + public final class ChordTypeField { + field public static final byte AUGMENTED = 17; // 0x11 + field public static final byte AUGMENTED_7TH = 18; // 0x12 + field public static final byte CLEAR_CHORD = 0; // 0x0 + field public static final byte DIMINISHED = 19; // 0x13 + field public static final byte DIMINISHED_7TH = 20; // 0x14 + field public static final byte DOMINANT = 13; // 0xd + field public static final byte DOMINANT_11TH = 15; // 0xf + field public static final byte DOMINANT_13TH = 16; // 0x10 + field public static final byte DOMINANT_9TH = 14; // 0xe + field public static final byte HALF_DIMINISHED = 21; // 0x15 + field @NonNull public static final dev.atsushieno.ktmidi.ChordTypeField INSTANCE; + field public static final byte MAJOR = 1; // 0x1 + field public static final byte MAJOR_11TH = 5; // 0x5 + field public static final byte MAJOR_13TH = 6; // 0x6 + field public static final byte MAJOR_6TH = 2; // 0x2 + field public static final byte MAJOR_7TH = 3; // 0x3 + field public static final byte MAJOR_9TH = 4; // 0x4 + field public static final byte MAJOR_MINOR = 22; // 0x16 + field public static final byte MINOR = 7; // 0x7 + field public static final byte MINOR_11TH = 11; // 0xb + field public static final byte MINOR_13TH = 12; // 0xc + field public static final byte MINOR_6TH = 8; // 0x8 + field public static final byte MINOR_7TH = 9; // 0x9 + field public static final byte MINOR_9TH = 10; // 0xa + field public static final byte MINOR_MAJOR = 22; // 0x16 + field public static final byte NO_CHORD = 0; // 0x0 + field public static final byte PEDAL = 23; // 0x17 + field public static final byte POWER = 24; // 0x18 + field public static final byte SEVENTH_SUSPENDED_4TH = 27; // 0x1b + field public static final byte SUSPENDED_2ND = 25; // 0x19 + field public static final byte SUSPENDED_4TH = 26; // 0x1a + } + public final class DefaultMidiModuleDatabase extends dev.atsushieno.ktmidi.MidiModuleDatabase { ctor public DefaultMidiModuleDatabase(); method @NonNull public Iterable all(); @@ -43,6 +96,28 @@ package dev.atsushieno.ktmidi { property @NonNull public Iterable outputs; } + public final class FlexDataAddress { + field public static final byte CHANNEL_FIELD = 0; // 0x0 + field public static final byte GROUP = 1; // 0x1 + field @NonNull public static final dev.atsushieno.ktmidi.FlexDataAddress INSTANCE; + } + + public final class FlexDataStatus { + field public static final byte CHORD_NAME = 6; // 0x6 + field @NonNull public static final dev.atsushieno.ktmidi.FlexDataStatus INSTANCE; + field public static final byte KEY_SIGNATURE = 5; // 0x5 + field public static final byte METRONOME = 2; // 0x2 + field public static final byte TEMPO = 0; // 0x0 + field public static final byte TIME_SIGNATURE = 1; // 0x1 + } + + public final class FlexDataStatusBank { + field @NonNull public static final dev.atsushieno.ktmidi.FlexDataStatusBank INSTANCE; + field public static final byte METADATA_TEXT = 1; // 0x1 + field public static final byte PERFORMANCE_TEXT = 2; // 0x2 + field public static final byte SETUP_AND_PERFORMANCE = 0; // 0x0 + } + public final class GeneralMidi { ctor public GeneralMidi(); field @NonNull public static final dev.atsushieno.ktmidi.GeneralMidi.Companion Companion; @@ -299,6 +374,23 @@ package dev.atsushieno.ktmidi { property @NonNull public final java.util.List list; } + public final class MetadataTextStatus { + field public static final byte ACCOMPANYING_PERFORMER = 10; // 0xa + field public static final byte ARRANGER = 7; // 0x7 + field public static final byte AUTHOR = 5; // 0x5 + field public static final byte COMPOSITION_NAME = 2; // 0x2 + field public static final byte COPYRIGHT = 4; // 0x4 + field @NonNull public static final dev.atsushieno.ktmidi.MetadataTextStatus INSTANCE; + field public static final byte LYRICIST = 6; // 0x6 + field public static final byte MIDI_CLIP_NAME = 3; // 0x3 + field public static final byte PRIMARY_PERFORMER = 9; // 0x9 + field public static final byte PROJECT_NAME = 1; // 0x1 + field public static final byte PUBLISHER = 8; // 0x8 + field public static final byte RECORDING_CONCERT_DATE = 11; // 0xb + field public static final byte RECORDING_CONCERT_LOCATION = 12; // 0xc + field public static final byte UNKNOWN = 0; // 0x0 + } + public final class Midi1Player extends dev.atsushieno.ktmidi.MidiPlayer { ctor public Midi1Player(@NonNull dev.atsushieno.ktmidi.MidiMusic music, @NonNull dev.atsushieno.ktmidi.MidiOutput output, optional @NonNull dev.atsushieno.ktmidi.MidiPlayerTimer timer, optional boolean shouldDisposeOutput); method public void addOnMessageListener(@NonNull dev.atsushieno.ktmidi.OnMidiMessageListener listener); @@ -398,7 +490,7 @@ package dev.atsushieno.ktmidi { property @NonNull public final Iterable usedChannels; } - public static interface Midi2Machine.Listener { + public static fun interface Midi2Machine.Listener { method public void onEvent(@NonNull dev.atsushieno.ktmidi.Ump e); } @@ -816,12 +908,14 @@ package dev.atsushieno.ktmidi { } public final class MidiMessageType { + field public static final int FlexData = 13; // 0xd field @NonNull public static final dev.atsushieno.ktmidi.MidiMessageType INSTANCE; field public static final int MIDI1 = 2; // 0x2 field public static final int MIDI2 = 4; // 0x4 field public static final int SYSEX7 = 3; // 0x3 field public static final int SYSEX8_MDS = 5; // 0x5 field public static final int SYSTEM = 1; // 0x1 + field public static final int UMP_STREAM = 15; // 0xf field public static final int UTILITY = 0; // 0x0 } @@ -1109,28 +1203,39 @@ package dev.atsushieno.ktmidi { } public final class MidiUtilityStatus { + field public static final int DCTPQ = 48; // 0x30 + field public static final int DELTA_CLOCKSTAMP = 64; // 0x40 field @NonNull public static final dev.atsushieno.ktmidi.MidiUtilityStatus INSTANCE; field public static final int JR_CLOCK = 16; // 0x10 field public static final int JR_TIMESTAMP = 32; // 0x20 field public static final int NOP = 0; // 0x0 } - public interface OnMidi2EventListener { + public fun interface OnMidi2EventListener { method public void onEvent(@NonNull dev.atsushieno.ktmidi.Ump e); } - public interface OnMidiEventListener { + public fun interface OnMidiEventListener { method public void onEvent(@NonNull dev.atsushieno.ktmidi.MidiEvent e); } - public interface OnMidiMessageListener { + public fun interface OnMidiMessageListener { method public void onMessage(@NonNull dev.atsushieno.ktmidi.MidiMessage m); } - public interface OnMidiReceivedEventListener { + public fun interface OnMidiReceivedEventListener { method public void onEventReceived(@NonNull byte[] data, int start, int length, long timestampInNanoseconds); } + public final class PerformanceTextStatus { + field @NonNull public static final dev.atsushieno.ktmidi.PerformanceTextStatus INSTANCE; + field public static final byte LYRICS = 1; // 0x1 + field public static final byte LYRICS_LANGUAGE = 2; // 0x2 + field public static final byte RUBY = 3; // 0x3 + field public static final byte RUBY_LANGUAGE = 4; // 0x4 + field public static final byte UNKNOWN = 0; // 0x0 + } + public enum PlayerState { enum_constant public static final dev.atsushieno.ktmidi.PlayerState PAUSED; enum_constant public static final dev.atsushieno.ktmidi.PlayerState PLAYING; @@ -1238,6 +1343,19 @@ package dev.atsushieno.ktmidi { method @Nullable public suspend Object getMidiPlayer(optional @Nullable dev.atsushieno.ktmidi.MidiPlayerTimer timeManager, optional @Nullable dev.atsushieno.ktmidi.MidiAccess midiAccess, optional @Nullable String resourceId, optional @NonNull kotlin.coroutines.Continuation p); } + public final class TonicNoteField { + field public static final byte A = 1; // 0x1 + field public static final byte B = 2; // 0x2 + field public static final byte C = 3; // 0x3 + field public static final byte D = 4; // 0x4 + field public static final byte E = 5; // 0x5 + field public static final byte F = 6; // 0x6 + field public static final byte G = 7; // 0x7 + field @NonNull public static final dev.atsushieno.ktmidi.TonicNoteField INSTANCE; + field public static final byte NON_STANDARD = 0; // 0x0 + field public static final byte UNKNOWN = 0; // 0x0 + } + public final class Ump { ctor public Ump(int int1, optional int int2, optional int int3, optional int int4); ctor public Ump(long long1, optional long long2); @@ -1268,21 +1386,46 @@ package dev.atsushieno.ktmidi { } public final class UmpFactory { + method @NonNull public dev.atsushieno.ktmidi.Ump chordName(byte group, byte address, byte channel, byte tonicSharpsFlats, byte chordTonic, byte chordType, int alter1, int alter2, int alter3, int alter4, byte bassSharpsFlats, byte bassNote, byte bassChordType, int bassAlter1, int bassAlter2); + method public int dctpq(short numberOfTicksPerQuarterNote); + method public int deltaClockstamp(int ticks20); + method @NonNull public dev.atsushieno.ktmidi.Ump deviceIdentityNotification(@NonNull dev.atsushieno.ktmidi.ci.DeviceDetails device); + method @NonNull public dev.atsushieno.ktmidi.Ump endOfClip(); + method @NonNull public dev.atsushieno.ktmidi.Ump endpointDiscovery(byte umpVersionMajor, byte umpVersionMinor, byte filterBitmap); + method @NonNull public dev.atsushieno.ktmidi.Ump endpointInfoNotification(byte umpVersionMajor, byte umpVersionMinor, boolean isStaticFunctionBlock, byte functionBlockCount, boolean midi2Capable, boolean midi1Capable, boolean supportsRxJR, boolean supportsTxJR); + method @NonNull public java.util.List endpointNameNotification(@NonNull String name); + method @NonNull public java.util.List endpointNameNotification(@NonNull byte[] name); + method public void flexDataProcess(byte group, byte address, byte channel, byte statusBank, byte status, @NonNull byte[] text, optional @Nullable Object context, optional @NonNull kotlin.jvm.functions.Function2 sendUMP128); + method @NonNull public java.util.List flexDataText(byte group, byte address, byte channel, byte statusBank, byte status, @NonNull String text); + method @NonNull public java.util.List flexDataText(byte group, byte address, byte channel, byte statusBank, byte status, @NonNull byte[] text); method @NonNull public Iterable fromPlatformBytes(@NonNull io.ktor.utils.io.core.ByteOrder byteOrder, @NonNull java.util.List bytes); method @NonNull public Iterable fromPlatformNativeBytes(@NonNull java.util.List bytes); - method public int jrClock(int group, int senderClockTime16); - method public int jrClock(int group, double senderClockTimeSeconds); - method public int jrTimestamp(int group, int senderClockTimestamp16); - method public int jrTimestamp(int group, double senderClockTimestampSeconds); - method @NonNull public kotlin.sequences.Sequence jrTimestamps(int group, long senderClockTimestampTicks); - method @NonNull public kotlin.sequences.Sequence jrTimestamps(int group, double senderClockTimestampSeconds); - method @NonNull public java.util.List mds(int group, @NonNull java.util.List data, optional byte mdsId); + method @NonNull public dev.atsushieno.ktmidi.Ump functionBlockDiscovery(byte fbNumber, byte filter); + method @NonNull public dev.atsushieno.ktmidi.Ump functionBlockInfoNotification(boolean isFbActive, byte fbNumber, byte uiHint, byte midi1, byte direction, byte firstGroup, byte numberOfGroupsSpanned, byte midiCIMessageVersionFormat, byte maxSysEx8Streams); + method @NonNull public java.util.List functionBlockNameNotification(byte blockNumber, @NonNull String name); + method public int jrClock(int senderClockTime16); + method @Deprecated public int jrClock(int group, int senderClockTime16); + method public int jrClock(double senderClockTimeSeconds); + method @Deprecated public int jrClock(int group, double senderClockTimeSeconds); + method public int jrTimestamp(int senderClockTimestamp16); + method @Deprecated public int jrTimestamp(int group, int senderClockTimestamp16); + method public int jrTimestamp(double senderClockTimestampSeconds); + method @Deprecated public int jrTimestamp(int group, double senderClockTimestampSeconds); + method @NonNull public kotlin.sequences.Sequence jrTimestamps(long senderClockTimestampTicks); + method @Deprecated @NonNull public kotlin.sequences.Sequence jrTimestamps(int group, long senderClockTimestampTicks); + method @NonNull public kotlin.sequences.Sequence jrTimestamps(double senderClockTimestampSeconds); + method @Deprecated @NonNull public kotlin.sequences.Sequence jrTimestamps(int group, double senderClockTimestampSeconds); + method @NonNull public dev.atsushieno.ktmidi.Ump keySignature(byte group, byte address, byte channel, byte sharpsOrFlats, byte tonicNote); + method @NonNull public java.util.List mds(byte group, @NonNull java.util.List data, optional byte mdsId); + method @Deprecated @NonNull public java.util.List mds(int group, @NonNull java.util.List data, optional byte mdsId); method public int mdsGetChunkCount(int numTotalBytesInMDS); method @NonNull public kotlin.Pair mdsGetHeader(byte group, byte mdsId, int numBytesInChunk16, int numChunks16, int chunkIndex16, int manufacturerId16, int deviceId16, int subId16, int subId2_16); - method public int mdsGetPayloadCount(int numTotalBytesinChunk); + method public int mdsGetPayloadCount(int numTotalBytesInChunk); method @NonNull public kotlin.Pair mdsGetPayloadOf(byte group, byte mdsId, int numBytes16, @NonNull java.util.List srcData, int offset); - method public void mdsProcess(int group, byte mdsId, @NonNull java.util.List data, optional @Nullable Object context, @NonNull kotlin.jvm.functions.Function5 sendUmp); - method @Deprecated public void mdsProcess(byte group, byte mdsId, @NonNull java.util.List data, int length, @NonNull kotlin.jvm.functions.Function5 sendUmp, @Nullable Object context); + method public void mdsProcess(byte group, byte mdsId, @NonNull java.util.List data, optional @Nullable Object context, @NonNull kotlin.jvm.functions.Function5 sendUmp); + method @NonNull public java.util.List metadataText(byte group, byte address, byte channel, byte status, @NonNull String text); + method @NonNull public java.util.List metadataText(byte group, byte address, byte channel, byte status, @NonNull byte[] text); + method @NonNull public dev.atsushieno.ktmidi.Ump metronome(byte group, byte channel, byte numClocksPerPrimeryClick, byte barAccent1, byte barAccent2, byte barAccent3, byte numSubdivisionClick1, byte numSubdivisionClick2); method public int midi1CAf(int group, int channel, byte data); method public int midi1CC(int group, int channel, byte index, byte data); method public int midi1Message(int group, byte code, int channel, byte byte3, byte byte4); @@ -1312,15 +1455,22 @@ package dev.atsushieno.ktmidi { method public long midi2RPN(int group, int channel, int bankAkaMSB8, int indexAkaLSB8, long dataAkaDTE32); method public long midi2RelativeNRPN(int group, int channel, int bankAkaMSB8, int indexAkaLSB8, long dataAkaDTE32); method public long midi2RelativeRPN(int group, int channel, int bankAkaMSB8, int indexAkaLSB8, long dataAkaDTE32); - method public int noop(int group); + method public int noop(); + method @Deprecated public int noop(int group); + method @NonNull public java.util.List performanceText(byte group, byte address, byte channel, byte status, @NonNull String text); + method @NonNull public java.util.List performanceText(byte group, byte address, byte channel, byte status, @NonNull byte[] text); method public int pitch7_9(double pitch); method public int pitch7_9Split(byte semitone, double microtone0To1); + method @NonNull public java.util.List productInstanceNotification(@NonNull String id); + method @NonNull public java.util.List productInstanceNotification(@NonNull byte[] id); + method @NonNull public dev.atsushieno.ktmidi.Ump startOfClip(); + method @NonNull public dev.atsushieno.ktmidi.Ump streamConfigNotification(byte protocol, boolean rxJRTimestamp, boolean txJRTimestamp); + method @NonNull public dev.atsushieno.ktmidi.Ump streamConfigRequest(byte protocol, boolean rxJRTimestamp, boolean txJRTimestamp); method @NonNull public java.util.List sysex7(int group, @NonNull java.util.List sysex); method public long sysex7Direct(int group, byte status, int numBytes, byte data1, byte data2, byte data3, byte data4, byte data5, byte data6); method public int sysex7GetPacketCount(int numSysex7Bytes); method public long sysex7GetPacketOf(int group, int numBytes, @NonNull java.util.List srcData, int index); method public int sysex7GetSysexLength(@NonNull java.util.List srcData); - method @Deprecated public void sysex7Process(int group, @NonNull java.util.List sysex, @NonNull kotlin.jvm.functions.Function2 sendUMP64, @Nullable Object context); method public void sysex7Process(int group, @NonNull java.util.List sysex, optional @Nullable Object context, optional @NonNull kotlin.jvm.functions.Function2 sendUMP64); method @NonNull public java.util.List sysex8(int group, @NonNull java.util.List sysex, optional byte streamId); method public int sysex8GetPacketCount(int numBytes); @@ -1328,6 +1478,8 @@ package dev.atsushieno.ktmidi { method @Deprecated public void sysex8Process(int group, @NonNull java.util.List sysex, byte streamId, @NonNull kotlin.jvm.functions.Function3 sendUMP128, @Nullable Object context); method public void sysex8Process(int group, @NonNull java.util.List sysex, optional byte streamId, optional @Nullable Object context, optional @NonNull kotlin.jvm.functions.Function3 sendUMP128); method public int systemMessage(int group, byte status, byte midi1Byte2, byte midi1Byte3); + method @NonNull public dev.atsushieno.ktmidi.Ump tempo(byte group, byte channel, int numberOf10NanosecondsPerQuarterNote); + method @NonNull public dev.atsushieno.ktmidi.Ump timeSignature(byte group, byte channel, byte numerator, byte denominator, byte numberOf32Notes); method public int umpGetNumBytes(int data); field @NonNull public static final dev.atsushieno.ktmidi.UmpFactory INSTANCE; } @@ -1340,8 +1492,15 @@ package dev.atsushieno.ktmidi { public final class UmpFactoryTest { ctor public UmpFactoryTest(); method public void fromPlatformBytes(); + method public void performanceText(); method public void sysex7Process(); method public void sysex8Process(); + method public void testChordName(); + method public void testKeySignature(); + method public void testMetadataText(); + method public void testMetronome(); + method public void testTempo(); + method public void testTimeSignature(); method public void testType0Messages(); method public void testType1Messages(); method public void testType2Messages(); @@ -1355,6 +1514,8 @@ package dev.atsushieno.ktmidi { public final class UmpRetrievalKt { method public static int getChannelInGroup(@NonNull dev.atsushieno.ktmidi.Ump); + method public static int getDctpq(@NonNull dev.atsushieno.ktmidi.Ump); + method public static int getDeltaClockstamp(@NonNull dev.atsushieno.ktmidi.Ump); method @Deprecated public static int getEventType(@NonNull dev.atsushieno.ktmidi.Ump); method public static int getGroup(@NonNull dev.atsushieno.ktmidi.Ump); method public static int getGroupAndChannel(@NonNull dev.atsushieno.ktmidi.Ump); @@ -1411,6 +1572,8 @@ package dev.atsushieno.ktmidi { method public static int getSysex7Size(@NonNull dev.atsushieno.ktmidi.Ump); method public static int getSysex8Size(@NonNull dev.atsushieno.ktmidi.Ump); method public static int getSysex8StreamId(@NonNull dev.atsushieno.ktmidi.Ump); + method public static boolean isDCTPQ(@NonNull dev.atsushieno.ktmidi.Ump); + method public static boolean isDeltaClockstamp(@NonNull dev.atsushieno.ktmidi.Ump); method public static boolean isJRClock(@NonNull dev.atsushieno.ktmidi.Ump); method public static boolean isJRTimestamp(@NonNull dev.atsushieno.ktmidi.Ump); method public static void toPlatformBytes(@NonNull dev.atsushieno.ktmidi.Ump, @NonNull byte[] bytes, int offset, @NonNull io.ktor.utils.io.core.ByteOrder byteOrder); diff --git a/ktmidi/src/commonMain/kotlin/dev/atsushieno/ktmidi/MidiConstants.kt b/ktmidi/src/commonMain/kotlin/dev/atsushieno/ktmidi/MidiConstants.kt index 045bcd412..c54113890 100644 --- a/ktmidi/src/commonMain/kotlin/dev/atsushieno/ktmidi/MidiConstants.kt +++ b/ktmidi/src/commonMain/kotlin/dev/atsushieno/ktmidi/MidiConstants.kt @@ -7,12 +7,16 @@ object MidiMessageType { // MIDI 2.0 const val SYSEX7 = 3 const val MIDI2 = 4 const val SYSEX8_MDS = 5 + const val FlexData = 0xD // June 2023 updates + const val UMP_STREAM = 0xF // June 2023 updates } object MidiUtilityStatus { - const val NOP = 0 - const val JR_CLOCK = 0x10 - const val JR_TIMESTAMP = 0x20 + const val NOP = 0x0000 + const val JR_CLOCK = 0x0010 + const val JR_TIMESTAMP = 0x0020 + const val DCTPQ = 0x0030 + const val DELTA_CLOCKSTAMP = 0x0040 } object MidiSystemStatus { @@ -55,6 +59,116 @@ object Midi2BinaryChunkStatus { const val MDS_PAYLOAD = 0x90 } +object FlexDataAddress { + const val CHANNEL_FIELD: Byte = 0 + const val GROUP: Byte = 1 + // 2, 3 -> reserved +} + +object FlexDataStatusBank { + const val SETUP_AND_PERFORMANCE: Byte = 0 + const val METADATA_TEXT: Byte = 1 + const val PERFORMANCE_TEXT: Byte = 2 + // 3-FF -> reserved +} + +object FlexDataStatus { + const val TEMPO: Byte = 0 + const val TIME_SIGNATURE: Byte = 1 + const val METRONOME: Byte = 2 + const val KEY_SIGNATURE: Byte = 5 + const val CHORD_NAME: Byte = 6 +} + +object MetadataTextStatus { + const val UNKNOWN: Byte = 0 + const val PROJECT_NAME: Byte = 1 + const val COMPOSITION_NAME: Byte = 2 + const val MIDI_CLIP_NAME: Byte = 3 + const val COPYRIGHT: Byte = 4 + const val AUTHOR: Byte = 5 + const val LYRICIST: Byte = 6 + const val ARRANGER: Byte = 7 + const val PUBLISHER: Byte = 8 + const val PRIMARY_PERFORMER: Byte = 9 + const val ACCOMPANYING_PERFORMER: Byte = 0xA + const val RECORDING_CONCERT_DATE: Byte = 0xB + const val RECORDING_CONCERT_LOCATION: Byte = 0xC +} + +object PerformanceTextStatus { + const val UNKNOWN: Byte = 0 + const val LYRICS: Byte = 1 + const val LYRICS_LANGUAGE: Byte = 2 + const val RUBY: Byte = 3 + const val RUBY_LANGUAGE: Byte = 4 +} + +// Used by "Key Signature" Flex Data messages +object TonicNoteField { + const val UNKNOWN: Byte = 0 + const val NON_STANDARD: Byte = 0 + const val A: Byte = 1 + const val B: Byte = 2 + const val C: Byte = 3 + const val D: Byte = 4 + const val E: Byte = 5 + const val F: Byte = 6 + const val G: Byte = 7 + // 8-FF -> reserved +} + +object ChordSharpFlatsField { + const val DOUBLE_SHARP: Byte = 2 + const val SHARP: Byte = 1 + const val NATURAL: Byte = 0 + const val FLAT: Byte = -1 + const val DOUBLE_FLAT: Byte = -2 + const val BASS_NOTE_AS_CHORD_TONIC_NOTE: Byte = -8 +} + +object ChordTypeField { + const val CLEAR_CHORD: Byte = 0 + const val NO_CHORD: Byte = 0 + const val MAJOR: Byte = 1 + const val MAJOR_6TH: Byte = 2 + const val MAJOR_7TH: Byte = 3 + const val MAJOR_9TH: Byte = 4 + const val MAJOR_11TH: Byte = 5 + const val MAJOR_13TH: Byte = 6 + const val MINOR: Byte = 7 + const val MINOR_6TH: Byte = 8 + const val MINOR_7TH: Byte = 9 + const val MINOR_9TH: Byte = 0xA + const val MINOR_11TH: Byte = 0xB + const val MINOR_13TH: Byte = 0xC + const val DOMINANT: Byte = 0xD + const val DOMINANT_9TH: Byte = 0xE + const val DOMINANT_11TH: Byte = 0xF + const val DOMINANT_13TH: Byte = 0x10 + const val AUGMENTED: Byte = 0x11 + const val AUGMENTED_7TH: Byte = 0x12 + const val DIMINISHED: Byte = 0x13 + const val DIMINISHED_7TH: Byte = 0x14 + const val HALF_DIMINISHED: Byte = 0x15 + const val MAJOR_MINOR: Byte = 0x16 + const val MINOR_MAJOR: Byte = 0x16 + const val PEDAL: Byte = 0x17 + const val POWER: Byte = 0x18 + const val SUSPENDED_2ND: Byte = 0x19 + const val SUSPENDED_4TH: Byte = 0x1A + const val SEVENTH_SUSPENDED_4TH: Byte = 0x1B +} + +// I choose Int because there will be degree to be added, then the results will become Int anyway! +object ChordAlterationType { + const val NO_ALTERATION: UByte = 0x00U + const val ADD_DEGREE: UByte = 0x10U + const val SUBTRACT_DEGREE: UByte = 0x20U + const val RAISE_DEGREE: UByte = 0x30U + const val LOWER_DEGREE: UByte = 0x40U +} + object MidiCIProtocolBytes { // MIDI 2.0 const val TYPE = 0 const val VERSION = 1 diff --git a/ktmidi/src/commonMain/kotlin/dev/atsushieno/ktmidi/UmpFactory.kt b/ktmidi/src/commonMain/kotlin/dev/atsushieno/ktmidi/UmpFactory.kt index 607e8e850..4602bc302 100644 --- a/ktmidi/src/commonMain/kotlin/dev/atsushieno/ktmidi/UmpFactory.kt +++ b/ktmidi/src/commonMain/kotlin/dev/atsushieno/ktmidi/UmpFactory.kt @@ -2,8 +2,11 @@ package dev.atsushieno.ktmidi +import dev.atsushieno.ktmidi.ci.DeviceDetails +import io.ktor.utils.io.charsets.* import io.ktor.utils.io.core.* import kotlin.experimental.and +import kotlin.math.min internal infix fun Byte.shl(n: Int): Int = this.toInt() shl n internal infix fun Byte.shr(n: Int): Int = this.toInt() shr n @@ -14,6 +17,7 @@ const val JR_TIMESTAMP_TICKS_PER_SECOND = 31250 const val MIDI_2_0_RESERVED: Byte = 0 typealias UmpMdsHandler = (Long, Long, Int, Int, Any?) -> Unit +typealias UmpStreamHandler = (Long, Long, Int, Int, Any?) -> Unit object UmpFactory { @@ -26,37 +30,58 @@ object UmpFactory { return 0 /* wrong */ } - // 4.8 Utility Messages - fun noop(group: Int): Int { - return group and 0xF shl 24 + // 4.8 Utility Messages (note that they became groupless since 2023 June updates) + fun noop(): Int { + return 0 } - fun jrClock(group: Int, senderClockTime16: Int): Int { - return noop(group) + (MidiUtilityStatus.JR_CLOCK shl 16) + senderClockTime16 + @Deprecated("group has vanished in UMP June 2023 updates", replaceWith = ReplaceWith("noop()")) + fun noop(group: Int) = noop() + + fun jrClock(senderClockTime16: Int): Int { + return (MidiUtilityStatus.JR_CLOCK shl 16) + senderClockTime16 } + @Deprecated("group has vanished in UMP June 2023 updates", replaceWith = ReplaceWith("jrClock(senderClockTime16)")) + fun jrClock(group: Int, senderClockTime16: Int) = jrClock(senderClockTime16) - fun jrClock(group: Int, senderClockTimeSeconds: Double): Int { + fun jrClock(senderClockTimeSeconds: Double): Int { val value = (senderClockTimeSeconds * JR_TIMESTAMP_TICKS_PER_SECOND).toInt() - return noop(group) + (MidiUtilityStatus.JR_CLOCK shl 16) + value + return (MidiUtilityStatus.JR_CLOCK shl 16) + value } + @Deprecated("group has vanished in UMP June 2023 updates", replaceWith = ReplaceWith("jrClock(senderClockTimeSeconds)")) + fun jrClock(group: Int, senderClockTimeSeconds: Double) = jrClock(senderClockTimeSeconds) - fun jrTimestamp(group: Int, senderClockTimestamp16: Int): Int { + fun jrTimestamp(senderClockTimestamp16: Int): Int { if (senderClockTimestamp16 > 0xFFFF) throw IllegalArgumentException("Argument timestamp value must be less than 65536. If you need multiple JR timestamps, use umpJRTimestamps() instead.") - return noop(group) + (MidiUtilityStatus.JR_TIMESTAMP shl 16) + senderClockTimestamp16 + return (MidiUtilityStatus.JR_TIMESTAMP shl 16) + senderClockTimestamp16 } + @Deprecated("group has vanished in UMP June 2023 updates", replaceWith = ReplaceWith("jrTimestamp(senderClockTimestamp16)")) + fun jrTimestamp(group: Int, senderClockTimestamp16: Int) = jrTimestamp(senderClockTimestamp16) - fun jrTimestamp(group: Int, senderClockTimestampSeconds: Double) = - jrTimestamp(group, ((senderClockTimestampSeconds * JR_TIMESTAMP_TICKS_PER_SECOND).toInt())) + fun jrTimestamp(senderClockTimestampSeconds: Double) = + jrTimestamp(((senderClockTimestampSeconds * JR_TIMESTAMP_TICKS_PER_SECOND).toInt())) + @Deprecated("group has vanished in UMP June 2023 updates", replaceWith = ReplaceWith("jrTimestamp(senderClockTimestampSeconds)")) + fun jrTimestamp(group: Int, senderClockTimestampSeconds: Double) = jrTimestamp(senderClockTimestampSeconds) - fun jrTimestamps(group: Int, senderClockTimestampTicks: Long): Sequence = sequence { + fun jrTimestamps(senderClockTimestampTicks: Long): Sequence = sequence { for (i in 0 until senderClockTimestampTicks / 0x10000) - yield(jrTimestamp(group, 0xFFFF)) - yield(jrTimestamp(group, (senderClockTimestampTicks % 0x10000).toInt())) + yield(jrTimestamp(0xFFFF)) + yield(jrTimestamp((senderClockTimestampTicks % 0x10000).toInt())) } + @Deprecated("group has vanished in UMP June 2023 updates", replaceWith = ReplaceWith("jrTimestamps(senderClockTimestampTicks)")) + fun jrTimestamps(group: Int, senderClockTimestampTicks: Long): Sequence = jrTimestamps(senderClockTimestampTicks) + + fun jrTimestamps(senderClockTimestampSeconds: Double) = + jrTimestamps((senderClockTimestampSeconds * JR_TIMESTAMP_TICKS_PER_SECOND).toLong()) + @Deprecated("group has vanished in UMP June 2023 updates", replaceWith = ReplaceWith("jrTimestamps(senderClockTimestampSeconds)")) + fun jrTimestamps(group: Int, senderClockTimestampSeconds: Double) = jrTimestamps(senderClockTimestampSeconds) + + // June 2023 updates + fun dctpq(numberOfTicksPerQuarterNote: UShort) = (MidiUtilityStatus.DCTPQ shl 16) + numberOfTicksPerQuarterNote.toInt() - fun jrTimestamps(group: Int, senderClockTimestampSeconds: Double) = - jrTimestamps(group, (senderClockTimestampSeconds * JR_TIMESTAMP_TICKS_PER_SECOND).toLong()) + // June 2023 updates + fun deltaClockstamp(ticks20: Int) = (MidiUtilityStatus.DELTA_CLOCKSTAMP shl 16) + (ticks20 and 0xFFFFF) // ticks20(bits) - 0..1048575 // 4.3 System Common and System Real Time Messages fun systemMessage(group: Int, status: Byte, midi1Byte2: Byte, midi1Byte3: Byte): Int { @@ -363,7 +388,7 @@ object UmpFactory { return (src shr (7 - index) * 8 and 0xFFu).toByte() } - private fun sysexGetPacketCount(numBytes: Int, radix: Int): Int { + private fun getPacketCountCommon(numBytes: Int, radix: Int): Int { return if (numBytes <= radix) 1 else (numBytes / radix + if (numBytes % radix != 0) 1 else 0) } @@ -396,7 +421,7 @@ object UmpFactory { status = Midi2BinaryChunkStatus.SYSEX_START size = radix } else { - val isEnd = index == sysexGetPacketCount(numBytes8, radix) - 1 + val isEnd = index == getPacketCountCommon(numBytes8, radix) - 1 if (isEnd) { size = if (numBytes8 % radix != 0) numBytes8 % radix else radix status = Midi2BinaryChunkStatus.SYSEX_END @@ -445,9 +470,7 @@ object UmpFactory { return i - if (srcData[0] == 0xF0.toByte()) 1 else 0 } - fun sysex7GetPacketCount(numSysex7Bytes: Int): Int { - return sysexGetPacketCount(numSysex7Bytes, 6) - } + fun sysex7GetPacketCount(numSysex7Bytes: Int): Int = getPacketCountCommon(numSysex7Bytes, 6) fun sysex7GetPacketOf(group: Int, numBytes: Int, srcData: List, index: Int): Long { val srcOffset = if (numBytes > 0 && srcData[0] == 0xF0.toByte()) 1 else 0 @@ -465,21 +488,6 @@ object UmpFactory { return result.first } - @Deprecated("Use another sysex7Process overload that has sendUMP64 as the last parameter") - fun sysex7Process( - group: Int, - sysex: List, - sendUMP64: (Long, Any?) -> Unit, - context: Any? - ) { - val length: Int = sysex7GetSysexLength(sysex) - val numPackets: Int = sysex7GetPacketCount(length) - for (p in 0 until numPackets) { - val ump = sysex7GetPacketOf(group, length, sysex, p) - sendUMP64(ump, context) - } - } - fun sysex7Process( group: Int, sysex: List, @@ -505,7 +513,7 @@ object UmpFactory { // 4.5 System Exclusive 8-Bit Messages fun sysex8GetPacketCount(numBytes: Int): Int { - return sysexGetPacketCount(numBytes, 13) + return getPacketCountCommon(numBytes, 13) } fun sysex8GetPacketOf( @@ -573,8 +581,8 @@ object UmpFactory { return numTotalBytesInMDS / radix + if (numTotalBytesInMDS % radix != 0) 1 else 0 } - fun mdsGetPayloadCount(numTotalBytesinChunk: Int): Int { - return numTotalBytesinChunk / 14 + if (numTotalBytesinChunk % 14 != 0) 1 else 0 + fun mdsGetPayloadCount(numTotalBytesInChunk: Int): Int { + return numTotalBytesInChunk / 14 + if (numTotalBytesInChunk % 14 != 0) 1 else 0 } private fun fillShort(dst8: ByteArray, offset: Int, v16: Int) { @@ -621,15 +629,11 @@ object UmpFactory { return Pair(readInt64Bytes(dst8.asList(), 0), readInt64Bytes(dst8.asList(), 8)) } - fun mdsProcess(group: Int, mdsId: Byte, data: List, context: Any? = null, sendUmp: UmpMdsHandler) = - mdsProcess(group.toByte(), mdsId, data, data.size, sendUmp, context) - - @Deprecated("Use another overload that takes sndUmp as the last parameter") - fun mdsProcess(group: Byte, mdsId: Byte, data: List, length: Int, sendUmp: UmpMdsHandler, context: Any?) { - val numChunks = mdsGetChunkCount(length) + fun mdsProcess(group: Byte, mdsId: Byte, data: List, context: Any? = null, sendUmp: UmpMdsHandler) { + val numChunks = mdsGetChunkCount(data.size) for (c in 0 until numChunks) { val maxChunkSize = 14 * 65535 - val chunkSize = if (c + 1 == numChunks) length % maxChunkSize else maxChunkSize + val chunkSize = if (c + 1 == numChunks) data.size % maxChunkSize else maxChunkSize val numPayloads = mdsGetPayloadCount(chunkSize) for (p in 0 until numPayloads) { val offset = 14 * (65536 * c + p) @@ -640,7 +644,7 @@ object UmpFactory { } fun mds( - group: Int, + group: Byte, data: List, mdsId: Byte = 0 ): List { @@ -648,6 +652,244 @@ object UmpFactory { mdsProcess(group, mdsId, data) { l1, l2, _, _, _ -> ret.add(Ump(l1, l2)) } return ret } + @Deprecated("Use group as Byte", ReplaceWith("mds(group.toByte(), data, mdsId)")) + fun mds(group: Int, data: List, mdsId: Byte = 0) = mds(group.toByte(), data, mdsId) + + // Some common functions for UMP Stream and Flex Data + + private fun textBytesToUmp(text: List): Int = // the text can be empty (we call drop() unchecked) + (if (text.isEmpty()) 0 else text[0] shl 24) + + (if (text.size < 2) 0 else (text[1] shl 16)) + + (if (text.size < 3) 0 else (text[2] shl 8)) + + if (text.size < 4) 0 else text[3] + + // UMP Stream (0xFn) - new in June 2023 updates + + // text split by 14 bytes + private fun umpStreamTextPacket(format: Byte, status: Byte, text: ByteArray, index: Int, dataPrefix: Byte?): Ump { + val common = ((MidiMessageType.UMP_STREAM shl 28) + (format shl 26) + (status shl 16)).toUnsigned() + val first = if (text.size <= index) 0 else text[index] + return if (dataPrefix != null) + Ump( + (common + (dataPrefix shl 8) + (first)).toInt(), + textBytesToUmp(text.drop(index + 1)), + textBytesToUmp(text.drop(index + 5)), + textBytesToUmp(text.drop(index + 9)) + ) + else + Ump( + (common + (first shl 8) + if (text.size < index + 2) 0 else text[index + 1]).toInt(), + textBytesToUmp(text.drop(index + 2)), + textBytesToUmp(text.drop(index + 6)), + textBytesToUmp(text.drop(index + 10)) + ) + } + + private fun umpStreamTextProcessCommon(status: Byte, text: ByteArray, + context: Any? = null, + capacity: Int = 14, + dataPrefix: Byte? = null, + sendUMP128: (Ump, Any?) -> Unit = { _, _ -> } + ) { + if (text.size <= capacity) + sendUMP128(umpStreamTextPacket(0, status, text, 0, dataPrefix), context) + else { + sendUMP128(umpStreamTextPacket(1, status, text, 0, dataPrefix), context) + val numPackets = text.size / capacity + if (text.size % capacity > 0) 1 else 0 + (1 until text.size / capacity - if (text.size % capacity != 0) 0 else 1).forEach { + sendUMP128(umpStreamTextPacket(2, status, text, it * capacity, dataPrefix), context) + } + sendUMP128(umpStreamTextPacket(3, status, text, (numPackets - 1) * capacity, dataPrefix), context) + } + } + + private fun umpStreamTextCommon(status: Byte, text: ByteArray) : List { + val ret = mutableListOf() + umpStreamTextProcessCommon(status, text) { ump, _ -> ret.add(ump) } + return ret + } + + fun endpointDiscovery(umpVersionMajor: Byte, umpVersionMinor: Byte, filterBitmap: Byte) = + Ump(((umpVersionMajor * 0x100 + umpVersionMinor) + 0xF000_0000L).toInt(), + filterBitmap.toInt() and 0x1F, + 0, 0) + + fun endpointInfoNotification(umpVersionMajor: Byte, umpVersionMinor: Byte, + isStaticFunctionBlock: Boolean, functionBlockCount: Byte, + midi2Capable: Boolean, midi1Capable: Boolean, + supportsRxJR: Boolean, supportsTxJR: Boolean): Ump = + Ump((0xF001_0000L + umpVersionMajor * 0x100 + umpVersionMinor).toInt(), + (functionBlockCount * 0x1_00_0000 + + (if (isStaticFunctionBlock) 0x80000000 else 0) + + (if (midi2Capable) 0x200 else 0) + + (if (midi1Capable) 0x100 else 0) + + (if (supportsRxJR) 2 else 0) + + if (supportsTxJR) 1 else 0 + ).toInt(), + 0, 0) + + fun deviceIdentityNotification(device: DeviceDetails) = + Ump(0xF002_0000L.toInt(), + device.manufacturer, + ((device.family.toUnsigned() shl 16) + device.familyModelNumber.toUnsigned()), + device.softwareRevisionLevel) + + fun endpointNameNotification(name: String) = endpointNameNotification(name.toByteArray()) + fun endpointNameNotification(name: ByteArray) = umpStreamTextCommon(3, name) + + fun productInstanceNotification(id: String) = productInstanceNotification(id.toByteArray()) + fun productInstanceNotification(id: ByteArray) = umpStreamTextCommon(4, id) + + fun streamConfigRequest(protocol: Byte, rxJRTimestamp: Boolean, txJRTimestamp: Boolean) = + Ump((0xF005_0000L + (protocol.toUnsigned() shl 8) + (if (rxJRTimestamp) 2 else 0) + if (txJRTimestamp) 1 else 0).toInt(), + 0, 0, 0) + + fun streamConfigNotification(protocol: Byte, rxJRTimestamp: Boolean, txJRTimestamp: Boolean) = + Ump((0xF006_0000L + (protocol.toUnsigned() shl 8) + (if (rxJRTimestamp) 2 else 0) + if (txJRTimestamp) 1 else 0).toInt(), + 0, 0, 0) + + fun functionBlockDiscovery(fbNumber: Byte, filter: Byte) = + Ump((0xF010_0000L + (fbNumber.toUnsigned() shl 8) + filter.toUnsigned()).toInt(), + 0, 0, 0) + + fun functionBlockInfoNotification(isFbActive: Boolean, fbNumber: Byte, uiHint: Byte, midi1: Byte, direction: Byte, + firstGroup: Byte, numberOfGroupsSpanned: Byte, + midiCIMessageVersionFormat: Byte, maxSysEx8Streams: UByte) = + Ump((0xF011_0000L + + (if (isFbActive) 0x8000 else 0) + (fbNumber.toUnsigned() shl 8) + + ((uiHint and 3) shl 4) + ((midi1 and 3) shl 2) + (direction and 3).toUnsigned()).toInt(), + (firstGroup.toUnsigned() shl 24) + (numberOfGroupsSpanned.toUnsigned() shl 16) + + (midiCIMessageVersionFormat.toUnsigned() shl 8) + maxSysEx8Streams.toInt(), + 0, 0) + + fun functionBlockNameNotification(blockNumber: Byte, name: String): List { + val ret = mutableListOf() + umpStreamTextProcessCommon(0x12, name.toByteArray(), capacity = 13, dataPrefix = blockNumber) { ump, _ -> ret.add(ump) } + return ret + } + + fun startOfClip() = Ump(0xF020_0000L.toInt(), 0, 0, 0) + fun endOfClip() = Ump(0xF021_0000L.toInt(), 0, 0, 0) + + // same as MDS... + /* + fun umpStreamGetChunkCount(numTotalBytesInStream: Int): Int { + val radix = 14 * 0x10000 + return numTotalBytesInStream / radix + if (numTotalBytesInStream % radix != 0) 1 else 0 + } + + // same as MDS... + fun umpStreamGetPayloadCount(numTotalBytesInChunk: Int): Int { + return numTotalBytesInChunk / 14 + if (numTotalBytesInChunk % 14 != 0) 1 else 0 + } + + // same as MDS... + fun umpStreamProcess(status: Byte, data: List, context: Any? = null, sendUmp: UmpStreamHandler) { + val numChunks = umpStreamGetChunkCount(data.size) + for (c in 0 until numChunks) { + val maxChunkSize = 14 * 65535 + val chunkSize = if (c + 1 == numChunks) data.size % maxChunkSize else maxChunkSize + val numPayloads = mdsGetPayloadCount(chunkSize) + for (p in 0 until numPayloads) { + val offset = 14 * (65536 * c + p) + val result = umpStreamGetPayloadOf(status, chunkSize, data, offset) + sendUmp(result.first, result.second, c, p, context) + } + } + } + + fun umpStream(status: Byte, data: List) { + val ret = mutableListOf() + umpStreamProcess(status, data) + } + */ + + // Flex Data (new in June 2023 updates) + + private fun flexDataPacket(group: Byte, + format: Byte, + address: Byte, channel: Byte, statusBank: Byte, status: Byte, text: ByteArray, index: Int) = Ump( + (MidiMessageType.FlexData shl 28) + (group shl 24) + (format shl 22) + (address shl 20) + + (channel shl 16) + (statusBank shl 8) + status, + textBytesToUmp(text.drop(index)), + textBytesToUmp(text.drop(index + 4)), + textBytesToUmp(text.drop(index + 8)) + ) + + fun flexDataProcess( + group: Byte, address: Byte, channel: Byte, statusBank: Byte, status: Byte, text: ByteArray, + context: Any? = null, + sendUMP128: (Ump, Any?) -> Unit = { _, _ -> } + ) { + if (text.size < 13) + sendUMP128(flexDataPacket(group, 0, address, channel, statusBank, status, text, 0), context) + else { + sendUMP128(flexDataPacket(group, 1, address, channel, statusBank, status, text, 0), context) + val numPackets = text.size / 12 + if (text.size % 12 > 0) 1 else 0 + (1 until text.size / 12 - if (text.size % 12 != 0) 0 else 1).forEach { + sendUMP128(flexDataPacket(group, 2, address, channel, statusBank, status, text, it * 12), context) + } + sendUMP128(flexDataPacket(group, 3, address, channel, statusBank, status, text, (numPackets - 1) * 12), context) + } + } + + fun flexDataText(group: Byte, address: Byte, channel: Byte, statusBank: Byte, status: Byte, text: String) = + flexDataText(group, address, channel, statusBank, status, text.toByteArray()) + + fun flexDataText(group: Byte, address: Byte, channel: Byte, statusBank: Byte, status: Byte, text: ByteArray) : List { + val ret = mutableListOf() + flexDataProcess(group, address, channel, statusBank, status, text) { ump, _ -> ret.add(ump) } + return ret + } + + private fun binaryFlexData(group: Byte, address: Byte, channel: Byte, statusByte: Byte, int2: Int, int3: Int = 0, int4: Int = 0): Ump { + val int1 = (MidiMessageType.FlexData shl 28) + (group shl 24) + (address shl 20) + (channel shl 16) + statusByte + return Ump(int1, int2, int3, int4) + } + + fun tempo(group: Byte, channel: Byte, numberOf10NanosecondsPerQuarterNote: Int) = + binaryFlexData(group, 1, channel, 0, numberOf10NanosecondsPerQuarterNote) + + fun timeSignature(group: Byte, channel: Byte, numerator: Byte, denominator: Byte, numberOf32Notes: Byte) = + binaryFlexData(group, 1, channel, 1, (numerator shl 24) + (denominator shl 16) + (numberOf32Notes shl 8)) + + fun metronome(group: Byte, channel: Byte, numClocksPerPrimeryClick: Byte, barAccent1: Byte, barAccent2: Byte, barAccent3: Byte, numSubdivisionClick1: Byte, numSubdivisionClick2: Byte) = + binaryFlexData(group, 1, channel, 2, + (numClocksPerPrimeryClick shl 24) + (barAccent1 shl 16) + (barAccent2 shl 8) + barAccent3, + (numSubdivisionClick1 shl 24) + (numSubdivisionClick2 shl 16)) + + private fun sharpOrFlatsToInt(v: Byte) = if (v < 0) v + 0x10 else v.toInt() + + fun keySignature(group: Byte, address: Byte, channel: Byte, sharpsOrFlats: Byte, tonicNote: Byte) = + binaryFlexData(group, address, channel, 5, + (sharpOrFlatsToInt(sharpsOrFlats) shl 28) + (tonicNote shl 24)) + + // Those "alteration" arguments are set to UInt as it will involve additions + fun chordName(group: Byte, address: Byte, channel: Byte, + tonicSharpsFlats: Byte, chordTonic: Byte, chordType: Byte, + alter1: UInt, alter2: UInt, alter3: UInt, alter4: UInt, + bassSharpsFlats: Byte, bassNote: Byte, bassChordType: Byte, + bassAlter1: UInt, + bassAlter2: UInt + ) = + binaryFlexData(group, address, channel, 6, + (sharpOrFlatsToInt(tonicSharpsFlats) shl 28) + (chordTonic shl 24) + (chordType shl 16) + (alter1 shl 8).toInt() + alter2.toInt(), + ((alter3 shl 24) + (alter4 shl 16)).toInt(), + (sharpOrFlatsToInt(bassSharpsFlats) shl 28) + (bassNote shl 24) + (bassChordType shl 16) + (bassAlter1 shl 8).toInt() + bassAlter2.toInt()) + + fun metadataText(group: Byte, address: Byte, channel: Byte, status: Byte, text: String) = + flexDataText(group, address, channel, 1, status, text) + + fun metadataText(group: Byte, address: Byte, channel: Byte, status: Byte, text: ByteArray) = + flexDataText(group, address, channel, 1, status, text) + + fun performanceText(group: Byte, address: Byte, channel: Byte, status: Byte, text: String) = + flexDataText(group, address, channel, 2, status, text) + + fun performanceText(group: Byte, address: Byte, channel: Byte, status: Byte, text: ByteArray) = + flexDataText(group, address, channel, 2, status, text) + + // Bytes conversions fun fromPlatformBytes(byteOrder: ByteOrder, bytes: List) : Iterable = sequence { diff --git a/ktmidi/src/commonMain/kotlin/dev/atsushieno/ktmidi/UmpRetrieval.kt b/ktmidi/src/commonMain/kotlin/dev/atsushieno/ktmidi/UmpRetrieval.kt index df90e3004..33975e74e 100644 --- a/ktmidi/src/commonMain/kotlin/dev/atsushieno/ktmidi/UmpRetrieval.kt +++ b/ktmidi/src/commonMain/kotlin/dev/atsushieno/ktmidi/UmpRetrieval.kt @@ -86,12 +86,22 @@ val Ump.groupAndChannel: Int // 0..255 val Ump.isJRClock get() = messageType == MidiMessageType.UTILITY && statusCode == MidiUtilityStatus.JR_CLOCK val Ump.jrClock - get() = if (isJRClock) (midi1Msb shl 8) + midi1Lsb else 0 + get() = if (isJRClock) int1 and 0xFFFF else 0 val Ump.isJRTimestamp get()= messageType == MidiMessageType.UTILITY && statusCode == MidiUtilityStatus.JR_TIMESTAMP val Ump.jrTimestamp - get() = if(isJRTimestamp) (midi1Msb shl 8) + midi1Lsb else 0 + get() = if(isJRTimestamp) int1 and 0xFFFF else 0 + +val Ump.isDCTPQ + get() = messageType == MidiMessageType.UTILITY && statusCode == MidiUtilityStatus.DCTPQ +val Ump.dctpq + get() = if(isDCTPQ) int1 and 0xFFFF else 0 + +val Ump.isDeltaClockstamp + get() = messageType == MidiMessageType.UTILITY && statusCode == MidiUtilityStatus.DELTA_CLOCKSTAMP +val Ump.deltaClockstamp + get() = if(isDeltaClockstamp) int2 and 0xFFFFF else 0 // 3rd. byte for MIDI 1.0 message val Ump.midi1Msb: Int 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 0382db808..9634f2f6f 100644 --- a/ktmidi/src/commonMain/kotlin/dev/atsushieno/ktmidi/ci/MidiCIConnection.kt +++ b/ktmidi/src/commonMain/kotlin/dev/atsushieno/ktmidi/ci/MidiCIConnection.kt @@ -9,9 +9,7 @@ import kotlin.random.Random import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.ExperimentalTime -import kotlin.time.TimeMark import kotlin.time.TimeSource -import kotlin.time.compareTo data class DeviceDetails(val manufacturer: Int = 0, val family: Short = 0, val familyModelNumber: Short = 0, val softwareRevisionLevel: Int = 0) { companion object { diff --git a/ktmidi/src/commonTest/kotlin/dev/atsushieno/ktmidi/UmpFactoryTest.kt b/ktmidi/src/commonTest/kotlin/dev/atsushieno/ktmidi/UmpFactoryTest.kt index eb2cbdd60..3b58ce3d6 100644 --- a/ktmidi/src/commonTest/kotlin/dev/atsushieno/ktmidi/UmpFactoryTest.kt +++ b/ktmidi/src/commonTest/kotlin/dev/atsushieno/ktmidi/UmpFactoryTest.kt @@ -1,5 +1,6 @@ package dev.atsushieno.ktmidi +import dev.atsushieno.ktmidi.ci.DeviceDetails import io.ktor.utils.io.core.* import kotlin.test.Test import kotlin.test.assertContentEquals @@ -13,12 +14,11 @@ class UmpFactoryTest { @Test fun testType0Messages() { /* type 0 */ - assertEqualsFlipped(UmpFactory.noop(0), 0) - assertEqualsFlipped(UmpFactory.noop(1) , 0x01000000) - assertEqualsFlipped(UmpFactory.jrClock(0, 0) , 0x00100000) - assertEqualsFlipped(UmpFactory.jrClock(0, 1.0) , 0x00107A12) - assertEqualsFlipped(UmpFactory.jrTimestamp(0, 0) , 0x00200000) - assertEqualsFlipped(UmpFactory.jrTimestamp(1, 1.0) , 0x01207A12) + assertEqualsFlipped(UmpFactory.noop(), 0) + assertEqualsFlipped(UmpFactory.jrClock(0) , 0x00100000) + assertEqualsFlipped(UmpFactory.jrClock(1.0) , 0x00107A12) + assertEqualsFlipped(UmpFactory.jrTimestamp(0) , 0x00200000) + assertEqualsFlipped(UmpFactory.jrTimestamp(1.0) , 0x00207A12) } @Test @@ -316,6 +316,270 @@ class UmpFactoryTest { assertContentEquals(arrayOf(27, 0, 0x32, 0x50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), bytes27_3.toTypedArray(), "bytes26.3") } + // Flex Data messages + + @Test + fun testTempo() { + val tempo1 = UmpFactory.tempo(0, 0, 50_000_000) + assertEquals(0xD010_0000L, tempo1.int1.toUnsigned(), "tempo1.int1") + assertEquals(0x02FA_F080L, tempo1.int2.toUnsigned(), "tempo1.int2") + assertEquals(0, tempo1.int3, "tempo1.int3") + assertEquals(0, tempo1.int4, "tempo1.int4") + + val tempo2 = UmpFactory.tempo(0xF, 0xE, 50_000_000) + assertEquals(0xDF1E_0000L, tempo2.int1.toUnsigned(), "tempo2.int1") + assertEquals(0x02FA_F080L, tempo2.int2.toUnsigned(), "tempo2.int2") + assertEquals(0, tempo2.int3, "tempo2.int3") + assertEquals(0, tempo2.int4, "tempo2.int4") + } + + @Test + fun testTimeSignature() { + val ts1 = UmpFactory.timeSignature(0, 0, 3, 4, 0) + assertEquals(0xD010_0001L, ts1.int1.toUnsigned(), "ts1.int1") + assertEquals(0x0304_0000L, ts1.int2.toUnsigned(), "ts1.int2") + assertEquals(0, ts1.int3, "ts1.int3") + assertEquals(0, ts1.int4, "ts1.int4") + + val ts2 = UmpFactory.timeSignature(0xF, 0xE, 5, 8, 32) + assertEquals(0xDF1E_0001L, ts2.int1.toUnsigned(), "ts2.int1") + assertEquals(0x0508_2000L, ts2.int2.toUnsigned(), "ts2.int2") + assertEquals(0, ts2.int3, "ts1.int3") + assertEquals(0, ts2.int4, "ts1.int4") + } + + @Test + fun testMetronome() { + val metronome1 = UmpFactory.metronome(0, 0, 3, 4, 4, 1, 0, 0) + assertEquals(0xD010_0002L, metronome1.int1.toUnsigned(), "metronome1.int1") + assertEquals(0x0304_0401L, metronome1.int2.toUnsigned(), "metronome1.int2") + assertEquals(0, metronome1.int3, "metronome1.int3") + assertEquals(0, metronome1.int4, "metronome1.int4") + + val metronome2 = UmpFactory.metronome(0xF, 0xE, 2, 3, 2, 0, 2, 3) + assertEquals(0xDF1E_0002L, metronome2.int1.toUnsigned(), "metronome2.int1") + assertEquals(0x0203_0200L, metronome2.int2.toUnsigned(), "metronome2.int2") + assertEquals(0x0203_0000L, metronome2.int3.toUnsigned(), "metronome2.int3") + assertEquals(0, metronome2.int4, "metronome2.int4") + } + + @Test + fun testKeySignature() { + val ks1 = UmpFactory.keySignature(0, 0, 0, ChordSharpFlatsField.DOUBLE_SHARP, TonicNoteField.F) // F is 5(in ABCDEFG... not 3 in CDEFGAB !) + assertEquals(0xD000_0005L, ks1.int1.toUnsigned(), "ks1.int1") + assertEquals(0x2600_0000L, ks1.int2.toUnsigned(), "ks1.int2") + assertEquals(0, ks1.int3, "ks1.int3") + assertEquals(0, ks1.int4, "ks1.int4") + + val ks2 = UmpFactory.keySignature(0xF, 1, 0xE, ChordSharpFlatsField.DOUBLE_FLAT, TonicNoteField.G) // G is 6, likewise + assertEquals(0xDF1E_0005L, ks2.int1.toUnsigned(), "ks2.int1") + assertEquals(0xE700_0000L, ks2.int2.toUnsigned(), "ks2.int2") + assertEquals(0, ks2.int3, "ks2.int3") + assertEquals(0, ks2.int4, "ks2.int4") + } + + @Test + fun testChordName() { + val chordName1 = UmpFactory.chordName(0, 0, 0, + ChordSharpFlatsField.SHARP, TonicNoteField.F, ChordTypeField.MAJOR, ChordAlterationType.ADD_DEGREE + 1U, 1U, 2U, 3U, + ChordSharpFlatsField.SHARP, TonicNoteField.C, ChordTypeField.MAJOR, 1U, 2U) + assertEquals(0xD000_0006L, chordName1.int1.toUnsigned(), "chordName1.int1") + assertEquals(0x1601_1101L, chordName1.int2.toUnsigned(), "chordName1.int2") + assertEquals(0x0203_0000L, chordName1.int3.toUnsigned(), "chordName1.int3") + assertEquals(0x1301_0102L, chordName1.int4.toUnsigned(), "chordName1.int4") + + val chordName2 = UmpFactory.chordName(0xF, 1, 0xE, + ChordSharpFlatsField.DOUBLE_FLAT, TonicNoteField.G, ChordTypeField.SEVENTH_SUSPENDED_4TH, ChordAlterationType.SUBTRACT_DEGREE + 1U, 0x21U, 0x32U, 3U, + ChordSharpFlatsField.FLAT, TonicNoteField.C, ChordTypeField.DIMINISHED_7TH, ChordAlterationType.RAISE_DEGREE + 0U, 2U) + assertEquals(0xDF1E_0006L, chordName2.int1.toUnsigned(), "chordName2.int1") + assertEquals(0xE71B_2121L, chordName2.int2.toUnsigned(), "chordName2.int2") + assertEquals(0x3203_0000L, chordName2.int3.toUnsigned(), "chordName2.int3") + assertEquals(0xF314_3002L, chordName2.int4.toUnsigned(), "chordName2.int4") + } + + @Test + fun testMetadataText() { + val text1 = UmpFactory.metadataText(0, 0, 0, MetadataTextStatus.UNKNOWN, "TEST STRING") + assertEquals(1, text1.size) + assertEquals(0xD000_0100, text1[0].int1.toUnsigned(), "text1.int1") + assertEquals(0x5445_5354, text1[0].int2.toUnsigned(), "text1.int2") + assertEquals(0x2053_5452, text1[0].int3.toUnsigned(), "text1.int3") + assertEquals(0x494e_4700, text1[0].int4.toUnsigned(), "text1.int4") + + // Text can end without \0. + val text2 = UmpFactory.metadataText(0, 0, 0, MetadataTextStatus.PROJECT_NAME, "TEST STRING1") + assertEquals(1, text2.size) + assertEquals(0xD000_0101, text2[0].int1.toUnsigned(), "text2.int1") + assertEquals(0x5445_5354, text2[0].int2.toUnsigned(), "text2.int2") + assertEquals(0x2053_5452, text2[0].int3.toUnsigned(), "text2.int3") + assertEquals(0x494e_4731, text2[0].int4.toUnsigned(), "text2.int4") + + // multiple packets. + val text3 = UmpFactory.metadataText(0, 0, 5, MetadataTextStatus.UNKNOWN, + "Test String That Spans More.") + assertEquals(3, text3.size) + //"Test String That Spans More.".forEach { print(it.code.toString(16)) } + assertEquals(0xD045_0100, text3[0].int1.toUnsigned(), "text3[0].int1") + assertEquals(0x5465_7374, text3[0].int2.toUnsigned(), "text3[0].int2") + assertEquals(0x2053_7472, text3[0].int3.toUnsigned(), "text3[0].int3") + assertEquals(0x696e_6720, text3[0].int4.toUnsigned(), "text3[0].int4") + assertEquals(0xD085_0100, text3[1].int1.toUnsigned(), "text3[1].int1") + assertEquals(0x5468_6174, text3[1].int2.toUnsigned(), "text3[1].int2") + assertEquals(0x2053_7061, text3[1].int3.toUnsigned(), "text3[1].int3") + assertEquals(0x6e73_204d, text3[1].int4.toUnsigned(), "text3[1].int4") + assertEquals(0xD0C5_0100, text3[2].int1.toUnsigned(), "text3[2].int1") + assertEquals(0x6f72_652e, text3[2].int2.toUnsigned(), "text3[2].int2") + assertEquals(0, text3[2].int3.toUnsigned(), "text3[2].int3") + assertEquals(0, text3[2].int4.toUnsigned(), "text3[2].int4") + } + + @Test + fun performanceText() { + // contains \0. + // LAMESPEC: does not this mean the rest of lyrics after the melisma ignored? + val text1 = UmpFactory.performanceText(0, 0, 5, PerformanceTextStatus.LYRICS, + "A melisma\u0000ah") + assertEquals(1, text1.size) + //"A melisma\u0000ah".forEach { print(it.code.toString(16)) } + assertEquals(0xD005_0201, text1[0].int1.toUnsigned(), "text1[0].int1") + assertEquals(0x4120_6d65, text1[0].int2.toUnsigned(), "text1[0].int2") + assertEquals(0x6c69_736d, text1[0].int3.toUnsigned(), "text1[0].int3") + assertEquals(0x6100_6168, text1[0].int4.toUnsigned(), "text1[0].int4") + } + + // UMP Stream messages + + @Test + fun testEndpointDiscovery() { + val ed1 = UmpFactory.endpointDiscovery(1, 1, 0x1F) + assertEquals(0xF000_0101L, ed1.int1.toUnsigned(), "ed1.int1") + assertEquals(0x1F, ed1.int2, "ed1.int2") + assertEquals(0, ed1.int3, "ed1.int3") + assertEquals(0, ed1.int4, "ed1.int4") + } + + @Test + fun testEndpointInfoNotification() { + val en1 = UmpFactory.endpointInfoNotification(1, 1, true, 2, + midi2Capable = true, + midi1Capable = true, + supportsRxJR = false, + supportsTxJR = true + ) + assertEquals(0xF001_0101L, en1.int1.toUnsigned(), "en1.int1") + assertEquals(0x8200_0301L, en1.int2.toUnsigned(), "en1.int2") + assertEquals(0, en1.int3, "en1.int3") + assertEquals(0, en1.int4, "en1.int4") + } + + @Test + fun testDeviceIdentityNotification() { + val dn1 = UmpFactory.deviceIdentityNotification(DeviceDetails(0x123456, 0x789A, 0x7654, 0x32106543)) + assertEquals(0xF002_0000L, dn1.int1.toUnsigned(), "dn1.int1") + assertEquals(0x0012_3456, dn1.int2, "dn1.int2") + assertEquals(0x789A_7654, dn1.int3, "dn1.int3") + assertEquals(0x3210_6543, dn1.int4, "dn1.int4") + } + + @Test + fun testEndpointNameNotification() { + val en1 = UmpFactory.endpointNameNotification("EndpointName12") // 14 bytes + assertEquals(1, en1.size, "en1.size") + //"EndpointName12".forEach { print(it.code.toString(16)) } + assertEquals(0xF003_456eL, en1[0].int1.toUnsigned(), "en1[0].int1") + assertEquals(0x6470_6f69, en1[0].int2, "en1[0].int2") + assertEquals(0x6e74_4e61, en1[0].int3, "en1[0].int3") + assertEquals(0x6d65_3132, en1[0].int4, "en1[0].int4") + + val en2 = UmpFactory.endpointNameNotification("EndpointName123") // 15 bytes + assertEquals(2, en2.size, "en2.size") + //"EndpointName123".forEach { print(it.code.toString(16)) } + assertEquals(0xF403_456eL, en2[0].int1.toUnsigned(), "en2[0].int1") + // ... skip the same ones + assertEquals(0xFC03_3300L, en2[1].int1.toUnsigned(), "en2[1].int1") + assertEquals(0, en2[1].int2, "en2[1].int2") + assertEquals(0, en2[1].int3, "en2[1].int3") + assertEquals(0, en2[1].int4, "en2[1].int4") + } + + @Test + fun testProductInstanceIdNotification() { + val pn1 = UmpFactory.productInstanceNotification("ProductName 123") // 15 bytes + assertEquals(2, pn1.size, "pn1.size") + //"ProductName 123".forEach { print(it.code.toString(16)) } + assertEquals(0xF404_5072L, pn1[0].int1.toUnsigned(), "pn1[0].int1") + assertEquals(0xFC04_3300L, pn1[1].int1.toUnsigned(), "pn1[1].int1") + assertEquals(0, pn1[1].int2, "pn1[1].int2") + assertEquals(0, pn1[1].int3, "pn1[1].int3") + assertEquals(0, pn1[1].int4, "pn1[1].int4") + } + + @Test + fun testStreamConfigRequest() { + val req1 = UmpFactory.streamConfigRequest(0x12, true, false) + assertEquals(0xF005_1202L, req1.int1.toUnsigned(), "req1.int1") + assertEquals(0, req1.int2, "req1.int2") + assertEquals(0, req1.int3, "req1.int3") + assertEquals(0, req1.int4, "req1.int4") + + } + + @Test + fun testStreamConfigNotification() { + val not1 = UmpFactory.streamConfigNotification(0x12, true, false) + assertEquals(0xF006_1202L, not1.int1.toUnsigned(), "not1.int1") + assertEquals(0, not1.int2, "not1.int2") + assertEquals(0, not1.int3, "not1.int3") + assertEquals(0, not1.int4, "not1.int4") + + } + + @Test + fun testFunctionBlockDiscovery() { + val d1 = UmpFactory.functionBlockDiscovery(5, 3) + assertEquals(0xF010_0503L, d1.int1.toUnsigned(), "d1.int1") + assertEquals(0, d1.int2, "d1.int2") + assertEquals(0, d1.int3, "d1.int3") + assertEquals(0, d1.int4, "d1.int4") + + } + + @Test + fun testFunctionBlockInfoNotification() { + val fb1 = UmpFactory.functionBlockInfoNotification(true, 5, + 3, 2, 1, + 0, 3, 1, 255U) + assertEquals(0xF011_8539L, fb1.int1.toUnsigned(), "fb1.int1") + assertEquals(0x000301FF, fb1.int2, "fb1.int2") + assertEquals(0, fb1.int3, "fb1.int3") + assertEquals(0, fb1.int4, "fb1.int4") + + } + + @Test + fun testFunctionBlockNameNotification() { + val fn1 = UmpFactory.functionBlockNameNotification(7, "FunctionName1") // 13 bytes + assertEquals(1, fn1.size, "en1.size") + //"FunctionName1".forEach { print(it.code.toString(16)) } + //616d653132 + assertEquals(0xF012_0746L, fn1[0].int1.toUnsigned(), "fn1[0].int1") + assertEquals(0x756e_6374, fn1[0].int2, "fn1[0].int2") + assertEquals(0x696f_6e4e, fn1[0].int3, "fn1[0].int3") + assertEquals(0x616d_6531, fn1[0].int4, "fn1[0].int4") + + val fn2 = UmpFactory.functionBlockNameNotification(7, "FunctionName12") // 14 bytes + assertEquals(2, fn2.size, "fn2.size") + //"FunctionName12".forEach { print(it.code.toString(16)) } + assertEquals(0xF412_0746L, fn2[0].int1.toUnsigned(), "fn2[0].int1") + // ... skip the same ones + assertEquals(0xFC12_0732L, fn2[1].int1.toUnsigned(), "fn2[1].int1") + assertEquals(0, fn2[1].int2, "fn2[1].int2") + assertEquals(0, fn2[1].int3, "fn2[1].int3") + assertEquals(0, fn2[1].int4, "fn2[1].int4") + } + + // Conversion + @Test fun fromPlatformBytes() { val bytes = listOf(0x40, 0x91, 0x40, 0, 0x64, 0, 0, 0, 0x00, 0x20, 0x00, 0xC0, 0x40, 0x81, 0x40, 0, 0, 0, 0, 0).map { it.toByte() }