Skip to content

Commit d55dc88

Browse files
committed
Added MIDI file header parsing
1 parent 81fd16c commit d55dc88

File tree

15 files changed

+986
-85
lines changed

15 files changed

+986
-85
lines changed

core/build.gradle.kts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ android {
1111
compileSdk = libs.versions.android.compileSdk.get().toInt()
1212
minSdk = libs.versions.android.minSdk.get().toInt()
1313
}
14+
15+
testOptions {
16+
unitTests {
17+
isReturnDefaultValues = true
18+
}
19+
}
1420
}
1521

1622
kotlin {

core/src/androidMain/kotlin/pl/lemanski/mikroSoundFont/io/File.android.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ actual fun loadFile(path: String): ByteArray {
1515
}
1616
}
1717

18-
actual fun saveFile(path: String, byteArray: ByteArray) {
18+
actual fun saveFile(byteArray: ByteArray, path: String) {
1919
val file = File(path)
2020
if (file.exists()) {
2121
file.createNewFile()
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package pl.lemanski.mikroSoundFont
2+
3+
sealed class MidiException(override val message: String?) : Exception(message)
4+
5+
class InvalidMidiDataException(override val message: String?) : MidiException(message)
6+

core/src/commonMain/kotlin/pl/lemanski/mikroSoundFont/io/ByteArrayUtils.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,8 @@ fun FloatArray.toByteArrayBigEndian(): ByteArray {
4141
}
4242
return byteArray
4343
}
44+
45+
46+
fun ByteArray.readShortAt(index: Int): Short {
47+
return ((this[index].toInt() and 0xFF) shl 8 or (this[index + 1].toInt() and 0xFF)).toShort()
48+
}

core/src/commonMain/kotlin/pl/lemanski/mikroSoundFont/io/File.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ package pl.lemanski.mikroSoundFont.io
22

33
expect fun loadFile(path: String): ByteArray
44

5-
expect fun saveFile(path: String, byteArray: ByteArray)
5+
expect fun saveFile(byteArray: ByteArray, path: String)
Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,47 @@
11
package pl.lemanski.mikroSoundFont.io.midi
22

3+
import pl.lemanski.mikroSoundFont.InvalidMidiDataException
4+
import pl.lemanski.mikroSoundFont.io.readShortAt
5+
36
data class MidiFileHeader(
4-
val format: Int,
5-
val numTracks: Int,
7+
val trackCount: Int,
68
val division: Int
7-
)
9+
) {
10+
companion object {
11+
@OptIn(ExperimentalStdlibApi::class)
12+
fun fromBytes(buffer: ByteArray): MidiFileHeader {
13+
val trackCount: Int
14+
val division: Int
15+
16+
if (buffer.size < 14) {
17+
throw InvalidMidiDataException("Unexpected buffer size. Buffer is too small.")
18+
}
19+
20+
val headerBytes = buffer.copyOfRange(0, 14)
21+
22+
if (headerBytes.copyOfRange(0, 4).decodeToString() != "MThd" || headerBytes[7] != 6.toByte() || headerBytes[9] > 2) {
23+
throw InvalidMidiDataException("Invalid MThd header: ${headerBytes.toHexString()}")
24+
}
25+
26+
if (headerBytes[12].toInt() and 0x80 != 0) {
27+
throw InvalidMidiDataException("Unsupported SMPTE timing: ${headerBytes.toHexString()}")
28+
}
29+
30+
trackCount = headerBytes.readShortAt(10).toInt()
31+
division = headerBytes.readShortAt(12).toInt() // ticks per beat
32+
33+
if (trackCount <= 0 ) {
34+
throw InvalidMidiDataException("Invalid track values: $trackCount")
35+
}
36+
37+
if (division <= 0) {
38+
throw InvalidMidiDataException("Invalid division values: $division")
39+
}
40+
41+
return MidiFileHeader(
42+
trackCount = trackCount,
43+
division = division
44+
)
45+
}
46+
}
47+
}
Lines changed: 141 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,13 @@
11
package pl.lemanski.mikroSoundFont.io.midi
22

3+
import pl.lemanski.mikroSoundFont.InvalidMidiDataException
34
import pl.lemanski.mikroSoundFont.getLogger
45
import pl.lemanski.mikroSoundFont.midi.MidiMessage
56
import pl.lemanski.mikroSoundFont.midi.MidiMessage.Type
6-
import pl.lemanski.mikroSoundFont.midi.MidiMessageControlChange
7-
import pl.lemanski.mikroSoundFont.midi.MidiMessageNoteOff
8-
import pl.lemanski.mikroSoundFont.midi.MidiMessageNoteOn
9-
import pl.lemanski.mikroSoundFont.midi.MidiMessageProgramChange
107

118
class MidiFileParser {
129
private val logger = getLogger()
1310

14-
fun parseMidiFile(data: ByteArray): List<MidiMessage> {
15-
val header = parseMidiHeader(data)
16-
val events = mutableListOf<MidiMessage>()
17-
18-
var trackStart = 14 // Header ends at byte 14
19-
repeat(header.numTracks) {
20-
events.addAll(parseTrack(data, trackStart))
21-
trackStart += 8 // Skip over track header and length (MTrk + track length)
22-
}
23-
24-
return events
25-
}
26-
2711
private fun readVariableLengthQuantity(data: ByteArray, index: Int): Pair<Int, Int> {
2812
var value = 0
2913
var i = index
@@ -38,17 +22,6 @@ class MidiFileParser {
3822
return Pair(value, i)
3923
}
4024

41-
private fun parseMidiHeader(data: ByteArray): MidiFileHeader {
42-
val chunkType = data.sliceArray(0..3).map { it.toInt().toChar() }.joinToString("")
43-
if (chunkType != "MThd") throw IllegalArgumentException("Invalid MIDI header")
44-
45-
val format = ((data[8].toInt() and 0xFF) shl 8) or (data[9].toInt() and 0xFF)
46-
val numTracks = ((data[10].toInt() and 0xFF) shl 8) or (data[11].toInt() and 0xFF)
47-
val division = ((data[12].toInt() and 0xFF) shl 8) or (data[13].toInt() and 0xFF)
48-
49-
return MidiFileHeader(format, numTracks, division)
50-
}
51-
5225
fun parseTrack(data: ByteArray, trackStart: Int): List<MidiMessage> {
5326
val events = mutableListOf<MidiMessage>()
5427
var index = trackStart + 8 // Skip "MTrk" and track length
@@ -59,6 +32,8 @@ class MidiFileParser {
5932
val (deltaTime, newIndex) = readVariableLengthQuantity(data, index)
6033
index = newIndex
6134

35+
if (index >= data.size - 1) break
36+
6237
val statusByte = data[index].toInt() and 0xFF
6338
index++
6439

@@ -73,44 +48,147 @@ class MidiFileParser {
7348
val channel = lastStatusByte and 0x0F
7449

7550
when (command.toType()) {
76-
Type.NOTE_OFF -> {
77-
val key = data[index].toInt() and 0xFF
78-
events.add(MidiMessageNoteOff(deltaTime, channel, key))
79-
index += 2
80-
}
81-
Type.NOTE_ON -> {
82-
val key = data[index].toInt() and 0xFF
83-
val velocity = data[index + 1].toInt() and 0xFF
84-
events.add(MidiMessageNoteOn(deltaTime, channel, key, velocity))
85-
index += 2
86-
}
87-
Type.PROGRAM_CHANGE -> {
88-
val program = data[index].toInt() and 0xFF
89-
events.add(MidiMessageProgramChange(deltaTime, channel, program))
90-
index++
91-
}
92-
Type.CONTROL_CHANGE -> {
93-
val control = data[index].toInt() and 0xFF
94-
val value = data[index + 1].toInt() and 0xFF
95-
events.add(MidiMessageControlChange(deltaTime, channel, control, value))
96-
index += 2
97-
}
98-
Type.KEY_PRESSURE -> {
99-
logger.log("KEY_PRESSURE")
100-
}
101-
Type.CHANNEL_PRESSURE -> {
102-
logger.log("CHANNEL_PRESSURE")
103-
}
104-
Type.PITCH_BEND -> {
105-
logger.log("PITCH_BEND")
106-
}
107-
Type.SET_TEMPO -> {
108-
logger.log("SET_TEMPO")
109-
}
51+
Type.ControlChange.TML_BANK_SELECT_MSB -> TODO()
52+
Type.ControlChange.TML_MODULATIONWHEEL_MSB -> TODO()
53+
Type.ControlChange.TML_BREATH_MSB -> TODO()
54+
Type.ControlChange.TML_FOOT_MSB -> TODO()
55+
Type.ControlChange.TML_PORTAMENTO_TIME_MSB -> TODO()
56+
Type.ControlChange.TML_DATA_ENTRY_MSB -> TODO()
57+
Type.ControlChange.TML_VOLUME_MSB -> TODO()
58+
Type.ControlChange.TML_BALANCE_MSB -> TODO()
59+
Type.ControlChange.TML_PAN_MSB -> TODO()
60+
Type.ControlChange.TML_EXPRESSION_MSB -> TODO()
61+
Type.ControlChange.TML_EFFECTS1_MSB -> TODO()
62+
Type.ControlChange.TML_EFFECTS2_MSB -> TODO()
63+
Type.ControlChange.TML_GPC1_MSB -> TODO()
64+
Type.ControlChange.TML_GPC2_MSB -> TODO()
65+
Type.ControlChange.TML_GPC3_MSB -> TODO()
66+
Type.ControlChange.TML_GPC4_MSB -> TODO()
67+
Type.ControlChange.TML_BANK_SELECT_LSB -> TODO()
68+
Type.ControlChange.TML_MODULATIONWHEEL_LSB -> TODO()
69+
Type.ControlChange.TML_BREATH_LSB -> TODO()
70+
Type.ControlChange.TML_FOOT_LSB -> TODO()
71+
Type.ControlChange.TML_PORTAMENTO_TIME_LSB -> TODO()
72+
Type.ControlChange.TML_DATA_ENTRY_LSB -> TODO()
73+
Type.ControlChange.TML_VOLUME_LSB -> TODO()
74+
Type.ControlChange.TML_BALANCE_LSB -> TODO()
75+
Type.ControlChange.TML_PAN_LSB -> TODO()
76+
Type.ControlChange.TML_EXPRESSION_LSB -> TODO()
77+
Type.ControlChange.TML_EFFECTS1_LSB -> TODO()
78+
Type.ControlChange.TML_EFFECTS2_LSB -> TODO()
79+
Type.ControlChange.TML_GPC1_LSB -> TODO()
80+
Type.ControlChange.TML_GPC2_LSB -> TODO()
81+
Type.ControlChange.TML_GPC3_LSB -> TODO()
82+
Type.ControlChange.TML_GPC4_LSB -> TODO()
83+
Type.ControlChange.TML_SUSTAIN_SWITCH -> TODO()
84+
Type.ControlChange.TML_PORTAMENTO_SWITCH -> TODO()
85+
Type.ControlChange.TML_SOSTENUTO_SWITCH -> TODO()
86+
Type.ControlChange.TML_SOFT_PEDAL_SWITCH -> TODO()
87+
Type.ControlChange.TML_LEGATO_SWITCH -> TODO()
88+
Type.ControlChange.TML_HOLD2_SWITCH -> TODO()
89+
Type.ControlChange.TML_SOUND_CTRL1 -> TODO()
90+
Type.ControlChange.TML_SOUND_CTRL2 -> TODO()
91+
Type.ControlChange.TML_SOUND_CTRL3 -> TODO()
92+
Type.ControlChange.TML_SOUND_CTRL4 -> TODO()
93+
Type.ControlChange.TML_SOUND_CTRL5 -> TODO()
94+
Type.ControlChange.TML_SOUND_CTRL6 -> TODO()
95+
Type.ControlChange.TML_SOUND_CTRL7 -> TODO()
96+
Type.ControlChange.TML_SOUND_CTRL8 -> TODO()
97+
Type.ControlChange.TML_SOUND_CTRL9 -> TODO()
98+
Type.ControlChange.TML_SOUND_CTRL10 -> TODO()
99+
Type.ControlChange.TML_GPC5 -> TODO()
100+
Type.ControlChange.TML_GPC6 -> TODO()
101+
Type.ControlChange.TML_GPC7 -> TODO()
102+
Type.ControlChange.TML_GPC8 -> TODO()
103+
Type.ControlChange.TML_PORTAMENTO_CTRL -> TODO()
104+
Type.ControlChange.TML_FX_REVERB -> TODO()
105+
Type.ControlChange.TML_FX_TREMOLO -> TODO()
106+
Type.ControlChange.TML_FX_CHORUS -> TODO()
107+
Type.ControlChange.TML_FX_CELESTE_DETUNE -> TODO()
108+
Type.ControlChange.TML_FX_PHASER -> TODO()
109+
Type.ControlChange.TML_DATA_ENTRY_INCR -> TODO()
110+
Type.ControlChange.TML_DATA_ENTRY_DECR -> TODO()
111+
Type.ControlChange.TML_NRPN_LSB -> TODO()
112+
Type.ControlChange.TML_NRPN_MSB -> TODO()
113+
Type.ControlChange.TML_RPN_LSB -> TODO()
114+
Type.ControlChange.TML_RPN_MSB -> TODO()
115+
Type.ControlChange.TML_ALL_SOUND_OFF -> TODO()
116+
Type.ControlChange.TML_ALL_CTRL_OFF -> TODO()
117+
Type.ControlChange.TML_LOCAL_CONTROL -> TODO()
118+
Type.ControlChange.TML_ALL_NOTES_OFF -> TODO()
119+
Type.ControlChange.TML_OMNI_OFF -> TODO()
120+
Type.ControlChange.TML_OMNI_ON -> TODO()
121+
Type.ControlChange.TML_POLY_OFF -> TODO()
122+
Type.ControlChange.TML_POLY_ON -> TODO()
123+
Type.Message.NOTE_OFF -> TODO()
124+
Type.Message.NOTE_ON -> TODO()
125+
Type.Message.KEY_PRESSURE -> TODO()
126+
Type.Message.CONTROL_CHANGE -> TODO()
127+
Type.Message.PROGRAM_CHANGE -> TODO()
128+
Type.Message.CHANNEL_PRESSURE -> TODO()
129+
Type.Message.PITCH_BEND -> TODO()
130+
Type.Message.SET_TEMPO -> TODO()
131+
Type.System.TEXT -> TODO()
132+
Type.System.COPYRIGHT -> TODO()
133+
Type.System.TRACK_NAME -> TODO()
134+
Type.System.INST_NAME -> TODO()
135+
Type.System.LYRIC -> TODO()
136+
Type.System.MARKER -> TODO()
137+
Type.System.CUE_POINT -> TODO()
138+
Type.System.EOT -> TODO()
139+
Type.System.SMPTE_OFFSET -> TODO()
140+
Type.System.TIME_SIGNATURE -> TODO()
141+
Type.System.KEY_SIGNATURE -> TODO()
142+
Type.System.SEQUENCER_EVENT -> TODO()
143+
Type.System.SYSEX -> TODO()
144+
Type.System.TIME_CODE -> TODO()
145+
Type.System.SONG_POSITION -> TODO()
146+
Type.System.SONG_SELECT -> TODO()
147+
Type.System.TUNE_REQUEST -> TODO()
148+
Type.System.EOX -> TODO()
149+
Type.System.SYNC -> TODO()
150+
Type.System.TICK -> TODO()
151+
Type.System.START -> TODO()
152+
Type.System.CONTINUE -> TODO()
153+
Type.System.STOP -> TODO()
154+
Type.System.ACTIVE_SENSING -> TODO()
155+
Type.System.SYSTEM_RESET -> TODO()
110156
}
111157
}
112158
return events
113159
}
114160

115-
private fun Int.toType(): Type = Type.entries.find { it.value == this } ?: throw IllegalArgumentException("Unknown MIDI event: $this")
161+
private fun Int.toType(): Type {
162+
println("event: $this")
163+
Type.ControlChange.entries.find { it.controllerNumber == this }?.let { return it }
164+
Type.Message.entries.find { it.value == this }?.let { return it }
165+
Type.System.entries.find { it.value == this }?.let { return it }
166+
167+
throw InvalidMidiDataException("Unknown event type: $this")
168+
}
169+
170+
private fun readVariableLength(buffer: ByteArray, startIndex: Int): Pair<Int, Int> {
171+
var result = 0
172+
var index = startIndex // Track current position in the array
173+
var i = 0
174+
175+
while (i < 4) {
176+
if (index >= buffer.size) {
177+
throw IllegalArgumentException("Unexpected end of file")
178+
}
179+
180+
val c = buffer[index]
181+
index++ // Move the pointer to the next byte
182+
183+
if (c.toInt() and 0x80 != 0) {
184+
result = (result or (c.toInt() and 0x7F)) shl 7
185+
} else {
186+
return Pair(result or c.toInt(), index) // Return the result and the new index
187+
}
188+
189+
i++
190+
}
191+
192+
throw IllegalArgumentException("Invalid variable length byte count")
193+
}
116194
}

0 commit comments

Comments
 (0)