Skip to content

Commit

Permalink
Add ktmidi-wasm-ext module.
Browse files Browse the repository at this point in the history
  • Loading branch information
atsushieno committed Aug 17, 2024
1 parent 49b41db commit d51a559
Show file tree
Hide file tree
Showing 7 changed files with 305 additions and 5 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ plugins {

allprojects {
group = 'dev.atsushieno'
version = '0.9.2'
version = '0.9.3'
}

apply from: "${rootDir}/publish-root.gradle"
Expand Down
14 changes: 10 additions & 4 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ agp = "8.2.2"
android-compileSdk = "35"
android-minSdk = "23"
android-targetSdk = "35"
androidx-activityCompose = "1.9.0"
androidx-activityCompose = "1.9.1"
androidx-appcompat = "1.7.0"
androidx-core-ktx = "1.13.1"
androidx-espresso-core = "3.6.1"
androidx-test-junit = "1.2.1"
compose = "1.6.8"
compose-plugin = "1.6.10"
compose-components-resources = "1.7.0-alpha02"
compose = "1.7.0-beta07"
compose-plugin = "1.7.0-alpha02"
junit = "4.13.2"
kotlin = "2.0.0"
kotlin = "2.0.10"
dokka = "1.9.20"
binary-compatibility-validator = "0.14.0"
junit-jupiter-api = "5.10.2"
Expand All @@ -22,6 +23,8 @@ kotlinx-coroutines-core = "1.9.0-RC"
kotlinx-datetime = "0.6.0"
kotlinxSerialization = "1.7.0-RC"
ktor-io = "2.3.11"
filekitCore = "0.7.0"
js-synthesizer = "1.10.0"

mpfilepicker = "3.1.0"
gradle-javacpp = "1.5.10"
Expand All @@ -30,6 +33,7 @@ alsa-javacpp = "0.1.0"
rtmidi-javacpp = "0.1.5"

[libraries]
compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "compose-components-resources" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
Expand All @@ -41,6 +45,8 @@ androidx-activity-compose = { module = "androidx.activity:activity-compose", ver
compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" }
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }
compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" }
filekit-compose = { module = "io.github.vinceglb:filekit-compose", version.ref = "filekitCore" }
filekit-core = { module = "io.github.vinceglb:filekit-core", version.ref = "filekitCore" }

alsa-javacpp = { module = "dev.atsushieno:alsa-javacpp", version.ref = "alsa-javacpp" }
junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit-jupiter-api" }
Expand Down
5 changes: 5 additions & 0 deletions kotlin-js-store/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1657,6 +1657,11 @@ jest-worker@^27.4.5:
merge-stream "^2.0.0"
supports-color "^8.0.0"

[email protected]:
version "1.10.0"
resolved "https://registry.yarnpkg.com/js-synthesizer/-/js-synthesizer-1.10.0.tgz#b13fd5589697cd4f8a3bea221586e9c5919e3bf7"
integrity sha512-OR8j8QcWgAPvVcJ8LOtCx3Ypdm7lfyumYGQmxLUWIpZmnpzoUlb/HDYagHMv79je1D/rS6mEkq27F48MS87LMw==

[email protected]:
version "4.1.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
Expand Down
112 changes: 112 additions & 0 deletions ktmidi-wasm-ext/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import org.jetbrains.compose.compose
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl

plugins {
kotlin("multiplatform")
id("maven-publish")
id("signing")
id("org.jetbrains.dokka")
}

kotlin {
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
browser {}
}
sourceSets {
val wasmJsMain by getting {
dependencies {
implementation(libs.compose.components.resources)
implementation(project(":ktmidi"))
implementation(npm("js-synthesizer", libs.versions.js.synthesizer.get()))
}
}
}
}

tasks.withType<AbstractPublishToMaven>().configureEach {
val signingTasks = tasks.withType<Sign>()
mustRunAfter(signingTasks)
}

tasks {
val dokkaOutputDir = "${layout.buildDirectory}/dokka"

dokkaHtml {
outputDirectory.set(file(dokkaOutputDir))
}

val deleteDokkaOutputDir by registering(Delete::class) {
delete(dokkaOutputDir)
}

register<Jar>("javadocJar") {
dependsOn(deleteDokkaOutputDir, dokkaHtml)
archiveClassifier.set("javadoc")
from(dokkaOutputDir)
}
}

