Initial commit

This commit is contained in:
2024-06-16 20:40:05 +02:00
commit 68b7ffffa8
42 changed files with 1729 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
package nl.astraeus.vst.chip
external class AudioContext {
var sampleRate: Int
}
external class AudioWorkletNode(
audioContext: dynamic,
name: String,
options: dynamic
)
class AudioWorkletNodeParameters(
@JsName("numberOfInputs")
val numberOfInputs: Int,
@JsName("outputChannelCount")
val outputChannelCount: Array<Int>
)

View File

@@ -0,0 +1,12 @@
package nl.astraeus.vst.chip
import kotlinx.browser.document
import nl.astraeus.komp.Komponent
import nl.astraeus.vst.chip.channel.Broadcaster
import nl.astraeus.vst.chip.view.MainView
fun main() {
Komponent.create(document.body!!, MainView)
Broadcaster.start()
}

View File

@@ -0,0 +1,10 @@
package nl.astraeus.vst.chip.audio
import nl.astraeus.vst.chip.AudioContext
object AudioContextHandler {
val audioContext: dynamic = AudioContext()
}

View File

@@ -0,0 +1,88 @@
package nl.astraeus.vst.chip.audio
import nl.astraeus.vst.chip.AudioWorkletNode
import nl.astraeus.vst.chip.AudioWorkletNodeParameters
import nl.astraeus.vst.chip.audio.AudioContextHandler.audioContext
import org.w3c.dom.MessageEvent
import org.w3c.dom.MessagePort
enum class ModuleStatus {
INIT,
LOADING,
READY
}
class AudioModule(
val jsFile: String
) {
var status = ModuleStatus.INIT
var module: dynamic = null
fun load(action: () -> Unit = {}) {
if (module == null && status == ModuleStatus.INIT) {
status = ModuleStatus.LOADING
module = audioContext.audioWorklet.addModule(
jsFile
)
module.then {
status = ModuleStatus.READY
action()
}
} else if (status == ModuleStatus.READY) {
action()
} else {
console.log("Module not yet loaded")
}
}
}
abstract class AudioNode(
val jsFile: String,
val processorName: String,
val numberOfInputs: Int = 0,
val outputChannelCount: Array<Int> = arrayOf(2),
val destination: dynamic = null,
val outputIndex: Int = 0,
val inputIndex: Int = 0
) {
val module = AudioModule(jsFile)
var created = false
var node: dynamic = null
var port: MessagePort? = null
abstract fun onMessage(message: MessageEvent)
open fun postMessage(msg: Any) {
port?.postMessage(msg)
}
// call from user gesture
fun create(done: (node: dynamic) -> Unit) {
module.load {
node = AudioWorkletNode(
audioContext,
processorName,
AudioWorkletNodeParameters(
numberOfInputs,
outputChannelCount
)
)
if (destination == null) {
node.connect(audioContext.destination)
} else {
node.connect(destination, outputIndex, inputIndex)
}
node.port.onmessage = ::onMessage
port = node.port as? MessagePort
created = true
done(node)
}
}
}

View File

@@ -0,0 +1,14 @@
package nl.astraeus.vst.chip.audio
import org.w3c.dom.MessageEvent
object VstChipWorklet : AudioNode(
"vst-chip-worklet.js",
"vst-chip-processor"
) {
override fun onMessage(message: MessageEvent) {
console.log("Message from worklet: ", message)
}
}

View File

@@ -0,0 +1,23 @@
package nl.astraeus.vst.chip.channel
import nl.astraeus.vst.chip.view.MainView
import org.w3c.dom.BroadcastChannel
import org.w3c.dom.MessageEvent
import kotlin.js.Date
object Broadcaster {
val channel = BroadcastChannel("audio-worklet")
fun start() {
channel.onmessage = ::onMessage
}
fun onMessage(event: MessageEvent) {
MainView.addMessage("Received message ${event.data} time ${Date().getTime()}")
}
fun send(message: String) {
channel.postMessage(message)
}
}

View File

