Skip to content

Commit 4eab06b

Browse files
committed
Added midi sound generator
1 parent 5a370d4 commit 4eab06b

File tree

17 files changed

+526
-192
lines changed

17 files changed

+526
-192
lines changed

app/build.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ kotlin {
1212
sourceSets {
1313
mingwMain.dependencies {
1414
implementation(libs.kotlinx.io)
15-
implementation(libs.pandamidi.core)
15+
// implementation(libs.pandamidi.core)
16+
implementation(projects.core)
1617
}
1718
}
1819
}

app/src/nativeMain/kotlin/Main.kt

Lines changed: 64 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,85 @@
11
import kotlinx.cinterop.ExperimentalForeignApi
2-
import kotlinx.cinterop.memScoped
3-
import kotlinx.coroutines.CoroutineScope
4-
import kotlinx.coroutines.Job
5-
import kotlinx.coroutines.launch
2+
import kotlinx.cinterop.FloatVar
3+
import kotlinx.cinterop.sizeOf
64
import kotlinx.io.Buffer
75
import kotlinx.io.files.Path
86
import kotlinx.io.files.SystemFileSystem
9-
import kotlinx.io.readByteArray
10-
import pl.lemanski.pandamidi.core.Note
11-
import pl.lemanski.pandamidi.io.getMidiProcessor
12-
import pl.lemanski.pandamidi.io.getMidiWavConverter
13-
import pl.lemanski.pandamidi.sequencer.MidiEvent
14-
import pl.lemanski.pandamidi.sequencer.MidiSequence
7+
import pl.lemanski.pandamidi.generator.MidiMessageNoteOff
8+
import pl.lemanski.pandamidi.generator.MidiMessageNoteOn
9+
import pl.lemanski.pandamidi.generator.getGenerator
10+
import pl.lemanski.pandamidi.io.toByteArrayLittleEndian
11+
import pl.lemanski.pandamidi.io.wav.WavFileHeader
12+
import pl.lemanski.pandamidi.io.wav.toByteArray
1513
import platform.posix.sleep
1614

1715
@OptIn(ExperimentalForeignApi::class)
1816
fun main() {
19-
val c = MidiEvent(MidiEvent.Type.NOTE_ON, Note(60), 0, 0)
20-
val e = MidiEvent(MidiEvent.Type.NOTE_ON, Note(64), 0, 0)
17+
val gOff = MidiMessageNoteOff(
18+
time = 10000u,
19+
channel = 5u,
20+
key = 32u,
21+
next = null
22+
)
2123

22-
val sequence = MidiSequence()
23-
sequence.addEvent(c)
24-
sequence.addEvent(e)
24+
val g = MidiMessageNoteOn(
25+
time = 2500u,
26+
channel = 5u,
27+
key = 43u,
28+
velocity = 80u,
29+
next = gOff
30+
)
2531

26-
val soundFontPath = Path("D:\\src\\MidiWavConverter\\Example\\florestan-subset.sf2")
27-
val midiPath = Path("D:\\src\\MidiWavConverter\\Example\\venture.mid")
32+
val eOff = MidiMessageNoteOff(
33+
time = 2000u,
34+
channel = 7u,
35+
key = 40u,
36+
next = g
37+
)
38+
39+
40+
val e = MidiMessageNoteOn(
41+
time = 1_000u,
42+
channel = 7u,
43+
key = 40u,
44+
velocity = 80u,
45+
next = eOff
46+
)
2847

29-
SystemFileSystem.metadataOrNull(midiPath)?.let {
30-
println("size of file is: ${it.size}")
31-
}
48+
val cOff = MidiMessageNoteOff(
49+
time = 100u,
50+
channel = 1u,
51+
key = 36u,
52+
next = e
53+
)
3254

33-
val sink = Buffer()
55+
val c = MidiMessageNoteOn(
56+
time = 0u,
57+
channel = 1u,
58+
key = 36u,
59+
velocity = 80u,
60+
next = cOff
61+
)
62+
63+
var bytes = ByteArray(0)
64+
val generator = getGenerator()
65+
66+
val soundFontPath = Path("D:\\src\\MidiWavConverter\\Example\\florestan-subset.sf2")
3467

35-
SystemFileSystem.source(midiPath).let {
36-
var bytesRead = it.readAtMostTo(sink, 100)
37-
while (bytesRead > 0) {
38-
bytesRead = it.readAtMostTo(sink, 100)
39-
}
40-
}
68+
generator.setSoundFont(soundFontPath.toString())
69+
val midiBytes = generator.generate(c)
70+
val numSamples = midiBytes.size.toUInt() / sizeOf<FloatVar>().toUInt()
71+
val wavFileHeader = WavFileHeader.write(44100u, numSamples, 2u)
4172

42-
val midiToWavConverter = getMidiWavConverter()
43-
val path = midiToWavConverter.generate(soundFontPath.toString(), midiPath.toString())
44-
println(path)
73+
bytes += wavFileHeader.toByteArray()
74+
bytes += midiBytes.toByteArrayLittleEndian()
4575

76+
println(bytes.size)
4677

47-
val midiProcessor = getMidiProcessor()
48-
midiProcessor.setSoundFontFromPath(soundFontPath.toString())
49-
midiProcessor.setAudioCallback { audioBytes ->
50-
println("Audio data received: ${audioBytes.size} bytes")
78+
val file = Buffer()
5179

52-
}
80+
file.write(bytes, 0, bytes.size)
5381

54-
memScoped {
55-
CoroutineScope(Job()).launch {
56-
midiProcessor.processMidiBytes(sink.readByteArray())
57-
}
58-
}
82+
SystemFileSystem.sink(Path("./output.wav")).write(file, file.size)
5983

6084
sleep(5u)
6185
}