val repositoryId: String? = System.getenv("OSSRH_STAGING_REPOSITORY_ID")
val moduleDescription = "Kotlin Multiplatform library for MIDI 1.0 and MIDI 2.0 - Native specific"
// copypasting
afterEvaluate {
publishing {
publications {
publications.withType<MavenPublication>{
// https://github.com/gradle/gradle/issues/26091#issuecomment-1681343496
val dokkaJar = project.tasks.register("${name}DokkaJar", Jar::class) {
group = JavaBasePlugin.DOCUMENTATION_GROUP
description = "Assembles Kotlin docs with Dokka into a Javadoc jar"
archiveClassifier.set("javadoc")
from(tasks.named("dokkaHtml"))

// Each archive name should be distinct, to avoid implicit dependency issues.
// We use the same format as the sources Jar tasks.
// https://youtrack.jetbrains.com/issue/KT-46466
archiveBaseName.set("${archiveBaseName.get()}-${name}")
}
artifact(dokkaJar)

pom {
name.set("$name")
description.set(moduleDescription)
url.set("https://github.com/atsushieno/ktmidi")
scm {
url.set("https://github.com/atsushieno/ktmidi")
}
licenses {
license {
name.set("the MIT License")
url.set("https://github.com/atsushieno/ktmidi/blob/main/LICENSE")
}
}
developers {
developer {
id.set("atsushieno")
name.set("Atsushi Eno")
email.set("[email protected]")
}
}
}
}
}

repositories {
maven {
name = "OSSRH"
//url = uri("https://s01.oss.sonatype.org/service/local/staging/deployByRepositoryId/$repositoryId/")
url = uri("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/")
credentials {
username = System.getenv("OSSRH_USERNAME")
password = System.getenv("OSSRH_PASSWORD")
}
}
}
}

// keep it as is. It is replaced by CI release builds
signing {}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
@file:JsModule("js-synthesizer")

package dev.atsushieno.ktmidi

import org.khronos.webgl.ArrayBuffer
import org.khronos.webgl.Uint8Array
import kotlin.js.Promise

external interface SynthesizerSettings : JsAny
external interface InterpolationValues : JsAny
external interface PlayerSetTempoType : JsAny

external interface ISynthesizer : JsAny {
fun isInitialized(): Boolean
fun init(sampleRate: Double, settings: SynthesizerSettings? = definedExternally)
fun close()
fun isPlaying(): Boolean
fun setInterpolation(value: InterpolationValues, channel: Double?)
fun getGain(): Double
fun setGain(gain: Double)
fun setChannelType(channel: Double, isDrum: Boolean)
fun waitForVoicesStopped(): Promise<JsAny?> // void

fun loadSFont(bin: ArrayBuffer): Promise<JsAny?> // number
fun unloadSFont(id: Double)
fun unloadSFontAsync(id: Double): Promise<JsAny?> // void
fun getSFontBankOffset(id: Double): Promise<JsAny?> // number
fun setSFontBankOffset(id: Double, offset: Double)

fun render(outBuffer: JsAny?) // AudioBuffer | Float32Array[]

fun midiNoteOn(chan: Byte, key: Byte, vel: Byte)
fun midiNoteOff(chan: Byte, key: Byte)
fun midiKeyPressure(chan: Byte, key: Byte, value: Byte)
fun midiControl(chan: Byte, ctrl: Byte, value: Byte)
fun midiProgramChange(chan: Byte, prognum: Byte)
fun midiChannelPressure(chan: Byte, value: Byte)
fun midiPitchBend(chan: Byte, value: Short)
fun midiSysEx(data: Uint8Array)

fun midiPitchWheelSensitivity(chan: Byte, value: Byte)
fun midiBankSelect(chan: Byte, bank: Byte)
fun midiSFontSelect(chan: Byte, sfontId: Int)
fun midiProgramSelect(chan: Byte, sfontId: Int, bank: Byte, presetNum: Byte)
fun midiUnsetProgram(chan: Byte)
fun midiProgramReset()
fun midiSystemReset()
fun midiAllNotesOff(chan: Byte?)
fun midiAllSoundsOff(chan: Byte?)
fun midiSetChannelType(chan: Byte, isDrum: Boolean)

fun resetPlayer(): Promise<JsAny?> // void
fun closePlayer()
fun isPlayerPlaying(): Boolean
fun addSMFDataToPlayer(bin: ArrayBuffer): Promise<JsAny?> // void
fun playPlayer(): Promise<JsAny?> // void
fun stopPlayer()
fun retrievePlayerCurrentTick(): Promise<JsAny?> // number
fun retrievePlayerTotalTicks(): Promise<JsAny?> // number
fun retrievePlayerBpm(): Promise<JsAny?> // number
fun retrievePlayerMIDITempo(): Promise<JsAny?> // number
fun seekPlayer(ticks: Double)
fun setPlayerLoop(loopTimes: Double)
fun setPlayerTempo(tempoType: PlayerSetTempoType, tempo: Double)
fun waitForPlayerStopped(): Promise<JsAny?> // void
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package dev.atsushieno.ktmidi

//import org.jetbrains.compose.resources.InternalResourceApi
//import org.jetbrains.compose.resources.readResourceBytes
import org.khronos.webgl.ArrayBuffer
import org.khronos.webgl.Uint8Array

private fun createSynthesizer(): ISynthesizer = js("new JSSynth.Synthesizer()")
private fun whee(): JsAny = js("""
console.log("test");
return null;
""")
private fun waitForReady(runnable: ()->Unit): Nothing = js("JSSynth.waitForReady().then(runnable)")

class JsSynthesizerMidiAccess(private val sfBinary: ArrayBuffer, sfName: String) : MidiAccess() {
companion object {

/*
@Suppress("CAST_NEVER_SUCCEEDS")
@OptIn(InternalResourceApi::class)
suspend fun createDefault() =
create(readResourceBytes("FluidR3_GM.sf2") as ArrayBuffer, "FluidR3_GM.sf2")
*/

fun callWhee() = whee()

fun create(sfBinary: ArrayBuffer, sfName: String): JsSynthesizerMidiAccess {
var synth: JsSynthesizerMidiAccess? = null
println("!!! JsSynthesizerMidiAccess.create() start.")
try {
waitForReady {
println("!!! in waitForReady().")
synth = JsSynthesizerMidiAccess(sfBinary, sfName)
println("!!! waitForReady() done.")
}
println("!!! JsSynthesizerMidiAccess.create() done.")
} catch (ex: Exception) {
println(ex)
throw ex
}
return synth!!
}
}

private val details = JsSynthesizerMidiOutputDetails(sfName)

override val name: String
get() = "JsSynthesizer"
override val inputs: Iterable<MidiPortDetails>
get() = listOf()
override val outputs: Iterable<MidiPortDetails>
get() = listOf(details)

override suspend fun openInput(portId: String): MidiInput {
throw UnsupportedOperationException()
}

override suspend fun openOutput(portId: String): MidiOutput {
val synth = createSynthesizer()
synth.init(44100.0)
if (sfBinary.byteLength > 0)
synth.loadSFont(sfBinary)
return JsSynthesizerMidiOutput(details, synth)
}

internal class JsSynthesizerMidiOutputDetails(
private val soundFontName: String,
override val version: String = "1.0"
) : MidiPortDetails {
override val id: String = soundFontName
override val manufacturer: String
get() = "JsSynthesizerMidiAccess"
override val name: String
get() = "js-synthesizer: $soundFontName"
override val midiTransportProtocol: Int
get() = MidiTransportProtocol.MIDI1
}

internal class JsSynthesizerMidiOutput(
override val details: MidiPortDetails,
private val synth: ISynthesizer
) : MidiOutput {
private var state = MidiPortConnectionState.OPEN

override fun send(mevent: ByteArray, offset: Int, length: Int, timestampInNanoseconds: Long) {
Midi1Message.convert(mevent, offset, length, Midi1SysExChunkProcessor()).onEach {
when(it.statusCode.toInt()) {
MidiChannelStatus.NOTE_OFF -> synth.midiNoteOff(it.channel, it.msb)
MidiChannelStatus.NOTE_ON -> synth.midiNoteOn(it.channel, it.msb, it.lsb)
MidiChannelStatus.PAF -> synth.midiKeyPressure(it.channel, it.msb, it.lsb)
MidiChannelStatus.CC -> synth.midiControl(it.channel, it.msb, it.lsb)
MidiChannelStatus.PROGRAM -> synth.midiProgramChange(it.channel, it.msb)
MidiChannelStatus.CAF -> synth.midiChannelPressure(it.channel, it.msb)
MidiChannelStatus.PITCH_BEND -> synth.midiPitchBend(it.channel, (it.msb * 128 + it.lsb).toShort())
else ->
if (it.statusByte.toInt() == Midi1Status.SYSEX)
synth.midiSysEx((it as Midi1CompoundMessage).extraData!! as Uint8Array)
}
}
}

override val connectionState: MidiPortConnectionState
get() = state

override fun close() {
synth.close()
state = MidiPortConnectionState.CLOSED
}
}
}
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ include(":ktmidi")
include(":ktmidi-ci")
include(":ktmidi-jvm-desktop")
include(":ktmidi-native-ext")
include(":ktmidi-wasm-ext")
include(":input-sample")
include(":player-sample")
include(':ktmidi-ci-tool')
Expand Down

0 comments on commit d51a559

Please sign in to comment.