Skip to content

Commit 156ebdc

Browse files
committed
Add audio generation from bytes
1 parent 39efaaa commit 156ebdc

File tree

15 files changed

+175
-25
lines changed

15 files changed

+175
-25
lines changed

app/build.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ kotlin {
1111

1212
sourceSets {
1313
mingwMain.dependencies {
14-
implementation(projects.core)
14+
implementation(libs.kotlinx.io)
15+
implementation(libs.pandamidi.core)
1516
}
1617
}
1718
}

app/src/nativeMain/kotlin/Main.kt

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
1+
import kotlinx.cinterop.ExperimentalForeignApi
2+
import kotlinx.cinterop.memScoped
3+
import kotlinx.coroutines.CoroutineScope
4+
import kotlinx.coroutines.Job
5+
import kotlinx.coroutines.launch
6+
import kotlinx.io.Buffer
7+
import kotlinx.io.files.Path
8+
import kotlinx.io.files.SystemFileSystem
9+
import kotlinx.io.readByteArray
110
import pl.lemanski.pandamidi.core.Note
11+
import pl.lemanski.pandamidi.io.getMidiProcessor
212
import pl.lemanski.pandamidi.io.getMidiWavConverter
313
import pl.lemanski.pandamidi.sequencer.MidiEvent
414
import pl.lemanski.pandamidi.sequencer.MidiSequence
15+
import platform.posix.sleep
516

