Skip to content

Commit 242466b

Browse files
committed
Added naive implementation of MIDI parser
1 parent 7fccda1 commit 242466b

File tree

16 files changed

+283
-6
lines changed

16 files changed

+283
-6
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package pl.lemanski.mikroSoundFont
2+
3+
import android.util.Log
4+
5+
actual fun getLogger(): MikroSoundFontLogger = object : MikroSoundFontLogger {
6+
override fun log(message: String) {
7+
Log.d("MikroSoundFont", message)
8+
}
9+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package pl.lemanski.mikroSoundFont.io
2+
3+
import java.io.File
4+
5+
actual fun loadFile(path: String): ByteArray {
6+
val file = File(path)
7+
if (!file.exists()) {
8+
throw IllegalArgumentException("File does not exist at: $path")
9+
}
10+
11+
return try {
12+
file.readBytes()
13+
} catch (ex: Exception) {
14+
byteArrayOf()
15+
}
16+
}
17+
18+
actual fun saveFile(path: String, byteArray: ByteArray) {
19+
val file = File(path)
20+
if (file.exists()) {
21+
file.createNewFile()
22+
}
23+
24+
file.writeBytes(byteArray)
25+
}

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package pl.lemanski.mikroSoundFont.io.wav
22

33
import java.nio.ByteBuffer
44
import java.nio.ByteOrder
5+
import java.util.Stack
56

67
actual fun WavFileHeader.toByteArray(): ByteArray {
78
val byteArray = ByteArray(44) // The size is based on the structure
@@ -36,4 +37,22 @@ actual fun WavFileHeader.toByteArray(): ByteArray {
3637
buffer.putInt(40, subchunk2Size.toInt())
3738

3839
return byteArray
40+
}
41+
42+
fun isValid(s: String): Boolean {
43+
if (s.length % 2 != 0) {
44+
return false
45+
}
46+
47+
val stack = mutableListOf<Char>()
48+
s.forEach {
49+
when (it) {
50+
'(', '{', '[' -> stack.add(it)
51+
')' -> if (stack.lastOrNull() == '(') stack.removeLast() else return false
52+
'}' -> if (stack.lastOrNull() == '{') stack.removeLast() else return false
53+
']' -> if (stack.lastOrNull() == '[') stack.removeLast() else return false
54+
}
55+
}
56+
57+
return stack.size == 0
3958
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package pl.lemanski.mikroSoundFont
2+
3+
interface MikroSoundFontLogger {
4+
fun log(message: String)
5+
}
6+
7+
expect fun getLogger(): MikroSoundFontLogger
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package pl.lemanski.mikroSoundFont.io
2+
3+
expect fun loadFile(path: String): ByteArray
4+
5+
expect fun saveFile(path: String, byteArray: ByteArray)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package pl.lemanski.mikroSoundFont.io.midi
2+
3+
data class MidiFileHeader(
4+
val format: Int,
5+
val numTracks: Int,
6+
val division: Int
7+
)
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package pl.lemanski.mikroSoundFont.io.midi
2+
3+
import pl.lemanski.mikroSoundFont.midi.EventType
4+
import pl.lemanski.mikroSoundFont.midi.MidiEvent
5+
6+
class MidiFileParser {
7+
fun parseMidiFile(data: ByteArray): List<MidiEvent> {
8+
val header = parseMidiHeader(data)
9+
val events = mutableListOf<MidiEvent>()
10+
11+
var trackStart = 14 // Header ends at byte 14
12+
repeat(header.numTracks) {
13+
events.addAll(parseTrack(data, trackStart))
14+
trackStart += 8 // Skip over track header and length (MTrk + track length)
15+
}
16+
17+
return events
18+
}
19+
20+
fun readVariableLengthQuantity(data: ByteArray, index: Int): Pair<Int, Int> {
21+
var value = 0
22+
var i = index
23+
var byte: Int
24+
25+
do {
26+
byte = data[i].toInt() and 0xFF
27+
value = (value shl 7) or (byte and 0x7F)
28+
i++
29+
} while (byte and 0x80 != 0)
30+
31+
return Pair(value, i)
32+
}
33+
34+
fun parseMidiHeader(data: ByteArray): MidiFileHeader {
35+
val chunkType = data.sliceArray(0..3).map { it.toInt().toChar() }.joinToString("")
36+
if (chunkType != "MThd") throw IllegalArgumentException("Invalid MIDI header")
37+
38+
val format = ((data[8].toInt() and 0xFF) shl 8) or (data[9].toInt() and 0xFF)
39+
val numTracks = ((data[10].toInt() and 0xFF) shl 8) or (data[11].toInt() and 0xFF)
40+
val division = ((data[12].toInt() and 0xFF) shl 8) or (data[13].toInt() and 0xFF)
41+
42+
return MidiFileHeader(format, numTracks, division)
43+
}
44+
45+
fun parseTrack(data: ByteArray, trackStart: Int): List<MidiEvent> {
46+
val events = mutableListOf<MidiEvent>()
47+
var index = trackStart + 8 // Skip "MTrk" and track length
48+
var lastStatusByte = 0
49+
50+
while (index < data.size) {
51+
// Read delta time
52+
val (deltaTime, newIndex) = readVariableLengthQuantity(data, index)
53+
index = newIndex
54+
55+
val statusByte = data[index].toInt() and 0xFF
56+
index++
57+
58+
// MIDI running status (if the status byte is missing, reuse the last one)
59+
if (statusByte and 0x80 == 0) {
60+
index--
61+
} else {
62+
lastStatusByte = statusByte
63+
}
64+
65+
val command = lastStatusByte and 0xF0
66+
val channel = lastStatusByte and 0x0F
67+
68+
when (command) {
69+
0x90 -> { // Note On
70+
val key = data[index].toInt() and 0xFF
71+
val velocity = data[index + 1].toInt() and 0xFF
72+
events.add(MidiEvent(deltaTime.toLong(), EventType.NOTE_ON, listOf(channel, key, velocity)))
73+
index += 2
74+
}
75+
0x80 -> { // Note Off
76+
val key = data[index].toInt() and 0xFF
77+
events.add(MidiEvent(deltaTime.toLong(), EventType.NOTE_OFF, listOf(channel, key)))
78+
index += 2
79+
}
80+
0xC0 -> { // Program Change
81+
val program = data[index].toInt() and 0xFF
82+
events.add(MidiEvent(deltaTime.toLong(), EventType.PROGRAM_CHANGE, listOf(channel, program)))
83+
index++
84+
}
85+
// Add more cases for other types of MIDI events
86+
}
87+
}
88+
return events
89+
}
90+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package pl.lemanski.mikroSoundFont.midi
2+
3+
data class MidiEvent(
4+
val deltaTime: Long,
5+
val eventType: EventType,
6+
val data: List<Int>
7+
)
8+
9+
enum class EventType {
10+
NOTE_ON,
11+
NOTE_OFF,
12+
PROGRAM_CHANGE,
13+
CONTROL_CHANGE
14+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
package pl.lemanski.mikroSoundFont.midi
2+
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package pl.lemanski.mikroSoundFont.midi
2+
3+
import pl.lemanski.mikroSoundFont.SoundFont
4+
5+
class MidiSequencer(
6+
private val soundFont: SoundFont
7+
) {
8+
private var currentTime = 0L
9+
private val events = mutableListOf<MidiEvent>()
10+
11+
fun loadMidiEvents(midiEvents: List<MidiEvent>) {
12+
events.clear()
13+
events.addAll(midiEvents)
14+
}
15+
16+
fun play() {
17+
events.forEach { event ->
18+
currentTime += event.deltaTime
19+
handleEvent(event)
20+
}
21+
}
22+
23+
private fun handleEvent(event: MidiEvent) {
24+
when (event.eventType) {
25+
EventType.NOTE_ON -> soundFont.noteOn(
26+
event.data[0],
27+
event.data[1],
28+
event.data[2] / 127.0f
29+
)
30+
EventType.NOTE_OFF -> soundFont.noteOff(event.data[0], event.data[1])
31+
EventType.PROGRAM_CHANGE -> soundFont.setBankPreset(
32+
0,
33+
event.data[0],
34+
event.data[1]
35+
)
36+
EventType.CONTROL_CHANGE -> handleControlChange(
37+
event.data[0],
38+
event.data[1],
39+
event.data[2]
40+
)
41+
}
42+
}
43+
44+
private fun handleControlChange(controller: Int, value: Int, channel: Int) {
45+
// Handle pitch bends, volume controls, etc.
46+
}
47+
}

0 commit comments

Comments
 (0)