From abf0d25c08f2bfc65a4e0acdd062e78adb8068fa Mon Sep 17 00:00:00 2001 From: Matt Rucker Date: Tue, 8 Aug 2023 16:02:18 -0700 Subject: [PATCH] Add JS MIDI support --- .../kotlin/baaahs/plugin/midi/MidiControl.kt | 66 +++++++++++ .../kotlin/baaahs/plugin/midi/MidiPlugin.kt | 9 +- .../kotlin/baaahs/plugin/midi/MidiViews.kt | 11 ++ .../templates/shows/Eve Rafters.sparkle | 3 - src/jsMain/kotlin/baaahs/JsMain.kt | 8 ++ src/jsMain/kotlin/baaahs/SheepSimulator.kt | 5 +- src/jsMain/kotlin/baaahs/di/JsModules.kt | 28 ++++- .../kotlin/baaahs/di/JsSimulatorModules.kt | 4 +- .../kotlin/baaahs/midi/BrowserMidiDevices.kt | 77 ++++++------ src/jsMain/kotlin/baaahs/midi/MIDIPage.kt | 65 ++++++++++ src/jsMain/kotlin/baaahs/midi/MIDIUi.kt | 26 ++++ .../kotlin/baaahs/plugin/midi/JsMidi.kt | 2 +- .../kotlin/baaahs/plugin/midi/JsMidiSource.kt | 77 ++++++++++++ .../kotlin/baaahs/plugin/midi/JsMidiViews.kt | 17 +++ .../baaahs/plugin/midi/MidiControlView.kt | 106 +++++++++++++++++ src/jsMain/kotlin/baaahs/sim/ui/MIDIView.kt | 112 ++++++++++++++++++ .../kotlin/baaahs/sim/ui/SimulatorAppView.kt | 8 +- src/jsMain/kotlin/external/midi/midi.kt | 29 +++-- src/jsMain/resources/midi/index.html | 46 +++++++ .../kotlin/baaahs/midi/JvmMidiDevices.kt | 1 - .../kotlin/baaahs/plugin/midi/JvmMidiViews.kt | 3 + .../kotlin/baaahs/sm/server/PinkyMain.kt | 3 + 22 files changed, 642 insertions(+), 64 deletions(-) create mode 100644 src/commonMain/kotlin/baaahs/plugin/midi/MidiControl.kt create mode 100644 src/commonMain/kotlin/baaahs/plugin/midi/MidiViews.kt create mode 100644 src/jsMain/kotlin/baaahs/midi/MIDIPage.kt create mode 100644 src/jsMain/kotlin/baaahs/midi/MIDIUi.kt create mode 100644 src/jsMain/kotlin/baaahs/plugin/midi/JsMidiSource.kt create mode 100644 src/jsMain/kotlin/baaahs/plugin/midi/JsMidiViews.kt create mode 100644 src/jsMain/kotlin/baaahs/plugin/midi/MidiControlView.kt create mode 100644 src/jsMain/kotlin/baaahs/sim/ui/MIDIView.kt create mode 100644 src/jsMain/resources/midi/index.html create mode 100644 src/jvmMain/kotlin/baaahs/plugin/midi/JvmMidiViews.kt diff --git a/src/commonMain/kotlin/baaahs/plugin/midi/MidiControl.kt b/src/commonMain/kotlin/baaahs/plugin/midi/MidiControl.kt new file mode 100644 index 0000000000..e5538bc1f2 --- /dev/null +++ b/src/commonMain/kotlin/baaahs/plugin/midi/MidiControl.kt @@ -0,0 +1,66 @@ +package baaahs.plugin.midi + +import baaahs.ShowPlayer +import baaahs.app.ui.dialog.DialogPanel +import baaahs.app.ui.editor.EditableManager +import baaahs.camelize +import baaahs.randomId +import baaahs.show.Control +import baaahs.show.live.ControlProps +import baaahs.show.live.OpenContext +import baaahs.show.live.OpenControl +import baaahs.show.mutable.MutableControl +import baaahs.show.mutable.MutableShow +import baaahs.show.mutable.ShowBuilder +import baaahs.ui.View +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import kotlinx.serialization.json.JsonElement + +@Serializable +@SerialName("baaahs.Midi:Midi") +data class MidiControl(@Transient private val `_`: Boolean = false) : Control { + override val title: String get() = "Midi" + + override fun createMutable(mutableShow: MutableShow): MutableControl { + return MutableMidiControl() + } + + override fun open(id: String, openContext: OpenContext, showPlayer: ShowPlayer): OpenControl { + return OpenMidiControl(id) + } +} + +class MutableMidiControl : MutableControl { + override val title: String get() = "Midi" + + override var asBuiltId: String? = null + + override fun getEditorPanels(editableManager: EditableManager<*>): List { + return emptyList() + } + + override fun buildControl(showBuilder: ShowBuilder): MidiControl { + return MidiControl() + } + + override fun previewOpen(): OpenControl { + return OpenMidiControl(randomId(title.camelize())) + } +} + +class OpenMidiControl( + override val id: String +) : OpenControl { + override fun getState(): Map? = null + + override fun applyState(state: Map) {} + + override fun toNewMutable(mutableShow: MutableShow): MutableControl { + return MutableMidiControl() + } + + override fun getView(controlProps: ControlProps): View = + midiViews.forControl(this, controlProps) +} \ No newline at end of file diff --git a/src/commonMain/kotlin/baaahs/plugin/midi/MidiPlugin.kt b/src/commonMain/kotlin/baaahs/plugin/midi/MidiPlugin.kt index 9e863ba23c..5ccb5ae060 100644 --- a/src/commonMain/kotlin/baaahs/plugin/midi/MidiPlugin.kt +++ b/src/commonMain/kotlin/baaahs/plugin/midi/MidiPlugin.kt @@ -137,12 +137,7 @@ class MidiPlugin internal constructor( MidiBridgePlugin(createServerMidiSource(pluginContext), pluginContext) override fun getServerPlugin(pluginContext: PluginContext, bridgeClient: BridgeClient) = - MidiPlugin( - PubSubPublisher( - PubSubSubscriber(bridgeClient.pubSub, simulatorDefaultMidi), - pluginContext - ) - ) + openForServer(pluginContext, object : Args { override val enableMidi: Boolean get() = true }) override fun getClientPlugin(pluginContext: PluginContext): OpenClientPlugin = openForClient(pluginContext) @@ -151,7 +146,7 @@ class MidiPlugin internal constructor( private val midiDataTopic = PubSub.Topic("plugins/$id/midiData", MidiData.serializer()) } - /** Copy beat data from [midiSource] to a bridge PubSub channel. */ + /** Copy midi data from [midiSource] to a bridge PubSub channel. */ class MidiBridgePlugin( private val midiSource: MidiSource, pluginContext: PluginContext diff --git a/src/commonMain/kotlin/baaahs/plugin/midi/MidiViews.kt b/src/commonMain/kotlin/baaahs/plugin/midi/MidiViews.kt new file mode 100644 index 0000000000..441218e53a --- /dev/null +++ b/src/commonMain/kotlin/baaahs/plugin/midi/MidiViews.kt @@ -0,0 +1,11 @@ +package baaahs.plugin.midi + +import baaahs.show.live.ControlProps +import baaahs.ui.View + +interface MidiViews { + fun forControl(openButtonControl: OpenMidiControl, controlProps: ControlProps): View +} + +val midiViews by lazy { getMidiViews() } +expect fun getMidiViews(): MidiViews \ No newline at end of file diff --git a/src/commonMain/resources/templates/shows/Eve Rafters.sparkle b/src/commonMain/resources/templates/shows/Eve Rafters.sparkle index 3c1a089914..7607b042b7 100644 --- a/src/commonMain/resources/templates/shows/Eve Rafters.sparkle +++ b/src/commonMain/resources/templates/shows/Eve Rafters.sparkle @@ -2058,9 +2058,6 @@ "beatLink": { "type": "baaahs.BeatLink:BeatLink" }, - "midi": { - "type": "baaahs.Midi:Midi" - }, "soundAnalysis": { "type": "baaahs.SoundAnalysis:SoundAnalysis" }, diff --git a/src/jsMain/kotlin/baaahs/JsMain.kt b/src/jsMain/kotlin/baaahs/JsMain.kt index 637c694c0d..39b371644d 100644 --- a/src/jsMain/kotlin/baaahs/JsMain.kt +++ b/src/jsMain/kotlin/baaahs/JsMain.kt @@ -3,6 +3,7 @@ package baaahs import baaahs.app.ui.PatchEditorApp import baaahs.client.WebClient import baaahs.di.* +import baaahs.midi.MIDIUi import baaahs.monitor.MonitorUi import baaahs.net.BrowserNetwork import baaahs.scene.SceneMonitor @@ -84,6 +85,11 @@ private fun launchUi(appName: String?) { koin.createScope().get() } + "MIDIUi" -> { + koin.loadModules(listOf(JsMidiWebClientModule().getModule())) + koin.createScope().get() + } + "PatchEditor" -> { koin.loadModules(listOf(JsUiWebClientModule().getModule())) koin.createScope().get() @@ -122,6 +128,7 @@ private fun launchSimulator( JsSimPinkyModule(sceneMonitor, pinkySettings, Dispatchers.Main, simMappingManager).getModule(), JsUiWebClientModule().getModule(), JsMonitorWebClientModule().getModule(), + JsMidiWebClientModule().getModule(), ) }.koin @@ -130,6 +137,7 @@ private fun launchSimulator( val hostedWebApp = when (val app = queryParams["app"] ?: "UI") { "Monitor" -> simulator.createMonitorApp() "UI" -> simulator.createWebClientApp() + "MIDIUi" -> simulator.createMIDIApp() else -> throw UnsupportedOperationException("unknown app $app") } hostedWebApp.onLaunch() diff --git a/src/jsMain/kotlin/baaahs/SheepSimulator.kt b/src/jsMain/kotlin/baaahs/SheepSimulator.kt index 678197799e..7bc8ab74c2 100644 --- a/src/jsMain/kotlin/baaahs/SheepSimulator.kt +++ b/src/jsMain/kotlin/baaahs/SheepSimulator.kt @@ -5,6 +5,7 @@ import baaahs.controller.ControllersManager import baaahs.monitor.MonitorUi import baaahs.sim.* import baaahs.sim.ui.LaunchItem +import baaahs.midi.MIDIUi import baaahs.visualizer.Visualizer import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch @@ -42,6 +43,7 @@ class SheepSimulator( fun createWebClientApp(): WebClient = getKoin().createScope().get() fun createMonitorApp(): MonitorUi = getKoin().createScope().get() + fun createMIDIApp(): MIDIUi = getKoin().createScope().get() private suspend fun cleanUpBrowserStorage() { val fs = BrowserSandboxFs("BrowserSandboxFs") @@ -78,7 +80,8 @@ class SheepSimulator( val launchItems: List = listOf( launchItem("Web UI") { createWebClientApp() }, - launchItem("Monitor") { createMonitorApp() } + launchItem("Monitor") { createMonitorApp() }, + launchItem("MIDIUi") { createMIDIApp() } ) } } \ No newline at end of file diff --git a/src/jsMain/kotlin/baaahs/di/JsModules.kt b/src/jsMain/kotlin/baaahs/di/JsModules.kt index 602a2214d4..f208147edd 100644 --- a/src/jsMain/kotlin/baaahs/di/JsModules.kt +++ b/src/jsMain/kotlin/baaahs/di/JsModules.kt @@ -14,6 +14,7 @@ import baaahs.gl.Toolchain import baaahs.io.PubSubRemoteFsClientBackend import baaahs.io.RemoteFsSerializer import baaahs.mapper.JsMapper +import baaahs.midi.MIDIUi import baaahs.midi.MidiDevices import baaahs.midi.RemoteMidiDevices import baaahs.monitor.MonitorUi @@ -93,8 +94,8 @@ open class JsUiWebClientModule : WebClientModule() { } } } - class JsMonitorWebClientModule : KModule { + override fun getModule(): Module = module { scope { scoped { get().link("monitor") } @@ -123,6 +124,31 @@ class JsMonitorWebClientModule : KModule { } } + private fun Scope.pinkyAddress(): Network.Address = + get(named(WebClientModule.Qualifier.PinkyAddress)) +} + +class JsMidiWebClientModule : KModule { + + override fun getModule(): Module = module { + scope { + scoped { get().link("midi") } + scoped { PluginContext(get(), get()) } + scoped { PubSub.Client(get(), pinkyAddress(), Ports.PINKY_UI_TCP) } + scoped { get() } + scoped { Plugins.buildForClient(get(), get(named(PluginsModule.Qualifier.ActivePlugins))) } + scoped { get() } + scoped { PubSubRemoteFsClientBackend(get()) } + scoped { SceneManager(get(), get(), get(), get(), get(), get()) } + scoped { SceneMonitor() } + scoped { get() } + scoped { FileDialog() } + scoped { get() } + scoped { Notifier(get()) } + scoped { MIDIUi() } + } + } + private fun Scope.pinkyAddress(): Network.Address = get(named(WebClientModule.Qualifier.PinkyAddress)) } \ No newline at end of file diff --git a/src/jsMain/kotlin/baaahs/di/JsSimulatorModules.kt b/src/jsMain/kotlin/baaahs/di/JsSimulatorModules.kt index 7658327f19..a2fc483192 100644 --- a/src/jsMain/kotlin/baaahs/di/JsSimulatorModules.kt +++ b/src/jsMain/kotlin/baaahs/di/JsSimulatorModules.kt @@ -11,7 +11,7 @@ import baaahs.io.Fs import baaahs.mapper.PinkyMapperHandlers import baaahs.mapping.MappingManager import baaahs.midi.MidiDevices -import baaahs.midi.NullMidiDevices +import baaahs.midi.BrowserMidiDevices import baaahs.net.BrowserNetwork import baaahs.net.Network import baaahs.plugin.Plugins @@ -101,7 +101,7 @@ class JsSimPinkyModule( override val Scope.dmxDriver: Dmx.Driver get() = SimDmxDriver(get(named("Fallback"))) override val Scope.midiDevices: MidiDevices - get() = NullMidiDevices() + get() = BrowserMidiDevices() override val Scope.pinkySettings: PinkySettings get() = pinkySettings_ override val Scope.sceneMonitor: SceneMonitor diff --git a/src/jsMain/kotlin/baaahs/midi/BrowserMidiDevices.kt b/src/jsMain/kotlin/baaahs/midi/BrowserMidiDevices.kt index f2ced919e3..22b1f77e1e 100644 --- a/src/jsMain/kotlin/baaahs/midi/BrowserMidiDevices.kt +++ b/src/jsMain/kotlin/baaahs/midi/BrowserMidiDevices.kt @@ -1,53 +1,60 @@ package baaahs.midi import baaahs.util.Logger +import external.midi.MIDIAccess +import external.midi.MIDIInput +import web.navigator.navigator class BrowserMidiDevices : MidiDevices { -// private val midiAccess = window.navigator.requestMIDIAccess() - private val transmitters = mutableMapOf() + private lateinit var midiAccess: MIDIAccess; + + init { + navigator.asDynamic().requestMIDIAccess().then {midiAccessResult: MIDIAccess -> + midiAccess = midiAccessResult + midiAccessResult + } + } override suspend fun listTransmitters(): List { val ids = mutableMapOf() + return buildList { -// MidiSystem.getMidiDeviceInfo().mapNotNull { info -> -// println("${info.name}: ${info.javaClass.simpleName}\n DESC=${info.description}\n VENDOR=${info.vendor}\n VERSION=${info.version}") -// val device = MidiSystem.getMidiDevice(info) -// val id = info.name.let { +// midiAccess.inputs.forEach { inputEntry -> +// val input = inputEntry.value +// println("${input.name}: DESC=${input.description}\n VENDOR=${input.manufacturer}\n VERSION=${input.version}") +// val id = input.name.let { // val idNum = ids.getOrPut(it) { Counter() }.count() // if (idNum == 0) it else "it #$idNum" // } // -// val maxTransmitters = device.maxTransmitters -// if (maxTransmitters == -1 || maxTransmitters > 0) { -// add(JvmMidiTransmitterTransmitter(id, device)) -// } +// add(JsMidiTransmitterTransmitter(id, input)) // } } } -// class JvmMidiTransmitterTransmitter( -// override val id: String, -// private val device: MidiDevice -// ) : MidiTransmitter { -// override val name: String -// get() = device.deviceInfo.name -// override val vendor: String -// get() = device.deviceInfo.vendor -// override val description: String -// get() = device.deviceInfo.description -// override val version: String -// get() = device.deviceInfo.version -// + class JsMidiTransmitterTransmitter( + override val id: String, + private val input: MIDIInput + ) : MidiTransmitter { + override val name: String + get() = input.name + override val vendor: String + get() = input.manufacturer + override val description: String + get() = "" + override val version: String + get() = input.version + // private var transmitter: Transmitter? = null -// -// override fun listen(callback: (MidiMessage) -> Unit) { + + override fun listen(callback: (MidiMessage) -> Unit) { // if (transmitter == null) { // transmitter = run { -// device.transmitter.also { device.open() } +// input.open() // } // } -// + // transmitter!!.receiver = object : Receiver { // override fun close() { // logger.debug { "$name closed." } @@ -67,16 +74,16 @@ class BrowserMidiDevices : MidiDevices { // } // } // } -// } -// -// override fun close() { + } + + override fun close() { // if (transmitter != null) { -// device.close() +// input.close() // } -// logger.debug { "$name closed." } -// } -// -// } + logger.debug { "$name closed." } + } + + } private class Counter(var value: Int = 0) { fun count(): Int = value++ diff --git a/src/jsMain/kotlin/baaahs/midi/MIDIPage.kt b/src/jsMain/kotlin/baaahs/midi/MIDIPage.kt new file mode 100644 index 0000000000..538f74a551 --- /dev/null +++ b/src/jsMain/kotlin/baaahs/midi/MIDIPage.kt @@ -0,0 +1,65 @@ +package baaahs.midi + +import dom.html.HTMLDivElement +import external.midi.MIDIAccess +import external.midi.MIDIOptions +import external.midi.MIDIInput +import kotlinx.js.jso +import react.RBuilder +import react.RComponent +import react.dom.div +import react.dom.header +import react.dom.b +import react.setState +import web.navigator.navigator + +class MIDIPage(props: MIDIPageProps) : RComponent(props) { + private val container = react.createRef() + override fun MIDIPageState.init() { + midiInputs = emptyList() + } + override fun componentDidMount() { + navigator.asDynamic().requestMIDIAccess(jso { sysex = true }).then { midiAccess: MIDIAccess -> + setState { + midiInputs = buildList { + midiAccess.inputs.asDynamic().forEach { input -> + add(input as MIDIInput) + } + } + } + console.log(midiAccess) + + true + } + container.current?.appendChild(props.containerDiv) + } + + override fun componentWillUnmount() { + container.current?.removeChild(props.containerDiv) + } + + override fun RBuilder.render() { + div { + ref = container + header { +"MIDI" } + div { + if (state.midiInputs != null) { + state.midiInputs.forEach { input -> + div { + console.log(input) + b { +input.name } + } + } + } + } + } + } +} + +external interface MIDIPageProps : react.Props { + var containerDiv: HTMLDivElement +} + +external interface MIDIPageState : react.State { + var midiInputs: List +} diff --git a/src/jsMain/kotlin/baaahs/midi/MIDIUi.kt b/src/jsMain/kotlin/baaahs/midi/MIDIUi.kt new file mode 100644 index 0000000000..88b6785e64 --- /dev/null +++ b/src/jsMain/kotlin/baaahs/midi/MIDIUi.kt @@ -0,0 +1,26 @@ +package baaahs.midi + +import baaahs.document +import baaahs.sim.HostedWebApp +import dom.html.HTMLDivElement +import kotlinx.js.jso +import react.ReactElement +import react.createElement +import react.react + +class MIDIUi() : HostedWebApp { + private val container = document.createElement("div") as HTMLDivElement + + init { + container.className = "adminMIDIDiagnosticContainer" + } + + override fun render(): ReactElement<*> { + return createElement(MIDIPage::class.react, jso { + this.containerDiv = container + }) + } + + override fun onClose() { + } +} diff --git a/src/jsMain/kotlin/baaahs/plugin/midi/JsMidi.kt b/src/jsMain/kotlin/baaahs/plugin/midi/JsMidi.kt index 31fbef8785..3004e0782e 100644 --- a/src/jsMain/kotlin/baaahs/plugin/midi/JsMidi.kt +++ b/src/jsMain/kotlin/baaahs/plugin/midi/JsMidi.kt @@ -3,4 +3,4 @@ package baaahs.plugin.midi import baaahs.plugin.PluginContext internal actual fun createServerMidiSource(pluginContext: PluginContext): MidiSource = - error("baaahs.plugin.midi.createServerMidiSource() not implemented in JS") + JsMidiSource(pluginContext.clock).also { it.start() } diff --git a/src/jsMain/kotlin/baaahs/plugin/midi/JsMidiSource.kt b/src/jsMain/kotlin/baaahs/plugin/midi/JsMidiSource.kt new file mode 100644 index 0000000000..ad8c64c950 --- /dev/null +++ b/src/jsMain/kotlin/baaahs/plugin/midi/JsMidiSource.kt @@ -0,0 +1,77 @@ +package baaahs.plugin.midi + +import baaahs.ui.Observable +import baaahs.util.Clock +import baaahs.util.Logger +import external.midi.MIDIAccess +import external.midi.MIDIInput +import external.midi.MIDIMessageEvent +import kotlinx.js.jso +import org.khronos.webgl.get +import web.navigator.navigator +import kotlin.experimental.and +import kotlin.jvm.Volatile + +/** + * Listens to a midi controller + * + * We pick the midi to sync shows to based on the following: + * 1) Hardcoded name of device on boot + */ +class JsMidiSource( + private val clock: Clock +) : Observable(), MidiSource { + + @Volatile + var currentMidiData: MidiData = MidiData(0) + + private val logger = Logger("JsMidiSource") + fun start() { + console.log("Starting Midi") + + val names = arrayOf( + "iCON iControls V2.04 Port 1", + "KOMPLETE KONTROL S88 MK2 Port 1", + "Keystation 61 MK3 (USB MIDI)" + ); + + navigator.asDynamic().requestMIDIAccess(jso { sysex = true }).then { midiAccess: MIDIAccess -> + midiAccess.onstatechange = {event -> + console.log(event) + } + val inputs: List = buildList { + midiAccess.inputs.asDynamic().forEach { inputEntry -> + add(inputEntry as MIDIInput) + } + } + val transmitterDevice = inputs.firstOrNull { it.name in names } + + transmitterDevice?.let { + it.open() + it.onmidimessage = { messageEvent: MIDIMessageEvent -> + val channel = (messageEvent.data[0] and (0x0F).toByte()).toInt() + val command = messageEvent.data[0] - channel + val data1 = messageEvent.data[1].toInt() + val data2 = messageEvent.data[2].toInt() + console.log(messageEvent) + console.log("MIDI: " + + "channel=${channel} command=${command} " + + "data1=${data1} data2=${data2}") + if (command == 144) { // Key pressed (NOTE_ON) + currentMidiData = MidiData(currentMidiData.sustainPedalCount, currentMidiData.noteCount + 1) + notifyChanged() + } else if (command == 176 && data1 == 64 && data2 > 63) { // Sustain pedal pressed down more than half way (CONTROL_CHANGE) + currentMidiData = MidiData(currentMidiData.sustainPedalCount + 1, currentMidiData.noteCount) + notifyChanged() + } + } + } + console.log("Started") + } + } + + + override fun getMidiData(): MidiData { + return currentMidiData + } +} \ No newline at end of file diff --git a/src/jsMain/kotlin/baaahs/plugin/midi/JsMidiViews.kt b/src/jsMain/kotlin/baaahs/plugin/midi/JsMidiViews.kt new file mode 100644 index 0000000000..5688c1fcb2 --- /dev/null +++ b/src/jsMain/kotlin/baaahs/plugin/midi/JsMidiViews.kt @@ -0,0 +1,17 @@ +package baaahs.plugin.midi + +import baaahs.plugin.midi.MidiViews +import baaahs.plugin.midi.OpenMidiControl +import baaahs.show.live.ControlProps +import baaahs.ui.renderWrapper + +object JsMidiViews : MidiViews { + override fun forControl(openButtonControl: OpenMidiControl, controlProps: ControlProps) = renderWrapper { + midiControl { + attrs.controlProps = controlProps + attrs.midiControl = openButtonControl + } + } +} + +actual fun getMidiViews(): MidiViews = JsMidiViews diff --git a/src/jsMain/kotlin/baaahs/plugin/midi/MidiControlView.kt b/src/jsMain/kotlin/baaahs/plugin/midi/MidiControlView.kt new file mode 100644 index 0000000000..e676d1ae85 --- /dev/null +++ b/src/jsMain/kotlin/baaahs/plugin/midi/MidiControlView.kt @@ -0,0 +1,106 @@ +package baaahs.plugin.midi + +import baaahs.app.ui.appContext +import baaahs.app.ui.shaderPreview +import baaahs.futureAsync +import baaahs.io.getResourceAsync +import baaahs.onAvailable +import baaahs.plugin.midi.MidiPlugin +import baaahs.show.Shader +import baaahs.show.live.ControlProps +import baaahs.ui.* +import baaahs.util.percent +import dom.html.HTMLElement +import kotlinx.css.* +import kotlinx.js.jso +import mui.material.Card +import react.Props +import react.RBuilder +import react.RHandler +import react.dom.div +import react.useContext +import styled.StyleSheet +import kotlin.math.roundToInt + +private val midiVisualizerShader = + futureAsync { + Shader("Midi Visualizer", getResourceAsync("baaahs/plugin/midi/MidiControl.glsl")) + } + +private val midiControl = xComponent("MidiControl") { _ -> + val appContext = useContext(appContext) + val midiSource = appContext.plugins.getPlugin().midiSource + + val sustainPedalCountDiv = ref() + val noteCountDiv = ref() + + fun update(midiData: MidiData) { + val noteCount = midiData.noteCount + noteCountDiv.current!!.innerText = "Note Count: $noteCount" + + val sustainPedalCount = midiData.sustainPedalCount + sustainPedalCountDiv.current!!.innerText = "Sustain Pedal Count: $sustainPedalCount" + } + + onMount { + val observer = midiSource.addObserver(fireImmediately = true) { midiSource -> + val midiData = midiSource.getMidiData() + update(midiData) + } + withCleanup { observer.remove() } + } + + var shader by state { null } + midiVisualizerShader.onAvailable { shader = it } + + Card { + attrs.classes = jso { this.root = -Styles.card } + div(+Styles.card) { + shaderPreview { + attrs.shader = shader + } + + div(+Styles.bpm) { ref = sustainPedalCountDiv } + div(+Styles.confidence) { ref = noteCountDiv } + } + } +} + +object Styles : StyleSheet("plugin-Midi", isStatic = true) { + val card by css { + display = Display.flex + flex(1.0, 0.0) + + // Needed because of [SharedGlContext]. TODO: remove that requirement. + important(::backgroundColor, Color.transparent) + userSelect = UserSelect.none + } + + val div by css { + position = Position.relative + } + + val bpm by css { + position = Position.absolute + bottom = 0.px + left = 0.px + color = Color.white + put("textShadow", "0px 1px 1px black") + } + + val confidence by css { + position = Position.absolute + bottom = 0.px + right = 0.px + color = Color.white + put("textShadow", "0px 1px 1px black") + } +} + +external interface MidiControlProps : Props { + var controlProps: ControlProps + var midiControl: OpenMidiControl +} + +fun RBuilder.midiControl(handler: RHandler) = + child(midiControl, handler = handler) \ No newline at end of file diff --git a/src/jsMain/kotlin/baaahs/sim/ui/MIDIView.kt b/src/jsMain/kotlin/baaahs/sim/ui/MIDIView.kt new file mode 100644 index 0000000000..019204c202 --- /dev/null +++ b/src/jsMain/kotlin/baaahs/sim/ui/MIDIView.kt @@ -0,0 +1,112 @@ +package baaahs.sim.ui + +import baaahs.SheepSimulator +import baaahs.app.ui.AllStyles +import baaahs.app.ui.AppContext +import baaahs.app.ui.appContext +import baaahs.ui.asTextNode +import baaahs.ui.diagnostics.patchDiagnostics +import baaahs.ui.unaryPlus +import baaahs.ui.withTChangeEvent +import baaahs.ui.xComponent +import kotlinx.js.jso +import mui.material.FormControlLabel +import mui.material.Size +import mui.material.Switch +import react.* +import react.dom.button +import react.dom.div +import react.dom.header +import react.dom.onClick + +val MIDIView = xComponent("MIDIView") { props -> + var isConsoleOpen by state { false } + var isGlslPaletteOpen by state { false } + val simulator = props.simulator + val simulatorContext = useContext(simulatorContext) + val stubAppContext = memo(simulatorContext) { + val allStyles = AllStyles(simulatorContext.styles.theme) + jso { + this.allStyles = allStyles + } + } + + observe(simulator) + observe(simulator.pinky) + + val handleIsConsoleOpenChange by eventHandler { isConsoleOpen = !isConsoleOpen } + val handleIsGlslPaletteOpenChange by eventHandler { isGlslPaletteOpen = !isGlslPaletteOpen } + val handlePauseChange by switchEventHandler { _, checked -> + props.simulator.pinky.isPaused = checked + } + + div(+SimulatorStyles.statusPanel) { + header { +"Brain" } + + div(+SimulatorStyles.statusPanelToolbar) { + FormControlLabel { + attrs.control = buildElement { + Switch { + attrs.size = Size.small + attrs.checked = isGlslPaletteOpen + attrs.onChange = handleIsGlslPaletteOpenChange.withTChangeEvent() + } + } + attrs.label = "Diagntics".asTextNode() + } + + FormControlLabel { + attrs.control = buildElement { + Switch { + attrs.size = Size.small + attrs.checked = props.simulator.pinky.isPaused + attrs.onChange = handlePauseChange + } + } + attrs.label = "Pase".asTextNode() + } + + FormControlLabel { + attrs.control = buildElement { + Switch { + attrs.size = Size.small + attrs.checked = isConsoleOpen + attrs.onChange = handleIsConsoleOpenChange.withTChangeEvent() + } + } + attrs.label = "Console".asTextNode() + } + } + + div(+SimulatorStyles.consoleContainer) { + if (isConsoleOpen) console { attrs.simulator = simulator } + } + + if (props.simulator.launchItems.isNotEmpty()) { + div(+SimulatorStyles.launchButtonsContainer) { + header { +"Launch:" } + props.simulator.launchItems.forEach { launchItem -> + button { + attrs.onClick = { launchItem.onLaunch() } + +launchItem.title + } + } + } + } + + if (isGlslPaletteOpen) { + appContext.Provider { + attrs.value = stubAppContext + + patchDiagnostics { + attrs.renderPlanMonitor = simulator.pinky.fixtureManager.renderPlanMonitor + attrs.onClose = handleIsGlslPaletteOpenChange as () -> Unit + } + } + } + } +} + +external interface MIDIViewProps : Props { + var simulator: SheepSimulator.Facade +} diff --git a/src/jsMain/kotlin/baaahs/sim/ui/SimulatorAppView.kt b/src/jsMain/kotlin/baaahs/sim/ui/SimulatorAppView.kt index 3727df6949..8a95768e5c 100644 --- a/src/jsMain/kotlin/baaahs/sim/ui/SimulatorAppView.kt +++ b/src/jsMain/kotlin/baaahs/sim/ui/SimulatorAppView.kt @@ -21,8 +21,8 @@ import styled.injectGlobal enum class SimulatorWindows { Visualizer, - Console, - UI + UI, + MIDI, } val simulatorContext = createContext() @@ -66,7 +66,7 @@ val SimulatorAppView = xComponent("SimulatorApp") { props -> direction = "column" splitPercentage = 50 first = SimulatorWindows.Visualizer - second = SimulatorWindows.Console + second = SimulatorWindows.MIDI } second = SimulatorWindows.UI } @@ -104,7 +104,7 @@ val SimulatorAppView = xComponent("SimulatorApp") { props -> SimulatorWindows.Visualizer -> createElement(ModelSimulationView, jso { this.simulator = props.simulator }) - SimulatorWindows.Console -> createElement(StatusPanelView, jso { + SimulatorWindows.MIDI -> createElement(MIDIView, jso { this.simulator = props.simulator }) SimulatorWindows.UI -> createElement(WebClientWindowView, jso { diff --git a/src/jsMain/kotlin/external/midi/midi.kt b/src/jsMain/kotlin/external/midi/midi.kt index 6c67379e71..73eece21bd 100644 --- a/src/jsMain/kotlin/external/midi/midi.kt +++ b/src/jsMain/kotlin/external/midi/midi.kt @@ -3,25 +3,36 @@ package external.midi import org.w3c.dom.events.Event import org.w3c.dom.events.EventTarget import react.dom.events.EventHandler +import org.khronos.webgl.Uint8Array -//external fun Navigator.requestMIDIAccess(options: MIDIOptions? = definedExternally): Promise - -external interface MIDIOptions { +external class MIDIOptions { var sysex: Boolean } -abstract external class MIDIAccess: EventTarget { +external class MIDIAccess: EventTarget { val inputs: MIDIInputMap val outputs: MIDIOutputMap - val onstatechange: EventHandler + var onstatechange: EventHandler val sysexEnabled: Boolean } -abstract external class MIDIConnectionEvent : Event { +external class MIDIConnectionEvent : Event { val port: MIDIPort } -abstract external class MIDIInputMap -abstract external class MIDIOutputMap +external class MIDIInput: MIDIPort +external class MIDIOutputMap +external class MIDIInputMap + +open external class MIDIPort { + val name: String + val manufacturer: String + val version: String + var onmidimessage: ((e: MIDIMessageEvent) -> Any)? = definedExternally + fun open() + fun close() +} -abstract external class MIDIPort \ No newline at end of file +external class MIDIMessageEvent { + val data: Uint8Array +} diff --git a/src/jsMain/resources/midi/index.html b/src/jsMain/resources/midi/index.html new file mode 100644 index 0000000000..665615225f --- /dev/null +++ b/src/jsMain/resources/midi/index.html @@ -0,0 +1,46 @@ + + + + + + Admin | sparklemotion + + + + + +
+
+ +
Patience...
+
+
+ + + + diff --git a/src/jvmMain/kotlin/baaahs/midi/JvmMidiDevices.kt b/src/jvmMain/kotlin/baaahs/midi/JvmMidiDevices.kt index f52c9e65e9..87ea2453d1 100644 --- a/src/jvmMain/kotlin/baaahs/midi/JvmMidiDevices.kt +++ b/src/jvmMain/kotlin/baaahs/midi/JvmMidiDevices.kt @@ -13,7 +13,6 @@ class JvmMidiDevices : MidiDevices { return buildList { MidiSystem.getMidiDeviceInfo().mapNotNull { info -> println("${info.name}: ${info.javaClass.simpleName}\n DESC=${info.description}\n VENDOR=${info.vendor}\n VERSION=${info.version}") - println("hello???") val device = MidiSystem.getMidiDevice(info) val id = info.name.let { val idNum = ids.getOrPut(it) { Counter() }.count() diff --git a/src/jvmMain/kotlin/baaahs/plugin/midi/JvmMidiViews.kt b/src/jvmMain/kotlin/baaahs/plugin/midi/JvmMidiViews.kt new file mode 100644 index 0000000000..ec85d9fdfe --- /dev/null +++ b/src/jvmMain/kotlin/baaahs/plugin/midi/JvmMidiViews.kt @@ -0,0 +1,3 @@ +package baaahs.plugin.midi + +actual fun getMidiViews(): MidiViews = error("Not available on JVM") \ No newline at end of file diff --git a/src/jvmMain/kotlin/baaahs/sm/server/PinkyMain.kt b/src/jvmMain/kotlin/baaahs/sm/server/PinkyMain.kt index 06fc2114b0..879e17b209 100644 --- a/src/jvmMain/kotlin/baaahs/sm/server/PinkyMain.kt +++ b/src/jvmMain/kotlin/baaahs/sm/server/PinkyMain.kt @@ -97,6 +97,7 @@ class PinkyMain(private val args: Array) { resources("htdocs") route("monitor/") { defaultResource("htdocs/monitor/index.html") } route("ui/") { defaultResource("htdocs/ui/index.html") } + route("midi/") { defaultResource("htdocs/midi/index.html") } defaultResource("htdocs/ui-index.html") } } @@ -120,6 +121,7 @@ class PinkyMain(private val args: Array) { files(jsResDir.toFile()) route("monitor/") { default("monitor/index.html") } route("ui/") { default("ui/index.html") } + route("midi/") { default("midi/index.html") } default("ui-index.html") } } @@ -134,6 +136,7 @@ class PinkyMain(private val args: Array) { static { get("monitor") { call.respondRedirect("monitor/") } get("ui") { call.respondRedirect("ui/") } + get("midi") { call.respondRedirect("midi/") } route("data/") { files(dataDirFile) } }