Skip to content

Commit aec2fa8

Browse files
committed
Added encoder implementation
1 parent df18a3b commit aec2fa8

File tree

9 files changed

+331
-85
lines changed

9 files changed

+331
-85
lines changed

core/src/androidInstrumentedTest/kotlin/pl/lemanski/mikroSoundFont/io/midi/MidiMessageParserTest.kt

Lines changed: 0 additions & 73 deletions
This file was deleted.
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package pl.lemanski.mikroSoundFont.io.midi.message
2+
3+
import junit.framework.TestCase.assertEquals
4+
import junit.framework.TestCase.assertTrue
5+
import kotlinx.io.Buffer
6+
import kotlinx.io.readByteArray
7+
import org.junit.Test
8+
import pl.lemanski.mikroSoundFont.InvalidMidiDataException
9+
10+
class BufferExtTest {
11+
12+
@OptIn(ExperimentalUnsignedTypes::class)
13+
@Test
14+
fun testSingleByteValues() {
15+
val buffer1 = buildBuffer(0x7Fu)
16+
assertEquals(0x7F, buffer1.readVarLen())
17+
18+
val buffer2 = buildBuffer(0x00u)
19+
assertEquals(0x00, buffer2.readVarLen())
20+
21+
val buffer3 = buildBuffer(0x40u)
22+
assertEquals(0x40, buffer3.readVarLen())
23+
}
24+
25+
@OptIn(ExperimentalUnsignedTypes::class)
26+
@Test
27+
fun testMultiByteValues() {
28+
val buffer1 = buildBuffer(0x81u, 0x00u) // 128
29+
assertEquals(0x80, buffer1.readVarLen())
30+
31+
val buffer2 = buildBuffer(0xC0u, 0x00u) // 8192
32+
assertEquals(0x2000, buffer2.readVarLen())
33+
34+
val buffer3 = buildBuffer(0xFFu, 0x7Fu) // 16,383 (maximum 2-byte value)
35+
assertEquals(0x3FFF, buffer3.readVarLen())
36+
37+
val buffer4 = buildBuffer(0x81u, 0x80u, 0x00u) // 16,384
38+
assertEquals(0x4000, buffer4.readVarLen())
39+
}
40+
41+
@OptIn(ExperimentalUnsignedTypes::class)
42+
@Test
43+
fun testMaximumValue() {
44+
val buffer = buildBuffer(0xFFu, 0xFFu, 0xFFu, 0x7Fu)
45+
assertEquals(0x0FFFFFFF, buffer.readVarLen())
46+
}
47+
48+
@OptIn(ExperimentalUnsignedTypes::class)
49+
@Test
50+
fun testInvalidInput() {
51+
val buffer = buildBuffer(0xFFu, 0xFFu, 0xFFu, 0xFFu, 0x7Fu)
52+
try {
53+
buffer.readVarLen()
54+
} catch (ex: InvalidMidiDataException) {
55+
assertTrue(true)
56+
return
57+
}
58+
59+
assertTrue(false)
60+
}
61+
62+
//---
63+
64+
@OptIn(ExperimentalUnsignedTypes::class)
65+
private fun buildBuffer(vararg bytes: UByte): Buffer {
66+
return Buffer().apply {
67+
write(bytes.toByteArray())
68+
}
69+
}
70+
71+
//---
72+
73+
@Test
74+
fun testWriteSingleByteValues() {
75+
val buffer1 = Buffer()
76+
buffer1.writeVarLen(0x7F)
77+
assertTrue(byteArrayOf(0x7F.toByte()).contentEquals(buffer1.readByteArray()))
78+
79+
val buffer2 = Buffer()
80+
buffer2.writeVarLen(0x00)
81+
assertTrue(byteArrayOf(0x00.toByte()).contentEquals(buffer2.readByteArray()))
82+
83+
val buffer3 = Buffer()
84+
buffer3.writeVarLen(0x40)
85+
assertTrue(byteArrayOf(0x40.toByte()).contentEquals(buffer3.readByteArray()))
86+
}
87+
88+
@Test
89+
fun testWriteMultiByteValues() {
90+
val buffer1 = Buffer()
91+
buffer1.writeVarLen(0x80) // 128
92+
assertTrue(byteArrayOf(0x81.toByte(), 0x00.toByte()).contentEquals(buffer1.readByteArray()))
93+
94+
val buffer2 = Buffer()
95+
buffer2.writeVarLen(0x2000) // 8192
96+
assertTrue(byteArrayOf(0xC0.toByte(), 0x00.toByte()).contentEquals(buffer2.readByteArray()))
97+
98+
val buffer3 = Buffer()
99+
buffer3.writeVarLen(0x3FFF) // 16,383
100+
assertTrue(byteArrayOf(0xFF.toByte(), 0x7F.toByte()).contentEquals(buffer3.readByteArray()))
101+
102+
val buffer4 = Buffer()
103+
buffer4.writeVarLen(0x4000) // 16,384
104+
assertTrue(byteArrayOf(0x81.toByte(), 0x80.toByte(), 0x00.toByte()).contentEquals(buffer4.readByteArray()))
105+
}
106+
107+
@Test
108+
fun testWriteMaximumValue() {
109+
val buffer = Buffer()
110+
buffer.writeVarLen(0x0FFFFFFF)
111+
assertTrue(byteArrayOf(0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0x7F.toByte()).contentEquals(buffer.readByteArray()))
112+
}
113+
114+
@Test
115+
fun testWriteAndReadVarLen() {
116+
val originalValues = listOf(0x00, 0x40, 0x7F, 0x80, 0x2000, 0x3FFF, 0x4000, 0x0FFFFFFF)
117+
118+
for (value in originalValues) {
119+
val buffer = Buffer()
120+
buffer.writeVarLen(value)
121+
val readValue = buffer.readVarLen()
122+
123+
assertEquals("Failed for value: $value", value, readValue)
124+
}
125+
}
126+
}

