diff --git a/audio-worklet/build.gradle.kts b/audio-worklet/build.gradle.kts index 44eb3ee..5567328 100644 --- a/audio-worklet/build.gradle.kts +++ b/audio-worklet/build.gradle.kts @@ -35,7 +35,6 @@ kotlin { } } } - jvm() sourceSets { val commonMain by getting { diff --git a/audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/chip/ChipProcessor.kt b/audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/chip/ChipProcessor.kt index c4ba008..19d57f2 100644 --- a/audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/chip/ChipProcessor.kt +++ b/audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/chip/ChipProcessor.kt @@ -8,6 +8,7 @@ import nl.astraeus.tba.SlicedByteArray import nl.astraeus.vst.ADSR import nl.astraeus.vst.AudioWorkletProcessor import nl.astraeus.vst.currentTime +import nl.astraeus.vst.midi.MidiMessageHandler import nl.astraeus.vst.registerProcessor import nl.astraeus.vst.sampleRate import org.khronos.webgl.Float32Array @@ -65,8 +66,8 @@ enum class RecordingState { @ExperimentalJsExport @JsExport class VstChipProcessor : AudioWorkletProcessor() { - var midiChannel = 0 val midiMessageBuffer = SortedTimedMidiMessageList() + val midiMessageHandler = MidiMessageHandler() val notes = Array(POLYPHONICS) { null } var waveform = Waveform.SINE.ordinal @@ -100,6 +101,82 @@ class VstChipProcessor : AudioWorkletProcessor() { init { this.port.onmessage = ::handleMessage Note.updateSampleRate(sampleRate) + + with(midiMessageHandler) { + addHandler(0x90) { b1, b2, b3 -> + val note = b2.toInt() and 0xff + val velocity = b3.toInt() and 0xff + + if (velocity > 0) { + console.log("Note on", note, velocity) + noteOn(note, velocity) + } else { + console.log("Note off", note) + noteOff(note) + } + } + addHandler(0x80) { b1, b2, b3 -> + val note = b2.toInt() and 0xff + + console.log("Note off", note) + noteOff(note) + } + addHandler(0xc9) { b1, b2, b3 -> + waveform = b2.toInt() and 0xff + } + addHandler(0xb0, 7) { b1, b2, b3 -> + volume = b3 / 127f + } + addHandler(0xb0, 0x47) { b1, b2, b3 -> + dutyCycle = b3 / 127.0 + } + addHandler(0xb0, 0x40) { b1, b2, b3 -> + fmFreq = b3 / 127.0 + } + addHandler(0xb0, 0x41) { b1, b2, b3 -> + fmAmp = b3 / 127.0 + } + addHandler(0xb0, 0x42) { b1, b2, b3 -> + amFreq = b3 / 127.0 + } + addHandler(0xb0, 0x43) { b1, b2, b3 -> + amAmp = b3 / 127.0 + } + addHandler(0xb0, 0x49) { b1, b2, b3 -> + attack = b3 / 127.0 + } + addHandler(0xb0, 0x4b) { b1, b2, b3 -> + decay = b3 / 127.0 + } + addHandler(0xb0, 0x46) { b1, b2, b3 -> + sustain = b3 / 127.0 + } + addHandler(0xb0, 0x48) { b1, b2, b3 -> + release = b3 / 127.0 + } + addHandler(0xb0, 0x4e) { b1, b2, b3 -> + delay = b3 / 127.0 + } + addHandler(0xb0, 0x4f) { b1, b2, b3 -> + delayDepth = b3 / 127.0 + } + addHandler(0xb0, 0x50) { b1, b2, b3 -> + feedback = b3 / 127.0 + } + addHandler(0xb0, 123) { b1, b2, b3 -> + for (note in notes) { + note?.noteRelease = currentTime + } + } + addHandler(0xe0) { b1, b2, b3 -> + if (b2.toInt() and 0xff > 0) { + val lsb = b2.toInt() and 0xff + val msb = b3.toInt() and 0xff + + amFreq = (((msb - 0x40) + (lsb / 127.0)) / 0x40) * 10.0 + } + } + } } private fun handleMessage(message: MessageEvent) { @@ -122,8 +199,9 @@ class VstChipProcessor : AudioWorkletProcessor() { data.startsWith("set_channel") -> { val parts = data.split('\n') if (parts.size == 2) { - midiChannel = parts[1].toInt() - println("Setting channel: $midiChannel") + midiMessageHandler.channel = parts[1].toByte() + + println("Setting channel: ${midiMessageHandler.channel}") } } @@ -166,8 +244,9 @@ class VstChipProcessor : AudioWorkletProcessor() { } private fun playBuffer() { - while (midiMessageBuffer.isNotEmpty() && (midiMessageBuffer.nextTimestamp() - ?: 0.0) < currentTime + while ( + midiMessageBuffer.isNotEmpty() && + (midiMessageBuffer.nextTimestamp() ?: 0.0) < currentTime ) { val midi = midiMessageBuffer.read() console.log("Message", currentTime, midi) @@ -178,163 +257,16 @@ class VstChipProcessor : AudioWorkletProcessor() { private fun playMidi(bytes: SlicedByteArray) { var index = 0 - console.log( - "--playMidi", - bytes.size, - index, - bytes[index + 0], - bytes[index + 1], - bytes[index + 2] - ) while (index < bytes.size && bytes[index].toUByte() > 0u) { console.log("playMidi", bytes, index, bytes[index + 0], bytes[index + 1], bytes[index + 2]) - index += playMidiFromBuffer(bytes, index) + val buffer = bytes.getBlob(index, 3) + playMidiFromBuffer(buffer) + index += 3 } } - private fun playMidiFromBuffer(bytes: SlicedByteArray, index: Int): Int { - if (bytes[index] == 0.toByte()) { - return 0 - } - - if (bytes.length > 0) { - var cmdByte = bytes[0].toInt() and 0xff - val channelCmd = ((cmdByte shr 4) and 0xf) != 0xf0 - val channel = cmdByte and 0xf - //println("Channel cmd: $channelCmd") - val byteLength = when (cmdByte) { - 0x90, 0xb0, 0xe0 -> 3 - 0x80, 0xc9 -> 2 - else -> throw IllegalArgumentException("Unknown command: $cmdByte") - } - - if (channelCmd && channel != midiChannel) { - console.log("Wrong channel", midiChannel, bytes) - return byteLength - } - - cmdByte = cmdByte and 0xf0 - - //console.log("Received", bytes) - when (cmdByte) { - 0x90 -> { - if (bytes.length >= 3) { - val note = bytes[1].toInt() and 0xff - val velocity = bytes[2].toInt() and 0xff - - if (velocity > 0) { - console.log("Note on", note, velocity) - noteOn(note, velocity) - } else { - console.log("Note off", note) - noteOff(note) - } - } - } - - 0x80 -> { - if (bytes.length >= 2) { - val note = bytes[1].toInt() and 0xff - - console.log("Note off", note) - noteOff(note) - } - } - - 0xc9 -> { - if (bytes.length >= 1) { - val waveform = bytes[1].toInt() and 0xff - - if (waveform < 4) { - this.waveform = waveform - } - } - } - - 0xb0 -> { - if (bytes.length >= 3) { - val knob = bytes[1].toInt() and 0xff - val value = bytes[2].toInt() and 0xff - - when (knob) { - 7 -> { - volume = value / 127f - } - - 0x47 -> { - dutyCycle = value / 127.0 - } - - 0x40 -> { - fmFreq = value / 127.0 - } - - 0x41 -> { - fmAmp = value / 127.0 - } - - 0x42 -> { - amFreq = value / 127.0 - } - - 0x43 -> { - amAmp = value / 127.0 - } - - 0x49 -> { - attack = value / 127.0 - } - - 0x4b -> { - decay = value / 127.0 - } - - 0x46 -> { - sustain = value / 127.0 - } - - 0x48 -> { - release = value / 127.0 - } - - 0x4e -> { - delay = value / 127.0 - println("Setting delay $delay") - } - - 0x4f -> { - delayDepth = value / 127.0 - println("Setting delayDepth $delayDepth") - } - - 0x50 -> { - feedback = value / 127.0 - println("Setting feedback $delayDepth") - } - - 123 -> { - for (note in notes) { - note?.noteRelease = currentTime - } - } - } - } - } - - 0xe0 -> { - if (bytes.length >= 3) { - val lsb = bytes[1] - val msb = bytes[2] - - amFreq = (((msb - 0x40) + (lsb / 127.0)) / 0x40) * 10.0 - } - } - } - - return byteLength - } - - throw IllegalArgumentException("Unable to handle empty byte array") + private fun playMidiFromBuffer(bytes: SlicedByteArray) { + midiMessageHandler.handle(bytes[0], bytes[1], bytes[2]) } private fun noteOn(note: Int, velocity: Int) { diff --git a/audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/chip/Note.kt b/audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/chip/Note.kt index 6e2f642..1973bb8 100644 --- a/audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/chip/Note.kt +++ b/audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/chip/Note.kt @@ -12,8 +12,6 @@ import kotlin.math.round * Time: 11:50 */ -@ExperimentalJsExport -@JsExport enum class Note( val sharp: String, val flat: String diff --git a/common.gradle.kts b/common.gradle.kts index 206179d..ad47d71 100644 --- a/common.gradle.kts +++ b/common.gradle.kts @@ -3,13 +3,12 @@ version = "0.1.0" allprojects { repositories { - mavenLocal() - mavenCentral() maven { url = uri("https://gitea.astraeus.nl/api/packages/rnentjes/maven") } maven { url = uri("https://gitea.astraeus.nl:8443/api/packages/rnentjes/maven") } + mavenCentral() } } diff --git a/settings.common.gradle.kts b/settings.common.gradle.kts index a00b896..ededdda 100644 --- a/settings.common.gradle.kts +++ b/settings.common.gradle.kts @@ -1,6 +1,6 @@ pluginManagement { plugins { - kotlin("multiplatform") version "2.0.21" + kotlin("multiplatform") version "2.1.0" } repositories { gradlePluginPortal() diff --git a/src/jsMain/kotlin/nl/astraeus/vst/chip/audio/VstChipWorklet.kt b/src/jsMain/kotlin/nl/astraeus/vst/chip/audio/VstChipWorklet.kt index a771908..9bbbdbb 100644 --- a/src/jsMain/kotlin/nl/astraeus/vst/chip/audio/VstChipWorklet.kt +++ b/src/jsMain/kotlin/nl/astraeus/vst/chip/audio/VstChipWorklet.kt @@ -2,6 +2,7 @@ package nl.astraeus.vst.chip.audio +import nl.astraeus.midi.message.TimedMidiMessage import nl.astraeus.vst.chip.PatchDTO import nl.astraeus.vst.chip.view.MainView import nl.astraeus.vst.chip.view.WaveformView @@ -134,7 +135,20 @@ object VstChipWorklet : AudioNode( super.postMessage(msg) } + override fun postMessage(msg: Any) { + if (msg is ByteArray) { + val tmm = TimedMidiMessage(msg) + val byte1 = tmm.midi[0] + + if (byte1.toInt() and 0xf0 == 0xb0) { + handleIncomingMidi(tmm.midi[1], tmm.midi[2]) + } + } + super.postMessage(msg) + } + override fun postMessage(vararg msg: Int) { + println("postMessage ${msg.size} bytes") if ( msg.size == 3 && (msg[0] and 0xf == midiChannel) @@ -144,12 +158,13 @@ object VstChipWorklet : AudioNode( val value = msg[2] handleIncomingMidi(knob.toByte(), value.toByte()) - } else { - super.postMessage(msg) } + + super.postMessage(msg) } private fun handleIncomingMidi(knob: Byte, value: Byte) { + println("Incoming knob: $knob, value: $value") when (knob) { 0x46.toByte() -> { volume = value / 127.0 diff --git a/src/jsMain/kotlin/nl/astraeus/vst/chip/view/MainView.kt b/src/jsMain/kotlin/nl/astraeus/vst/chip/view/MainView.kt index 126d9f0..0b24f79 100644 --- a/src/jsMain/kotlin/nl/astraeus/vst/chip/view/MainView.kt +++ b/src/jsMain/kotlin/nl/astraeus/vst/chip/view/MainView.kt @@ -35,6 +35,8 @@ import nl.astraeus.css.style.cls import nl.astraeus.komp.HtmlBuilder import nl.astraeus.komp.Komponent import nl.astraeus.komp.currentElement +import nl.astraeus.midi.message.TimedMidiMessage +import nl.astraeus.midi.message.getCurrentTime import nl.astraeus.vst.chip.audio.VstChipWorklet import nl.astraeus.vst.chip.audio.VstChipWorklet.midiChannel import nl.astraeus.vst.chip.midi.Midi @@ -46,7 +48,6 @@ import nl.astraeus.vst.ui.css.Css.defineCss import nl.astraeus.vst.ui.css.Css.noTextSelect import nl.astraeus.vst.ui.css.CssName import nl.astraeus.vst.ui.css.hover -import nl.astraeus.vst.ui.util.uInt8ArrayOf import org.khronos.webgl.get import org.w3c.dom.CanvasRenderingContext2D import org.w3c.dom.HTMLCanvasElement @@ -114,6 +115,11 @@ object MainView : Komponent(), CssName { requestUpdate() } + override fun renderUpdate() { + println("Rendering MainView") + super.renderUpdate() + } + override fun HtmlBuilder.render() { div(MainDivCss.name) { if (!started) { @@ -190,7 +196,8 @@ object MainView : Komponent(), CssName { +"STOP" onClickFunction = { VstChipWorklet.postDirectlyToWorklet( - uInt8ArrayOf(0xb0 + midiChannel, 123, 0) + TimedMidiMessage(getCurrentTime(), (0xb0 + midiChannel).toByte(), 123, 0) + .data.buffer.data ) } }