@@ -0,0 +1,184 @@
package nl.astraeus.vst.chip.view
import daw.style.Css
import daw.style.Css.defineCss
import daw.style.Css.noTextSelect
import daw.style.CssId
import daw.style.CssName
import daw.style.hover
import kotlinx.html.FlowContent
import kotlinx.html.a
import kotlinx.html.classes
import kotlinx.html.div
import kotlinx.html.h1
import kotlinx.html.hr
import kotlinx.html.js.onClickFunction
import kotlinx.html.js.onMouseDownFunction
import kotlinx.html.js.onMouseUpFunction
import kotlinx.html.span
import nl.astraeus.css.properties.BoxSizing
import nl.astraeus.css.properties.FontWeight
import nl.astraeus.css.properties.prc
import nl.astraeus.css.properties.px
import nl.astraeus.css.properties.rem
import nl.astraeus.css.style.cls
import nl.astraeus.komp.HtmlBuilder
import nl.astraeus.komp.Komponent
import nl.astraeus.vst.Note
import nl.astraeus.vst.chip.audio.VstChipWorklet
import nl.astraeus.vst.chip.channel.Broadcaster
import org.khronos.webgl.Int32Array
object MainView : Komponent() {
private var messages: MutableList<String> = ArrayList()
init {
MainViewCss
}
fun addMessage(message: String) {
messages.add(message)
while (messages.size > 10) {
messages.removeAt(0)
}
requestUpdate()
}
override fun HtmlBuilder.render() {
div {
h1 {
+"VST Chip"
}
div {
+"Hello, World!"
}
div {
if (VstChipWorklet.created) {
+"Worklet created"
} else {
a {
href = "#"
+"Create worklet"
onClickFunction = {
VstChipWorklet.create {
requestUpdate()
}
}
}
}
}
div {
a {
href = "#"
+"Send broadcast test message"
onClickFunction = {
Broadcaster.send("Test message")
}
}
}
div {
a {
href = "#"
+"Note on"
onClickFunction = {
VstChipWorklet.postMessage("test_on")
}
}
a {
href = "#"
+"Note off"
onClickFunction = {
VstChipWorklet.postMessage("test_off")
}
}
}
hr {}
repeat(9) {
div(classes = MainViewCss.NoteBarCss.name) {
for (index in it*12+12..it*12+23) {
notePlayer(Note.entries[index])
}
}
}
hr {}
for (message in messages) {
div {
+message
}
}
}
}
private fun FlowContent.notePlayer(note: Note) {
span {
a(classes = MainViewCss.ButtonCss.name) {
href = "#"
+note.sharp
onMouseDownFunction = {
VstChipWorklet.postMessage(Int32Array(arrayOf(0x90, note.ordinal, 32)))
}
onMouseUpFunction = {
VstChipWorklet.postMessage(Int32Array(arrayOf(0x90, note.ordinal, 0)))
}
/*
onMouseOutFunction = {
VstChipWorklet.postMessage(Int32Array(arrayOf(0x90, note.ordinal, 0)))
}
*/
}
}
}
object MainViewCss : CssId("main") {
object ActiveCss : CssName()
object ButtonCss : CssName()
object NoteBarCss : CssName()
init {
defineCss {
select("*") {
select("*:before") {
select("*:after") {
boxSizing(BoxSizing.borderBox)
}
}
}
select("html", "body") {
margin(0.px)
padding(0.px)
height(100.prc)
color(Css.currentStyle.mainFontColor)
backgroundColor(Css.currentStyle.mainBackgroundColor)
fontFamily("JetbrainsMono, monospace")
fontSize(14.px)
fontWeight(FontWeight.bold)
//transition()
noTextSelect()
}
select(cls(ButtonCss)) {
margin(1.rem)
padding(1.rem)
backgroundColor(Css.currentStyle.buttonBackgroundColor)
hover {
backgroundColor(Css.currentStyle.buttonBackgroundColor.hover())
}
}
select(cls(ActiveCss)) {
backgroundColor(Css.currentStyle.selectedBackgroundColor)
}
select(cls(NoteBarCss)) {
minHeight(4.rem)
}
}
}
}
}

View File

