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.
554 lines
15 KiB
Kotlin
554 lines
15 KiB
Kotlin
@file:OptIn(ExperimentalJsExport::class)
|
|
|
|
package nl.astraeus.vst.chip.view
|
|
|
|
import kotlinx.browser.window
|
|
import kotlinx.html.InputType
|
|
import kotlinx.html.canvas
|
|
import kotlinx.html.classes
|
|
import kotlinx.html.div
|
|
import kotlinx.html.h1
|
|
import kotlinx.html.input
|
|
import kotlinx.html.js.onChangeFunction
|
|
import kotlinx.html.js.onClickFunction
|
|
import kotlinx.html.js.onInputFunction
|
|
import kotlinx.html.option
|
|
import kotlinx.html.select
|
|
import kotlinx.html.span
|
|
import nl.astraeus.css.properties.AlignItems
|
|
import nl.astraeus.css.properties.BoxSizing
|
|
import nl.astraeus.css.properties.Display
|
|
import nl.astraeus.css.properties.FlexDirection
|
|
import nl.astraeus.css.properties.FontWeight
|
|
import nl.astraeus.css.properties.JustifyContent
|
|
import nl.astraeus.css.properties.Position
|
|
import nl.astraeus.css.properties.Transform
|
|
import nl.astraeus.css.properties.em
|
|
import nl.astraeus.css.properties.hsla
|
|
import nl.astraeus.css.properties.prc
|
|
import nl.astraeus.css.properties.px
|
|
import nl.astraeus.css.properties.rem
|
|
import nl.astraeus.css.properties.vh
|
|
import nl.astraeus.css.properties.vw
|
|
import nl.astraeus.css.style.Style
|
|
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
|
|
import nl.astraeus.vst.chip.ws.WebsocketClient
|
|
import nl.astraeus.vst.ui.components.ExpKnobComponent
|
|
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.get
|
|
import org.w3c.dom.CanvasRenderingContext2D
|
|
import org.w3c.dom.HTMLCanvasElement
|
|
import org.w3c.dom.HTMLInputElement
|
|
import org.w3c.dom.HTMLSelectElement
|
|
|
|
object WaveformView: Komponent() {
|
|
|
|
init {
|
|
window.requestAnimationFrame(::onAnimationFrame)
|
|
}
|
|
|
|
fun onAnimationFrame(time: Double) {
|
|
if (MainView.started) {
|
|
VstChipWorklet.postMessage("start_recording")
|
|
}
|
|
|
|
window.requestAnimationFrame(::onAnimationFrame)
|
|
}
|
|
|
|
override fun HtmlBuilder.render() {
|
|
div {
|
|
if (VstChipWorklet.recording != null) {
|
|
canvas {
|
|
width = "1000"
|
|
height = "400"
|
|
val ctx = (currentElement() as? HTMLCanvasElement)?.getContext("2d") as? CanvasRenderingContext2D
|
|
val data = VstChipWorklet.recording
|
|
if (ctx != null && data != null) {
|
|
val width = ctx.canvas.width.toDouble()
|
|
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(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)
|
|
}
|
|
ctx.stroke()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
object MainView : Komponent(), CssName {
|
|
private var messages: MutableList<String> = ArrayList()
|
|
var started = false
|
|
|
|
init {
|
|
css()
|
|
}
|
|
|
|
fun addMessage(message: String) {
|
|
messages.add(message)
|
|
while (messages.size > 10) {
|
|
messages.removeAt(0)
|
|
}
|
|
requestUpdate()
|
|
}
|
|
|
|
override fun renderUpdate() {
|
|
println("Rendering MainView")
|
|
super.renderUpdate()
|
|
}
|
|
|
|
override fun HtmlBuilder.render() {
|
|
div(MainDivCss.name) {
|
|
if (!started) {
|
|
div(StartSplashCss.name) {
|
|
div(StartBoxCss.name) {
|
|
div(StartButtonCss.name) {
|
|
+"START"
|
|
}
|
|
}
|
|
onClickFunction = {
|
|
VstChipWorklet.create {
|
|
started = true
|
|
requestUpdate()
|
|
WebsocketClient.send("LOAD\n")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
h1 {
|
|
+"VST Chip"
|
|
}
|
|
div {
|
|
span {
|
|
+"Midi input: "
|
|
select {
|
|
option {
|
|
+"None"
|
|
value = "none"
|
|
}
|
|
for (mi in Midi.inputs) {
|
|
option {
|
|
+mi.name
|
|
value = mi.id
|
|
selected = mi.id == Midi.currentInput?.id
|
|
}
|
|
}
|
|
|
|
onChangeFunction = { event ->
|
|
val target = event.target as HTMLSelectElement
|
|
if (target.value == "none") {
|
|
Midi.setInput(null)
|
|
} else {
|
|
Midi.setInput(target.value)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
span {
|
|
+"channel:"
|
|
input {
|
|
type = InputType.number
|
|
value = VstChipWorklet.midiChannel.toString()
|
|
onInputFunction = { event ->
|
|
val target = event.target as HTMLInputElement
|
|
println("onInput channel: $target")
|
|
VstChipWorklet.midiChannel = target.value.toInt()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
div {
|
|
span(ButtonBarCss.name) {
|
|
+"SAVE"
|
|
onClickFunction = {
|
|
val patch = VstChipWorklet.save().copy(
|
|
midiId = Midi.currentInput?.id ?: "",
|
|
midiName = Midi.currentInput?.name ?: ""
|
|
)
|
|
|
|
WebsocketClient.send("SAVE\n${JSON.stringify(patch)}")
|
|
}
|
|
}
|
|
span(ButtonBarCss.name) {
|
|
+"STOP"
|
|
onClickFunction = {
|
|
VstChipWorklet.postDirectlyToWorklet(
|
|
TimedMidiMessage(getCurrentTime(), (0xb0 + midiChannel).toByte(), 123, 0)
|
|
.data.buffer.data
|
|
)
|
|
}
|
|
}
|
|
}
|
|
div {
|
|
span(ButtonBarCss.name) {
|
|
+"Sine"
|
|
if (VstChipWorklet.waveform == 0) {
|
|
classes += SelectedCss.name
|
|
}
|
|
onClickFunction = {
|
|
VstChipWorklet.waveform = 0
|
|
requestUpdate()
|
|
}
|
|
}
|
|
span(ButtonBarCss.name) {
|
|
+"Square"
|
|
if (VstChipWorklet.waveform == 1) {
|
|
classes += SelectedCss.name
|
|
}
|
|
onClickFunction = {
|
|
VstChipWorklet.waveform = 1
|
|
requestUpdate()
|
|
}
|
|
}
|
|
span(ButtonBarCss.name) {
|
|
+"Triangle"
|
|
if (VstChipWorklet.waveform == 2) {
|
|
classes += SelectedCss.name
|
|
}
|
|
onClickFunction = {
|
|
VstChipWorklet.waveform = 2
|
|
requestUpdate()
|
|
}
|
|
}
|
|
span(ButtonBarCss.name) {
|
|
+"Sawtooth"
|
|
if (VstChipWorklet.waveform == 3) {
|
|
classes += SelectedCss.name
|
|
}
|
|
onClickFunction = {
|
|
VstChipWorklet.waveform = 3
|
|
requestUpdate()
|
|
}
|
|
}
|
|
}
|
|
div(ControlsCss.name) {
|
|
include(
|
|
ExpKnobComponent(
|
|
value = VstChipWorklet.volume,
|
|
label = "Volume",
|
|
minValue = 0.005,
|
|
maxValue = 1.0,
|
|
step = 5.0 / 127.0,
|
|
width = 100,
|
|
height = 120,
|
|
) { value ->
|
|
VstChipWorklet.volume = value
|
|
}
|
|
)
|
|
include(
|
|
KnobComponent(
|
|
value = VstChipWorklet.dutyCycle,
|
|
label = "Duty cycle",
|
|
minValue = 0.0,
|
|
maxValue = 1.0,
|
|
step = 2.0 / 127.0,
|
|
width = 100,
|
|
height = 120,
|
|
) { value ->
|
|
VstChipWorklet.dutyCycle = value
|
|
}
|
|
)
|
|
include(
|
|
ExpKnobComponent(
|
|
value = VstChipWorklet.fmModFreq,
|
|
label = "FM Freq",
|
|
minValue = 0.005,
|
|
maxValue = 1.0,
|
|
step = 5.0 / 127.0,
|
|
width = 100,
|
|
height = 120,
|
|
) { value ->
|
|
VstChipWorklet.fmModFreq = value
|
|
}
|
|
)
|
|
include(
|
|
ExpKnobComponent(
|
|
value = VstChipWorklet.fmModAmp,
|
|
label = "FM Ampl",
|
|
minValue = 0.005,
|
|
maxValue = 1.0,
|
|
step = 5.0 / 127.0,
|
|
width = 100,
|
|
height = 120,
|
|
) { value ->
|
|
VstChipWorklet.fmModAmp = value
|
|
}
|
|
)
|
|
include(
|
|
ExpKnobComponent(
|
|
value = VstChipWorklet.amModFreq,
|
|
label = "AM Freq",
|
|
minValue = 0.005,
|
|
maxValue = 1.0,
|
|
step = 5.0 / 127.0,
|
|
width = 100,
|
|
height = 120,
|
|
) { value ->
|
|
VstChipWorklet.amModFreq = value
|
|
}
|
|
)
|
|
include(
|
|
ExpKnobComponent(
|
|
value = VstChipWorklet.amModAmp,
|
|
label = "AM Ampl",
|
|
minValue = 0.005,
|
|
maxValue = 1.0,
|
|
step = 5.0 / 127.0,
|
|
width = 100,
|
|
height = 120,
|
|
) { value ->
|
|
VstChipWorklet.amModAmp = value
|
|
}
|
|
)
|
|
include(
|
|
ExpKnobComponent(
|
|
value = VstChipWorklet.feedback,
|
|
label = "Feedback",
|
|
minValue = 0.005,
|
|
maxValue = 1.0,
|
|
step = 5.0 / 127.0,
|
|
width = 100,
|
|
height = 120,
|
|
) { value ->
|
|
VstChipWorklet.feedback = value
|
|
}
|
|
)
|
|
include(
|
|
ExpKnobComponent(
|
|
value = VstChipWorklet.delay,
|
|
label = "Delay",
|
|
minValue = 0.005,
|
|
maxValue = 1.0,
|
|
step = 5.0 / 127.0,
|
|
width = 100,
|
|
height = 120,
|
|
) { value ->
|
|
VstChipWorklet.delay = value
|
|
}
|
|
)
|
|
include(
|
|
ExpKnobComponent(
|
|
value = VstChipWorklet.delayDepth,
|
|
label = "Delay depth",
|
|
minValue = 0.005,
|
|
maxValue = 1.0,
|
|
step = 5.0 / 127.0,
|
|
width = 100,
|
|
height = 120,
|
|
) { value ->
|
|
VstChipWorklet.delayDepth = value
|
|
}
|
|
)
|
|
}
|
|
div(ControlsCss.name) {
|
|
include(
|
|
ExpKnobComponent(
|
|
value = VstChipWorklet.attack,
|
|
label = "Attack",
|
|
minValue = 0.005,
|
|
maxValue = 1.0,
|
|
step = 5.0 / 127.0,
|
|
width = 100,
|
|
height = 120,
|
|
) { value ->
|
|
VstChipWorklet.attack = value
|
|
}
|
|
)
|
|
include(
|
|
ExpKnobComponent(
|
|
value = VstChipWorklet.decay,
|
|
label = "Decay",
|
|
minValue = 0.005,
|
|
maxValue = 1.0,
|
|
step = 5.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(
|
|
ExpKnobComponent(
|
|
value = VstChipWorklet.release,
|
|
label = "Release",
|
|
minValue = 0.005,
|
|
maxValue = 1.0,
|
|
step = 5.0 / 127.0,
|
|
width = 100,
|
|
height = 120,
|
|
) { value ->
|
|
VstChipWorklet.release = value
|
|
}
|
|
)
|
|
}
|
|
include(WaveformView)
|
|
}
|
|
}
|
|
|
|
object MainDivCss : CssName
|
|
object ActiveCss : CssName
|
|
object ButtonCss : CssName
|
|
object ButtonBarCss : CssName
|
|
object SelectedCss : CssName
|
|
object NoteBarCss : CssName
|
|
object StartSplashCss : CssName
|
|
object StartBoxCss : CssName
|
|
object StartButtonCss : CssName
|
|
object ControlsCss : CssName
|
|
|
|
private fun css() {
|
|
defineCss {
|
|
select("*") {
|
|
select("*:before") {
|
|
select("*:after") {
|
|
boxSizing(BoxSizing.borderBox)
|
|
}
|
|
}
|
|
}
|
|
select("html", "body") {
|
|
margin(0.px)
|
|
padding(0.px)
|
|
height(100.prc)
|
|
}
|
|
select("html", "body") {
|
|
backgroundColor(Css.currentStyle.mainBackgroundColor)
|
|
color(Css.currentStyle.mainFontColor)
|
|
|
|
fontFamily("JetbrainsMono, monospace")
|
|
fontSize(14.px)
|
|
fontWeight(FontWeight.bold)
|
|
|
|
//transition()
|
|
noTextSelect()
|
|
}
|
|
select("input", "textarea") {
|
|
backgroundColor(Css.currentStyle.inputBackgroundColor)
|
|
color(Css.currentStyle.mainFontColor)
|
|
border("none")
|
|
}
|
|
select(cls(ButtonCss)) {
|
|
margin(1.rem)
|
|
commonButton()
|
|
}
|
|
select(cls(ButtonBarCss)) {
|
|
margin(1.rem, 0.px)
|
|
commonButton()
|
|
}
|
|
select(cls(ActiveCss)) {
|
|
//backgroundColor(Css.currentStyle.selectedBackgroundColor)
|
|
}
|
|
select(cls(NoteBarCss)) {
|
|
minHeight(4.rem)
|
|
}
|
|
select(cls(MainDivCss)) {
|
|
margin(1.rem)
|
|
}
|
|
select("select") {
|
|
plain("appearance", "none")
|
|
border("0")
|
|
outline("0")
|
|
width(20.rem)
|
|
padding(0.5.rem, 2.rem, 0.5.rem, 0.5.rem)
|
|
backgroundImage("url('https://upload.wikimedia.org/wikipedia/commons/9/9d/Caret_down_font_awesome_whitevariation.svg')")
|
|
background("right 0.8em center/1.4em")
|
|
backgroundColor(Css.currentStyle.inputBackgroundColor)
|
|
color(Css.currentStyle.mainFontColor)
|
|
borderRadius(0.25.em)
|
|
}
|
|
select(cls(StartSplashCss)) {
|
|
position(Position.fixed)
|
|
left(0.px)
|
|
top(0.px)
|
|
width(100.vw)
|
|
height(100.vh)
|
|
zIndex(100)
|
|
backgroundColor(hsla(32, 0, 5, 0.65))
|
|
|
|
select(cls(StartBoxCss)) {
|
|
position(Position.relative)
|
|
left(25.vw)
|
|
top(25.vh)
|
|
width(50.vw)
|
|
height(50.vh)
|
|
backgroundColor(hsla(239, 50, 10, 1.0))
|
|
borderColor(Css.currentStyle.mainFontColor)
|
|
borderWidth(2.px)
|
|
|
|
select(cls(StartButtonCss)) {
|
|
position(Position.absolute)
|
|
left(50.prc)
|
|
top(50.prc)
|
|
transform(Transform("translate(-50%, -50%)"))
|
|
padding(1.rem)
|
|
backgroundColor(Css.currentStyle.buttonBackgroundColor)
|
|
cursor("pointer")
|
|
}
|
|
}
|
|
}
|
|
select(ControlsCss.cls()) {
|
|
display(Display.flex)
|
|
flexDirection(FlexDirection.row)
|
|
justifyContent(JustifyContent.flexStart)
|
|
alignItems(AlignItems.center)
|
|
margin(1.rem)
|
|
padding(1.rem)
|
|
backgroundColor(Css.currentStyle.mainBackgroundColor)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun Style.commonButton() {
|
|
display(Display.inlineBlock)
|
|
padding(1.rem)
|
|
backgroundColor(Css.currentStyle.buttonBackgroundColor)
|
|
borderColor(Css.currentStyle.buttonBorderColor)
|
|
borderWidth(Css.currentStyle.buttonBorderWidth)
|
|
color(Css.currentStyle.mainFontColor)
|
|
|
|
hover {
|
|
backgroundColor(Css.currentStyle.buttonBackgroundColor.hover())
|
|
}
|
|
and(SelectedCss.cls()) {
|
|
backgroundColor(Css.currentStyle.buttonBackgroundColor.hover().hover().hover())
|
|
}
|
|
}
|
|
|
|
|
|
}
|