Skip to content

Commit f56e804

Browse files
committed
Improved architecture
1 parent e7f4ac9 commit f56e804

24 files changed

+641
-482
lines changed

core/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ kotlin {
3737
sourceSets {
3838

3939
commonMain.dependencies {
40-
implementation(libs.coroutines.core)
4140
implementation(projects.lib)
41+
implementation(libs.kotlinx.io)
4242
}
4343

4444
commonTest.dependencies {

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

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -57,32 +57,4 @@ fun ByteArray.readIntAt(index: Int): Int {
5757

5858
fun ByteArray.readByteAt(index: Int): Byte {
5959
return this[index] and 0xFF.toByte()
60-
}
61-
62-
fun ByteArray.readVariableLengthAt(index: Int): Pair<Int, Int>? {
63-
var pos = index
64-
var result = 0
65-
var i = 0
66-
var countinuation: Int
67-
68-
while (i < 4) {
69-
if (pos >= size) {
70-
break
71-
}
72-
73-
countinuation = this[pos].toInt() and 0xFF
74-
pos++
75-
76-
if (countinuation and 0x80 != 0) {
77-
// store data bits in result
78-
result = ((result or (countinuation and 0x7F)) shl 7)
79-
} else {
80-
// return result
81-
return (result or countinuation) to pos
82-
}
83-
84-
i++
85-
}
86-
87-
return null
8860
}
Lines changed: 8 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,13 @@
11
package pl.lemanski.mikroSoundFont.io.midi
22

3-
import pl.lemanski.mikroSoundFont.midi.MidiMessage
4-
import pl.lemanski.mikroSoundFont.midi.MidiMessageType
53
import pl.lemanski.mikroSoundFont.midi.MidiTrack
64

7-
class MidiFile(
8-
private val buffer: ByteArray
5+
data class MidiFile(
6+
val header: Header,
7+
val tracks: List<MidiTrack>
98
) {
10-
val header: MidiFileHeader
11-
val tracks: Array<MidiTrack>
12-
13-
init {
14-
header = MidiFileHeader.fromBytes(buffer)
15-
var data = buffer.copyOfRange(MidiFileHeader.SIZE_IN_BYTES, buffer.size)
16-
tracks = Array(header.trackCount) {
17-
val track = MidiTrack.fromBytes(data)
18-
data = data.copyOfRange(track.rawData.size + 8, data.size)
19-
track
20-
}
21-
}
22-
23-
/**
24-
* Messages are ordered by the absolute time of occurrence
25-
* meaning that delta times has been converted to absolute times.
26-
*
27-
* @return messages parsed from all tracks.
28-
*/
29-
fun messages(): List<MidiMessage> {
30-
val actualMessages = mutableListOf<MidiMessage>()
31-
var totalTicks = 0
32-
var tempoMsec = 0
33-
var tempoTicks = 0
34-
var ticksToTime = 500_000.0 / (1000.0 * header.division)
35-
36-
for (track in tracks) {
37-
track.messages.sortedBy { it.time }
38-
for (msg in track.messages) {
39-
totalTicks += msg.time
40-
val actualTime = tempoMsec + ((totalTicks - tempoTicks) * ticksToTime).toInt()
41-
val actual = msg.copy(time = actualTime)
42-
43-
if (actual.type is MidiMessageType.SetTempo) {
44-
val tempo = actual.type.tempo
45-
ticksToTime = (tempo[0].toInt() shl 16 or (tempo[1].toInt() shl 8) or tempo[2].toInt()) / (1000.0 * header.division)
46-
tempoMsec = actualTime
47-
tempoTicks = totalTicks
48-
}
49-
50-
actualMessages.add(actual)
51-
}
52-
}
53-
54-
return actualMessages
55-
}
56-
}
9+
data class Header(
10+
val trackCount: Int,
11+
val division: Int,
12+
)
13+
}

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

Lines changed: 0 additions & 49 deletions
This file was deleted.
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package pl.lemanski.mikroSoundFont.io.midi
2+
3+
import kotlinx.io.Buffer
4+
import kotlinx.io.readString
5+
import kotlinx.io.readUInt
6+
import kotlinx.io.readUShort
7+
import pl.lemanski.mikroSoundFont.InvalidMidiDataException
8+
import pl.lemanski.mikroSoundFont.io.midi.message.MidiMessageContext
9+
import pl.lemanski.mikroSoundFont.midi.MidiMessage
10+
import pl.lemanski.mikroSoundFont.midi.MidiTrack
11+
12+
internal class MidiFileParser(
13+
private val data: ByteArray,
14+
) {
15+
private val buffer = Buffer().apply {
16+
write(data)
17+
}
18+
19+
fun parse(): MidiFile {
20+
val header = parseHeader()
21+
val tracks = parseTracks(header.trackCount)
22+
23+
return MidiFile(
24+
header = header,
25+
tracks = tracks
26+
)
27+
}
28+
29+
internal fun parseHeader(): MidiFile.Header {
30+
val hSize = 14
31+
32+
if (buffer.size < hSize) {
33+
throw InvalidMidiDataException("Unexpected buffer size. Buffer is too small.")
34+
}
35+
36+
if (buffer.readString(4) != "MThd") {
37+
throw InvalidMidiDataException("Invalid MThd header: $buffer")
38+
}
39+
40+
if (buffer.readUInt() != 6u) {
41+
throw InvalidMidiDataException("Invalid MThd header: $buffer")
42+
}
43+
44+
if (buffer.readUShort() > 2u) {
45+
throw InvalidMidiDataException("Invalid MThd header: $buffer")
46+
}
47+
48+
val trackCount: Int = buffer.readShort().toInt()
49+
val division: Int = buffer.readShort().toInt() // ticks per beat
50+
51+
if ((division shl 8) and 0x80 != 0) {
52+
throw InvalidMidiDataException("Unsupported SMPTE timing: $buffer")
53+
}
54+
55+
if (trackCount <= 0) {
56+
throw InvalidMidiDataException("Invalid track values: $trackCount")
57+
}
58+
59+
if (division <= 0) {
60+
throw InvalidMidiDataException("Invalid division values: $division")
61+
}
62+
63+
return MidiFile.Header(
64+
trackCount = trackCount,
65+
division = division
66+
)
67+
}
68+
69+
internal fun parseTracks(n: Int): List<MidiTrack> = List(n) { idx -> parseTrack(idx) }
70+
71+
internal fun parseTrack(id: Int): MidiTrack {
72+
val trackHeader = parseTrackHeader()
73+
val trackBuffer = Buffer()
74+
val messages = mutableListOf<MidiMessage>()
75+
76+
buffer.copyTo(trackBuffer, 0L, trackHeader.dataSize.toLong())
77+
78+
val messageContext = MidiMessageContext(trackBuffer)
79+
while (trackBuffer.size > 0) {
80+
messages.add(messageContext.readMessage())
81+
}
82+
83+
return MidiTrack(
84+
header = trackHeader,
85+
messages = messages
86+
)
87+
}
88+
89+
internal fun parseTrackHeader(): MidiTrack.Header {
90+
if (buffer.readString(4) != "MTrk") {
91+
throw InvalidMidiDataException("Invalid MTrk header: $buffer")
92+
}
93+
94+
val trackLength = buffer.readInt()
95+
if (trackLength < 0) {
96+
throw InvalidMidiDataException("Invalid MTrk header.Track length is negative: $buffer")
97+
}
98+
99+
return MidiTrack.Header(dataSize = trackLength)
100+
}
101+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package pl.lemanski.mikroSoundFont.io.midi.message
2+
3+
import kotlinx.io.Buffer
4+
import kotlinx.io.readUByte
5+
import pl.lemanski.mikroSoundFont.InvalidMidiDataException
6+
7+
internal fun Buffer.readVarLen(): Int {
8+
var r = 0
9+
var c: Int
10+
var bytesRead = 0
11+
12+
do {
13+
c = readUByte().toInt()
14+
bytesRead++
15+
r = (r shl 7) or (c and 0x7F) // add 7 LSB of c to r
16+
} while (c and 0x80 != 0 && bytesRead < 4) // until c MSB is 1
17+
18+
if (bytesRead >= 4) {
19+
throw InvalidMidiDataException("Malformed MIDI. Variable length quantity exceeds 32 bits.")
20+
}
21+
22+
return r
23+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package pl.lemanski.mikroSoundFont.io.midi.message
2+
3+
import kotlinx.io.Buffer
4+
import kotlinx.io.readUByte
5+
import pl.lemanski.mikroSoundFont.io.midi.message.read.MidiMessageParser
6+
import pl.lemanski.mikroSoundFont.io.midi.message.read.MidiMetaMessageParser
7+
import pl.lemanski.mikroSoundFont.io.midi.message.read.MidiSystemMessageParser
8+
import pl.lemanski.mikroSoundFont.io.midi.message.read.MidiVoiceMessageParser
9+
import pl.lemanski.mikroSoundFont.midi.MidiMessage
10+
import pl.lemanski.mikroSoundFont.midi.UnsupportedMidiMessage
11+
12+
internal class MidiMessageContext(
13+
private val buffer: Buffer
14+
) {
15+
private val systemMessageParser = MidiSystemMessageParser(buffer)
16+
private val voiceMessageParser = MidiVoiceMessageParser(buffer)
17+
private val metaMessageParser = MidiMetaMessageParser(buffer)
18+
private lateinit var parser: MidiMessageParser
19+
20+
fun readMessage(): MidiMessage {
21+
val dt = buffer.readVarLen()
22+
val status = buffer.readUByte().toInt()
23+
24+
parser = when (status) {
25+
in systemMessageParser.supportedTypes() -> systemMessageParser
26+
in metaMessageParser.supportedTypes() -> metaMessageParser
27+
in voiceMessageParser.supportedTypes() -> voiceMessageParser
28+
else -> return UnsupportedMidiMessage
29+
}
30+
31+
return parser.parse(status, dt)
32+
}
33+
34+
fun writeMessage() {
35+
// TODO
36+
}
37+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package pl.lemanski.mikroSoundFont.io.midi.message.read
2+
3+
import pl.lemanski.mikroSoundFont.midi.MidiMessage
4+
5+
/**
6+
* Parser for midi messages.
7+
*/
8+
internal interface MidiMessageParser {
9+
/**
10+
* Parses a midi message.
11+
* @param deltaTime the delta time since the last message
12+
*
13+
* @return [MidiMessage]
14+
*/
15+
fun parse(status: Int, deltaTime: Int = 0): MidiMessage
16+
17+
/**
18+
* @return the types of midi messages that this parser supports.
19+
*/
20+
fun supportedTypes(): Set<Int>
21+
}

0 commit comments

Comments
 (0)