diff --git a/build.gradle.kts b/build.gradle.kts index 740027b..dfb6a14 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,7 +10,7 @@ plugins { } group = "nl.astraeus" -version = "2.1.0-SNAPSHOT" +version = "2.1.0" repositories { mavenCentral() diff --git a/src/jsMain/kotlin/nl/astraeus/vst/midi/Broadcaster.kt b/src/jsMain/kotlin/nl/astraeus/vst/midi/Broadcaster.kt new file mode 100644 index 0000000..ab2347f --- /dev/null +++ b/src/jsMain/kotlin/nl/astraeus/vst/midi/Broadcaster.kt @@ -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() + + 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()) + } + } + +} diff --git a/src/jsMain/kotlin/nl/astraeus/vst/midi/Midi.kt b/src/jsMain/kotlin/nl/astraeus/vst/midi/Midi.kt new file mode 100644 index 0000000..5f0c374 --- /dev/null +++ b/src/jsMain/kotlin/nl/astraeus/vst/midi/Midi.kt @@ -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() + var outputs = mutableListOf() + 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) + } + +}