Save patch

This commit is contained in:
2024-06-30 20:32:43 +02:00
parent 194857d687
commit 976328ed69
24 changed files with 1155 additions and 162 deletions

View File

@@ -1,15 +1,13 @@
package nl.astraeus.vst.chip
import kotlinx.browser.document
import kotlinx.browser.window
import nl.astraeus.komp.Komponent
import nl.astraeus.komp.UnsafeMode
import nl.astraeus.vst.chip.midi.Broadcaster
import nl.astraeus.vst.chip.logger.log
import nl.astraeus.vst.chip.midi.Midi
import nl.astraeus.vst.chip.midi.MidiMessage
import nl.astraeus.vst.chip.view.MainView
import nl.astraeus.vst.chip.ws.WebsocketClient
import nl.astraeus.vst.ui.css.CssSettings
import org.khronos.webgl.Uint8Array
fun main() {
CssSettings.shortId = false
@@ -20,16 +18,7 @@ fun main() {
Midi.start()
console.log("Performance", window.performance)
Broadcaster.getChannel(0).postMessage(
MidiMessage(
Uint8Array(arrayOf(0x80.toByte(), 60, 60)),
window.performance.now()
)
)
window.setInterval({
Broadcaster.sync()
}, 1000)
WebsocketClient.connect {
log.debug { "Connected to server" }
}
}

View File

@@ -1,5 +1,6 @@
package nl.astraeus.vst.chip.audio
import nl.astraeus.vst.chip.PatchDTO
import nl.astraeus.vst.chip.view.MainView
import nl.astraeus.vst.chip.view.WaveformView
import nl.astraeus.vst.ui.util.uInt8ArrayOf
@@ -10,7 +11,7 @@ import org.w3c.dom.MessageEvent
import kotlin.experimental.and
object VstChipWorklet : AudioNode(
"vst-chip-worklet.js",
"/vst-chip-worklet.js",
"vst-chip-processor"
) {
var waveform: Int = 0
@@ -30,44 +31,74 @@ object VstChipWorklet : AudioNode(
set(value) {
field = value
super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x46, (value * 127).toInt())
uInt8ArrayOf(0xb0 + midiChannel, 7, (value * 127).toInt())
)
}
var dutyCycle = 0.5
set(value) {
field = value
super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x4a, (value * 127).toInt())
uInt8ArrayOf(0xb0 + midiChannel, 0x47, (value * 127).toInt())
)
}
var fmModFreq = 0.0
set(value) {
field = value
super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x4b, (value * 127).toInt())
uInt8ArrayOf(0xb0 + midiChannel, 0x4a, (value * 127).toInt())
)
}
var fmModAmp = 0.0
set(value) {
field = value
super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x4c, (value * 127).toInt())
uInt8ArrayOf(0xb0 + midiChannel, 0x4b, (value * 127).toInt())
)
}
var amModFreq = 0.0
set(value) {
field = value
super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x47, (value * 127).toInt())
uInt8ArrayOf(0xb0 + midiChannel, 0x4c, (value * 127).toInt())
)
}
var amModAmp = 0.0
set(value) {
field = value
super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x4d, (value * 127).toInt())
)
}
var attack = 0.1
set(value) {
field = value
super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x49, (value * 127).toInt())
)
}
var decay = 0.2
set(value) {
field = value
super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x4b, (value * 127).toInt())
)
}
var sustain = 0.5
set(value) {
field = value
super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x46, (value * 127).toInt())
)
}
var release = 0.2
set(value) {
field = value
super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x48, (value * 127).toInt())
)
}
var recording: Float32Array? = null
override fun onMessage(message: MessageEvent) {
@@ -80,6 +111,10 @@ object VstChipWorklet : AudioNode(
}
}
fun postDirectlyToWorklet(msg: Any) {
super.postMessage(msg)
}
override fun postMessage(msg: Any) {
if (msg is Uint8Array) {
if (
@@ -133,4 +168,36 @@ object VstChipWorklet : AudioNode(
}
}
fun load(patch: PatchDTO) {
waveform = patch.waveform
midiChannel = patch.midiChannel
volume = patch.volume
dutyCycle = patch.dutyCycle
fmModFreq = patch.fmModFreq
fmModAmp = patch.fmModAmp
amModFreq = patch.amModFreq
amModAmp = patch.amModAmp
attack = patch.attack
decay = patch.decay
sustain = patch.sustain
release = patch.release
}
fun save(): PatchDTO {
return PatchDTO(
waveform = waveform,
midiChannel = midiChannel,
volume = volume,
dutyCycle = dutyCycle,
fmModFreq = fmModFreq,
fmModAmp = fmModAmp,
amModFreq = amModFreq,
amModAmp = amModAmp,
attack = attack,
decay = decay,
sustain = sustain,
release = release
)
}
}

View File

@@ -74,6 +74,13 @@ object Midi {
)
}
fun setInput(id: String) {
val selected = inputs.find { it.id == id }
if (selected != null) {
setInput(selected)
}
}
fun setInput(input: MIDIInput?) {
console.log("Setting input", input)
currentInput?.close()

View File

@@ -34,14 +34,16 @@ import nl.astraeus.komp.HtmlBuilder
import nl.astraeus.komp.Komponent
import nl.astraeus.komp.currentElement
import nl.astraeus.vst.chip.audio.VstChipWorklet
import nl.astraeus.vst.chip.audio.VstChipWorklet.midiChannel
import nl.astraeus.vst.chip.midi.Midi
import nl.astraeus.vst.chip.ws.WebsocketClient
import nl.astraeus.vst.ui.components.KnobComponent
import nl.astraeus.vst.ui.css.Css
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 org.khronos.webgl.Uint8Array
import nl.astraeus.vst.ui.util.uInt8ArrayOf
import org.khronos.webgl.get
import org.w3c.dom.CanvasRenderingContext2D
import org.w3c.dom.HTMLCanvasElement
@@ -75,10 +77,11 @@ object WaveformView: Komponent() {
val height = ctx.canvas.height.toDouble()
val halfHeight = height / 2.0
ctx.lineWidth = 2.0
ctx.clearRect(0.0, 0.0, width, height)
val step = 1000.0 / data.length
ctx.beginPath()
ctx.strokeStyle = "rgba(255, 255, 255, 0.5)"
ctx.strokeStyle = "rgba(0, 255, 255, 0.5)"
ctx.moveTo(0.0, halfHeight)
for (i in 0 until data.length) {
ctx.lineTo(i * step, halfHeight - data[i] * halfHeight)
@@ -119,6 +122,7 @@ object MainView : Komponent(), CssName {
VstChipWorklet.create {
started = true
requestUpdate()
WebsocketClient.send("LOAD\n")
}
}
}
@@ -140,6 +144,7 @@ object MainView : Komponent(), CssName {
option {
+mi.name
value = mi.id
selected = mi.id == Midi.currentInput?.id
}
}
@@ -148,12 +153,7 @@ object MainView : Komponent(), CssName {
if (target.value == "none") {
Midi.setInput(null)
} else {
val selected = Midi.inputs.find { it.id == target.value }
if (selected != null) {
Midi.setInput(selected)
} else if (target.value == "midi-broadcast") {
//
}
Midi.setInput(target.value)
}
}
}
@@ -172,71 +172,20 @@ object MainView : Komponent(), CssName {
}
}
div {
span {
+"Midi output: "
select {
option {
+"None"
value = "none"
}
for (mi in Midi.outputs) {
option {
+mi.name
value = mi.id
}
}
span(ButtonBarCss.name) {
+"SAVE"
onClickFunction = {
val patch = VstChipWorklet.save().copy(midiId = Midi.currentInput?.id ?: "")
onChangeFunction = { event ->
val target = event.target as HTMLSelectElement
if (target.value == "none") {
Midi.setOutput(null)
} else {
val selected = Midi.outputs.find { it.id == target.value }
if (selected != null) {
Midi.setOutput(selected)
}
}
}
WebsocketClient.send("SAVE\n${JSON.stringify(patch)}")
}
}
span {
+"channel:"
input {
type = InputType.number
value = Midi.outputChannel.toString()
onInputFunction = { event ->
val target = event.target as HTMLInputElement
Midi.outputChannel = target.value.toInt()
}
}
}
}
div {
span(ButtonCss.name) {
+"Send note on to output"
span(ButtonBarCss.name) {
+"STOP"
onClickFunction = {
val data = Uint8Array(
arrayOf(
0x90.toByte(),
0x3c.toByte(),
0x70.toByte()
)
VstChipWorklet.postDirectlyToWorklet(
uInt8ArrayOf(0xb0 + midiChannel, 123, 0)
)
Midi.send(data, window.performance.now() + 1000)
Midi.send(data, window.performance.now() + 2000)
}
}
span(ButtonCss.name) {
+"Send note off to output"
onClickFunction = {
val data = Uint8Array(
arrayOf(
0x90.toByte(),
0x3c.toByte(),
0x0.toByte(),
)
)
Midi.send(data)
}
}
}
@@ -362,6 +311,60 @@ object MainView : Komponent(), CssName {
}
)
}
div(ControlsCss.name) {
include(
KnobComponent(
value = VstChipWorklet.attack,
label = "Attack",
minValue = 0.0,
maxValue = 1.0,
step = 2.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.attack = value
}
)
include(
KnobComponent(
value = VstChipWorklet.decay,
label = "Decay",
minValue = 0.0,
maxValue = 1.0,
step = 2.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.decay = value
}
)
include(
KnobComponent(
value = VstChipWorklet.sustain,
label = "Sustain",
minValue = 0.0,
maxValue = 1.0,
step = 2.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.sustain = value
}
)
include(
KnobComponent(
value = VstChipWorklet.release,
label = "Release",
minValue = 0.0,
maxValue = 1.0,
step = 2.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.release = value
}
)
}
include(WaveformView)
}
}

