Refactor MIDI handling and update dependencies.
Streamlined MIDI message handling by introducing `MidiMessageHandler` and removed redundant code. Added better handler support for specific message types and parameters. Also upgraded Kotlin to version 2.1.0 and adjusted build configurations.
This commit is contained in:
@@ -35,7 +35,6 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jvm()
|
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
val commonMain by getting {
|
val commonMain by getting {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import nl.astraeus.tba.SlicedByteArray
|
|||||||
import nl.astraeus.vst.ADSR
|
import nl.astraeus.vst.ADSR
|
||||||
import nl.astraeus.vst.AudioWorkletProcessor
|
import nl.astraeus.vst.AudioWorkletProcessor
|
||||||
import nl.astraeus.vst.currentTime
|
import nl.astraeus.vst.currentTime
|
||||||
|
import nl.astraeus.vst.midi.MidiMessageHandler
|
||||||
import nl.astraeus.vst.registerProcessor
|
import nl.astraeus.vst.registerProcessor
|
||||||
import nl.astraeus.vst.sampleRate
|
import nl.astraeus.vst.sampleRate
|
||||||
import org.khronos.webgl.Float32Array
|
import org.khronos.webgl.Float32Array
|
||||||
@@ -65,8 +66,8 @@ enum class RecordingState {
|
|||||||
@ExperimentalJsExport
|
@ExperimentalJsExport
|
||||||
@JsExport
|
@JsExport
|
||||||
class VstChipProcessor : AudioWorkletProcessor() {
|
class VstChipProcessor : AudioWorkletProcessor() {
|
||||||
var midiChannel = 0
|
|
||||||
val midiMessageBuffer = SortedTimedMidiMessageList()
|
val midiMessageBuffer = SortedTimedMidiMessageList()
|
||||||
|
val midiMessageHandler = MidiMessageHandler()
|
||||||
val notes = Array<PlayingNote?>(POLYPHONICS) { null }
|
val notes = Array<PlayingNote?>(POLYPHONICS) { null }
|
||||||
|
|
||||||
var waveform = Waveform.SINE.ordinal
|
var waveform = Waveform.SINE.ordinal
|
||||||
@@ -100,6 +101,82 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
|||||||
init {
|
init {
|
||||||
this.port.onmessage = ::handleMessage
|
this.port.onmessage = ::handleMessage
|
||||||
Note.updateSampleRate(sampleRate)
|
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) {
|
private fun handleMessage(message: MessageEvent) {
|
||||||
@@ -122,8 +199,9 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
|||||||
data.startsWith("set_channel") -> {
|
data.startsWith("set_channel") -> {
|
||||||
val parts = data.split('\n')
|
val parts = data.split('\n')
|
||||||
if (parts.size == 2) {
|
if (parts.size == 2) {
|
||||||
midiChannel = parts[1].toInt()
|
midiMessageHandler.channel = parts[1].toByte()
|
||||||
println("Setting channel: $midiChannel")
|
|
||||||
|
println("Setting channel: ${midiMessageHandler.channel}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,8 +244,9 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun playBuffer() {
|
private fun playBuffer() {
|
||||||
while (midiMessageBuffer.isNotEmpty() && (midiMessageBuffer.nextTimestamp()
|
while (
|
||||||
?: 0.0) < currentTime
|
midiMessageBuffer.isNotEmpty() &&
|
||||||
|
(midiMessageBuffer.nextTimestamp() ?: 0.0) < currentTime
|
||||||
) {
|
) {
|
||||||
val midi = midiMessageBuffer.read()
|
val midi = midiMessageBuffer.read()
|
||||||
console.log("Message", currentTime, midi)
|
console.log("Message", currentTime, midi)
|
||||||
@@ -178,163 +257,16 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
|||||||
private fun playMidi(bytes: SlicedByteArray) {
|
private fun playMidi(bytes: SlicedByteArray) {
|
||||||
var index = 0
|
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) {
|
while (index < bytes.size && bytes[index].toUByte() > 0u) {
|
||||||
console.log("playMidi", bytes, index, bytes[index + 0], bytes[index + 1], bytes[index + 2])
|
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 {
|
private fun playMidiFromBuffer(bytes: SlicedByteArray) {
|
||||||
if (bytes[index] == 0.toByte()) {
|
midiMessageHandler.handle(bytes[0], bytes[1], bytes[2])
|
||||||
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 noteOn(note: Int, velocity: Int) {
|
private fun noteOn(note: Int, velocity: Int) {
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ import kotlin.math.round
|
|||||||
* Time: 11:50
|
* Time: 11:50
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ExperimentalJsExport
|
|
||||||
@JsExport
|
|
||||||
enum class Note(
|
enum class Note(
|
||||||
val sharp: String,
|
val sharp: String,
|
||||||
val flat: String
|
val flat: String
|
||||||
|
|||||||
@@ -3,13 +3,12 @@ version = "0.1.0"
|
|||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
mavenLocal()
|
|
||||||
mavenCentral()
|
|
||||||
maven {
|
maven {
|
||||||
url = uri("https://gitea.astraeus.nl/api/packages/rnentjes/maven")
|
url = uri("https://gitea.astraeus.nl/api/packages/rnentjes/maven")
|
||||||
}
|
}
|
||||||
maven {
|
maven {
|
||||||
url = uri("https://gitea.astraeus.nl:8443/api/packages/rnentjes/maven")
|
url = uri("https://gitea.astraeus.nl:8443/api/packages/rnentjes/maven")
|
||||||
}
|
}
|
||||||
|
mavenCentral()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
pluginManagement {
|
pluginManagement {
|
||||||
plugins {
|
plugins {
|
||||||
kotlin("multiplatform") version "2.0.21"
|
kotlin("multiplatform") version "2.1.0"
|
||||||
}
|
}
|
||||||
repositories {
|
repositories {
|
||||||
gradlePluginPortal()
|
gradlePluginPortal()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
package nl.astraeus.vst.chip.audio
|
package nl.astraeus.vst.chip.audio
|
||||||
|
|
||||||
|
import nl.astraeus.midi.message.TimedMidiMessage
|
||||||
import nl.astraeus.vst.chip.PatchDTO
|
import nl.astraeus.vst.chip.PatchDTO
|
||||||
import nl.astraeus.vst.chip.view.MainView
|
import nl.astraeus.vst.chip.view.MainView
|
||||||
import nl.astraeus.vst.chip.view.WaveformView
|
import nl.astraeus.vst.chip.view.WaveformView
|
||||||
@@ -134,7 +135,20 @@ object VstChipWorklet : AudioNode(
|
|||||||
super.postMessage(msg)
|
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) {
|
override fun postMessage(vararg msg: Int) {
|
||||||
|
println("postMessage ${msg.size} bytes")
|
||||||
if (
|
if (
|
||||||
msg.size == 3
|
msg.size == 3
|
||||||
&& (msg[0] and 0xf == midiChannel)
|
&& (msg[0] and 0xf == midiChannel)
|
||||||
@@ -144,12 +158,13 @@ object VstChipWorklet : AudioNode(
|
|||||||
val value = msg[2]
|
val value = msg[2]
|
||||||
|
|
||||||
handleIncomingMidi(knob.toByte(), value.toByte())
|
handleIncomingMidi(knob.toByte(), value.toByte())
|
||||||
} else {
|
|
||||||
super.postMessage(msg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
super.postMessage(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleIncomingMidi(knob: Byte, value: Byte) {
|
private fun handleIncomingMidi(knob: Byte, value: Byte) {
|
||||||
|
println("Incoming knob: $knob, value: $value")
|
||||||
when (knob) {
|
when (knob) {
|
||||||
0x46.toByte() -> {
|
0x46.toByte() -> {
|
||||||
volume = value / 127.0
|
volume = value / 127.0
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ import nl.astraeus.css.style.cls
|
|||||||
import nl.astraeus.komp.HtmlBuilder
|
import nl.astraeus.komp.HtmlBuilder
|
||||||
import nl.astraeus.komp.Komponent
|
import nl.astraeus.komp.Komponent
|
||||||
import nl.astraeus.komp.currentElement
|
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
|
||||||
import nl.astraeus.vst.chip.audio.VstChipWorklet.midiChannel
|
import nl.astraeus.vst.chip.audio.VstChipWorklet.midiChannel
|
||||||
import nl.astraeus.vst.chip.midi.Midi
|
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.Css.noTextSelect
|
||||||
import nl.astraeus.vst.ui.css.CssName
|
import nl.astraeus.vst.ui.css.CssName
|
||||||
import nl.astraeus.vst.ui.css.hover
|
import nl.astraeus.vst.ui.css.hover
|
||||||
import nl.astraeus.vst.ui.util.uInt8ArrayOf
|
|
||||||
import org.khronos.webgl.get
|
import org.khronos.webgl.get
|
||||||
import org.w3c.dom.CanvasRenderingContext2D
|
import org.w3c.dom.CanvasRenderingContext2D
|
||||||
import org.w3c.dom.HTMLCanvasElement
|
import org.w3c.dom.HTMLCanvasElement
|
||||||
@@ -114,6 +115,11 @@ object MainView : Komponent(), CssName {
|
|||||||
requestUpdate()
|
requestUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun renderUpdate() {
|
||||||
|
println("Rendering MainView")
|
||||||
|
super.renderUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
override fun HtmlBuilder.render() {
|
override fun HtmlBuilder.render() {
|
||||||
div(MainDivCss.name) {
|
div(MainDivCss.name) {
|
||||||
if (!started) {
|
if (!started) {
|
||||||
@@ -190,7 +196,8 @@ object MainView : Komponent(), CssName {
|
|||||||
+"STOP"
|
+"STOP"
|
||||||
onClickFunction = {
|
onClickFunction = {
|
||||||
VstChipWorklet.postDirectlyToWorklet(
|
VstChipWorklet.postDirectlyToWorklet(
|
||||||
uInt8ArrayOf(0xb0 + midiChannel, 123, 0)
|
TimedMidiMessage(getCurrentTime(), (0xb0 + midiChannel).toByte(), 123, 0)
|
||||||
|
.data.buffer.data
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user