core/src/androidInstrumentedTest/kotlin/pl/lemanski/mikroSoundFont/midi/MidiTest.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import org.junit.Test
1010
import pl.lemanski.mikroSoundFont.MikroSoundFont
1111
import pl.lemanski.mikroSoundFont.io.midi.MidiFileParser
1212
import pl.lemanski.mikroSoundFont.io.saveFile
13-
import pl.lemanski.mikroSoundFont.io.toByteArrayBigEndian
1413
import pl.lemanski.mikroSoundFont.io.toByteArrayLittleEndian
1514
import pl.lemanski.mikroSoundFont.io.wav.WavFileHeader
1615
import pl.lemanski.mikroSoundFont.io.wav.toByteArray
@@ -30,13 +29,15 @@ class MidiTest {
3029
val path = "${Environment.getExternalStorageDirectory().absolutePath}/Download/"
3130

3231
val midi = context.resources.assets.open("gmajor.mid")
32+
val sf = context.resources.assets.open("font.sf2")
3333

3434
val midiBuffer = midi.readBytes()
35+
val sfBuffer = sf.readBytes()
3536

3637
val midiFile = MidiFileParser(midiBuffer).parse()
3738
val messages = midiFile.getMessagesOverTime()
3839

39-
val soundFont = MikroSoundFont.load("$path/font.sf2")
40+
val soundFont = MikroSoundFont.load(sfBuffer)
4041

4142
val sequencer = MidiSequencer(soundFont, 44_100)
4243
sequencer.loadMidiEvents(messages)

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

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

33
import kotlinx.io.Buffer
44
import pl.lemanski.mikroSoundFont.InvalidMidiDataException
5+
import kotlin.experimental.or
56

67
internal fun Buffer.readVarLen(): Int {
78
var r = 0
@@ -19,4 +20,24 @@ internal fun Buffer.readVarLen(): Int {
1920
}
2021

2122
return r
23+
}
24+
25+
internal fun Buffer.writeVarLen(value: Int) {
26+
var buffer = value
27+
val byteStack = mutableListOf<Byte>()
28+
29+
do {
30+
var byte = (buffer and 0x7F).toByte()
31+
buffer = buffer shr 7
32+
33+
if (byteStack.isNotEmpty()) {
34+
byte = (byte or 0x80.toByte())
35+
}
36+
37+
byteStack.add(0, byte)
38+
} while (buffer > 0)
39+
40+
for (b in byteStack) {
41+
writeByte(b)
42+
}
2243
}

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

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,15 @@ import pl.lemanski.mikroSoundFont.io.midi.message.read.MidiMessageParser
77
import pl.lemanski.mikroSoundFont.io.midi.message.read.MidiMetaMessageParser
88
import pl.lemanski.mikroSoundFont.io.midi.message.read.MidiSystemMessageParser
99
import pl.lemanski.mikroSoundFont.io.midi.message.read.MidiVoiceMessageParser
10+
import pl.lemanski.mikroSoundFont.io.midi.message.write.MidiMessageEncoder
11+
import pl.lemanski.mikroSoundFont.io.midi.message.write.MidiMetaMessageEncoder
12+
import pl.lemanski.mikroSoundFont.io.midi.message.write.MidiSystemMessageEncoder
13+
import pl.lemanski.mikroSoundFont.io.midi.message.write.MidiVoiceMessageEncoder
1014
import pl.lemanski.mikroSoundFont.midi.MidiMessage
15+
import pl.lemanski.mikroSoundFont.midi.MidiMetaMessage
16+
import pl.lemanski.mikroSoundFont.midi.MidiSystemMessage
17+
import pl.lemanski.mikroSoundFont.midi.MidiVoiceMessage
18+
import pl.lemanski.mikroSoundFont.midi.UnsupportedMidiMessage
1119

1220
internal class MidiMessageContext(
1321
private val buffer: Buffer
@@ -16,6 +24,12 @@ internal class MidiMessageContext(
1624
private val voiceMessageParser = MidiVoiceMessageParser(buffer)
1725
private val metaMessageParser = MidiMetaMessageParser(buffer)
1826
private lateinit var parser: MidiMessageParser
27+
28+
//
29+
private val systemMessageEncoder = MidiSystemMessageEncoder()
30+
private val voiceMessageEncoder = MidiVoiceMessageEncoder()
31+
private val metaMessageEncoder = MidiMetaMessageEncoder()
32+
private lateinit var encoder: MidiMessageEncoder
1933
private var lastStatus: Int = 0
2034

2135
fun readMessage(): MidiMessage {
@@ -31,8 +45,17 @@ internal class MidiMessageContext(
3145
return parser.parse(status, dt)
3246
}
3347

34-
fun writeMessage() {
35-
// TODO
48+
fun writeMessage(message: MidiMessage) {
49+
buffer.writeVarLen(message.time)
50+
51+
encoder = when (message) {
52+
is MidiMetaMessage -> metaMessageEncoder
53+
is MidiSystemMessage -> systemMessageEncoder
54+
is MidiVoiceMessage -> voiceMessageEncoder
55+
is UnsupportedMidiMessage -> return
56+
}
57+
58+
buffer.write(encoder.encode(message))
3659
}
3760

3861
//---
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package pl.lemanski.mikroSoundFont.io.midi.message.write
2+
3+
import pl.lemanski.mikroSoundFont.midi.MidiMessage
4+
import pl.lemanski.mikroSoundFont.midi.MidiMessageType
5+
import pl.lemanski.mikroSoundFont.midi.MidiMetaMessage
6+
7+
internal class MidiMetaMessageEncoder : MidiMessageEncoder {
8+
private val metaMessageStatus = 0xFF // Meta messages always have status 0xFF.
9+
10+
override fun encode(message: MidiMessage): ByteArray {
11+
return when (message) {
12+
is MidiMetaMessage.EndOfTrack -> encodeEndOfTrack(message)
13+
is MidiMetaMessage.SetTempo -> encodeSetTempo(message)
14+
else -> byteArrayOf() // skip unsupported messages
15+
}
16+
}
17+
18+
private fun encodeEndOfTrack(message: MidiMetaMessage.EndOfTrack): ByteArray {
19+
val bytes = mutableListOf<Byte>()
20+
bytes.add(metaMessageStatus.toByte())
21+
bytes.add(MidiMessageType.END_OF_TRACK.value.toByte())
22+
bytes.add(0)
23+
24+
return bytes.toByteArray()
25+
}
26+
27+
private fun encodeSetTempo(message: MidiMetaMessage.SetTempo): ByteArray {
28+
val bytes = mutableListOf<Byte>()
29+
bytes.add(metaMessageStatus.toByte())
30+
bytes.add(MidiMessageType.SET_TEMPO.value.toByte())
31+
bytes.add(3)
32+
33+
val tempo = message.tempo
34+
bytes.add((tempo shr 16 and 0xFF).toByte())
35+
bytes.add((tempo shr 8 and 0xFF).toByte())
36+
bytes.add((tempo and 0xFF).toByte())
37+
38+
return bytes.toByteArray()
39+
}
40+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package pl.lemanski.mikroSoundFont.io.midi.message.write
2+
3+
import pl.lemanski.mikroSoundFont.midi.MidiMessage
4+
import pl.lemanski.mikroSoundFont.midi.MidiMessageType
5+
import pl.lemanski.mikroSoundFont.midi.MidiSystemMessage
6+
7+
internal class MidiSystemMessageEncoder : MidiMessageEncoder {
8+
override fun encode(message: MidiMessage): ByteArray {
9+
return when (message) {
10+
is MidiSystemMessage.Sysex -> encodeSysex()
11+
is MidiSystemMessage.Eox -> encodeEox()
12+
else -> byteArrayOf() // skip unsupported messages
13+
}
14+
}
15+
16+
private fun encodeSysex(): ByteArray {
17+
val bytes = mutableListOf<Byte>()
18+
bytes.add(MidiMessageType.SYS_EX.value.toByte())
19+
// Typically, SysEx messages contain data and an end byte, but for now, we'll keep it simple
20+
bytes.add(0x00) // Placeholder for the data size (or more complex data)
21+
return bytes.toByteArray()
22+
}
23+
24+
private fun encodeEox(): ByteArray {
25+
val bytes = mutableListOf<Byte>()
26+
bytes.add(MidiMessageType.EOX.value.toByte()) // EOX status byte
27+
// No additional data for EOX
28+
return bytes.toByteArray()
29+
}
30+
}

0 commit comments

Comments
 (0)