Initial commit
This commit is contained in:
18
src/jsMain/kotlin/nl/astraeus/vst/chip/Externals.kt
Normal file
18
src/jsMain/kotlin/nl/astraeus/vst/chip/Externals.kt
Normal 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>
|
||||
)
|
||||
12
src/jsMain/kotlin/nl/astraeus/vst/chip/Main.kt
Normal file
12
src/jsMain/kotlin/nl/astraeus/vst/chip/Main.kt
Normal 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()
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package nl.astraeus.vst.chip.audio
|
||||
|
||||
import nl.astraeus.vst.chip.AudioContext
|
||||
|
||||
object AudioContextHandler {
|
||||
val audioContext: dynamic = AudioContext()
|
||||
|
||||
|
||||
|
||||
}
|
||||
88
src/jsMain/kotlin/nl/astraeus/vst/chip/audio/AudioModule.kt
Normal file
88
src/jsMain/kotlin/nl/astraeus/vst/chip/audio/AudioModule.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
184
src/jsMain/kotlin/nl/astraeus/vst/chip/view/MainView.kt
Normal file
184
src/jsMain/kotlin/nl/astraeus/vst/chip/view/MainView.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
137
src/jsMain/kotlin/nl/astraeus/vst/chip/view/css/Css.kt
Normal file
137
src/jsMain/kotlin/nl/astraeus/vst/chip/view/css/Css.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
65
src/jsMain/kotlin/nl/astraeus/vst/chip/view/css/CssName.kt
Normal file
65
src/jsMain/kotlin/nl/astraeus/vst/chip/view/css/CssName.kt
Normal 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()
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user