Skip to content

Commit df18a3b

Browse files
committed
Fixed MIDI generation on Android
1 parent 7bb9319 commit df18a3b

File tree

29 files changed

+539
-302
lines changed

29 files changed

+539
-302
lines changed

core/build.gradle.kts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ android {
1010
defaultConfig {
1111
compileSdk = libs.versions.android.compileSdk.get().toInt()
1212
minSdk = libs.versions.android.minSdk.get().toInt()
13+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
1314
}
1415

1516
testOptions {
@@ -44,5 +45,10 @@ kotlin {
4445
commonTest.dependencies {
4546
implementation(libs.kotlin.test)
4647
}
48+
49+
getByName("androidInstrumentedTest").dependencies {
50+
implementation(libs.androidX.testRunner)
51+
implementation(libs.test.rules)
52+
}
4753
}
4854
}
Binary file not shown.
138 Bytes
Binary file not shown.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package pl.lemanski.mikroSoundFont.io
2+
3+
import org.junit.Assert.assertTrue
4+
import org.junit.Test
5+
6+
class FileTest {
7+
private val dir = "C:\\Users\\Mikolaj\\Desktop\\midi"
8+
9+
@Test
10+
fun should_load_file_as_byte_array() {
11+
val byteArray = loadFile("$dir\\venture.mid")
12+
assertTrue("File should not be empty", byteArray.isNotEmpty())
13+
}
14+
15+
@Test
16+
fun should_save_file() {
17+
val byteArray = loadFile("$dir\\venture.mid")
18+
saveFile(byteArray, "$dir\\venture_copy.mid")
19+
val byteArrayCopy = loadFile("$dir\\venture_copy.mid")
20+
21+
assertTrue("Content should be equal", byteArrayCopy.contentEquals(byteArray))
22+
}
23+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package pl.lemanski.mikroSoundFont.io.midi
2+
3+
import junit.framework.TestCase.assertEquals
4+
import junit.framework.TestCase.assertTrue
5+
import org.junit.Test
6+
import pl.lemanski.mikroSoundFont.InvalidMidiDataException
7+
8+
@OptIn(ExperimentalStdlibApi::class)
9+
class MidiFileHeaderTest {
10+
11+
private val validBuffer = "4d546864000000060001000901e0".hexToByteArray()
12+
private val invalidLengthBuffer = "4e5468640000000600010009".hexToByteArray()
13+
private val invalidHeaderStringBuffer = "3d546864000000060001000901e0".hexToByteArray()
14+
private val invalidHeaderByteBuffer = "4d546864000000070001000901e0".hexToByteArray()
15+
private val invalidHeaderFormatBuffer = "4d546864000000060005000901e0".hexToByteArray()
16+
private val invalidTimingBuffer = "4d546864000000060001000980e0".hexToByteArray()
17+
private val invalidTrackCountBuffer = "4d546864000000060001ffff01e0".hexToByteArray()
18+
private val invalidDivisionBuffer = "4d5468640000000600010009ffff".hexToByteArray()
19+
20+
@Test
21+
fun should_parse_midi_file_header() {
22+
val expectedHeader = MidiFile.Header(
23+
trackCount = 9,
24+
division = 480
25+
)
26+
27+
val parser = MidiFileParser(validBuffer)
28+
val header = parser.parseHeader()
29+
30+
assertEquals(expectedHeader, header)
31+
}
32+
33+
@Test
34+
fun should_throw_invalid_length_exception() {
35+
try {
36+
MidiFileParser(invalidLengthBuffer).parseHeader()
37+
} catch (e: InvalidMidiDataException) {
38+
assertTrue(true)
39+
return
40+
}
41+
42+
assertTrue(false)
43+
}
44+
45+
@Test
46+
fun should_throw_invalid_mthd_exception() {
47+
try {
48+
MidiFileParser(invalidHeaderStringBuffer).parseHeader()
49+
} catch (e: InvalidMidiDataException) {
50+
assertTrue(true)
51+
return
52+
}
53+
54+
assertTrue(false)
55+
}
56+
57+
@Test
58+
fun should_throw_invalid_mthd_exception_2() {
59+
try {
60+
MidiFileParser(invalidHeaderByteBuffer).parseHeader()
61+
} catch (e: InvalidMidiDataException) {
62+
assertTrue(true)
63+
return
64+
}
65+
66+
assertTrue(false)
67+
}
68+
69+
@Test
70+
fun should_throw_invalid_mthd_exception_3() {
71+
try {
72+
MidiFileParser(invalidHeaderFormatBuffer).parseHeader()
73+
} catch (e: InvalidMidiDataException) {
74+
assertTrue(true)
75+
return
76+
}
77+
78+
assertTrue(false)
79+
}
80+
81+
@Test
82+
fun should_throw_unsupported_timing() {
83+
try {
84+
MidiFileParser(invalidTimingBuffer).parseHeader()
85+
} catch (e: InvalidMidiDataException) {
86+
assertTrue(true)
87+
return
88+
}
89+
90+
assertTrue(false)
91+
}
92+
93+
@Test
94+
fun should_throw_invalid_track_count() {
95+
try {
96+
MidiFileParser(invalidTrackCountBuffer).parseHeader()
97+
} catch (e: InvalidMidiDataException) {
98+
assertTrue(true)
99+
return
100+
}
101+
102+
assertTrue(false)
103+
}
104+
105+
@Test
106+
fun should_throw_invalid_division() {
107+
try {
108+
MidiFileParser(invalidDivisionBuffer).parseHeader()
109+
} catch (e: InvalidMidiDataException) {
110+
assertTrue(true)
111+
return
112+
}
113+
114+
assertTrue(false)
115+
}
116+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package pl.lemanski.mikroSoundFont.io.midi
2+
3+
import junit.framework.TestCase.assertEquals
4+
import junit.framework.TestCase.assertTrue
5+
import kotlinx.io.Buffer
6+
import org.junit.Test
7+
import pl.lemanski.mikroSoundFont.InvalidMidiDataException
8+
import pl.lemanski.mikroSoundFont.io.midi.message.readVarLen
9+
10+
class MidiMessageParserTest {
11+
12+
@OptIn(ExperimentalUnsignedTypes::class)
13+
@Test
14+
fun testSingleByteValues() {
15+
// Single-byte values (no continuation bits)
16+
val buffer1 = buildBuffer(0x7Fu)
17+
assertEquals(0x7F, buffer1.readVarLen())
18+
19+
val buffer2 = buildBuffer(0x00u)
20+
assertEquals(0x00, buffer2.readVarLen())
21+
22+
val buffer3 = buildBuffer(0x40u)
23+
assertEquals(0x40, buffer3.readVarLen())
24+
}
25+
26+
@OptIn(ExperimentalUnsignedTypes::class)
27+
@Test
28+
fun testMultiByteValues() {
29+
// Multi-byte values (with continuation bits)
30+
val buffer1 = buildBuffer(0x81u, 0x00u) // 128
31+
assertEquals(0x80, buffer1.readVarLen())
32+
33+
val buffer2 = buildBuffer(0xC0u, 0x00u) // 8192
34+
assertEquals(0x2000, buffer2.readVarLen())
35+
36+
val buffer3 = buildBuffer(0xFFu, 0x7Fu) // 16,383 (maximum 2-byte value)
37+
assertEquals(0x3FFF, buffer3.readVarLen())
38+
39+
val buffer4 = buildBuffer(0x81u, 0x80u, 0x00u) // 16,384
40+
assertEquals(0x4000, buffer4.readVarLen())
41+
}
42+
43+
@OptIn(ExperimentalUnsignedTypes::class)
44+
@Test
45+
fun testMaximumValue() {
46+
// Test the maximum value that can be represented in a 4-byte MIDI variable-length
47+
val buffer = buildBuffer(0xFFu, 0xFFu, 0xFFu, 0x7Fu)
48+
assertEquals(0x0FFFFFFF, buffer.readVarLen())
49+
}
50+
51+
@OptIn(ExperimentalUnsignedTypes::class)
52+
@Test
53+
fun testInvalidInput() {
54+
val buffer = buildBuffer(0xFFu, 0xFFu, 0xFFu, 0xFFu, 0x7Fu)
55+
try {
56+
buffer.readVarLen()
57+
} catch (ex: InvalidMidiDataException) {
58+
assertTrue(true)
59+
return
60+
}
61+
62+
assertTrue(false)
63+
}
64+
65+
//---
66+
67+
@OptIn(ExperimentalUnsignedTypes::class)
68+
private fun buildBuffer(vararg bytes: UByte): Buffer {
69+
return Buffer().apply {
70+
write(bytes.toByteArray())
71+
}
72+
}
73+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package pl.lemanski.mikroSoundFont.io.wav
2+
3+
import junit.framework.TestCase.assertEquals
4+
import org.junit.Test
5+
6+
class WavFileHeaderTest {
7+
@OptIn(ExperimentalStdlibApi::class)
8+
@Test
9+
fun should_write_correct_bytes_for_empty_file_header() {
10+
val wavFileHex = WavFileHeader.write(44100u, 0u, 2u).toByteArray().toHexString().uppercase()
11+
val expectedHex = "524946462400000057415645666d7420100000000300020044ac000020620500080020006461746100000000".uppercase()
12+
assertEquals("", expectedHex, wavFileHex)
13+
}
14+
15+
@OptIn(ExperimentalStdlibApi::class)
16+
@Test
17+
fun should_write_correct_bytes_for_non_empty_file_header() {
18+
val wavFileHex = WavFileHeader.write(44100u, 1024u, 2u).toByteArray().toHexString().uppercase()
19+
val expectedHex = "524946462420000057415645666D7420100000000300020044AC000020620500080020006461746100200000".uppercase()
20+
assertEquals("", expectedHex, wavFileHex)
21+
}
22+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package pl.lemanski.mikroSoundFont.midi
2+
3+
import android.Manifest
4+
import android.os.Environment
5+
import android.util.Log
6+
import androidx.test.platform.app.InstrumentationRegistry
7+
import androidx.test.rule.GrantPermissionRule
8+
import org.junit.Rule
9+
import org.junit.Test
10+
import pl.lemanski.mikroSoundFont.MikroSoundFont
11+
import pl.lemanski.mikroSoundFont.io.midi.MidiFileParser
12+
import pl.lemanski.mikroSoundFont.io.saveFile
13+
import pl.lemanski.mikroSoundFont.io.toByteArrayBigEndian
14+
import pl.lemanski.mikroSoundFont.io.toByteArrayLittleEndian
15+
import pl.lemanski.mikroSoundFont.io.wav.WavFileHeader
16+
import pl.lemanski.mikroSoundFont.io.wav.toByteArray
17+
import java.io.File
18+
19+
20+
class MidiTest {
21+
22+
@JvmField
23+
@Rule
24+
var mRuntimePermissionRule: GrantPermissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
25+
26+
@Test
27+
fun testMidi() {
28+
val context = InstrumentationRegistry.getInstrumentation().context
29+
Log.e("test", "1")
30+
val path = "${Environment.getExternalStorageDirectory().absolutePath}/Download/"
31+
32+
val midi = context.resources.assets.open("gmajor.mid")
33+
34+
val midiBuffer = midi.readBytes()
35+
36+
val midiFile = MidiFileParser(midiBuffer).parse()
37+
val messages = midiFile.getMessagesOverTime()
38+
39+
val soundFont = MikroSoundFont.load("$path/font.sf2")
40+
41+
val sequencer = MidiSequencer(soundFont, 44_100)
42+
sequencer.loadMidiEvents(messages)
43+
val wavBytes = sequencer.generate()
44+
45+
val wavHeader = WavFileHeader.write(44_100u, wavBytes.size.toUInt(), 2u)
46+
val file = wavHeader.toByteArray() + wavBytes.toByteArrayLittleEndian()
47+
48+
File("$path/output.wav").delete()
49+
saveFile(file, "$path/output.wav")
50+
}
51+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
2+
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
3+
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
4+
</manifest>

core/src/commonMain/kotlin/pl/lemanski/mikroSoundFont/core/Chord.kt

Lines changed: 0 additions & 11 deletions
This file was deleted.

0 commit comments

Comments
 (0)