Compare commits
13 Commits
538aa6b9ae
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| ed0b76dc06 | |||
| 7c87274a04 | |||
| 5da8424c40 | |||
| 8ee8f17f96 | |||
| 0bdaa5c94f | |||
| 3746ced387 | |||
| 4d7c46093c | |||
| 2871697329 | |||
| c1f756eb79 | |||
| 1d02a6ee16 | |||
| 9c9962d7db | |||
| 5c16b57ae9 | |||
| 9ab909cf6c |
@@ -10,7 +10,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
group = "nl.astraeus"
|
group = "nl.astraeus"
|
||||||
version = "2.1.1-SNAPSHOT"
|
version = "2.2.0-alpha-5"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
@@ -47,7 +47,7 @@ kotlin {
|
|||||||
}
|
}
|
||||||
val jsMain by getting {
|
val jsMain by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
api("nl.astraeus:kotlin-komponent:1.2.7")
|
api("nl.astraeus:kotlin-komponent:1.2.8")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val jsTest by getting {
|
val jsTest by getting {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package nl.astraeus.vst.ui.components
|
package nl.astraeus.vst.ui.components
|
||||||
|
|
||||||
|
import kotlinx.html.SVG
|
||||||
import kotlinx.html.div
|
import kotlinx.html.div
|
||||||
import kotlinx.html.js.onMouseDownFunction
|
import kotlinx.html.js.onMouseDownFunction
|
||||||
import kotlinx.html.js.onMouseLeaveFunction
|
import kotlinx.html.js.onMouseLeaveFunction
|
||||||
@@ -37,34 +38,41 @@ class KeyboardComponent(
|
|||||||
initialOctave: Int = 4,
|
initialOctave: Int = 4,
|
||||||
val keyboardWidth: Int = 210,
|
val keyboardWidth: Int = 210,
|
||||||
val keyboardHeight: Int = keyboardWidth / 2,
|
val keyboardHeight: Int = keyboardWidth / 2,
|
||||||
|
val rounding: Int = 4,
|
||||||
val onNoteDown: (Int) -> Unit = {},
|
val onNoteDown: (Int) -> Unit = {},
|
||||||
val onNoteUp: (Int) -> Unit = {}
|
val onNoteUp: (Int) -> Unit = {}
|
||||||
) : Komponent() {
|
) : Komponent() {
|
||||||
|
|
||||||
|
// Define a data class for keyboard state
|
||||||
|
data class KeyboardState(
|
||||||
|
val octave: Int,
|
||||||
|
val pressedNotes: Set<Int> = emptySet()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Use immutable state
|
||||||
|
private var state = KeyboardState(initialOctave)
|
||||||
|
|
||||||
// Current octave with range validation
|
// Current octave with range validation
|
||||||
private var _octave: Int = initialOctave
|
|
||||||
var octave: Int
|
var octave: Int
|
||||||
get() = _octave
|
get() = state.octave
|
||||||
set(value) {
|
set(value) {
|
||||||
_octave = when {
|
updateOctave(value)
|
||||||
value < 0 -> 0
|
|
||||||
value > 9 -> 9
|
|
||||||
else -> value
|
|
||||||
}
|
|
||||||
requestUpdate()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set to track which notes are currently pressed
|
// Update state with functions that return new state
|
||||||
private val pressedNotes = mutableSetOf<Int>()
|
private fun updateOctave(newOctave: Int) {
|
||||||
|
state = state.copy(octave = newOctave.coerceIn(MIN_OCTAVE, MAX_OCTAVE))
|
||||||
|
requestUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
// MIDI note numbers for C4 to B4 (one octave)
|
// MIDI note numbers for one octave
|
||||||
private val whiteKeys = listOf(60, 62, 64, 65, 67, 69, 71)
|
private val whiteKeys = BASE_WHITE_KEYS
|
||||||
private val blackKeys = listOf(61, 63, 66, 68, 70)
|
private val blackKeys = BASE_BLACK_KEYS
|
||||||
|
|
||||||
// Key dimensions
|
// Key dimensions
|
||||||
private val whiteKeyWidth = keyboardWidth / 7
|
private val whiteKeyWidth = keyboardWidth / WHITE_KEYS_PER_OCTAVE
|
||||||
private val blackKeyWidth = (keyboardWidth / 9)
|
private val blackKeyWidth = (keyboardWidth / BLACK_KEY_WIDTH_DIVISOR)
|
||||||
private val blackKeyHeight = keyboardHeight * 60 / 100
|
private val blackKeyHeight = keyboardHeight * BLACK_KEY_HEIGHT_PERCENTAGE / 100
|
||||||
|
|
||||||
// Calculate positions for black keys
|
// Calculate positions for black keys
|
||||||
private val blackKeyPositions = listOf(
|
private val blackKeyPositions = listOf(
|
||||||
@@ -76,30 +84,56 @@ class KeyboardComponent(
|
|||||||
)
|
)
|
||||||
|
|
||||||
fun noteDown(midiNote: Int) {
|
fun noteDown(midiNote: Int) {
|
||||||
pressedNotes.add(midiNote)
|
state = state.copy(pressedNotes = state.pressedNotes + midiNote)
|
||||||
onNoteDown(midiNote)
|
onNoteDown(midiNote)
|
||||||
requestUpdate()
|
requestUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun noteUp(midiNote: Int) {
|
fun noteUp(midiNote: Int) {
|
||||||
pressedNotes.remove(midiNote)
|
state = state.copy(pressedNotes = state.pressedNotes - midiNote)
|
||||||
onNoteUp(midiNote)
|
onNoteUp(midiNote)
|
||||||
requestUpdate()
|
requestUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun releaseAllNotes() {
|
private fun releaseAllNotes() {
|
||||||
// Create a copy of the set to avoid concurrent modification
|
// Create a copy of the set to avoid concurrent modification
|
||||||
val notesToRelease = pressedNotes.toSet()
|
val notesToRelease = state.pressedNotes.toSet()
|
||||||
for (note in notesToRelease) {
|
for (note in notesToRelease) {
|
||||||
noteUp(note)
|
noteUp(note)
|
||||||
}
|
}
|
||||||
// Clear the set just to be safe
|
// Just to be safe, clear all pressed notes
|
||||||
pressedNotes.clear()
|
state = state.copy(pressedNotes = emptySet())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getOctaveOffset(): Int = (octave - OCTAVE_BASE) * NOTES_PER_OCTAVE
|
||||||
|
|
||||||
|
private fun getMidiNoteFromMousePosition(x: Double, y: Double): Int? {
|
||||||
|
// Check if click is on a black key (black keys are on top of white keys)
|
||||||
|
if (y <= blackKeyHeight) {
|
||||||
|
for (j in 0 until BLACK_KEYS_PER_OCTAVE) {
|
||||||
|
if (x >= blackKeyPositions[j] && x <= blackKeyPositions[j] + blackKeyWidth) {
|
||||||
|
return blackKeys[j] + getOctaveOffset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no black key was pressed, check for white key
|
||||||
|
val keyIndex = (x / whiteKeyWidth).toInt()
|
||||||
|
if (keyIndex in 0 until WHITE_KEYS_PER_OCTAVE) {
|
||||||
|
return whiteKeys[keyIndex] + getOctaveOffset()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If y > blackKeyHeight, it's definitely a white key
|
||||||
|
val keyIndex = (x / whiteKeyWidth).toInt()
|
||||||
|
if (keyIndex in 0 until WHITE_KEYS_PER_OCTAVE) {
|
||||||
|
return whiteKeys[keyIndex] + getOctaveOffset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun HtmlBuilder.render() {
|
override fun HtmlBuilder.render() {
|
||||||
div(KeyboardCls.name) {
|
div(KeyboardCls.name) {
|
||||||
style = "width: ${keyboardWidth}px; height: ${keyboardHeight + 60}px"
|
style = "width: ${keyboardWidth}px; height: ${keyboardHeight + KEYBOARD_CONTROLS_HEIGHT}px"
|
||||||
|
|
||||||
onMouseLeaveFunction = { event ->
|
onMouseLeaveFunction = { event ->
|
||||||
if (event is MouseEvent) {
|
if (event is MouseEvent) {
|
||||||
@@ -115,7 +149,7 @@ class KeyboardComponent(
|
|||||||
+"<"
|
+"<"
|
||||||
onMouseDownFunction = { event ->
|
onMouseDownFunction = { event ->
|
||||||
if (event is MouseEvent) {
|
if (event is MouseEvent) {
|
||||||
octave--
|
updateOctave(octave - 1)
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,7 +164,8 @@ class KeyboardComponent(
|
|||||||
|
|
||||||
div(KeyboardOctaveCls.name) {
|
div(KeyboardOctaveCls.name) {
|
||||||
// Show current octave the piano is being played at
|
// Show current octave the piano is being played at
|
||||||
+"Octave: $octave"
|
// minus one to keep it in line with custom notes ranges
|
||||||
|
+"Octave: ${octave -1}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +175,7 @@ class KeyboardComponent(
|
|||||||
+">"
|
+">"
|
||||||
onMouseDownFunction = { event ->
|
onMouseDownFunction = { event ->
|
||||||
if (event is MouseEvent) {
|
if (event is MouseEvent) {
|
||||||
octave++
|
updateOctave(octave + 1)
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,109 +192,62 @@ class KeyboardComponent(
|
|||||||
// Define mouse event handlers at the SVG level
|
// Define mouse event handlers at the SVG level
|
||||||
onMouseDownFunction = { event ->
|
onMouseDownFunction = { event ->
|
||||||
if (event is MouseEvent) {
|
if (event is MouseEvent) {
|
||||||
val x = event.offsetX
|
getMidiNoteFromMousePosition(event.offsetX, event.offsetY)?.let { noteDown(it) }
|
||||||
val y = event.offsetY
|
|
||||||
|
|
||||||
// Check if click is on a black key (black keys are on top of white keys)
|
|
||||||
if (y <= blackKeyHeight) {
|
|
||||||
var blackKeyPressed = false
|
|
||||||
for (j in 0 until 5) {
|
|
||||||
if (x >= blackKeyPositions[j] && x <= blackKeyPositions[j] + blackKeyWidth) {
|
|
||||||
noteDown(blackKeys[j] + (octave - 5) * 12)
|
|
||||||
blackKeyPressed = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no black key was pressed, check for white key
|
|
||||||
if (!blackKeyPressed) {
|
|
||||||
// Check if click is on a white key
|
|
||||||
val keyIndex = (x / whiteKeyWidth).toInt()
|
|
||||||
if (keyIndex in 0..6) {
|
|
||||||
noteDown(whiteKeys[keyIndex] + (octave - 5) * 12)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If y > blackKeyHeight, it's definitely a white key
|
|
||||||
val keyIndex = (x / whiteKeyWidth).toInt()
|
|
||||||
if (keyIndex in 0..6) {
|
|
||||||
noteDown(whiteKeys[keyIndex] + (octave - 5) * 12)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseUpFunction = { event ->
|
onMouseUpFunction = { event ->
|
||||||
if (event is MouseEvent) {
|
if (event is MouseEvent) {
|
||||||
val x = event.offsetX
|
getMidiNoteFromMousePosition(event.offsetX, event.offsetY)?.let { noteUp(it) }
|
||||||
val y = event.offsetY
|
|
||||||
|
|
||||||
// Check if release is on a black key (black keys are on top of white keys)
|
|
||||||
if (y <= blackKeyHeight) {
|
|
||||||
var blackKeyReleased = false
|
|
||||||
for (j in 0 until 5) {
|
|
||||||
if (x >= blackKeyPositions[j] && x <= blackKeyPositions[j] + blackKeyWidth) {
|
|
||||||
noteUp(blackKeys[j] + (octave - 5) * 12)
|
|
||||||
blackKeyReleased = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no black key was released, check for white key
|
|
||||||
if (!blackKeyReleased) {
|
|
||||||
// Check if release is on a white key
|
|
||||||
val keyIndex = (x / whiteKeyWidth).toInt()
|
|
||||||
if (keyIndex in 0..6) {
|
|
||||||
noteUp(whiteKeys[keyIndex] + (octave - 5) * 12)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If y > blackKeyHeight, it's definitely a white key
|
|
||||||
val keyIndex = (x / whiteKeyWidth).toInt()
|
|
||||||
if (keyIndex in 0..6) {
|
|
||||||
noteUp(whiteKeys[keyIndex] + (octave - 5) * 12)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw white keys
|
// Draw white keys
|
||||||
for (i in 0 until 7) {
|
this.renderWhiteKeys()
|
||||||
val midiNote = whiteKeys[i] + (octave - 5) * 12
|
|
||||||
val isPressed = pressedNotes.contains(midiNote)
|
|
||||||
rect(
|
|
||||||
i * whiteKeyWidth,
|
|
||||||
0,
|
|
||||||
whiteKeyWidth,
|
|
||||||
keyboardHeight,
|
|
||||||
0,
|
|
||||||
if (isPressed) WhiteKeyPressedCls.name else WhiteKeyCls.name
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw black keys
|
// Draw black keys
|
||||||
for (i in 0 until 5) {
|
this.renderBlackKeys()
|
||||||
val midiNote = blackKeys[i] + (octave - 5) * 12
|
|
||||||
val isPressed = pressedNotes.contains(midiNote)
|
|
||||||
rect(
|
|
||||||
blackKeyPositions[i],
|
|
||||||
0,
|
|
||||||
blackKeyWidth,
|
|
||||||
blackKeyHeight,
|
|
||||||
0,
|
|
||||||
if (isPressed) BlackKeyPressedCls.name else BlackKeyCls.name
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render white keys
|
||||||
|
private fun SVG.renderWhiteKeys() {
|
||||||
|
for (i in 0 until WHITE_KEYS_PER_OCTAVE) {
|
||||||
|
val midiNote = whiteKeys[i] + getOctaveOffset()
|
||||||
|
val isPressed = state.pressedNotes.contains(midiNote)
|
||||||
|
rect(
|
||||||
|
i * whiteKeyWidth,
|
||||||
|
0,
|
||||||
|
whiteKeyWidth,
|
||||||
|
keyboardHeight,
|
||||||
|
rounding,
|
||||||
|
if (isPressed) WhiteKeyPressedCls.name else WhiteKeyCls.name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render black keys
|
||||||
|
private fun SVG.renderBlackKeys() {
|
||||||
|
for (i in 0 until BLACK_KEYS_PER_OCTAVE) {
|
||||||
|
val midiNote = blackKeys[i] + getOctaveOffset()
|
||||||
|
val isPressed = state.pressedNotes.contains(midiNote)
|
||||||
|
rect(
|
||||||
|
blackKeyPositions[i],
|
||||||
|
0,
|
||||||
|
blackKeyWidth,
|
||||||
|
blackKeyHeight,
|
||||||
|
rounding,
|
||||||
|
if (isPressed) BlackKeyPressedCls.name else BlackKeyCls.name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object : CssId("keyboard") {
|
companion object : CssId("keyboard") {
|
||||||
|
// CSS class names
|
||||||
object KeyboardCls : CssName()
|
object KeyboardCls : CssName()
|
||||||
object KeyboardControlsCls : CssName()
|
object KeyboardControlsCls : CssName()
|
||||||
object KeyboardInfoCls : CssName()
|
object KeyboardInfoCls : CssName()
|
||||||
@@ -272,6 +260,21 @@ class KeyboardComponent(
|
|||||||
object WhiteKeyPressedCls : CssName()
|
object WhiteKeyPressedCls : CssName()
|
||||||
object BlackKeyPressedCls : CssName()
|
object BlackKeyPressedCls : CssName()
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
private const val OCTAVE_BASE = 5
|
||||||
|
private const val NOTES_PER_OCTAVE = 12
|
||||||
|
private const val BLACK_KEY_HEIGHT_PERCENTAGE = 60
|
||||||
|
private const val MIN_OCTAVE = 0
|
||||||
|
private const val MAX_OCTAVE = 9
|
||||||
|
private const val WHITE_KEYS_PER_OCTAVE = 7
|
||||||
|
private const val BLACK_KEYS_PER_OCTAVE = 5
|
||||||
|
private const val BLACK_KEY_WIDTH_DIVISOR = 9
|
||||||
|
private const val KEYBOARD_CONTROLS_HEIGHT = 60
|
||||||
|
|
||||||
|
// MIDI note numbers for C5 to B5 (one octave)
|
||||||
|
private val BASE_WHITE_KEYS = listOf(60, 62, 64, 65, 67, 69, 71) // C, D, E, F, G, A, B
|
||||||
|
private val BASE_BLACK_KEYS = listOf(61, 63, 66, 68, 70) // C#, D#, F#, G#, A#
|
||||||
|
|
||||||
init {
|
init {
|
||||||
defineCss {
|
defineCss {
|
||||||
select(cls(KeyboardCls)) {
|
select(cls(KeyboardCls)) {
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
package nl.astraeus.vst.ui.components
|
|
||||||
|
|
||||||
import kotlinx.html.div
|
|
||||||
import nl.astraeus.komp.HtmlBuilder
|
|
||||||
import nl.astraeus.komp.Komponent
|
|
||||||
import nl.astraeus.vst.ui.css.Css.defineCss
|
|
||||||
import nl.astraeus.vst.ui.css.CssId
|
|
||||||
import nl.astraeus.vst.ui.css.CssName
|
|
||||||
|
|
||||||
class KeyboardInputComponent : Komponent() {
|
|
||||||
override fun HtmlBuilder.render() {
|
|
||||||
div {
|
|
||||||
+"Keyboard component"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object : CssId("keyboard-input") {
|
|
||||||
object KeyboardInputCss : CssName()
|
|
||||||
|
|
||||||
init {
|
|
||||||
defineCss {
|
|
||||||
select(KeyboardInputCss.cls()) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -14,6 +14,10 @@ fun SVG.height(height: Int) {
|
|||||||
this.attributes["height"] = "$height"
|
this.attributes["height"] = "$height"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun SVG.viewbox(viewbox: String) {
|
||||||
|
this.attributes["viewbox"] = viewbox
|
||||||
|
}
|
||||||
|
|
||||||
fun SVG.svgStyle(
|
fun SVG.svgStyle(
|
||||||
name: String,
|
name: String,
|
||||||
vararg props: Pair<String, String>
|
vararg props: Pair<String, String>
|
||||||
@@ -41,12 +45,12 @@ fun SVG.rect(
|
|||||||
y: Int,
|
y: Int,
|
||||||
width: Int,
|
width: Int,
|
||||||
height: Int,
|
height: Int,
|
||||||
rx: Int,
|
rounding: Int,
|
||||||
cls: String
|
cls: String,
|
||||||
) {
|
) {
|
||||||
this.unsafe {
|
this.unsafe {
|
||||||
+ """
|
+ """
|
||||||
<rect class="$cls" x="$x" y="$y" width="$width" height="$height" rx="$rx" />
|
<rect class="$cls" x="$x" y="$y" width="$width" height="$height" rx="$rounding" rx="$rounding" />
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ object Settings {
|
|||||||
var port = 9004
|
var port = 9004
|
||||||
var connectionTimeout = 30000
|
var connectionTimeout = 30000
|
||||||
var jdbcStatsPort = 6001
|
var jdbcStatsPort = 6001
|
||||||
|
var dataDir = "data"
|
||||||
|
|
||||||
var jdbcDriver = "nl.astraeus.jdbc.Driver"
|
var jdbcDriver = "nl.astraeus.jdbc.Driver"
|
||||||
val jdbcConnectionUrl
|
val jdbcConnectionUrl
|
||||||
get() = "jdbc:stat:webServerPort=$jdbcStatsPort:jdbc:sqlite:data/vst.db"
|
get() = "jdbc:stat:webServerPort=$jdbcStatsPort:jdbc:sqlite:$dataDir/vst.db"
|
||||||
var jdbcUser = "sa"
|
var jdbcUser = "sa"
|
||||||
var jdbcPassword = ""
|
var jdbcPassword = ""
|
||||||
|
|
||||||
@@ -35,6 +36,8 @@ object Settings {
|
|||||||
|
|
||||||
port = properties.getProperty("port", port.toString()).toInt()
|
port = properties.getProperty("port", port.toString()).toInt()
|
||||||
jdbcStatsPort = properties.getProperty("jdbcStatsPort", jdbcStatsPort.toString()).toInt()
|
jdbcStatsPort = properties.getProperty("jdbcStatsPort", jdbcStatsPort.toString()).toInt()
|
||||||
|
dataDir = properties.getProperty("dataDir", dataDir)
|
||||||
|
|
||||||
connectionTimeout =
|
connectionTimeout =
|
||||||
properties.getProperty("connectionTimeout", connectionTimeout.toString()).toInt()
|
properties.getProperty("connectionTimeout", connectionTimeout.toString()).toInt()
|
||||||
jdbcDriver = properties.getProperty("jdbcDriver", jdbcDriver)
|
jdbcDriver = properties.getProperty("jdbcDriver", jdbcDriver)
|
||||||
|
|||||||
11
src/jvmMain/kotlin/nl/astraeus/vst/base/db/BinaryDao.kt
Normal file
11
src/jvmMain/kotlin/nl/astraeus/vst/base/db/BinaryDao.kt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package nl.astraeus.vst.base.db
|
||||||
|
|
||||||
|
object BinaryDao {
|
||||||
|
val queryProvider = SampleEntityQueryProvider
|
||||||
|
|
||||||
|
fun getSample(waveHash: String): ByteArray {
|
||||||
|
return byteArrayOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
49
src/jvmMain/kotlin/nl/astraeus/vst/base/db/BinaryEntity.kt
Normal file
49
src/jvmMain/kotlin/nl/astraeus/vst/base/db/BinaryEntity.kt
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package nl.astraeus.vst.base.db
|
||||||
|
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
|
||||||
|
data class SampleEntity(
|
||||||
|
var sha1Hash: String,
|
||||||
|
var filename: String,
|
||||||
|
var length: Int,
|
||||||
|
var created: Instant = Clock.System.now(),
|
||||||
|
var updated: Instant = Clock.System.now(),
|
||||||
|
) : Entity {
|
||||||
|
override fun getPK(): Array<Any> = arrayOf(sha1Hash)
|
||||||
|
|
||||||
|
override fun setPK(pks: Array<Any>) {
|
||||||
|
sha1Hash = pks[0] as String
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SamplePartEntity(
|
||||||
|
var sha1Hash: String,
|
||||||
|
var part: Int,
|
||||||
|
var from: Int,
|
||||||
|
var to: Int,
|
||||||
|
var data: ByteArray,
|
||||||
|
) : Entity {
|
||||||
|
override fun getPK(): Array<Any> = arrayOf(sha1Hash, part)
|
||||||
|
|
||||||
|
override fun setPK(pks: Array<Any>) {
|
||||||
|
sha1Hash = pks[0] as String
|
||||||
|
part = pks[1] as Int
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is SamplePartEntity) return false
|
||||||
|
|
||||||
|
if (sha1Hash != other.sha1Hash) return false
|
||||||
|
if (part != other.part) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = sha1Hash.hashCode()
|
||||||
|
result = 31 * result + part
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package nl.astraeus.vst.base.db
|
||||||
|
|
||||||
|
import java.sql.ResultSet
|
||||||
|
|
||||||
|
val BINARY_CREATE_QUERY = """
|
||||||
|
CREATE TABLE BINARIESYSAMPLES (
|
||||||
|
SHA1HASH TEXT,
|
||||||
|
FILENAME TEXT,
|
||||||
|
LENGTH NUMBER,
|
||||||
|
CREATED TIMESTAMP
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
object SampleEntityQueryProvider : QueryProvider<SampleEntity>() {
|
||||||
|
override val tableName: String
|
||||||
|
get() = "SAMPLES"
|
||||||
|
override val resultSetMapper: (ResultSet) -> SampleEntity
|
||||||
|
get() = { rs ->
|
||||||
|
SampleEntity(
|
||||||
|
rs.getString(1),
|
||||||
|
rs.getString(2),
|
||||||
|
rs.getInt(3),
|
||||||
|
rs.getTimestamp(4).toDateTimeInstant()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
override val insert: SqlStatement<SampleEntity>
|
||||||
|
get() = SqlStatement(
|
||||||
|
"""
|
||||||
|
INSERT INTO $tableName (
|
||||||
|
SHA1HASH,
|
||||||
|
LENGTH,
|
||||||
|
CREATED
|
||||||
|
) VALUES (
|
||||||
|
?,?,?,?
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
) { ps ->
|
||||||
|
ps.setString(1, sha1Hash)
|
||||||
|
ps.setString(2, filename)
|
||||||
|
ps.setInt(3, length)
|
||||||
|
ps.setTimestamp(4, updated.toSqlTimestamp())
|
||||||
|
}
|
||||||
|
override val update: SqlStatement<SampleEntity>
|
||||||
|
get() = TODO("Not yet implemented")
|
||||||
|
}
|
||||||
@@ -10,16 +10,6 @@ import io.undertow.server.session.Session
|
|||||||
import io.undertow.server.session.SessionCookieConfig
|
import io.undertow.server.session.SessionCookieConfig
|
||||||
import io.undertow.server.session.SessionManager
|
import io.undertow.server.session.SessionManager
|
||||||
import io.undertow.util.Headers
|
import io.undertow.util.Headers
|
||||||
import io.undertow.websockets.WebSocketConnectionCallback
|
|
||||||
import io.undertow.websockets.core.AbstractReceiveListener
|
|
||||||
import io.undertow.websockets.core.BufferedBinaryMessage
|
|
||||||
import io.undertow.websockets.core.BufferedTextMessage
|
|
||||||
import io.undertow.websockets.core.WebSocketChannel
|
|
||||||
import io.undertow.websockets.core.WebSockets
|
|
||||||
import io.undertow.websockets.spi.WebSocketHttpExchange
|
|
||||||
import nl.astraeus.vst.base.db.Database
|
|
||||||
import nl.astraeus.vst.base.db.PatchDao
|
|
||||||
import nl.astraeus.vst.base.db.PatchEntity
|
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
|
|
||||||
object VstSessionConfig {
|
object VstSessionConfig {
|
||||||
@@ -30,61 +20,6 @@ object VstSessionConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WebsocketHandler(
|
|
||||||
val session: Session?
|
|
||||||
) : AbstractReceiveListener(), WebSocketConnectionCallback {
|
|
||||||
|
|
||||||
override fun onConnect(exchange: WebSocketHttpExchange, channel: WebSocketChannel) {
|
|
||||||
channel.receiveSetter.set(this)
|
|
||||||
channel.resumeReceives()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFullTextMessage(channel: WebSocketChannel, message: BufferedTextMessage) {
|
|
||||||
val vstSession = session?.getAttribute("html-session") as? VstSession
|
|
||||||
|
|
||||||
val data = message.data
|
|
||||||
val commandLength = data.indexOf('\n')
|
|
||||||
if (commandLength > 0) {
|
|
||||||
val command = data.substring(0, commandLength)
|
|
||||||
val value = data.substring(commandLength + 1)
|
|
||||||
|
|
||||||
when (command) {
|
|
||||||
"SAVE" -> {
|
|
||||||
val patchId = vstSession?.patchId
|
|
||||||
if (patchId != null) {
|
|
||||||
Database.transaction {
|
|
||||||
val patchEntity = PatchDao.findById(patchId)
|
|
||||||
|
|
||||||
if (patchEntity != null) {
|
|
||||||
PatchDao.update(patchEntity.copy(patch = value))
|
|
||||||
} else {
|
|
||||||
PatchDao.insert(PatchEntity(0, patchId, value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
WebSockets.sendText("SAVED\n$patchId", channel, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
"LOAD" -> {
|
|
||||||
val patchId = vstSession?.patchId
|
|
||||||
if (patchId != null) {
|
|
||||||
Database.transaction {
|
|
||||||
val patchEntity = PatchDao.findById(patchId)
|
|
||||||
|
|
||||||
if (patchEntity != null) {
|
|
||||||
WebSockets.sendText("LOAD\n${patchEntity.patch}", channel, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFullBinaryMessage(channel: WebSocketChannel?, message: BufferedBinaryMessage?) {
|
|
||||||
// do nothing yet
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object WebsocketConnectHandler : HttpHandler {
|
object WebsocketConnectHandler : HttpHandler {
|
||||||
override fun handleRequest(exchange: HttpServerExchange) {
|
override fun handleRequest(exchange: HttpServerExchange) {
|
||||||
@@ -112,6 +47,7 @@ class PatchHandler(
|
|||||||
}
|
}
|
||||||
httpSession?.setAttribute("html-session", VstSession(patchId))
|
httpSession?.setAttribute("html-session", VstSession(patchId))
|
||||||
|
|
||||||
|
exchange.responseHeaders.put(Headers.CONTENT_TYPE, "text/html; charset=utf-8")
|
||||||
exchange.responseSender.send(generateIndex(title, scriptName, null))
|
exchange.responseSender.send(generateIndex(title, scriptName, null))
|
||||||
} else {
|
} else {
|
||||||
val patchId = generateId()
|
val patchId = generateId()
|
||||||
|
|||||||
197
src/jvmMain/kotlin/nl/astraeus/vst/base/web/WebsocketHandler.kt
Normal file
197
src/jvmMain/kotlin/nl/astraeus/vst/base/web/WebsocketHandler.kt
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
package nl.astraeus.vst.base.web
|
||||||
|
|
||||||
|
import io.undertow.server.session.Session
|
||||||
|
import io.undertow.websockets.WebSocketConnectionCallback
|
||||||
|
import io.undertow.websockets.core.AbstractReceiveListener
|
||||||
|
import io.undertow.websockets.core.BufferedBinaryMessage
|
||||||
|
import io.undertow.websockets.core.BufferedTextMessage
|
||||||
|
import io.undertow.websockets.core.WebSocketChannel
|
||||||
|
import io.undertow.websockets.core.WebSockets
|
||||||
|
import io.undertow.websockets.spi.WebSocketHttpExchange
|
||||||
|
import nl.astraeus.vst.base.Settings
|
||||||
|
import nl.astraeus.vst.base.db.Database
|
||||||
|
import nl.astraeus.vst.base.db.PatchDao
|
||||||
|
import nl.astraeus.vst.base.db.PatchEntity
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.security.MessageDigest
|
||||||
|
|
||||||
|
class WebsocketHandler(
|
||||||
|
val session: Session?
|
||||||
|
) : AbstractReceiveListener(), WebSocketConnectionCallback {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// Ensure the data directory exists
|
||||||
|
private val filesDir = File(Settings.dataDir, "files").apply {
|
||||||
|
if (!exists()) {
|
||||||
|
mkdirs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fileExists(hash: String): Boolean {
|
||||||
|
check(hash.length == 64) { "Hash must be 64 characters long" }
|
||||||
|
|
||||||
|
var currentDir = filesDir
|
||||||
|
var remaining = hash
|
||||||
|
while(remaining.length > 8) {
|
||||||
|
val subDir = remaining.substring(0, 8)
|
||||||
|
currentDir = File(currentDir, subDir)
|
||||||
|
if (!currentDir.exists()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
remaining = remaining.substring(8)
|
||||||
|
}
|
||||||
|
|
||||||
|
return File(currentDir, remaining).exists()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get file from hash, using subdirectories based on hash
|
||||||
|
fun getFileFromHash(hash: String): File {
|
||||||
|
check(hash.length == 64) { "Hash must be 64 characters long" }
|
||||||
|
|
||||||
|
var currentDir = filesDir
|
||||||
|
var remaining = hash
|
||||||
|
while(remaining.length > 8) {
|
||||||
|
val subDir = remaining.substring(0, 8)
|
||||||
|
currentDir = File(currentDir, subDir)
|
||||||
|
if (!currentDir.exists()) {
|
||||||
|
currentDir.mkdirs()
|
||||||
|
}
|
||||||
|
remaining = remaining.substring(8)
|
||||||
|
}
|
||||||
|
|
||||||
|
return File(currentDir, remaining)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create SHA-1 hash from binary data
|
||||||
|
fun createHashFromBytes(bytes: ByteArray): String {
|
||||||
|
val md = MessageDigest.getInstance("SHA-256")
|
||||||
|
val digest = md.digest(bytes)
|
||||||
|
return digest.joinToString("") { "%02x".format(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConnect(exchange: WebSocketHttpExchange, channel: WebSocketChannel) {
|
||||||
|
channel.receiveSetter.set(this)
|
||||||
|
channel.resumeReceives()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFullTextMessage(channel: WebSocketChannel, message: BufferedTextMessage) {
|
||||||
|
val vstSession = session?.getAttribute("html-session") as? VstSession
|
||||||
|
|
||||||
|
val data = message.data
|
||||||
|
val commandLength = data.indexOf('\n')
|
||||||
|
if (commandLength > 0) {
|
||||||
|
val command = data.substring(0, commandLength)
|
||||||
|
val value = data.substring(commandLength + 1)
|
||||||
|
|
||||||
|
when (command) {
|
||||||
|
"SAVE" -> {
|
||||||
|
val patchId = vstSession?.patchId
|
||||||
|
if (patchId != null) {
|
||||||
|
Database.transaction {
|
||||||
|
val patchEntity = PatchDao.findById(patchId)
|
||||||
|
|
||||||
|
if (patchEntity != null) {
|
||||||
|
PatchDao.update(patchEntity.copy(patch = value))
|
||||||
|
} else {
|
||||||
|
PatchDao.insert(PatchEntity(0, patchId, value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WebSockets.sendText(
|
||||||
|
"SAVED\n$patchId",
|
||||||
|
channel,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"LOAD" -> {
|
||||||
|
val patchId = vstSession?.patchId
|
||||||
|
if (patchId != null) {
|
||||||
|
Database.transaction {
|
||||||
|
val patchEntity = PatchDao.findById(patchId)
|
||||||
|
|
||||||
|
if (patchEntity != null) {
|
||||||
|
WebSockets.sendText(
|
||||||
|
"LOAD\n${patchEntity.patch}",
|
||||||
|
channel,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"LOAD_BINARY" -> {
|
||||||
|
val hash = value.trim()
|
||||||
|
if (hash.isNotEmpty()) {
|
||||||
|
if (fileExists(hash)) {
|
||||||
|
val file = getFileFromHash(hash)
|
||||||
|
if (file.exists() && file.isFile) {
|
||||||
|
val bytes = file.readBytes()
|
||||||
|
WebSockets.sendBinary(
|
||||||
|
ByteBuffer.wrap(bytes),
|
||||||
|
channel,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFullBinaryMessage(
|
||||||
|
channel: WebSocketChannel?,
|
||||||
|
message: BufferedBinaryMessage?
|
||||||
|
) {
|
||||||
|
// Process binary message: create hash from binary, save file in data/files/ directory,
|
||||||
|
// sub directories are 5 characters of the hash per directory
|
||||||
|
if (channel != null && message != null) {
|
||||||
|
try {
|
||||||
|
// Get the binary data
|
||||||
|
val pooled = message.data
|
||||||
|
val resources = pooled.resource
|
||||||
|
|
||||||
|
// Collect all bytes from all buffers
|
||||||
|
var totalSize = 0
|
||||||
|
|
||||||
|
// First pass: collect all bytes and calculate total size
|
||||||
|
for (buffer in resources) {
|
||||||
|
totalSize += buffer.remaining()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine all bytes into a single array
|
||||||
|
val combinedBytes = ByteArray(totalSize)
|
||||||
|
var position = 0
|
||||||
|
for (buffer in resources) {
|
||||||
|
val remaining = buffer.remaining()
|
||||||
|
buffer.get(combinedBytes, position, remaining)
|
||||||
|
position += remaining
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create hash from combined binary data
|
||||||
|
val hash = createHashFromBytes(combinedBytes)
|
||||||
|
|
||||||
|
// Save file in data/files/ directory with subdirectories
|
||||||
|
val file = getFileFromHash(hash)
|
||||||
|
|
||||||
|
// Use FileOutputStream to write all bytes at once
|
||||||
|
FileOutputStream(file).use { outputStream ->
|
||||||
|
outputStream.write(combinedBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the hash back to the client
|
||||||
|
WebSockets.sendText("BINARY_SAVED\n$hash", channel, null)
|
||||||
|
|
||||||
|
// Free the pooled resource after processing
|
||||||
|
pooled.free()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
WebSockets.sendText("ERROR\n${e.message}", channel, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import nl.astraeus.komp.Komponent
|
|||||||
import nl.astraeus.komp.UnsafeMode
|
import nl.astraeus.komp.UnsafeMode
|
||||||
import nl.astraeus.vst.ui.css.CssSettings
|
import nl.astraeus.vst.ui.css.CssSettings
|
||||||
import nl.astraeus.vst.ui.view.MainView
|
import nl.astraeus.vst.ui.view.MainView
|
||||||
|
import nl.astraeus.vst.ui.ws.WebsocketClient
|
||||||
|
|
||||||
val mainView: MainView by lazy {
|
val mainView: MainView by lazy {
|
||||||
MainView()
|
MainView()
|
||||||
@@ -16,4 +17,8 @@ fun main() {
|
|||||||
|
|
||||||
Komponent.unsafeMode = UnsafeMode.UNSAFE_SVG_ONLY
|
Komponent.unsafeMode = UnsafeMode.UNSAFE_SVG_ONLY
|
||||||
Komponent.create(document.body!!, mainView)
|
Komponent.create(document.body!!, mainView)
|
||||||
|
|
||||||
|
WebsocketClient.connect {
|
||||||
|
println("Connected")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,15 @@
|
|||||||
|
|
||||||
package nl.astraeus.vst.ui.view
|
package nl.astraeus.vst.ui.view
|
||||||
|
|
||||||
|
import kotlinx.html.InputType
|
||||||
import kotlinx.html.div
|
import kotlinx.html.div
|
||||||
import kotlinx.html.h1
|
import kotlinx.html.h1
|
||||||
import kotlinx.html.hr
|
import kotlinx.html.hr
|
||||||
|
import kotlinx.html.input
|
||||||
|
import kotlinx.html.js.onChangeFunction
|
||||||
import kotlinx.html.js.onClickFunction
|
import kotlinx.html.js.onClickFunction
|
||||||
import kotlinx.html.option
|
import kotlinx.html.option
|
||||||
|
import kotlinx.html.org.w3c.dom.events.Event
|
||||||
import kotlinx.html.select
|
import kotlinx.html.select
|
||||||
import kotlinx.html.span
|
import kotlinx.html.span
|
||||||
import kotlinx.html.style
|
import kotlinx.html.style
|
||||||
@@ -37,6 +41,11 @@ import nl.astraeus.vst.ui.css.Css.defineCss
|
|||||||
import nl.astraeus.vst.ui.css.Css.noTextSelect
|
import nl.astraeus.vst.ui.css.Css.noTextSelect
|
||||||
import nl.astraeus.vst.ui.css.CssName
|
import nl.astraeus.vst.ui.css.CssName
|
||||||
import nl.astraeus.vst.ui.css.hover
|
import nl.astraeus.vst.ui.css.hover
|
||||||
|
import nl.astraeus.vst.ui.ws.WebsocketClient
|
||||||
|
import org.w3c.dom.HTMLInputElement
|
||||||
|
import org.w3c.files.File
|
||||||
|
import org.w3c.files.FileList
|
||||||
|
import org.w3c.files.get
|
||||||
|
|
||||||
class MainView : Komponent() {
|
class MainView : Komponent() {
|
||||||
private var messages: MutableList<String> = ArrayList()
|
private var messages: MutableList<String> = ArrayList()
|
||||||
@@ -113,6 +122,16 @@ class MainView : Komponent() {
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
span {
|
||||||
|
+"Upload file: "
|
||||||
|
input {
|
||||||
|
type = InputType.file
|
||||||
|
onChangeFunction = {
|
||||||
|
fileInputSelectHandler(it)
|
||||||
|
requestUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
@@ -134,209 +153,226 @@ class MainView : Komponent() {
|
|||||||
}
|
}
|
||||||
hr {}
|
hr {}
|
||||||
div {
|
div {
|
||||||
include(KeyboardComponent(
|
include(
|
||||||
keyboardWidth = keyboardWidth,
|
KeyboardComponent(
|
||||||
keyboardHeight = keyboardWidth / 2,
|
keyboardWidth = keyboardWidth,
|
||||||
onNoteDown = { println("Note down: $it") },
|
keyboardHeight = keyboardWidth / 2,
|
||||||
onNoteUp = { println("Note up: $it") },
|
onNoteDown = { println("Note down: $it") },
|
||||||
))
|
onNoteUp = { println("Note up: $it") },
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
div {
|
div {
|
||||||
span(ButtonBarCss.name) {
|
span(ButtonBarCss.name) {
|
||||||
+"SAVE"
|
+"SAVE"
|
||||||
onClickFunction = {
|
onClickFunction = {
|
||||||
val patch = VstChipWorklet.save().copy(
|
val patch = VstChipWorklet.save().copy(
|
||||||
midiId = Midi.currentInput?.id ?: "",
|
midiId = Midi.currentInput?.id ?: "",
|
||||||
midiName = Midi.currentInput?.name ?: ""
|
midiName = Midi.currentInput?.name ?: ""
|
||||||
)
|
)
|
||||||
|
|
||||||
WebsocketClient.send("SAVE\n${JSON.stringify(patch)}")
|
WebsocketClient.send("SAVE\n${JSON.stringify(patch)}")
|
||||||
}
|
|
||||||
}
|
|
||||||
span(ButtonBarCss.name) {
|
|
||||||
+"STOP"
|
|
||||||
onClickFunction = {
|
|
||||||
VstChipWorklet.postDirectlyToWorklet(
|
|
||||||
TimedMidiMessage(getCurrentTime(), (0xb0 + midiChannel).toByte(), 123, 0)
|
|
||||||
.data.buffer.data
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
span(ButtonBarCss.name) {
|
||||||
|
+"STOP"
|
||||||
|
onClickFunction = {
|
||||||
|
VstChipWorklet.postDirectlyToWorklet(
|
||||||
|
TimedMidiMessage(getCurrentTime(), (0xb0 + midiChannel).toByte(), 123, 0)
|
||||||
|
.data.buffer.data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
*/
|
*/
|
||||||
div(ControlsCss.name) {
|
div(ControlsCss.name) {
|
||||||
include(
|
include(
|
||||||
ExpKnobComponent(
|
ExpKnobComponent(
|
||||||
value = 0.001,
|
value = 0.001,
|
||||||
label = "Volume",
|
label = "Volume",
|
||||||
minValue = 0.001,
|
minValue = 0.001,
|
||||||
maxValue = 1.0,
|
maxValue = 1.0,
|
||||||
step = 10.0 / 127.0,
|
step = 10.0 / 127.0,
|
||||||
width = 100,
|
width = 100,
|
||||||
height = 120,
|
height = 120,
|
||||||
) { value ->
|
) { value ->
|
||||||
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
include(
|
include(
|
||||||
KnobComponent(
|
KnobComponent(
|
||||||
value = 0.5,
|
value = 0.5,
|
||||||
label = "Duty cycle",
|
label = "Duty cycle",
|
||||||
minValue = 0.0,
|
minValue = 0.0,
|
||||||
maxValue = 1.0,
|
maxValue = 1.0,
|
||||||
step = 2.0 / 127.0,
|
step = 2.0 / 127.0,
|
||||||
width = 100,
|
width = 100,
|
||||||
height = 120,
|
height = 120,
|
||||||
) { value ->
|
) { value ->
|
||||||
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
include(
|
include(
|
||||||
KnobComponent(
|
KnobComponent(
|
||||||
value = 0.5,
|
value = 0.5,
|
||||||
label = "Duty cycle",
|
label = "Duty cycle",
|
||||||
minValue = 0.0,
|
minValue = 0.0,
|
||||||
maxValue = 1.0,
|
maxValue = 1.0,
|
||||||
step = 2.0 / 127.0,
|
step = 2.0 / 127.0,
|
||||||
width = 500,
|
width = 500,
|
||||||
height = 600,
|
height = 600,
|
||||||
) { value ->
|
) { value ->
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fileInputSelectHandler(event: Event) {
|
||||||
|
val target = event.target as? HTMLInputElement
|
||||||
|
val list: FileList? = target?.files
|
||||||
|
|
||||||
|
if (list == null || list.length != 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val file: File? = list[0]
|
||||||
|
|
||||||
|
file?.let { f: File ->
|
||||||
|
WebsocketClient.send(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object MainViewCss : CssName() {
|
||||||
|
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()
|
||||||
|
|
||||||
|
init {
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object MainViewCss : CssName() {
|
private fun Style.commonButton() {
|
||||||
object MainDivCss : CssName()
|
display(Display.inlineBlock)
|
||||||
object ActiveCss : CssName()
|
padding(1.rem)
|
||||||
object ButtonCss : CssName()
|
backgroundColor(Css.currentStyle.buttonBackgroundColor)
|
||||||
object ButtonBarCss : CssName()
|
borderColor(Css.currentStyle.buttonBorderColor)
|
||||||
object SelectedCss : CssName()
|
borderWidth(Css.currentStyle.buttonBorderWidth)
|
||||||
object NoteBarCss : CssName()
|
color(Css.currentStyle.mainFontColor)
|
||||||
object StartSplashCss : CssName()
|
|
||||||
object StartBoxCss : CssName()
|
|
||||||
object StartButtonCss : CssName()
|
|
||||||
object ControlsCss : CssName()
|
|
||||||
|
|
||||||
init {
|
hover {
|
||||||
defineCss {
|
backgroundColor(Css.currentStyle.buttonBackgroundColor.hover())
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
and(SelectedCss.cls()) {
|
||||||
private fun Style.commonButton() {
|
backgroundColor(Css.currentStyle.buttonBackgroundColor.hover().hover().hover())
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
@file:OptIn(ExperimentalJsExport::class)
|
||||||
|
|
||||||
|
package nl.astraeus.vst.ui.ws
|
||||||
|
|
||||||
|
import kotlinx.browser.window
|
||||||
|
import org.khronos.webgl.ArrayBuffer
|
||||||
|
import org.w3c.dom.MessageEvent
|
||||||
|
import org.w3c.dom.WebSocket
|
||||||
|
import org.w3c.dom.events.Event
|
||||||
|
import org.w3c.files.Blob
|
||||||
|
|
||||||
|
private const val WEBSOCKET_PART_SIZE = 65000
|
||||||
|
|
||||||
|
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}:${window.location.port}/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")
|
||||||
|
} else if (data is ArrayBuffer) {
|
||||||
|
console.log("Received binary message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun send(file: Blob) {
|
||||||
|
websocket?.send(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user