Save patch
This commit is contained in:
@@ -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" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
120
src/jsMain/kotlin/nl/astraeus/vst/chip/ws/WebsocketClient.kt
Normal file
120
src/jsMain/kotlin/nl/astraeus/vst/chip/ws/WebsocketClient.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user