@@ -0,0 +1,137 @@
package daw.style
import kotlinx.browser.document
import nl.astraeus.css.properties.Color
import nl.astraeus.css.properties.UserSelect
import nl.astraeus.css.properties.hsl
import nl.astraeus.css.properties.hsla
import nl.astraeus.css.style
import nl.astraeus.css.style.ConditionalStyle
import nl.astraeus.css.style.Style
class StyleDefinition(
val mainFontColorNumber: Int = 32,
val mainFontColor: Color = hsla(mainFontColorNumber, 70, 55, 1.0),
val mainBackgroundColor: Color,
val selectedBackgroundColor: Color = hsl((Css.mainFontColorNumber + 180) % 360, 40, 30),
val mainBorderColor: Color = hsla((Css.mainFontColorNumber + 270) % 360, 0, 20, 0.5),
val headerFontColor: Color = hsl(Css.mainFontColorNumber, 50, 95),
val entryFontColor: Color = hsl(Css.mainFontColorNumber, 70, 55),
val successColor: Color = hsl(120, 70, 55),
val highlightFontColor : Color = hsl(Css.mainFontColorNumber, 70, 65),
val menuFontColor : Color = hsl(Css.mainFontColorNumber, 50, 100),
val menuBackgroundColor : Color = hsl(Css.mainFontColorNumber, 50, 16),
val backgroundComponentColor : Color = hsl(0, 0, 16),
val inputBackgroundColor : Color = hsl(0, 0, 20),
val inputBorderColor : Color = hsl(Css.mainFontColorNumber, 50, 50),
val buttonFontColor : Color = hsl(Css.mainFontColorNumber, 50, 95),
val buttonBackgroundColor : Color = hsl(Css.mainFontColorNumber, 70, 55),
val tabColor: Color = mainFontColor.darken(20),
val tabSelectedColor: Color = mainFontColor.lighten(10),
val tabHoverColor: Color = tabSelectedColor.lighten(20),
)
object NoTextSelectCls : CssName("no-text-select")
object SelectedCls : CssName("selected")
object ActiveCls : CssName("active")
fun Color.hover(): Color = if (Css.currentStyle == Css.darkStyle) {
this.lighten(15)
} else {
this.darken(15)
}
object Css {
var minified = false
var mainFontColorNumber = 32
var dynamicStyles = mutableMapOf<CssId, ConditionalStyle.() -> Unit>()
fun CssId.defineCss(conditionalStyle: ConditionalStyle.() -> Unit) {
check(!dynamicStyles.containsKey(this)) {
"CssId with name ${this.name} already defined!"
}
updateCss(conditionalStyle)
}
private fun CssId.updateCss(conditionalStyle: ConditionalStyle.() -> Unit) {
val elementId = this.description()
var dynamicStyleElement = document.getElementById(elementId)
dynamicStyles[this] = conditionalStyle
if (dynamicStyleElement == null) {
dynamicStyleElement = document.createElement("style")
dynamicStyleElement.id = elementId
document.head?.append(dynamicStyleElement)
}
val css = style(conditionalStyle)
dynamicStyleElement.innerHTML = css.generateCss(minified = false)
}
val darkStyle = StyleDefinition(
mainFontColorNumber = mainFontColorNumber,
mainBackgroundColor = hsl(0, 0, 10)
)
val lightStyle = StyleDefinition(
mainFontColorNumber = (mainFontColorNumber + 120) % 360,
mainBackgroundColor = hsl(0, 0, 98),
mainBorderColor = hsla((Css.mainFontColorNumber + 270) % 360, 0, 20, 0.15),
headerFontColor = hsl(mainFontColorNumber, 50, 5),
backgroundComponentColor = hsl(0, 0, 84),
)
var currentStyle: StyleDefinition = darkStyle
fun updateStyle() {
for ((cssId, dynStyle) in dynamicStyles) {
cssId.apply {
updateCss(dynStyle)
}
}
}
fun switchLayout() {
currentStyle = if (currentStyle == darkStyle) {
lightStyle
} else {
darkStyle
}
updateStyle()
}
fun Style.transition() {
transition("all 0.5s ease")
}
fun Style.noTextSelect() {
plain("-webkit-touch-callout", "none")
plain("-webkit-user-select", "none")
plain("-moz-user-select", "none")
plain("-ms-user-select", "none")
userSelect(UserSelect.none)
select("::selection") {
background("none")
}
}
object GenericCss : CssId("generic") {
init {
fun generateStyle(): String {
val css = style {
}
return css.generateCss(minified = minified)
}
}
}
}

View File