core/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ kotlin {
1616
}
1717
}
1818

19-
main.cinterops.create("libmwc") {
20-
definitionFile = File(rootDir, "native/libmwc.def")
19+
main.cinterops.create("libtsf") {
20+
definitionFile = File(rootDir, "native/libtsf.def")
2121
includeDirs.headerFilterOnly("$rootDir\\native\\include")
2222
extraOpts("-libraryPath", "$rootDir\\native\\lib")
2323
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package pl.lemanski.pandamidi.generator
2+
3+
interface Generator {
4+
fun setSoundFont(path: String)
5+
fun generate(midiMessage: MidiMessage): FloatArray
6+
}
7+
8+
expect fun getGenerator(): Generator
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package pl.lemanski.pandamidi.generator
2+
3+
sealed interface MidiMessage {
4+
val time: UInt
5+
val channel: UInt
6+
val type: Type
7+
var next: MidiMessage?
8+
9+
enum class Type(val value: UInt) {
10+
NOTE_OFF(0x80u),
11+
NOTE_ON(0x90u),
12+
KEY_PRESSURE(0xA0u),
13+
CONTROL_CHANGE(0xB0u),
14+
PROGRAM_CHANGE(0xC0u),
15+
CHANNEL_PRESSURE(0xD0u),
16+
PITCH_BEND(0xE0u),
17+
SET_TEMPO(0x51u)
18+
}
19+
}
20+
21+
// ---
22+
23+
data class MidiMessageNoteOn(
24+
override val time: UInt,
25+
override val channel: UInt,
26+
val key: UInt,
27+
val velocity: UInt,
28+
override var next: MidiMessage?,
29+
) : MidiMessage {
30+
override val type: MidiMessage.Type = MidiMessage.Type.NOTE_ON
31+
}
32+
33+
// ---
34+
35+
data class MidiMessageNoteOff(
36+
override val time: UInt,
37+
override val channel: UInt,
38+
val key: UInt,
39+
override var next: MidiMessage?,
40+
) : MidiMessage {
41+
override val type: MidiMessage.Type = MidiMessage.Type.NOTE_OFF
42+
}
43+
44+
// ---
45+
46+
data class MidiMessageProgramChange(
47+
override val time: UInt,
48+
override val channel: UInt,
49+
val program: UInt,
50+
override var next: MidiMessage?,
51+
) : MidiMessage {
52+
override val type: MidiMessage.Type = MidiMessage.Type.PROGRAM_CHANGE
53+
}
54+
55+
// ---
56+
57+
data class MidiMessagePitchBend(
58+
override val time: UInt,
59+
override val channel: UInt,
60+
val pitchBend: UInt,
61+
override var next: MidiMessage?,
62+
) : MidiMessage {
63+
override val type: MidiMessage.Type = MidiMessage.Type.PITCH_BEND
64+
}
65+
66+
// ---
67+
68+
data class MidiMessageControlChange(
69+
override val time: UInt,
70+
override val channel: UInt,
71+
val control: UInt,
72+
val controlValue: UInt,
73+
override var next: MidiMessage?,
74+
): MidiMessage {
75+
override val type: MidiMessage.Type = MidiMessage.Type.CONTROL_CHANGE
76+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package pl.lemanski.pandamidi.io
2+
3+
/**
4+
* Convert byte array to float array with little endian order
5+
*/
6+
fun ByteArray.toFloatArray(): FloatArray {
7+
val floatArray = FloatArray(size / Float.SIZE_BYTES)
8+
for (i in floatArray.indices) {
9+
val intBits = ((this[i * Float.SIZE_BYTES + 3].toInt() and 0xFF) shl 24) or
10+
((this[i * Float.SIZE_BYTES + 2].toInt() and 0xFF) shl 16) or
11+
((this[i * Float.SIZE_BYTES + 1].toInt() and 0xFF) shl 8) or
12+
(this[i * Float.SIZE_BYTES].toInt() and 0xFF)
13+
floatArray[i] = Float.fromBits(intBits)
14+
}
15+
return floatArray
16+
}
17+
18+
/**
19+
* Convert float array with little endian order to byte array
20+
*/
21+
fun FloatArray.toByteArrayLittleEndian(): ByteArray {
22+
val byteArray = ByteArray(size * Float.SIZE_BYTES)
23+
for (i in indices) {
24+
val intBits = this[i].toRawBits()
25+
byteArray[i * Float.SIZE_BYTES] = (intBits and 0xFF).toByte()
26+
byteArray[i * Float.SIZE_BYTES + 1] = ((intBits shr 8) and 0xFF).toByte()
27+
byteArray[i * Float.SIZE_BYTES + 2] = ((intBits shr 16) and 0xFF).toByte()
28+
byteArray[i * Float.SIZE_BYTES + 3] = ((intBits shr 24) and 0xFF).toByte()
29+
}
30+
return byteArray
31+
}
32+
33+
fun FloatArray.toByteArrayBigEndian(): ByteArray {
34+
val byteArray = ByteArray(size * Float.SIZE_BYTES)
35+
for (i in indices) {
36+
val intBits = this[i].toRawBits()
37+
byteArray[i * Float.SIZE_BYTES] = ((intBits shr 24) and 0xFF).toByte()
38+
byteArray[i * Float.SIZE_BYTES + 1] = ((intBits shr 16) and 0xFF).toByte()
39+
byteArray[i * Float.SIZE_BYTES + 2] = ((intBits shr 8) and 0xFF).toByte()
40+
byteArray[i * Float.SIZE_BYTES + 3] = (intBits and 0xFF).toByte()
41+
}
42+
return byteArray
43+
}

core/src/commonMain/kotlin/pl/lemanski/pandamidi/io/MidiProcessor.kt

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

core/src/commonMain/kotlin/pl/lemanski/pandamidi/io/MidiWavConverter.kt

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

core/src/commonMain/kotlin/pl/lemanski/pandamidi/sequencer/MidiEvent.kt

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

core/src/commonMain/kotlin/pl/lemanski/pandamidi/sequencer/Sequence.kt

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

0 commit comments

Comments
 (0)