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 {
|
||||
val commonMain by getting {
|
||||
|
||||
@@ -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<PlayingNote?>(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) {
|
||||
|
||||
@@ -12,8 +12,6 @@ import kotlin.math.round
|
||||
* Time: 11:50
|
||||
*/
|
||||
|
||||
@ExperimentalJsExport
|
||||
@JsExport
|
||||
enum class Note(
|
||||
val sharp: String,
|
||||
val flat: String
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pluginManagement {
|
||||
plugins {
|
||||
kotlin("multiplatform") version "2.0.21"
|
||||
kotlin("multiplatform") version "2.1.0"
|
||||
}
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user