@@ -0,0 +1,65 @@
package daw.style
import nl.astraeus.css.style.DescriptionProvider
import nl.astraeus.css.style.cls
private val CAPITAL_LETTER = Regex("[A-Z]")
fun String.hyphenize(): String =
replace(CAPITAL_LETTER) {
"-${it.value.lowercase()}"
}
private val shortId = false
private var nextCssId = 1
private fun nextShortId(): String {
var id = nextCssId++
val result = StringBuilder()
while(id > 0) {
val ch = ((id % 26) + 'a'.code).toChar()
result.append(ch)
id /= 26
}
return result.toString()
}
open class CssName(name: String? = null) : DescriptionProvider {
val name: String = if (shortId) {
nextShortId()
} else if (name != null) {
"daw-$name"
} else {
"daw${this::class.simpleName?.hyphenize() ?: this::class}"
}
override fun description() = name
}
fun CssName.cls() : DescriptionProvider = cls(this)
open class CssId(name: String) : DescriptionProvider {
val name: String = if (shortId) {
nextShortId()
} else {
"daw-$name-css"
}
override fun description() = name
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is CssId) return false
if (name != other.name) return false
return true
}
override fun hashCode(): Int {
return name.hashCode()
}
}

View File

@@ -0,0 +1,2 @@
package nl.astraeus.vst.chip

View File

@@ -0,0 +1,20 @@
package nl.astraeus.vst.chip
import io.undertow.Undertow
import io.undertow.UndertowOptions
fun main() {
Thread.setDefaultUncaughtExceptionHandler { _, e ->
e.printStackTrace()
}
val server = Undertow.builder()
.addHttpListener(Settings.port, "0.0.0.0")
.setIoThreads(4)
.setHandler(RequestHandler)
.setServerOption(UndertowOptions.SHUTDOWN_TIMEOUT, 1000)
.build()
println("Starting server at port ${Settings.port}...")
server?.start()
}

View File

@@ -0,0 +1,16 @@
package nl.astraeus.vst.chip
import io.undertow.server.HttpHandler
import io.undertow.server.HttpServerExchange
import io.undertow.server.handlers.resource.PathResourceManager
import io.undertow.server.handlers.resource.ResourceHandler
import java.nio.file.Paths
object RequestHandler : HttpHandler {
val resourceHandler = ResourceHandler(PathResourceManager(Paths.get("web")))
override fun handleRequest(exchange: HttpServerExchange) {
resourceHandler.handleRequest(exchange)
}
}

View File

@@ -0,0 +1,50 @@
package nl.astraeus.vst.chip
import java.io.File
import java.io.FileInputStream
import java.util.*
object Settings {
var runningAsRoot: Boolean = false
var port = 9000
var sslPort = 8443
var connectionTimeout = 30000
var jdbcDriver = "nl.astraeus.jdbc.Driver"
var jdbcConnectionUrl = "jdbc:stat:webServerPort=6001:jdbc:sqlite:data/srp.db"
var jdbcUser = "sa"
var jdbcPassword = ""
var adminUser = "rnentjes"
var adminPassword = "9/SG_Bd}9gWz~?j\\A.U]n9]OO"
fun getPropertiesFromFile(filename: String): Properties? {
val propertiesFile = File(filename)
return if (propertiesFile.exists()) {
val properties = Properties()
FileInputStream(propertiesFile).use {
properties.load(it)
}
properties
} else {
null
}
}
fun readProperties(args: Array<String>) {
val filename = if (args.isNotEmpty()) args[0] else "srp.properties"
val properties = getPropertiesFromFile(filename) ?: return // return if properties couldn't be loaded
runningAsRoot = properties.getProperty("runningAsRoot", runningAsRoot.toString()).toBoolean()
port = properties.getProperty("port", port.toString()).toInt()
sslPort = properties.getProperty("sslPort", sslPort.toString()).toInt()
connectionTimeout = properties.getProperty("connectionTimeout", connectionTimeout.toString()).toInt()
jdbcDriver = properties.getProperty("jdbcDriver", jdbcDriver)
jdbcConnectionUrl = properties.getProperty("jdbcConnectionUrl", jdbcConnectionUrl)
jdbcUser = properties.getProperty("jdbcUser", jdbcUser)
jdbcPassword = properties.getProperty("jdbcPassword", jdbcPassword)
adminUser = properties.getProperty("adminUser", adminUser)
adminPassword = properties.getProperty("adminPassword", adminPassword)
}
}

View File

@@ -0,0 +1,6 @@
<!DOCTYPE html>
<html lang="en">
<body>
<script type="application/javascript" src="vst-chip-worklet-ui.js"></script>
</body>
</html>