View File

@@ -0,0 +1,120 @@
package nl.astraeus.vst.chip.ws
import kotlinx.browser.window
import nl.astraeus.vst.chip.PatchDTO
import nl.astraeus.vst.chip.audio.VstChipWorklet
import nl.astraeus.vst.chip.midi.Midi
import nl.astraeus.vst.chip.view.MainView
import org.w3c.dom.MessageEvent
import org.w3c.dom.WebSocket
import org.w3c.dom.events.Event
object WebsocketClient {
var websocket: WebSocket? = null
var interval: Int = 0
fun connect(onConnect: () -> Unit) {
close()
websocket = if (window.location.hostname.contains("localhost") || window.location.hostname.contains("192.168")) {
WebSocket("ws://${window.location.hostname}:9000/ws")
} else {
WebSocket("wss://${window.location.hostname}/ws")
}
websocket?.also { ws ->
ws.onopen = {
onOpen(ws, it)
onConnect()
}
ws.onmessage = { onMessage(ws, it) }
ws.onclose = { onClose(ws, it) }
ws.onerror = { onError(ws, it) }
}
}
fun close() {
websocket?.close(-1, "Application closed socket.")
}
fun onOpen(
ws: WebSocket,
event: Event
) {
interval = window.setInterval({
val actualWs = websocket
if (actualWs == null) {
window.clearInterval(interval)
console.log("Connection to the server was lost!\\nPlease try again later.")
reconnect()
}
}, 10000)
}
fun reconnect() {
val actualWs = websocket
if (actualWs != null) {
if (actualWs.readyState == WebSocket.OPEN) {
console.log("Connection to the server was lost!\\nPlease try again later.")
} else {
window.setTimeout({
reconnect()
}, 1000)
}
} else {
connect {}
window.setTimeout({
reconnect()
}, 1000)
}
}
fun onMessage(
ws: WebSocket,
event: Event
) {
if (event is MessageEvent) {
val data = event.data
if (data is String) {
console.log("Received message: $data")
if (data.startsWith("LOAD")) {
val patchJson = data.substring(5)
val patch = JSON.parse<PatchDTO>(patchJson)
Midi.setInput(patch.midiId)
VstChipWorklet.load(patch)
MainView.requestUpdate()
}
}
}
}
fun onClose(
ws: WebSocket,
event: Event
): dynamic {
websocket = null
return "dynamic"
}
fun onError(
ws: WebSocket,
event: Event
): dynamic {
console.log("Error websocket!", ws, event)
websocket = null
return "dynamic"
}
fun send(message: String) {
websocket?.send(message)
}
}