Release version 2.1.0 and add MIDI broadcasting and handling

Updated `build.gradle.kts` to finalize version 2.1.0. Introduced `Broadcaster` and `Midi` classes for MIDI message broadcasting, synchronization, and handling. Added support for MIDI input/output devices with state management and message processing capabilities.
This commit is contained in:
2025-06-06 19:55:44 +02:00
parent e6b7c9b288
commit 770607d5e6
3 changed files with 271 additions and 1 deletions

View File

@@ -10,7 +10,7 @@ plugins {
}
group = "nl.astraeus"
version = "2.1.0-SNAPSHOT"
version = "2.1.0"
repositories {
mavenCentral()

View File

@@ -0,0 +1,110 @@
@file:OptIn(ExperimentalJsExport::class)
package nl.astraeus.vst.midi
import kotlinx.browser.window
import org.khronos.webgl.Uint8Array
import org.w3c.dom.BroadcastChannel
import org.w3c.dom.MessageEvent
import kotlin.math.min
@JsExport
enum class MessageType {
SYNC,
MIDI
}
@JsExport
class SyncMessage(
@JsName("timeOrigin")
val timeOrigin: Double = window.performance.asDynamic().timeOrigin,
@JsName("now")
val now: Double = window.performance.now()
) {
@JsName("type")
val type: String = MessageType.SYNC.name
}
// time -> syncOrigin
// receive syn message
// syncOrigin = my timeOrigin - sync.timeOrigin
// - sync.timeOrigin = 50
// - my.timeOrigin = 100
// - syncOrigin = -50
// - sync.timeOrigin = 49
// - my.timeOrigin = 100
// - syncOrigin = update to -51
@JsExport
class MidiMessage(
@JsName("data")
val data: Uint8Array,
@JsName("timestamp")
val timestamp: dynamic = null,
@JsName("timeOrigin")
val timeOrigin: dynamic = window.performance.asDynamic().timeOrigin
) {
@JsName("type")
val type = MessageType.MIDI.name
}
object Sync {
var syncOrigin = 0.0
fun update(sync: SyncMessage) {
syncOrigin = min(syncOrigin, window.performance.asDynamic().timeOrigin - sync.timeOrigin)
}
fun now(): Double = window.performance.now() + syncOrigin
}
object Broadcaster {
val channels = mutableMapOf<Int, BroadcastChannel>()
fun getChannel(channel: Int): BroadcastChannel = channels.getOrPut(channel) {
println("Opening broadcast channel $channel")
val bcChannel = BroadcastChannel("audio-worklet-$channel")
bcChannel.onmessage = { event ->
onMessage(channel, event)
}
bcChannel
}
private fun onMessage(channel: Int, event: MessageEvent) {
val data: dynamic = event.data.asDynamic()
console.log(
"Received broadcast message on channel $channel",
event
)
if (data.type == MessageType.SYNC.name) {
val syncMessage = SyncMessage(
data.timeOrigin,
data.now
)
Sync.update(syncMessage)
} else {
console.log(
"Received broadcast message on channel $channel",
event,
window.performance,
window.performance.now() - event.timeStamp.toDouble()
)
}
}
fun send(channel: Int, message: Any) {
console.log("Sending broadcast message on channel $channel:", message)
getChannel(channel).postMessage(message)
}
fun sync() {
for (channel in channels.values) {
channel.postMessage(SyncMessage())
}
}
}

View File

@@ -0,0 +1,160 @@
package nl.astraeus.vst.midi
import kotlinx.browser.window
import org.khronos.webgl.Uint8Array
import org.khronos.webgl.get
external class MIDIInput {
val connection: String
val id: String
val manufacturer: String
val name: String
val state: String
val type: String
val version: String
var onmidimessage: (dynamic) -> Unit
var onstatechange: (dynamic) -> Unit
fun open()
fun close()
}
external class MIDIOutput {
val connection: String
val id: String
val manufacturer: String
val name: String
val state: String
val type: String
val version: String
fun send(message: dynamic, timestamp: dynamic)
fun open()
fun close()
}
object Midi {
var outputChannel: Int = -1
var inputs = mutableListOf<MIDIInput>()
var outputs = mutableListOf<MIDIOutput>()
var currentInput: MIDIInput? = null
var currentOutput: MIDIOutput? = null
fun start(
onUpdate: () -> Unit,
) {
val navigator = window.navigator.asDynamic()
navigator.requestMIDIAccess().then(
{ midiAccess ->
val inp = midiAccess.inputs
val outp = midiAccess.outputs
console.log("Midi inputs:", inputs)
console.log("Midi outputs:", outputs)
inp.forEach() { input ->
console.log("Midi input:", input)
inputs.add(input)
console.log("Name: ${(input as? MIDIInput)?.name}")
}
outp.forEach() { output ->
console.log("Midi output:", output)
outputs.add(output)
}
onUpdate()
},
{ e ->
println("Failed to get MIDI access - $e")
}
)
}
fun setInput(
id: String,
name: String = "",
onMidiInput: (data: Uint8Array) -> Unit
) {
var selected = inputs.find { it.id == id }
if (selected == null) {
var maxMatchChar = 0
inputs.forEach {
val matchChars = matchChars(it.name, name)
if (matchChars > maxMatchChar) {
selected = it
maxMatchChar = matchChars
}
}
}
setInput(selected, onMidiInput)
}
private fun matchChars(str1: String, str2: String): Int {
var result = 0
if (str1.length > str2.length) {
for (ch in str1.toCharArray()) {
if (str2.contains(ch)) {
result++
}
}
} else {
for (ch in str2.toCharArray()) {
if (str1.contains(ch)) {
result++
}
}
}
return result
}
fun setInput(
input: MIDIInput?,
onMidiInput: (data: Uint8Array) -> Unit,
) {
console.log("Setting input", input)
currentInput?.close()
currentInput = input
currentInput?.onstatechange = { message ->
console.log("State change:", message)
}
currentInput?.onmidimessage = { message ->
val data = message.data as Uint8Array
val hex = StringBuilder()
for (index in 0 until data.length) {
hex.append(data[index].toString(16))
hex.append(" ")
}
console.log("Midi message:", hex)
onMidiInput(message.data)
}
currentInput?.open()
}
fun setOutput(
output: MIDIOutput?
) {
console.log("Setting output", output)
currentOutput?.close()
currentOutput = output
currentOutput?.open()
}
fun send(
data: Uint8Array,
timestamp: dynamic = null
) {
currentOutput?.send(data, timestamp)
}
}