Skip to content

Commit c5a3c4e

Browse files
committed
Initial working parser
1 parent f56e804 commit c5a3c4e

File tree

9 files changed

+162
-86
lines changed

9 files changed

+162
-86
lines changed

core/src/commonMain/kotlin/pl/lemanski/mikroSoundFont/io/midi/MidiFileParser.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,14 @@ internal class MidiFileParser(
6666
)
6767
}
6868

69-
internal fun parseTracks(n: Int): List<MidiTrack> = List(n) { idx -> parseTrack(idx) }
69+
internal fun parseTracks(n: Int): List<MidiTrack> = List(n) { parseTrack() }
7070

71-
internal fun parseTrack(id: Int): MidiTrack {
71+
internal fun parseTrack(): MidiTrack {
7272
val trackHeader = parseTrackHeader()
7373
val trackBuffer = Buffer()
7474
val messages = mutableListOf<MidiMessage>()
7575

76-
buffer.copyTo(trackBuffer, 0L, trackHeader.dataSize.toLong())
76+
buffer.readAtMostTo(trackBuffer, trackHeader.dataSize.toLong())
7777

7878
val messageContext = MidiMessageContext(trackBuffer)
7979
while (trackBuffer.size > 0) {
@@ -88,12 +88,12 @@ internal class MidiFileParser(
8888

8989
internal fun parseTrackHeader(): MidiTrack.Header {
9090
if (buffer.readString(4) != "MTrk") {
91-
throw InvalidMidiDataException("Invalid MTrk header: $buffer")
91+
throw InvalidMidiDataException("Invalid MTrk header")
9292
}
9393

9494
val trackLength = buffer.readInt()
9595
if (trackLength < 0) {
96-
throw InvalidMidiDataException("Invalid MTrk header.Track length is negative: $buffer")
96+
throw InvalidMidiDataException("Invalid MTrk header.Track length is negative.")
9797
}
9898

9999
return MidiTrack.Header(dataSize = trackLength)

core/src/commonMain/kotlin/pl/lemanski/mikroSoundFont/io/midi/message/BufferExt.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package pl.lemanski.mikroSoundFont.io.midi.message
22

33
import kotlinx.io.Buffer
4-
import kotlinx.io.readUByte
54
import pl.lemanski.mikroSoundFont.InvalidMidiDataException
65

76
internal fun Buffer.readVarLen(): Int {
@@ -10,7 +9,7 @@ internal fun Buffer.readVarLen(): Int {
109
var bytesRead = 0
1110

1211
do {
13-
c = readUByte().toInt()
12+
c = readByte().toInt() and 0xFF
1413
bytesRead++
1514
r = (r shl 7) or (c and 0x7F) // add 7 LSB of c to r
1615
} while (c and 0x80 != 0 && bytesRead < 4) // until c MSB is 1

core/src/commonMain/kotlin/pl/lemanski/mikroSoundFont/io/midi/message/MidiMessageContext.kt

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ package pl.lemanski.mikroSoundFont.io.midi.message
22

33
import kotlinx.io.Buffer
44
import kotlinx.io.readUByte
5+
import pl.lemanski.mikroSoundFont.InvalidMidiDataException
56
import pl.lemanski.mikroSoundFont.io.midi.message.read.MidiMessageParser
67
import pl.lemanski.mikroSoundFont.io.midi.message.read.MidiMetaMessageParser
78
import pl.lemanski.mikroSoundFont.io.midi.message.read.MidiSystemMessageParser
89
import pl.lemanski.mikroSoundFont.io.midi.message.read.MidiVoiceMessageParser
910
import pl.lemanski.mikroSoundFont.midi.MidiMessage
10-
import pl.lemanski.mikroSoundFont.midi.UnsupportedMidiMessage
1111

1212
internal class MidiMessageContext(
1313
private val buffer: Buffer
@@ -16,16 +16,16 @@ internal class MidiMessageContext(
1616
private val voiceMessageParser = MidiVoiceMessageParser(buffer)
1717
private val metaMessageParser = MidiMetaMessageParser(buffer)
1818
private lateinit var parser: MidiMessageParser
19+
private var lastStatus: Int = 0
1920

2021
fun readMessage(): MidiMessage {
2122
val dt = buffer.readVarLen()
22-
val status = buffer.readUByte().toInt()
23+
val status: Int = getStatusByte()
2324

2425
parser = when (status) {
2526
in systemMessageParser.supportedTypes() -> systemMessageParser
2627
in metaMessageParser.supportedTypes() -> metaMessageParser
27-
in voiceMessageParser.supportedTypes() -> voiceMessageParser
28-
else -> return UnsupportedMidiMessage
28+
else -> voiceMessageParser
2929
}
3030

3131
return parser.parse(status, dt)
@@ -34,4 +34,23 @@ internal class MidiMessageContext(
3434
fun writeMessage() {
3535
// TODO
3636
}
37+
38+
//---
39+
40+
private fun getStatusByte(): Int {
41+
val tmp = buffer.peek().readUByte().toInt() // do not consume, might have running status
42+
43+
return if (tmp.isValidStatus()) {
44+
val statusByte = buffer.readUByte().toInt()
45+
lastStatus = statusByte
46+
statusByte
47+
} else {
48+
if (!lastStatus.isValidStatus()) {
49+
throw InvalidMidiDataException("Invalid status: ${tmp.toString(16)} and no running status detected")
50+
}
51+
lastStatus
52+
}
53+
}
54+
55+
private fun Int.isValidStatus(): Boolean = this and 0x80 != 0
3756
}

core/src/commonMain/kotlin/pl/lemanski/mikroSoundFont/io/midi/message/read/MidiMetaMessageParser.kt

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package pl.lemanski.mikroSoundFont.io.midi.message.read
22

33
import kotlinx.io.Buffer
44
import pl.lemanski.mikroSoundFont.InvalidMidiDataException
5+
import pl.lemanski.mikroSoundFont.io.midi.message.readVarLen
56
import pl.lemanski.mikroSoundFont.midi.MidiMessage
67
import pl.lemanski.mikroSoundFont.midi.MidiMessageType
78
import pl.lemanski.mikroSoundFont.midi.MidiMetaMessage
@@ -24,33 +25,45 @@ internal class MidiMetaMessageParser(
2425
}
2526

2627
val metaType = buffer.readByte().toInt()
28+
val dataSize = buffer.readVarLen()
2729

2830
return messageParserMap[metaType]
29-
?.let { it(deltaTime) }
30-
?: UnsupportedMidiMessage
31+
?.let { it(deltaTime, dataSize) }
32+
?: skipMessage(dataSize)
3133
}
3234

33-
private fun parseEndOfTrack(deltaTime: Int): MidiMetaMessage.EndOfTrack = MidiMetaMessage.EndOfTrack(
34-
time = deltaTime
35-
)
36-
37-
private fun parseSetTempo(deltaTime: Int): MidiMetaMessage.SetTempo {
38-
val tempoBytes = buffer.readByte().toInt()
39-
if (tempoBytes != 0x03) {
40-
throw InvalidMidiDataException("Malformed MIDI. Set Tempo message must be 3 bytes long.")
35+
private fun parseEndOfTrack(deltaTime: Int, dataSize: Int): MidiMetaMessage.EndOfTrack {
36+
if (dataSize != 0) {
37+
throw InvalidMidiDataException("Malformed MIDI. End Of Track message must be empty.")
4138
}
4239

43-
var tempoValue = 0
44-
repeat(tempoBytes) {
45-
buffer.readByte()
46-
tempoValue = tempoValue shl 8 or buffer.readByte().toInt()
47-
}
40+
return MidiMetaMessage.EndOfTrack(
41+
time = deltaTime
42+
)
43+
}
4844

49-
val bpm = 60_000_000 / tempoValue // 1 minute in microseconds / tempoValue
45+
private fun parseSetTempo(deltaTime: Int, dataSize: Int): MidiMessage = skipMessage(dataSize)
46+
// {
47+
// if (dataSize != 3) {
48+
// throw InvalidMidiDataException("Malformed MIDI. Set Tempo message must be 3 bytes long.")
49+
// }
50+
//
51+
// var tempoValue = 0
52+
// repeat(dataSize) {
53+
// buffer.readByte()
54+
// tempoValue = tempoValue shl 8 or buffer.readByte().toInt()
55+
// }
56+
//
57+
// val bpm = 60_000_000 / tempoValue // 1 minute in microseconds / tempoValue
58+
//
59+
// return MidiMetaMessage.SetTempo(
60+
// time = deltaTime,
61+
// bpm = bpm
62+
// )
63+
// }
5064

51-
return MidiMetaMessage.SetTempo(
52-
time = deltaTime,
53-
bpm = bpm
54-
)
65+
private fun skipMessage(dataSize: Int): MidiMessage {
66+
buffer.skip(dataSize.toLong())
67+
return UnsupportedMidiMessage
5568
}
5669
}

core/src/commonMain/kotlin/pl/lemanski/mikroSoundFont/io/midi/message/read/MidiVoiceMessageParser.kt

Lines changed: 74 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -6,41 +6,6 @@ import pl.lemanski.mikroSoundFont.midi.MidiMessage
66
import pl.lemanski.mikroSoundFont.midi.MidiMessageType
77
import pl.lemanski.mikroSoundFont.midi.MidiVoiceMessage
88

9-
/**
10-
* else //channel message
11-
* {
12-
* int param;
13-
* if ((param = tml_readbyte(p)) < 0) { TML_WARN("Unexpected end of file"); return -1; }
14-
* evt->key = (param & 0x7f);
15-
* evt->channel = (status & 0x0f);
16-
* switch (evt->type = (status & 0xf0))
17-
* {
18-
* case TML_NOTE_OFF:
19-
* case TML_NOTE_ON:
20-
* case TML_KEY_PRESSURE:
21-
* case TML_CONTROL_CHANGE:
22-
* if ((param = tml_readbyte(p)) < 0) { TML_WARN("Unexpected end of file"); return -1; }
23-
* evt->velocity = (param & 0x7f);
24-
* break;
25-
*
26-
* case TML_PITCH_BEND:
27-
* if ((param = tml_readbyte(p)) < 0) { TML_WARN("Unexpected end of file"); return -1; }
28-
* evt->pitch_bend = ((param & 0x7f) << 7) | evt->key;
29-
* break;
30-
*
31-
* case TML_PROGRAM_CHANGE:
32-
* case TML_CHANNEL_PRESSURE:
33-
* evt->velocity = 0;
34-
* break;
35-
*
36-
* default: //ignore system/manufacture messages
37-
* evt->type = 0;
38-
* break;
39-
* }
40-
* }
41-
*/
42-
43-
449
internal class MidiVoiceMessageParser(
4510
private val buffer: Buffer
4611
) : MidiMessageParser {
@@ -57,35 +22,93 @@ internal class MidiVoiceMessageParser(
5722
override fun supportedTypes(): Set<Int> = messageParserMap.keys
5823

5924
override fun parse(status: Int, deltaTime: Int): MidiMessage = messageParserMap[status and 0xf0]
60-
?.let { it(deltaTime) }
25+
?.let { it(status, deltaTime) }
6126
?: throw InvalidMidiDataException("Unknown midi voice message type: ${(status and 0xf0).toString(16)}")
6227

63-
private fun parseNoteOff(deltaTime: Int): MidiVoiceMessage.NoteOff {
64-
TODO()
28+
private fun parseNoteOff(status: Int, deltaTime: Int): MidiVoiceMessage.NoteOff {
29+
val channel = status and 0x0f
30+
val note = buffer.readByte().toInt()
31+
val velocity = buffer.readByte().toInt()
32+
33+
return MidiVoiceMessage.NoteOff(
34+
time = deltaTime,
35+
channel = channel,
36+
key = note,
37+
velocity = velocity
38+
)
6539
}
6640

67-
private fun parseNoteOn(deltaTime: Int): MidiVoiceMessage.NoteOn {
68-
TODO()
41+
private fun parseNoteOn(status: Int, deltaTime: Int): MidiVoiceMessage.NoteOn {
42+
val channel = status and 0x0f
43+
val note = buffer.readByte().toInt()
44+
val velocity = buffer.readByte().toInt()
45+
46+
return MidiVoiceMessage.NoteOn(
47+
time = deltaTime,
48+
channel = channel,
49+
key = note,
50+
velocity = velocity
51+
)
6952
}
7053

71-
private fun parseKeyPressure(deltaTime: Int): MidiVoiceMessage.KeyPressure {
72-
TODO()
54+
private fun parseKeyPressure(status: Int, deltaTime: Int): MidiVoiceMessage.KeyPressure {
55+
val channel = status and 0x0f
56+
val note = buffer.readByte().toInt()
57+
val keyPressure = buffer.readByte().toInt()
58+
59+
return MidiVoiceMessage.KeyPressure(
60+
time = deltaTime,
61+
channel = channel,
62+
key = note,
63+
keyPressure = keyPressure
64+
)
7365
}
7466

75-
private fun parseControlChange(deltaTime: Int): MidiVoiceMessage.ControlChange {
76-
TODO()
67+
private fun parseControlChange(status: Int, deltaTime: Int): MidiVoiceMessage.ControlChange {
68+
val channel = status and 0x0f
69+
val control = buffer.readByte().toInt()
70+
val controlValue = buffer.readByte().toInt()
71+
72+
return MidiVoiceMessage.ControlChange(
73+
time = deltaTime,
74+
channel = channel,
75+
control = control,
76+
controlValue = controlValue
77+
)
7778
}
7879

79-
private fun parseProgramChange(deltaTime: Int): MidiVoiceMessage.ProgramChange {
80-
TODO()
80+
private fun parseProgramChange(status: Int, deltaTime: Int): MidiVoiceMessage.ProgramChange {
81+
val channel = status and 0x0f
82+
val program = buffer.readByte().toInt()
83+
84+
return MidiVoiceMessage.ProgramChange(
85+
time = deltaTime,
86+
channel = channel,
87+
program = program
88+
)
8189
}
8290

83-
private fun parseChannelPressure(deltaTime: Int): MidiVoiceMessage.ChannelPressure {
84-
TODO()
91+
private fun parseChannelPressure(status: Int, deltaTime: Int): MidiVoiceMessage.ChannelPressure {
92+
val channel = status and 0x0f
93+
val channelPressure = buffer.readByte().toInt()
94+
95+
return MidiVoiceMessage.ChannelPressure(
96+
time = deltaTime,
97+
channel = channel,
98+
channelPressure = channelPressure
99+
)
85100
}
86101

87-
private fun parsePitchBend(deltaTime: Int): MidiVoiceMessage.PitchBend {
88-
TODO()
102+
private fun parsePitchBend(status: Int, deltaTime: Int): MidiVoiceMessage.PitchBend {
103+
val channel = status and 0x0f
104+
val lsb = buffer.readByte().toInt() and 0x7F
105+
val msb = buffer.readByte().toInt() and 0x7F
106+
val pitchBend = (msb shl 7) or lsb
107+
108+
return MidiVoiceMessage.PitchBend(
109+
time = deltaTime,
110+
channel = channel,
111+
pitchBend = pitchBend
112+
)
89113
}
90114
}
91-

core/src/commonMain/kotlin/pl/lemanski/mikroSoundFont/midi/MidiMessage.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ package pl.lemanski.mikroSoundFont.midi
4242
sealed interface MidiMessage {
4343
val type: MidiMessageType
4444
val time: Int
45-
// Add track number ???
4645
}
4746

4847
sealed class MidiSystemMessage : MidiMessage {
@@ -99,6 +98,7 @@ sealed class MidiMetaMessage : MidiMessage {
9998
sealed class MidiVoiceMessage : MidiMessage {
10099
data class NoteOn(
101100
override val time: Int,
101+
val channel: Int,
102102
val key: Int,
103103
val velocity: Int,
104104
) : MidiVoiceMessage() {
@@ -107,6 +107,7 @@ sealed class MidiVoiceMessage : MidiMessage {
107107

108108
data class NoteOff(
109109
override val time: Int,
110+
val channel: Int,
110111
val key: Int,
111112
val velocity: Int,
112113
) : MidiVoiceMessage() {
@@ -115,6 +116,7 @@ sealed class MidiVoiceMessage : MidiMessage {
115116

116117
data class KeyPressure(
117118
override val time: Int,
119+
val channel: Int,
118120
val key: Int,
119121
val keyPressure: Int,
120122
) : MidiVoiceMessage() {
@@ -123,6 +125,7 @@ sealed class MidiVoiceMessage : MidiMessage {
123125

124126
data class ControlChange(
125127
override val time: Int,
128+
val channel: Int,
126129
val control: Int,
127130
val controlValue: Int,
128131
) : MidiVoiceMessage() {
@@ -131,20 +134,23 @@ sealed class MidiVoiceMessage : MidiMessage {
131134

132135
data class ProgramChange(
133136
override val time: Int,
137+
val channel: Int,
134138
val program: Int,
135139
) : MidiVoiceMessage() {
136140
override val type: MidiMessageType = MidiMessageType.PROGRAM_CHANGE
137141
}
138142

139143
data class ChannelPressure(
140144
override val time: Int,
145+
val channel: Int,
141146
val channelPressure: Int,
142147
) : MidiVoiceMessage() {
143148
override val type: MidiMessageType = MidiMessageType.CHANNEL_PRESSURE
144149
}
145150

146151
data class PitchBend(
147152
override val time: Int,
153+
val channel: Int,
148154
val pitchBend: Int,
149155
) : MidiVoiceMessage() {
150156
override val type: MidiMessageType = MidiMessageType.PITCH_BEND

0 commit comments

Comments
 (0)