17+
@OptIn(ExperimentalForeignApi::class)
618
fun main() {
719
val c = MidiEvent(MidiEvent.Type.NOTE_ON, Note(60), 0, 0)
820
val e = MidiEvent(MidiEvent.Type.NOTE_ON, Note(64), 0, 0)
@@ -11,10 +23,39 @@ fun main() {
1123
sequence.addEvent(c)
1224
sequence.addEvent(e)
1325

14-
val soundFontPath = "C:\\Users\\lenovo\\Desktop\\midiWavConverter\\florestan-subset.sf2"
15-
val midiPath = "C:\\Users\\lenovo\\Desktop\\midiWavConverter\\venture.mid"
26+
val soundFontPath = Path("D:\\src\\MidiWavConverter\\Example\\florestan-subset.sf2")
27+
val midiPath = Path("D:\\src\\MidiWavConverter\\Example\\venture.mid")
28+
29+
SystemFileSystem.metadataOrNull(midiPath)?.let {
30+
println("size of file is: ${it.size}")
31+
}
32+
33+
val sink = Buffer()
34+
35+
SystemFileSystem.source(midiPath).let {
36+
var bytesRead = it.readAtMostTo(sink, 100)
37+
while (bytesRead > 0) {
38+
bytesRead = it.readAtMostTo(sink, 100)
39+
}
40+
}
1641

1742
val midiToWavConverter = getMidiWavConverter()
18-
val path = midiToWavConverter.generate(soundFontPath, midiPath)
43+
val path = midiToWavConverter.generate(soundFontPath.toString(), midiPath.toString())
1944
println(path)
45+
46+
47+
val midiProcessor = getMidiProcessor()
48+
midiProcessor.setSoundFontFromPath(soundFontPath.toString())
49+
midiProcessor.setAudioCallback { audioBytes ->
50+
println("Audio data received: ${audioBytes.size} bytes")
51+
52+
}
53+
54+
memScoped {
55+
CoroutineScope(Job()).launch {
56+
midiProcessor.processMidiBytes(sink.readByteArray())
57+
}
58+
}
59+
60+
sleep(5u)
2061
}

core/build.gradle.kts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,41 @@
11
plugins {
22
alias(libs.plugins.kotlinMultiplatform)
3+
id("maven-publish")
34
}
45

6+
group = "pl.lemanski.pandamidi"
7+
version = "0.0.1"
8+
59
kotlin {
610
mingwX64().apply {
711
val main by compilations.getting
812

9-
main.cinterops.create("mwc") {
10-
packageName = "pl.lemanski.mwc"
11-
extraOpts("-header", "$rootDir\\native\\include\\midi_wav_converter.h")
13+
main.compileTaskProvider.configure {
14+
compilerOptions {
15+
freeCompilerArgs.add("-Xbinary=gc=noop")
16+
}
17+
}
18+
19+
main.cinterops.create("libmwc") {
20+
definitionFile = File(rootDir, "native/libmwc.def")
21+
includeDirs.headerFilterOnly("$rootDir\\native\\include")
1222
extraOpts("-libraryPath", "$rootDir\\native\\lib")
13-
extraOpts("-staticLibrary", "libmwc.a")
1423
}
1524
}
1625

1726
sourceSets {
1827
commonMain.dependencies {
19-
//put your multiplatform dependencies here
28+
implementation(libs.coroutines.core)
2029
}
2130

2231
commonTest.dependencies {
2332
implementation(libs.kotlin.test)
2433
}
2534
}
35+
}
36+
37+
publishing {
38+
repositories {
39+
mavenLocal()
40+
}
2641
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package pl.lemanski.pandamidi.io
2+
3+
/**
4+
* Represents a MIDI processor that can process MIDI bytes and generate audio data.
5+
*/
6+
interface MidiProcessor : AutoCloseable {
7+
/**
8+
* Sets the callback function to be called when audio data is available.
9+
*/
10+
fun setAudioCallback(callback: (ByteArray) -> Unit)
11+
12+
/**
13+
* Sets the sound font from a path.
14+
*/
15+
fun setSoundFontFromPath(soundFontPath: String)
16+
17+
/**
18+
* Processes MIDI bytes and generates audio data.
19+
*/
20+
suspend fun processMidiBytes(midiBytes: ByteArray)
21+
}
22+
23+
expect fun getMidiProcessor(): MidiProcessor
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package pl.lemanski.pandamidi.io
2+
3+
import kotlinx.cinterop.CPointer
4+
import kotlinx.cinterop.ExperimentalForeignApi
5+
import kotlinx.cinterop.ULongVar
6+
import kotlinx.cinterop.memScoped
7+
import kotlinx.cinterop.pointed
8+
import kotlinx.cinterop.readBytes
9+
import kotlinx.cinterop.refTo
10+
import kotlinx.cinterop.reinterpret
11+
import kotlinx.cinterop.value
12+
import platform.posix.malloc
13+
14+
@OptIn(ExperimentalForeignApi::class)
15+
actual fun getMidiProcessor(): MidiProcessor {
16+
return object : MidiProcessor {
17+
private var audioCallback: (ByteArray) -> Unit = { }
18+
19+
override fun setAudioCallback(callback: (ByteArray) -> Unit) {
20+
audioCallback = callback
21+
}
22+
23+
override fun setSoundFontFromPath(soundFontPath: String) {
24+
val sanitizedSoundFontPath = soundFontPath.replace("/", "\\")
25+
libmwc.initialize_soundfont(sanitizedSoundFontPath)
26+
}
27+
28+
override suspend fun processMidiBytes(midiBytes: ByteArray) {
29+
memScoped {
30+
val pOutputBufferSize: CPointer<ULongVar> = malloc(8u)?.reinterpret() ?: throw RuntimeException("Memory allocation failed")
31+
val pUByteArray = midiBytes.toUByteArray().refTo(0)
32+
val midiSize = midiBytes.size.toULong()
33+
34+
val buffer = libmwc.generate_from_midi_buffer(pUByteArray, midiSize, pOutputBufferSize) ?: throw RuntimeException("Failed to generate audio buffer")
35+
val bufferBytes = buffer.readBytes(pOutputBufferSize.pointed.value.toInt())
36+
37+
audioCallback(bufferBytes)
38+
}
39+
}
40+
41+
override fun close() {
42+
libmwc.release_soundfont()
43+
}
44+
}
45+
}

core/src/mingwX64Main/kotlin/pl/lemanski/pandamidi/io/MidiWavConverter.mingwX64.kt

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,8 @@
11
package pl.lemanski.pandamidi.io
22

33
import kotlinx.cinterop.ExperimentalForeignApi
4-
import kotlinx.cinterop.addressOf
5-
import kotlinx.cinterop.toKString
6-
import kotlinx.cinterop.usePinned
7-
import platform.posix.getcwd
8-
9-
@OptIn(ExperimentalForeignApi::class)
10-
internal fun getCwd(): String? {
11-
return ByteArray(1024).usePinned { getcwd(it.addressOf(0), 1024) }?.toKString()
12-
}
4+
import kotlinx.cinterop.memScoped
5+
import pl.lemanski.pandamidi.util.getCwd
136

147
@OptIn(ExperimentalForeignApi::class)
158
actual fun getMidiWavConverter(): MidiWavConverter {
@@ -21,7 +14,11 @@ actual fun getMidiWavConverter(): MidiWavConverter {
2114

2215
val midiFileName = sanitizedMidiFilePath.split("\\").last().replace(".mid", "")
2316
val wavFilePath = "$pwd\\$midiFileName.wav"
24-
pl.lemanski.mwc.generate(sanitizedSoundFontPath, sanitizedMidiFilePath, wavFilePath)
17+
memScoped {
18+
libmwc.initialize_soundfont(sanitizedSoundFontPath)
19+
libmwc.generate(sanitizedMidiFilePath, wavFilePath)
20+
libmwc.release_soundfont()
21+
}
2522
return wavFilePath
2623
}
2724
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package pl.lemanski.pandamidi.util
2+
3+
import kotlinx.cinterop.ExperimentalForeignApi
4+
import kotlinx.cinterop.addressOf
5+
import kotlinx.cinterop.toKString
6+
import kotlinx.cinterop.usePinned
7+
import platform.posix.getcwd
8+
9+
10+
@OptIn(ExperimentalForeignApi::class)
11+
internal fun getCwd(): String? {
12+
return ByteArray(1024).usePinned { getcwd(it.addressOf(0), 1024) }?.toKString()
13+
}

core/src/nativeInterop/cinterop/tsf.def

Lines changed: 0 additions & 1 deletion
This file was deleted.

gradle.properties

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,4 @@ org.gradle.configuration-cache=true
55

66
#Kotlin
77
kotlin.code.style=official
8-
9-
#Android
10-
android.useAndroidX=true
11-
android.nonTransitiveRClass=true
8+
kotlin.mpp.enableCInteropCommonization=true

gradle/libs.versions.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
[versions]
22
kotlin = "2.0.0"
3+
coroutines = "1.9.0-RC"
4+
pandaMidi = "0.0.1"
5+
kotlinxIo = "0.5.1"
36

47
[libraries]
58
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
9+
kotlinx-io = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinxIo" }
10+
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
11+
pandamidi-core = { module = "pl.lemanski.pandamidi:core", version.ref = "pandaMidi" }
612

713
[plugins]
814
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }

0 commit comments

Comments
 (0)