Save patch
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -41,7 +41,7 @@ bin/
|
|||||||
### Mac OS ###
|
### Mac OS ###
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
web
|
/web
|
||||||
|
|
||||||
.kotlin
|
.kotlin
|
||||||
.idea
|
.idea
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
package nl.astraeus.vst.chip
|
package nl.astraeus.vst.chip
|
||||||
|
|
||||||
|
import nl.astraeus.vst.ADSR
|
||||||
import nl.astraeus.vst.AudioWorkletProcessor
|
import nl.astraeus.vst.AudioWorkletProcessor
|
||||||
import nl.astraeus.vst.Note
|
import nl.astraeus.vst.Note
|
||||||
|
import nl.astraeus.vst.currentTime
|
||||||
import nl.astraeus.vst.registerProcessor
|
import nl.astraeus.vst.registerProcessor
|
||||||
import nl.astraeus.vst.sampleRate
|
import nl.astraeus.vst.sampleRate
|
||||||
import org.khronos.webgl.Float32Array
|
import org.khronos.webgl.Float32Array
|
||||||
@@ -19,14 +21,6 @@ import kotlin.math.sin
|
|||||||
val POLYPHONICS = 10
|
val POLYPHONICS = 10
|
||||||
val PI2 = PI * 2
|
val PI2 = PI * 2
|
||||||
|
|
||||||
@ExperimentalJsExport
|
|
||||||
@JsExport
|
|
||||||
enum class NoteState {
|
|
||||||
ON,
|
|
||||||
RELEASED,
|
|
||||||
OFF
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExperimentalJsExport
|
@ExperimentalJsExport
|
||||||
@JsExport
|
@JsExport
|
||||||
class PlayingNote(
|
class PlayingNote(
|
||||||
@@ -35,17 +29,15 @@ class PlayingNote(
|
|||||||
) {
|
) {
|
||||||
fun retrigger(velocity: Int) {
|
fun retrigger(velocity: Int) {
|
||||||
this.velocity = velocity
|
this.velocity = velocity
|
||||||
state = NoteState.ON
|
|
||||||
sample = 0
|
sample = 0
|
||||||
attackSamples = 2500
|
noteStart = currentTime
|
||||||
releaseSamples = 10000
|
noteRelease = null
|
||||||
}
|
}
|
||||||
|
|
||||||
var state = NoteState.OFF
|
var noteStart = currentTime
|
||||||
|
var noteRelease: Double? = null
|
||||||
var cycleOffset = 0.0
|
var cycleOffset = 0.0
|
||||||
var sample = 0
|
var sample = 0
|
||||||
var attackSamples = 2500
|
|
||||||
var releaseSamples = 10000
|
|
||||||
var actualVolume = 0f
|
var actualVolume = 0f
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,11 +60,8 @@ enum class RecordingState {
|
|||||||
@JsExport
|
@JsExport
|
||||||
class VstChipProcessor : AudioWorkletProcessor() {
|
class VstChipProcessor : AudioWorkletProcessor() {
|
||||||
var midiChannel = 0
|
var midiChannel = 0
|
||||||
val notes = Array(POLYPHONICS) {
|
val notes = Array<PlayingNote?>(POLYPHONICS) { null }
|
||||||
PlayingNote(
|
|
||||||
0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
var waveform = Waveform.SINE.ordinal
|
var waveform = Waveform.SINE.ordinal
|
||||||
var volume = 0.75f
|
var volume = 0.75f
|
||||||
var dutyCycle = 0.5
|
var dutyCycle = 0.5
|
||||||
@@ -80,6 +69,12 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
|||||||
var fmAmp = 0.0
|
var fmAmp = 0.0
|
||||||
var amFreq = 0.0
|
var amFreq = 0.0
|
||||||
var amAmp = 0.0
|
var amAmp = 0.0
|
||||||
|
|
||||||
|
var attack = 0.1
|
||||||
|
var decay = 0.2
|
||||||
|
var sustain = 0.5
|
||||||
|
var release = 0.2
|
||||||
|
|
||||||
val sampleLength = 1 / sampleRate.toDouble()
|
val sampleLength = 1 / sampleRate.toDouble()
|
||||||
|
|
||||||
val recordingBuffer = Float32Array(sampleRate / 60)
|
val recordingBuffer = Float32Array(sampleRate / 60)
|
||||||
@@ -198,27 +193,49 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
|||||||
val value = bytes[2]
|
val value = bytes[2]
|
||||||
|
|
||||||
when (knob) {
|
when (knob) {
|
||||||
0x46 -> {
|
7 -> {
|
||||||
volume = value / 127f
|
volume = value / 127f
|
||||||
}
|
}
|
||||||
0x4a -> {
|
|
||||||
dutyCycle = value / 127.0
|
|
||||||
}
|
|
||||||
|
|
||||||
0x4b -> {
|
|
||||||
fmFreq = value / 127.0
|
|
||||||
}
|
|
||||||
|
|
||||||
0x4c -> {
|
|
||||||
fmAmp = value / 127.0
|
|
||||||
}
|
|
||||||
|
|
||||||
0x47 -> {
|
0x47 -> {
|
||||||
|
dutyCycle = value / 127.0
|
||||||
|
}
|
||||||
|
|
||||||
|
0x4a -> {
|
||||||
|
fmFreq = value / 127.0
|
||||||
|
}
|
||||||
|
|
||||||
|
0x4b -> {
|
||||||
|
fmAmp = value / 127.0
|
||||||
|
}
|
||||||
|
|
||||||
|
0x4c -> {
|
||||||
amFreq = value / 127.0
|
amFreq = value / 127.0
|
||||||
}
|
}
|
||||||
|
0x4d -> {
|
||||||
|
amAmp = value / 127.0
|
||||||
|
}
|
||||||
|
|
||||||
|
0x49 -> {
|
||||||
|
attack = value / 127.0
|
||||||
|
}
|
||||||
|
|
||||||
|
0x4b -> {
|
||||||
|
decay = value / 127.0
|
||||||
|
}
|
||||||
|
|
||||||
|
0x46 -> {
|
||||||
|
sustain = value / 127.0
|
||||||
|
}
|
||||||
|
|
||||||
0x48 -> {
|
0x48 -> {
|
||||||
amAmp = value / 127.0
|
release = value / 127.0
|
||||||
|
}
|
||||||
|
|
||||||
|
123 -> {
|
||||||
|
for (note in notes) {
|
||||||
|
note?.noteRelease = currentTime
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,21 +255,17 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
|||||||
|
|
||||||
private fun noteOn(note: Int, velocity: Int) {
|
private fun noteOn(note: Int, velocity: Int) {
|
||||||
for (i in 0 until POLYPHONICS) {
|
for (i in 0 until POLYPHONICS) {
|
||||||
if (notes[i].note == note) {
|
if (notes[i]?.note == note) {
|
||||||
notes[i].retrigger(velocity)
|
notes[i]?.retrigger(velocity)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (i in 0 until POLYPHONICS) {
|
for (i in 0 until POLYPHONICS) {
|
||||||
if (notes[i].state == NoteState.OFF) {
|
if (notes[i] == null) {
|
||||||
notes[i] = PlayingNote(
|
notes[i] = PlayingNote(
|
||||||
note,
|
note,
|
||||||
velocity
|
velocity
|
||||||
)
|
)
|
||||||
notes[i].state = NoteState.ON
|
|
||||||
|
|
||||||
val n = Note.fromMidi(note)
|
|
||||||
//console.log("Playing note: ${n.sharp} (${n.freq})")
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -260,8 +273,8 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
|||||||
|
|
||||||
private fun noteOff(note: Int) {
|
private fun noteOff(note: Int) {
|
||||||
for (i in 0 until POLYPHONICS) {
|
for (i in 0 until POLYPHONICS) {
|
||||||
if (notes[i].note == note && notes[i].state == NoteState.ON) {
|
if (notes[i]?.note == note) {
|
||||||
notes[i].state = NoteState.RELEASED
|
notes[i]?.noteRelease = currentTime
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -279,7 +292,7 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
|||||||
|
|
||||||
var lowestNote = 200
|
var lowestNote = 200
|
||||||
for (note in notes) {
|
for (note in notes) {
|
||||||
if (note.state != NoteState.OFF) {
|
if (note != null) {
|
||||||
lowestNote = min(lowestNote, note.note)
|
lowestNote = min(lowestNote, note.note)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -289,23 +302,25 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
|||||||
recordingStart = 0
|
recordingStart = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
for (note in notes) {
|
for ((index, note) in notes.withIndex()) {
|
||||||
if (note.state != NoteState.OFF) {
|
if (note != null) {
|
||||||
val sampleDelta = Note.fromMidi(note.note).sampleDelta
|
val sampleDelta = Note.fromMidi(note.note).sampleDelta
|
||||||
|
|
||||||
for (i in 0 until samples) {
|
for (i in 0 until samples) {
|
||||||
var targetVolume = note.velocity / 127f
|
var targetVolume = note.velocity / 127f
|
||||||
if (note.state == NoteState.ON && note.sample < note.attackSamples) {
|
targetVolume *= ADSR.calculate(
|
||||||
note.attackSamples--
|
attack,
|
||||||
targetVolume *= ( 1f - (note.attackSamples / 2500f))
|
decay,
|
||||||
} else if (note.state == NoteState.RELEASED) {
|
sustain,
|
||||||
note.releaseSamples--
|
release,
|
||||||
targetVolume *= (note.releaseSamples / 10000f)
|
note.noteStart,
|
||||||
}
|
currentTime,
|
||||||
|
note.noteRelease
|
||||||
|
).toFloat()
|
||||||
note.actualVolume += (targetVolume - note.actualVolume) * 0.0001f
|
note.actualVolume += (targetVolume - note.actualVolume) * 0.0001f
|
||||||
|
|
||||||
if (note.state == NoteState.RELEASED && note.actualVolume <= 0) {
|
if (note.noteRelease != null && note.actualVolume <= 0.01) {
|
||||||
note.state = NoteState.OFF
|
notes[index] = null
|
||||||
}
|
}
|
||||||
|
|
||||||
var cycleOffset = note.cycleOffset
|
var cycleOffset = note.cycleOffset
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ kotlin {
|
|||||||
implementation(project(":common"))
|
implementation(project(":common"))
|
||||||
//base
|
//base
|
||||||
api("nl.astraeus:kotlin-css-generator:1.0.7")
|
api("nl.astraeus:kotlin-css-generator:1.0.7")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val jsMain by getting {
|
val jsMain by getting {
|
||||||
@@ -57,8 +58,13 @@ kotlin {
|
|||||||
//base
|
//base
|
||||||
|
|
||||||
implementation("io.undertow:undertow-core:2.3.14.Final")
|
implementation("io.undertow:undertow-core:2.3.14.Final")
|
||||||
|
implementation("io.undertow:undertow-websockets-jsr:2.3.14.Final")
|
||||||
implementation("org.jboss.xnio:xnio-nio:3.8.16.Final")
|
implementation("org.jboss.xnio:xnio-nio:3.8.16.Final")
|
||||||
|
|
||||||
|
implementation("org.xerial:sqlite-jdbc:3.46.0.0")
|
||||||
|
implementation("com.zaxxer:HikariCP:4.0.3")
|
||||||
|
implementation("nl.astraeus:simple-jdbc-stats:1.6.1")
|
||||||
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.11.0")
|
implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.11.0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
32
src/commonMain/kotlin/nl/astraeus/vst/chip/PatchDTO.kt
Normal file
32
src/commonMain/kotlin/nl/astraeus/vst/chip/PatchDTO.kt
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package nl.astraeus.vst.chip
|
||||||
|
|
||||||
|
import kotlin.js.JsName
|
||||||
|
|
||||||
|
data class PatchDTO(
|
||||||
|
@JsName("waveform")
|
||||||
|
val waveform: Int = 0,
|
||||||
|
@JsName("midiId")
|
||||||
|
val midiId: String = "",
|
||||||
|
@JsName("midiChannel")
|
||||||
|
var midiChannel: Int = 0,
|
||||||
|
@JsName("volume")
|
||||||
|
var volume: Double = 0.75,
|
||||||
|
@JsName("dutyCycle")
|
||||||
|
var dutyCycle: Double = 0.5,
|
||||||
|
@JsName("fmModFreq")
|
||||||
|
var fmModFreq: Double = 0.0,
|
||||||
|
@JsName("fmModAmp")
|
||||||
|
var fmModAmp: Double = 0.0,
|
||||||
|
@JsName("amModFreq")
|
||||||
|
var amModFreq: Double = 0.0,
|
||||||
|
@JsName("amModAmp")
|
||||||
|
var amModAmp: Double = 0.0,
|
||||||
|
@JsName("attack")
|
||||||
|
var attack: Double = 0.1,
|
||||||
|
@JsName("decay")
|
||||||
|
var decay: Double = 0.2,
|
||||||
|
@JsName("sustain")
|
||||||
|
var sustain: Double = 0.5,
|
||||||
|
@JsName("release")
|
||||||
|
var release: Double = 0.2,
|
||||||
|
)
|
||||||
53
src/commonMain/kotlin/nl/astraeus/vst/chip/logger/Logger.kt
Normal file
53
src/commonMain/kotlin/nl/astraeus/vst/chip/logger/Logger.kt
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package nl.astraeus.vst.chip.logger
|
||||||
|
|
||||||
|
val log = Logger
|
||||||
|
|
||||||
|
enum class LogLevel {
|
||||||
|
TRACE,
|
||||||
|
DEBUG,
|
||||||
|
INFO,
|
||||||
|
WARN,
|
||||||
|
ERROR,
|
||||||
|
FATAL
|
||||||
|
}
|
||||||
|
|
||||||
|
object Logger {
|
||||||
|
var level: LogLevel = LogLevel.INFO
|
||||||
|
|
||||||
|
fun trace(message: () -> String?) {
|
||||||
|
if (level.ordinal <= LogLevel.TRACE.ordinal) {
|
||||||
|
println("TRACE: ${message()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun debug(message: () -> String?) {
|
||||||
|
if (level.ordinal <= LogLevel.DEBUG.ordinal) {
|
||||||
|
println("DEBUG: ${message()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun info(message: () -> String?) {
|
||||||
|
if (level.ordinal <= LogLevel.INFO.ordinal) {
|
||||||
|
println("INFO: ${message()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun warn(e: Throwable? = null, message: () -> String?) {
|
||||||
|
if (level.ordinal <= LogLevel.WARN.ordinal) {
|
||||||
|
println("WARN: ${message()}")
|
||||||
|
e?.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun error(e: Throwable? = null, message: () -> String?) {
|
||||||
|
if (level.ordinal <= LogLevel.ERROR.ordinal) {
|
||||||
|
println("ERROR: ${message()}")
|
||||||
|
e?.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fatal(e: Throwable, message: () -> String?) {
|
||||||
|
println("FATAL: ${message()}")
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,13 @@
|
|||||||
package nl.astraeus.vst.chip
|
package nl.astraeus.vst.chip
|
||||||
|
|
||||||
import kotlinx.browser.document
|
import kotlinx.browser.document
|
||||||
import kotlinx.browser.window
|
|
||||||
import nl.astraeus.komp.Komponent
|
import nl.astraeus.komp.Komponent
|
||||||
import nl.astraeus.komp.UnsafeMode
|
import nl.astraeus.komp.UnsafeMode
|
||||||
import nl.astraeus.vst.chip.midi.Broadcaster
|
import nl.astraeus.vst.chip.logger.log
|
||||||
import nl.astraeus.vst.chip.midi.Midi
|
import nl.astraeus.vst.chip.midi.Midi
|
||||||
import nl.astraeus.vst.chip.midi.MidiMessage
|
|
||||||
import nl.astraeus.vst.chip.view.MainView
|
import nl.astraeus.vst.chip.view.MainView
|
||||||
|
import nl.astraeus.vst.chip.ws.WebsocketClient
|
||||||
import nl.astraeus.vst.ui.css.CssSettings
|
import nl.astraeus.vst.ui.css.CssSettings
|
||||||
import org.khronos.webgl.Uint8Array
|
|
||||||
|
|
||||||
fun main() {
|
fun main() {
|
||||||
CssSettings.shortId = false
|
CssSettings.shortId = false
|
||||||
@@ -20,16 +18,7 @@ fun main() {
|
|||||||
|
|
||||||
Midi.start()
|
Midi.start()
|
||||||
|
|
||||||
console.log("Performance", window.performance)
|
WebsocketClient.connect {
|
||||||
Broadcaster.getChannel(0).postMessage(
|
log.debug { "Connected to server" }
|
||||||
MidiMessage(
|
}
|
||||||
Uint8Array(arrayOf(0x80.toByte(), 60, 60)),
|
|
||||||
window.performance.now()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
window.setInterval({
|
|
||||||
Broadcaster.sync()
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package nl.astraeus.vst.chip.audio
|
package nl.astraeus.vst.chip.audio
|
||||||
|
|
||||||
|
import nl.astraeus.vst.chip.PatchDTO
|
||||||
import nl.astraeus.vst.chip.view.MainView
|
import nl.astraeus.vst.chip.view.MainView
|
||||||
import nl.astraeus.vst.chip.view.WaveformView
|
import nl.astraeus.vst.chip.view.WaveformView
|
||||||
import nl.astraeus.vst.ui.util.uInt8ArrayOf
|
import nl.astraeus.vst.ui.util.uInt8ArrayOf
|
||||||
@@ -10,7 +11,7 @@ import org.w3c.dom.MessageEvent
|
|||||||
import kotlin.experimental.and
|
import kotlin.experimental.and
|
||||||
|
|
||||||
object VstChipWorklet : AudioNode(
|
object VstChipWorklet : AudioNode(
|
||||||
"vst-chip-worklet.js",
|
"/vst-chip-worklet.js",
|
||||||
"vst-chip-processor"
|
"vst-chip-processor"
|
||||||
) {
|
) {
|
||||||
var waveform: Int = 0
|
var waveform: Int = 0
|
||||||
@@ -30,44 +31,74 @@ object VstChipWorklet : AudioNode(
|
|||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
super.postMessage(
|
super.postMessage(
|
||||||
uInt8ArrayOf(0xb0 + midiChannel, 0x46, (value * 127).toInt())
|
uInt8ArrayOf(0xb0 + midiChannel, 7, (value * 127).toInt())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
var dutyCycle = 0.5
|
var dutyCycle = 0.5
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
super.postMessage(
|
super.postMessage(
|
||||||
uInt8ArrayOf(0xb0 + midiChannel, 0x4a, (value * 127).toInt())
|
uInt8ArrayOf(0xb0 + midiChannel, 0x47, (value * 127).toInt())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
var fmModFreq = 0.0
|
var fmModFreq = 0.0
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
super.postMessage(
|
super.postMessage(
|
||||||
uInt8ArrayOf(0xb0 + midiChannel, 0x4b, (value * 127).toInt())
|
uInt8ArrayOf(0xb0 + midiChannel, 0x4a, (value * 127).toInt())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
var fmModAmp = 0.0
|
var fmModAmp = 0.0
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
super.postMessage(
|
super.postMessage(
|
||||||
uInt8ArrayOf(0xb0 + midiChannel, 0x4c, (value * 127).toInt())
|
uInt8ArrayOf(0xb0 + midiChannel, 0x4b, (value * 127).toInt())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
var amModFreq = 0.0
|
var amModFreq = 0.0
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
super.postMessage(
|
super.postMessage(
|
||||||
uInt8ArrayOf(0xb0 + midiChannel, 0x47, (value * 127).toInt())
|
uInt8ArrayOf(0xb0 + midiChannel, 0x4c, (value * 127).toInt())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
var amModAmp = 0.0
|
var amModAmp = 0.0
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
super.postMessage(
|
||||||
|
uInt8ArrayOf(0xb0 + midiChannel, 0x4d, (value * 127).toInt())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var attack = 0.1
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
super.postMessage(
|
||||||
|
uInt8ArrayOf(0xb0 + midiChannel, 0x49, (value * 127).toInt())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
var decay = 0.2
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
super.postMessage(
|
||||||
|
uInt8ArrayOf(0xb0 + midiChannel, 0x4b, (value * 127).toInt())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
var sustain = 0.5
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
super.postMessage(
|
||||||
|
uInt8ArrayOf(0xb0 + midiChannel, 0x46, (value * 127).toInt())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
var release = 0.2
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
super.postMessage(
|
super.postMessage(
|
||||||
uInt8ArrayOf(0xb0 + midiChannel, 0x48, (value * 127).toInt())
|
uInt8ArrayOf(0xb0 + midiChannel, 0x48, (value * 127).toInt())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var recording: Float32Array? = null
|
var recording: Float32Array? = null
|
||||||
|
|
||||||
override fun onMessage(message: MessageEvent) {
|
override fun onMessage(message: MessageEvent) {
|
||||||
@@ -80,6 +111,10 @@ object VstChipWorklet : AudioNode(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun postDirectlyToWorklet(msg: Any) {
|
||||||
|
super.postMessage(msg)
|
||||||
|
}
|
||||||
|
|
||||||
override fun postMessage(msg: Any) {
|
override fun postMessage(msg: Any) {
|
||||||
if (msg is Uint8Array) {
|
if (msg is Uint8Array) {
|
||||||
if (
|
if (
|
||||||
@@ -133,4 +168,36 @@ object VstChipWorklet : AudioNode(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun load(patch: PatchDTO) {
|
||||||
|
waveform = patch.waveform
|
||||||
|
midiChannel = patch.midiChannel
|
||||||
|
volume = patch.volume
|
||||||
|
dutyCycle = patch.dutyCycle
|
||||||
|
fmModFreq = patch.fmModFreq
|
||||||
|
fmModAmp = patch.fmModAmp
|
||||||
|
amModFreq = patch.amModFreq
|
||||||
|
amModAmp = patch.amModAmp
|
||||||
|
attack = patch.attack
|
||||||
|
decay = patch.decay
|
||||||
|
sustain = patch.sustain
|
||||||
|
release = patch.release
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save(): PatchDTO {
|
||||||
|
return PatchDTO(
|
||||||
|
waveform = waveform,
|
||||||
|
midiChannel = midiChannel,
|
||||||
|
volume = volume,
|
||||||
|
dutyCycle = dutyCycle,
|
||||||
|
fmModFreq = fmModFreq,
|
||||||
|
fmModAmp = fmModAmp,
|
||||||
|
amModFreq = amModFreq,
|
||||||
|
amModAmp = amModAmp,
|
||||||
|
attack = attack,
|
||||||
|
decay = decay,
|
||||||
|
sustain = sustain,
|
||||||
|
release = release
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,13 @@ object Midi {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setInput(id: String) {
|
||||||
|
val selected = inputs.find { it.id == id }
|
||||||
|
if (selected != null) {
|
||||||
|
setInput(selected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun setInput(input: MIDIInput?) {
|
fun setInput(input: MIDIInput?) {
|
||||||
console.log("Setting input", input)
|
console.log("Setting input", input)
|
||||||
currentInput?.close()
|
currentInput?.close()
|
||||||
|
|||||||
@@ -34,14 +34,16 @@ import nl.astraeus.komp.HtmlBuilder
|
|||||||
import nl.astraeus.komp.Komponent
|
import nl.astraeus.komp.Komponent
|
||||||
import nl.astraeus.komp.currentElement
|
import nl.astraeus.komp.currentElement
|
||||||
import nl.astraeus.vst.chip.audio.VstChipWorklet
|
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.midi.Midi
|
||||||
|
import nl.astraeus.vst.chip.ws.WebsocketClient
|
||||||
import nl.astraeus.vst.ui.components.KnobComponent
|
import nl.astraeus.vst.ui.components.KnobComponent
|
||||||
import nl.astraeus.vst.ui.css.Css
|
import nl.astraeus.vst.ui.css.Css
|
||||||
import nl.astraeus.vst.ui.css.Css.defineCss
|
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 org.khronos.webgl.Uint8Array
|
import nl.astraeus.vst.ui.util.uInt8ArrayOf
|
||||||
import org.khronos.webgl.get
|
import org.khronos.webgl.get
|
||||||
import org.w3c.dom.CanvasRenderingContext2D
|
import org.w3c.dom.CanvasRenderingContext2D
|
||||||
import org.w3c.dom.HTMLCanvasElement
|
import org.w3c.dom.HTMLCanvasElement
|
||||||
@@ -75,10 +77,11 @@ object WaveformView: Komponent() {
|
|||||||
val height = ctx.canvas.height.toDouble()
|
val height = ctx.canvas.height.toDouble()
|
||||||
val halfHeight = height / 2.0
|
val halfHeight = height / 2.0
|
||||||
|
|
||||||
|
ctx.lineWidth = 2.0
|
||||||
ctx.clearRect(0.0, 0.0, width, height)
|
ctx.clearRect(0.0, 0.0, width, height)
|
||||||
val step = 1000.0 / data.length
|
val step = 1000.0 / data.length
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.strokeStyle = "rgba(255, 255, 255, 0.5)"
|
ctx.strokeStyle = "rgba(0, 255, 255, 0.5)"
|
||||||
ctx.moveTo(0.0, halfHeight)
|
ctx.moveTo(0.0, halfHeight)
|
||||||
for (i in 0 until data.length) {
|
for (i in 0 until data.length) {
|
||||||
ctx.lineTo(i * step, halfHeight - data[i] * halfHeight)
|
ctx.lineTo(i * step, halfHeight - data[i] * halfHeight)
|
||||||
@@ -119,6 +122,7 @@ object MainView : Komponent(), CssName {
|
|||||||
VstChipWorklet.create {
|
VstChipWorklet.create {
|
||||||
started = true
|
started = true
|
||||||
requestUpdate()
|
requestUpdate()
|
||||||
|
WebsocketClient.send("LOAD\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -140,6 +144,7 @@ object MainView : Komponent(), CssName {
|
|||||||
option {
|
option {
|
||||||
+mi.name
|
+mi.name
|
||||||
value = mi.id
|
value = mi.id
|
||||||
|
selected = mi.id == Midi.currentInput?.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,12 +153,7 @@ object MainView : Komponent(), CssName {
|
|||||||
if (target.value == "none") {
|
if (target.value == "none") {
|
||||||
Midi.setInput(null)
|
Midi.setInput(null)
|
||||||
} else {
|
} else {
|
||||||
val selected = Midi.inputs.find { it.id == target.value }
|
Midi.setInput(target.value)
|
||||||
if (selected != null) {
|
|
||||||
Midi.setInput(selected)
|
|
||||||
} else if (target.value == "midi-broadcast") {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,71 +172,20 @@ object MainView : Komponent(), CssName {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
span {
|
span(ButtonBarCss.name) {
|
||||||
+"Midi output: "
|
+"SAVE"
|
||||||
select {
|
onClickFunction = {
|
||||||
option {
|
val patch = VstChipWorklet.save().copy(midiId = Midi.currentInput?.id ?: "")
|
||||||
+"None"
|
|
||||||
value = "none"
|
|
||||||
}
|
|
||||||
for (mi in Midi.outputs) {
|
|
||||||
option {
|
|
||||||
+mi.name
|
|
||||||
value = mi.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeFunction = { event ->
|
WebsocketClient.send("SAVE\n${JSON.stringify(patch)}")
|
||||||
val target = event.target as HTMLSelectElement
|
|
||||||
if (target.value == "none") {
|
|
||||||
Midi.setOutput(null)
|
|
||||||
} else {
|
|
||||||
val selected = Midi.outputs.find { it.id == target.value }
|
|
||||||
if (selected != null) {
|
|
||||||
Midi.setOutput(selected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
span {
|
span(ButtonBarCss.name) {
|
||||||
+"channel:"
|
+"STOP"
|
||||||
input {
|
|
||||||
type = InputType.number
|
|
||||||
value = Midi.outputChannel.toString()
|
|
||||||
onInputFunction = { event ->
|
|
||||||
val target = event.target as HTMLInputElement
|
|
||||||
Midi.outputChannel = target.value.toInt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div {
|
|
||||||
span(ButtonCss.name) {
|
|
||||||
+"Send note on to output"
|
|
||||||
onClickFunction = {
|
onClickFunction = {
|
||||||
val data = Uint8Array(
|
VstChipWorklet.postDirectlyToWorklet(
|
||||||
arrayOf(
|
uInt8ArrayOf(0xb0 + midiChannel, 123, 0)
|
||||||
0x90.toByte(),
|
|
||||||
0x3c.toByte(),
|
|
||||||
0x70.toByte()
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
Midi.send(data, window.performance.now() + 1000)
|
|
||||||
Midi.send(data, window.performance.now() + 2000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
span(ButtonCss.name) {
|
|
||||||
+"Send note off to output"
|
|
||||||
onClickFunction = {
|
|
||||||
val data = Uint8Array(
|
|
||||||
arrayOf(
|
|
||||||
0x90.toByte(),
|
|
||||||
0x3c.toByte(),
|
|
||||||
0x0.toByte(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Midi.send(data)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -362,6 +311,60 @@ object MainView : Komponent(), CssName {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
div(ControlsCss.name) {
|
||||||
|
include(
|
||||||
|
KnobComponent(
|
||||||
|
value = VstChipWorklet.attack,
|
||||||
|
label = "Attack",
|
||||||
|
minValue = 0.0,
|
||||||
|
maxValue = 1.0,
|
||||||
|
step = 2.0 / 127.0,
|
||||||
|
width = 100,
|
||||||
|
height = 120,
|
||||||
|
) { value ->
|
||||||
|
VstChipWorklet.attack = value
|
||||||
|
}
|
||||||
|
)
|
||||||
|
include(
|
||||||
|
KnobComponent(
|
||||||
|
value = VstChipWorklet.decay,
|
||||||
|
label = "Decay",
|
||||||
|
minValue = 0.0,
|
||||||
|
maxValue = 1.0,
|
||||||
|
step = 2.0 / 127.0,
|
||||||
|
width = 100,
|
||||||
|
height = 120,
|
||||||
|
) { value ->
|
||||||
|
VstChipWorklet.decay = value
|
||||||
|
}
|
||||||
|
)
|
||||||
|
include(
|
||||||
|
KnobComponent(
|
||||||
|
value = VstChipWorklet.sustain,
|
||||||
|
label = "Sustain",
|
||||||
|
minValue = 0.0,
|
||||||
|
maxValue = 1.0,
|
||||||
|
step = 2.0 / 127.0,
|
||||||
|
width = 100,
|
||||||
|
height = 120,
|
||||||
|
) { value ->
|
||||||
|
VstChipWorklet.sustain = value
|
||||||
|
}
|
||||||
|
)
|
||||||
|
include(
|
||||||
|
KnobComponent(
|
||||||
|
value = VstChipWorklet.release,
|
||||||
|
label = "Release",
|
||||||
|
minValue = 0.0,
|
||||||
|
maxValue = 1.0,
|
||||||
|
step = 2.0 / 127.0,
|
||||||
|
width = 100,
|
||||||
|
height = 120,
|
||||||
|
) { value ->
|
||||||
|
VstChipWorklet.release = value
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
include(WaveformView)
|
include(WaveformView)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
120
src/jsMain/kotlin/nl/astraeus/vst/chip/ws/WebsocketClient.kt
Normal file
120
src/jsMain/kotlin/nl/astraeus/vst/chip/ws/WebsocketClient.kt
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
package nl.astraeus.vst.chip.ws
|
||||||
|
|
||||||
|
import kotlinx.browser.window
|
||||||
|
import nl.astraeus.vst.chip.PatchDTO
|
||||||
|
import nl.astraeus.vst.chip.audio.VstChipWorklet
|
||||||
|
import nl.astraeus.vst.chip.midi.Midi
|
||||||
|
import nl.astraeus.vst.chip.view.MainView
|
||||||
|
import org.w3c.dom.MessageEvent
|
||||||
|
import org.w3c.dom.WebSocket
|
||||||
|
import org.w3c.dom.events.Event
|
||||||
|
|
||||||
|
object WebsocketClient {
|
||||||
|
var websocket: WebSocket? = null
|
||||||
|
var interval: Int = 0
|
||||||
|
|
||||||
|
fun connect(onConnect: () -> Unit) {
|
||||||
|
close()
|
||||||
|
|
||||||
|
websocket = if (window.location.hostname.contains("localhost") || window.location.hostname.contains("192.168")) {
|
||||||
|
WebSocket("ws://${window.location.hostname}:9000/ws")
|
||||||
|
} else {
|
||||||
|
WebSocket("wss://${window.location.hostname}/ws")
|
||||||
|
}
|
||||||
|
|
||||||
|
websocket?.also { ws ->
|
||||||
|
ws.onopen = {
|
||||||
|
onOpen(ws, it)
|
||||||
|
onConnect()
|
||||||
|
}
|
||||||
|
ws.onmessage = { onMessage(ws, it) }
|
||||||
|
ws.onclose = { onClose(ws, it) }
|
||||||
|
ws.onerror = { onError(ws, it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun close() {
|
||||||
|
websocket?.close(-1, "Application closed socket.")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onOpen(
|
||||||
|
ws: WebSocket,
|
||||||
|
event: Event
|
||||||
|
) {
|
||||||
|
interval = window.setInterval({
|
||||||
|
val actualWs = websocket
|
||||||
|
|
||||||
|
if (actualWs == null) {
|
||||||
|
window.clearInterval(interval)
|
||||||
|
|
||||||
|
console.log("Connection to the server was lost!\\nPlease try again later.")
|
||||||
|
reconnect()
|
||||||
|
}
|
||||||
|
}, 10000)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reconnect() {
|
||||||
|
val actualWs = websocket
|
||||||
|
|
||||||
|
if (actualWs != null) {
|
||||||
|
if (actualWs.readyState == WebSocket.OPEN) {
|
||||||
|
console.log("Connection to the server was lost!\\nPlease try again later.")
|
||||||
|
} else {
|
||||||
|
window.setTimeout({
|
||||||
|
reconnect()
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
connect {}
|
||||||
|
|
||||||
|
window.setTimeout({
|
||||||
|
reconnect()
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onMessage(
|
||||||
|
ws: WebSocket,
|
||||||
|
event: Event
|
||||||
|
) {
|
||||||
|
if (event is MessageEvent) {
|
||||||
|
val data = event.data
|
||||||
|
|
||||||
|
if (data is String) {
|
||||||
|
console.log("Received message: $data")
|
||||||
|
if (data.startsWith("LOAD")) {
|
||||||
|
val patchJson = data.substring(5)
|
||||||
|
val patch = JSON.parse<PatchDTO>(patchJson)
|
||||||
|
|
||||||
|
Midi.setInput(patch.midiId)
|
||||||
|
VstChipWorklet.load(patch)
|
||||||
|
MainView.requestUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onClose(
|
||||||
|
ws: WebSocket,
|
||||||
|
event: Event
|
||||||
|
): dynamic {
|
||||||
|
websocket = null
|
||||||
|
|
||||||
|
return "dynamic"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onError(
|
||||||
|
ws: WebSocket,
|
||||||
|
event: Event
|
||||||
|
): dynamic {
|
||||||
|
console.log("Error websocket!", ws, event)
|
||||||
|
|
||||||
|
websocket = null
|
||||||
|
|
||||||
|
return "dynamic"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun send(message: String) {
|
||||||
|
websocket?.send(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/jvmMain/kotlin/nl/astraeus/vst/chip/GenerateId.kt
Normal file
16
src/jvmMain/kotlin/nl/astraeus/vst/chip/GenerateId.kt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package nl.astraeus.vst.chip
|
||||||
|
|
||||||
|
import java.security.SecureRandom
|
||||||
|
|
||||||
|
val idChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
val random = SecureRandom()
|
||||||
|
|
||||||
|
fun generateId(): String {
|
||||||
|
val id = StringBuilder()
|
||||||
|
|
||||||
|
for (i in 0 until 8) {
|
||||||
|
id.append(idChars[random.nextInt(idChars.length)])
|
||||||
|
}
|
||||||
|
|
||||||
|
return id.toString()
|
||||||
|
}
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
package nl.astraeus.vst.chip
|
|
||||||
|
|
||||||
@@ -1,17 +1,46 @@
|
|||||||
package nl.astraeus.vst.chip
|
package nl.astraeus.vst.chip
|
||||||
|
|
||||||
|
import com.zaxxer.hikari.HikariConfig
|
||||||
import io.undertow.Undertow
|
import io.undertow.Undertow
|
||||||
import io.undertow.UndertowOptions
|
import io.undertow.UndertowOptions
|
||||||
|
import io.undertow.server.session.InMemorySessionManager
|
||||||
|
import io.undertow.server.session.SessionAttachmentHandler
|
||||||
|
import io.undertow.server.session.SessionCookieConfig
|
||||||
|
import nl.astraeus.vst.chip.db.Database
|
||||||
|
import nl.astraeus.vst.chip.logger.LogLevel
|
||||||
|
import nl.astraeus.vst.chip.logger.Logger
|
||||||
|
import nl.astraeus.vst.chip.web.RequestHandler
|
||||||
|
|
||||||
fun main() {
|
fun main() {
|
||||||
|
Logger.level = LogLevel.DEBUG
|
||||||
|
|
||||||
Thread.setDefaultUncaughtExceptionHandler { _, e ->
|
Thread.setDefaultUncaughtExceptionHandler { _, e ->
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Class.forName("nl.astraeus.jdbc.Driver")
|
||||||
|
|
||||||
|
Database.initialize(HikariConfig().apply {
|
||||||
|
driverClassName = "nl.astraeus.jdbc.Driver"
|
||||||
|
jdbcUrl = "jdbc:stat:webServerPort=6002:jdbc:sqlite:data/chip.db"
|
||||||
|
username = "sa"
|
||||||
|
password = ""
|
||||||
|
maximumPoolSize = 25
|
||||||
|
isAutoCommit = false
|
||||||
|
|
||||||
|
validate()
|
||||||
|
})
|
||||||
|
|
||||||
|
val sessionHandler = SessionAttachmentHandler(
|
||||||
|
InMemorySessionManager("vst-session-manager"),
|
||||||
|
SessionCookieConfig()
|
||||||
|
)
|
||||||
|
sessionHandler.setNext(RequestHandler)
|
||||||
|
|
||||||
val server = Undertow.builder()
|
val server = Undertow.builder()
|
||||||
.addHttpListener(Settings.port, "localhost")
|
.addHttpListener(Settings.port, "localhost")
|
||||||
.setIoThreads(4)
|
.setIoThreads(4)
|
||||||
.setHandler(RequestHandler)
|
.setHandler(sessionHandler)
|
||||||
.setServerOption(UndertowOptions.SHUTDOWN_TIMEOUT, 1000)
|
.setServerOption(UndertowOptions.SHUTDOWN_TIMEOUT, 1000)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
170
src/jvmMain/kotlin/nl/astraeus/vst/chip/db/BaseDao.kt
Normal file
170
src/jvmMain/kotlin/nl/astraeus/vst/chip/db/BaseDao.kt
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
package nl.astraeus.vst.chip.db
|
||||||
|
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import nl.astraeus.vst.chip.logger.log
|
||||||
|
import java.sql.PreparedStatement
|
||||||
|
import java.sql.ResultSet
|
||||||
|
import java.sql.Timestamp
|
||||||
|
|
||||||
|
fun Instant.toSqlTimestamp() = Timestamp(this.toEpochMilliseconds())
|
||||||
|
fun Timestamp.toDateTimeInstant() = Instant.fromEpochMilliseconds(this.time)
|
||||||
|
|
||||||
|
data class SqlStatement<T : Entity>(
|
||||||
|
val sql: String,
|
||||||
|
val prepareParameters: T.(PreparedStatement) -> Unit
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SqlQuery<T : Entity>(
|
||||||
|
val sql: String,
|
||||||
|
val resultMapper: (ResultSet) -> T
|
||||||
|
)
|
||||||
|
|
||||||
|
abstract class QueryProvider<T : Entity> {
|
||||||
|
abstract val tableName: String
|
||||||
|
open val idQuery: String
|
||||||
|
get() = "SELECT * FROM $tableName WHERE ID = ?"
|
||||||
|
abstract val resultSetMapper: (ResultSet) -> T
|
||||||
|
open val find: SqlQuery<T>
|
||||||
|
get() = SqlQuery(
|
||||||
|
idQuery,
|
||||||
|
resultSetMapper
|
||||||
|
)
|
||||||
|
abstract val insert: SqlStatement<T>
|
||||||
|
abstract val update: SqlStatement<T>
|
||||||
|
open val delete: SqlStatement<T>
|
||||||
|
get() = SqlStatement(
|
||||||
|
"DELETE FROM $tableName WHERE ID = ?"
|
||||||
|
) { ps ->
|
||||||
|
ps.setLong(1, getPK()[0] as Long)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class BaseDao<T : Entity> {
|
||||||
|
abstract val queryProvider: QueryProvider<T>
|
||||||
|
open val autogeneratedPrimaryKey: Boolean = true
|
||||||
|
|
||||||
|
open fun insert(entity: T) {
|
||||||
|
executeInsert(entity, "insert", queryProvider.insert)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun update(entity: T): Int = executeUpdate(
|
||||||
|
entity,
|
||||||
|
"update",
|
||||||
|
queryProvider.update,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
open fun upsert(entity: T) {
|
||||||
|
if ((entity.getPK()[0] as Long) == 0L) {
|
||||||
|
insert(entity)
|
||||||
|
} else {
|
||||||
|
update(entity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun delete(entity: T) {
|
||||||
|
executeUpdate(entity, "delete", queryProvider.delete, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun find(
|
||||||
|
id: Long
|
||||||
|
): T? {
|
||||||
|
return executeQuery(
|
||||||
|
"find",
|
||||||
|
queryProvider.find
|
||||||
|
) { ps ->
|
||||||
|
ps.setLong(1, id)
|
||||||
|
}.firstOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun executeSQLUpdate(
|
||||||
|
sql: String,
|
||||||
|
parameterSetter: (PreparedStatement) -> Unit
|
||||||
|
): Int {
|
||||||
|
return transaction { con ->
|
||||||
|
con.prepareStatement(sql).use { ps ->
|
||||||
|
parameterSetter(ps)
|
||||||
|
|
||||||
|
ps.executeUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun executeQuery(
|
||||||
|
label: String,
|
||||||
|
statement: SqlQuery<T>,
|
||||||
|
prepareParameters: (PreparedStatement) -> Unit,
|
||||||
|
): List<T> {
|
||||||
|
return transaction { con ->
|
||||||
|
log.debug { "Executing query [$label] - [${statement.sql}]" }
|
||||||
|
val result = mutableListOf<T>()
|
||||||
|
|
||||||
|
con.prepareStatement(statement.sql)?.use { ps ->
|
||||||
|
prepareParameters(ps)
|
||||||
|
|
||||||
|
val rs = ps.executeQuery()
|
||||||
|
|
||||||
|
while (rs.next()) {
|
||||||
|
result.add(statement.resultMapper(rs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun executeInsert(
|
||||||
|
entity: T,
|
||||||
|
label: String,
|
||||||
|
statement: SqlStatement<T>,
|
||||||
|
checkSingleRow: Boolean = false
|
||||||
|
) {
|
||||||
|
transaction { con ->
|
||||||
|
log.debug { "Executing insert [$label] - [${statement.sql}] - [$entity]" }
|
||||||
|
con.prepareStatement(statement.sql)?.use { ps ->
|
||||||
|
statement.prepareParameters(entity, ps)
|
||||||
|
|
||||||
|
val rows = if (checkSingleRow) {
|
||||||
|
ps.execute()
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
ps.executeUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (autogeneratedPrimaryKey) {
|
||||||
|
val keyResult = ps.generatedKeys
|
||||||
|
if (keyResult.next()) {
|
||||||
|
entity.setPK(arrayOf(keyResult.getLong(1)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
check(rows == 1) {
|
||||||
|
"Statement [$label] affected more than 1 row! [${statement.sql}]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun executeUpdate(
|
||||||
|
entity: T,
|
||||||
|
label: String,
|
||||||
|
statement: SqlStatement<T>,
|
||||||
|
checkSingleRow: Boolean = false
|
||||||
|
): Int = transaction { con ->
|
||||||
|
var rows = 1
|
||||||
|
|
||||||
|
log.debug { "Executing update [$label] - [${statement.sql}] - [$entity]" }
|
||||||
|
con.prepareStatement(statement.sql)?.use { ps ->
|
||||||
|
statement.prepareParameters(entity, ps)
|
||||||
|
|
||||||
|
rows = ps.executeUpdate()
|
||||||
|
|
||||||
|
check(checkSingleRow || rows == 1) {
|
||||||
|
"Statement [$label] affected more than 1 row! [${statement.sql}]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rows
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
99
src/jvmMain/kotlin/nl/astraeus/vst/chip/db/Database.kt
Normal file
99
src/jvmMain/kotlin/nl/astraeus/vst/chip/db/Database.kt
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package nl.astraeus.vst.chip.db
|
||||||
|
|
||||||
|
import com.zaxxer.hikari.HikariConfig
|
||||||
|
import com.zaxxer.hikari.HikariDataSource
|
||||||
|
import java.sql.Connection
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
|
enum class TxScope {
|
||||||
|
REQUIRED,
|
||||||
|
/* if needed we need to switch db, sqlite only allows one writer/connection */
|
||||||
|
//REQUIRES_NEW
|
||||||
|
}
|
||||||
|
|
||||||
|
private val currentConnection = ThreadLocal<Connection>()
|
||||||
|
|
||||||
|
fun <T> transaction(
|
||||||
|
scope: TxScope = TxScope.REQUIRED,
|
||||||
|
block: (Connection) -> T
|
||||||
|
): T {
|
||||||
|
val hasConnection = currentConnection.get() != null
|
||||||
|
var oldConnection: Connection? = null
|
||||||
|
|
||||||
|
if (!hasConnection) {
|
||||||
|
currentConnection.set(Database.getConnection())
|
||||||
|
/*
|
||||||
|
} else if (scope == TxScope.REQUIRES_NEW) {
|
||||||
|
oldConnection = currentConnection.get()
|
||||||
|
|
||||||
|
currentConnection.set(Database.getConnection())
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
val connection = currentConnection.get()
|
||||||
|
|
||||||
|
try {
|
||||||
|
val result = block(connection)
|
||||||
|
|
||||||
|
connection.commit()
|
||||||
|
|
||||||
|
return result
|
||||||
|
} finally {
|
||||||
|
if (!hasConnection) {
|
||||||
|
currentConnection.set(oldConnection)
|
||||||
|
connection.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object Database {
|
||||||
|
|
||||||
|
var ds: HikariDataSource? = null
|
||||||
|
|
||||||
|
fun initialize(config: HikariConfig) {
|
||||||
|
val properties = Properties()
|
||||||
|
properties["journal_mode"] = "WAL"
|
||||||
|
|
||||||
|
|
||||||
|
config.dataSourceProperties = properties
|
||||||
|
config.addDataSourceProperty("cachePrepStmts", "true")
|
||||||
|
config.addDataSourceProperty("prepStmtCacheSize", "250")
|
||||||
|
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048")
|
||||||
|
|
||||||
|
ds = HikariDataSource(config)
|
||||||
|
Migrations.databaseVersionTableCreated = AtomicBoolean(false)
|
||||||
|
Migrations.updateDatabaseIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getConnection() = ds?.connection ?: error("Database has not been initialized!")
|
||||||
|
|
||||||
|
/*
|
||||||
|
val ds: HikariDataSource
|
||||||
|
|
||||||
|
init {
|
||||||
|
val properties = Properties()
|
||||||
|
properties["journal_mode"] = "WAL"
|
||||||
|
|
||||||
|
val config = HikariConfig().apply {
|
||||||
|
driverClassName = "nl.astraeus.jdbc.Driver"
|
||||||
|
jdbcUrl = "jdbc:stat:webServerPort=6001:jdbc:sqlite:data/daw3.db"
|
||||||
|
username = "sa"
|
||||||
|
password = ""
|
||||||
|
maximumPoolSize = 25
|
||||||
|
isAutoCommit = false
|
||||||
|
dataSourceProperties = properties
|
||||||
|
validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
config.addDataSourceProperty("cachePrepStmts", "true")
|
||||||
|
config.addDataSourceProperty("prepStmtCacheSize", "250")
|
||||||
|
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048")
|
||||||
|
|
||||||
|
ds = HikariDataSource(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getConnection() = ds.connection
|
||||||
|
*/
|
||||||
|
|
||||||
|
}
|
||||||
16
src/jvmMain/kotlin/nl/astraeus/vst/chip/db/Entity.kt
Normal file
16
src/jvmMain/kotlin/nl/astraeus/vst/chip/db/Entity.kt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package nl.astraeus.vst.chip.db
|
||||||
|
|
||||||
|
interface Entity {
|
||||||
|
fun getPK(): Array<Any>
|
||||||
|
fun setPK(pks: Array<Any>)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EntityId : Entity {
|
||||||
|
var id: Long
|
||||||
|
|
||||||
|
override fun getPK(): Array<Any> = arrayOf(id)
|
||||||
|
|
||||||
|
override fun setPK(pks: Array<Any>) {
|
||||||
|
id = pks[0] as Long
|
||||||
|
}
|
||||||
|
}
|
||||||
106
src/jvmMain/kotlin/nl/astraeus/vst/chip/db/Migrations.kt
Normal file
106
src/jvmMain/kotlin/nl/astraeus/vst/chip/db/Migrations.kt
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
package nl.astraeus.vst.chip.db
|
||||||
|
|
||||||
|
import nl.astraeus.vst.chip.logger.log
|
||||||
|
import java.sql.Connection
|
||||||
|
import java.sql.SQLException
|
||||||
|
import java.sql.Timestamp
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
|
sealed class Migration {
|
||||||
|
class Query(
|
||||||
|
val query: String
|
||||||
|
) : Migration() {
|
||||||
|
override fun toString(): String {
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Code(
|
||||||
|
val code: (Connection) -> Unit
|
||||||
|
) : Migration() {
|
||||||
|
override fun toString(): String {
|
||||||
|
return code.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val DATABASE_MIGRATIONS = arrayOf<Migration>(
|
||||||
|
Migration.Query(
|
||||||
|
"""
|
||||||
|
CREATE TABLE DATABASE_VERSION (
|
||||||
|
ID INTEGER PRIMARY KEY,
|
||||||
|
QUERY TEXT,
|
||||||
|
EXECUTED TIMESTAMP
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
),
|
||||||
|
Migration.Query(PATCH_CREATE_QUERY),
|
||||||
|
)
|
||||||
|
|
||||||
|
object Migrations {
|
||||||
|
var databaseVersionTableCreated = AtomicBoolean(false)
|
||||||
|
|
||||||
|
fun updateDatabaseIfNeeded() {
|
||||||
|
try {
|
||||||
|
transaction { con ->
|
||||||
|
con.prepareStatement(
|
||||||
|
"""
|
||||||
|
SELECT MAX(ID) FROM DATABASE_VERSION
|
||||||
|
""".trimIndent()
|
||||||
|
).use { ps ->
|
||||||
|
ps.executeQuery().use { rs ->
|
||||||
|
databaseVersionTableCreated.compareAndSet(false, true)
|
||||||
|
|
||||||
|
if (rs.next()) {
|
||||||
|
val maxId = rs.getInt(1)
|
||||||
|
|
||||||
|
for (index in maxId + 1..<DATABASE_MIGRATIONS.size) {
|
||||||
|
executeMigration(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: SQLException) {
|
||||||
|
if (databaseVersionTableCreated.compareAndSet(false, true)) {
|
||||||
|
executeMigration(0)
|
||||||
|
updateDatabaseIfNeeded()
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun executeMigration(index: Int) {
|
||||||
|
transaction { con ->
|
||||||
|
log.debug {
|
||||||
|
"Executing migration $index - [${DATABASE_MIGRATIONS[index]}]"
|
||||||
|
}
|
||||||
|
val description = when (
|
||||||
|
val migration = DATABASE_MIGRATIONS[index]
|
||||||
|
) {
|
||||||
|
is Migration.Query -> {
|
||||||
|
con.prepareStatement(migration.query).use { ps ->
|
||||||
|
ps.execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
migration.query
|
||||||
|
}
|
||||||
|
|
||||||
|
is Migration.Code -> {
|
||||||
|
migration.code(con)
|
||||||
|
|
||||||
|
migration.code.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
con.prepareStatement("INSERT INTO DATABASE_VERSION VALUES (?, ?, ?)").use { ps ->
|
||||||
|
ps.setInt(1, index)
|
||||||
|
ps.setString(2, description)
|
||||||
|
ps.setTimestamp(3, Timestamp(System.currentTimeMillis()))
|
||||||
|
|
||||||
|
ps.execute()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
31
src/jvmMain/kotlin/nl/astraeus/vst/chip/db/PatchDao.kt
Normal file
31
src/jvmMain/kotlin/nl/astraeus/vst/chip/db/PatchDao.kt
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package nl.astraeus.vst.chip.db
|
||||||
|
|
||||||
|
object PatchDao : BaseDao<PatchEntity>() {
|
||||||
|
|
||||||
|
override val queryProvider: QueryProvider<PatchEntity>
|
||||||
|
get() = PatchEntityQueryProvider
|
||||||
|
|
||||||
|
fun create(
|
||||||
|
patchId: String,
|
||||||
|
patch: String
|
||||||
|
): PatchEntity {
|
||||||
|
val result = PatchEntity(
|
||||||
|
0,
|
||||||
|
patchId,
|
||||||
|
patch
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findById(patchId: String): PatchEntity? = executeQuery(
|
||||||
|
"findById",
|
||||||
|
SqlQuery(
|
||||||
|
"SELECT * FROM ${queryProvider.tableName} WHERE PATCH_ID = ?",
|
||||||
|
queryProvider.resultSetMapper
|
||||||
|
)
|
||||||
|
) { ps ->
|
||||||
|
ps.setString(1, patchId)
|
||||||
|
}.firstOrNull()
|
||||||
|
|
||||||
|
}
|
||||||
12
src/jvmMain/kotlin/nl/astraeus/vst/chip/db/PatchEntity.kt
Normal file
12
src/jvmMain/kotlin/nl/astraeus/vst/chip/db/PatchEntity.kt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package nl.astraeus.vst.chip.db
|
||||||
|
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
|
||||||
|
data class PatchEntity(
|
||||||
|
override var id: Long,
|
||||||
|
var patchId: String,
|
||||||
|
var patch: String,
|
||||||
|
var created: Instant = Clock.System.now(),
|
||||||
|
var updated: Instant = Clock.System.now(),
|
||||||
|
) : EntityId
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package nl.astraeus.vst.chip.db
|
||||||
|
|
||||||
|
import java.sql.ResultSet
|
||||||
|
import java.sql.Types
|
||||||
|
|
||||||
|
val PATCH_CREATE_QUERY = """
|
||||||
|
CREATE TABLE INSTRUMENTS (
|
||||||
|
ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
PATCH_ID TEXT,
|
||||||
|
PATCH TEXT,
|
||||||
|
CREATED TIMESTAMP,
|
||||||
|
UPDATED TIMESTAMP
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
object PatchEntityQueryProvider : QueryProvider<PatchEntity>() {
|
||||||
|
override val tableName: String
|
||||||
|
get() = "INSTRUMENTS"
|
||||||
|
override val resultSetMapper: (ResultSet) -> PatchEntity
|
||||||
|
get() = { rs ->
|
||||||
|
PatchEntity(
|
||||||
|
rs.getLong(1),
|
||||||
|
rs.getString(2),
|
||||||
|
rs.getString(3),
|
||||||
|
rs.getTimestamp(4).toDateTimeInstant(),
|
||||||
|
rs.getTimestamp(5).toDateTimeInstant()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
override val insert: SqlStatement<PatchEntity>
|
||||||
|
get() = SqlStatement(
|
||||||
|
"""
|
||||||
|
INSERT INTO $tableName (
|
||||||
|
ID,
|
||||||
|
PATCH_ID,
|
||||||
|
PATCH,
|
||||||
|
CREATED,
|
||||||
|
UPDATED
|
||||||
|
) VALUES (
|
||||||
|
?,?,?,?,?
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
) { ps ->
|
||||||
|
ps.setNull(1, Types.BIGINT)
|
||||||
|
ps.setString(2, patchId)
|
||||||
|
ps.setString(3, patch)
|
||||||
|
ps.setTimestamp(4, created.toSqlTimestamp())
|
||||||
|
ps.setTimestamp(5, updated.toSqlTimestamp())
|
||||||
|
}
|
||||||
|
override val update: SqlStatement<PatchEntity>
|
||||||
|
get() = SqlStatement(
|
||||||
|
"""
|
||||||
|
UPDATE $tableName
|
||||||
|
SET PATCH_ID = ?,
|
||||||
|
PATCH = ?,
|
||||||
|
UPDATED = ?
|
||||||
|
WHERE ID = ?
|
||||||
|
""".trimIndent()
|
||||||
|
) { ps ->
|
||||||
|
ps.setString(1, patchId)
|
||||||
|
ps.setString(2, patch)
|
||||||
|
ps.setTimestamp(3, updated.toSqlTimestamp())
|
||||||
|
ps.setLong(4, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/jvmMain/kotlin/nl/astraeus/vst/chip/web/Index.kt
Normal file
42
src/jvmMain/kotlin/nl/astraeus/vst/chip/web/Index.kt
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package nl.astraeus.vst.chip.web
|
||||||
|
|
||||||
|
import kotlinx.html.body
|
||||||
|
import kotlinx.html.head
|
||||||
|
import kotlinx.html.html
|
||||||
|
import kotlinx.html.meta
|
||||||
|
import kotlinx.html.script
|
||||||
|
import kotlinx.html.stream.appendHTML
|
||||||
|
import kotlinx.html.title
|
||||||
|
|
||||||
|
fun generateIndex(patch: String?): String {
|
||||||
|
val result = StringBuilder();
|
||||||
|
|
||||||
|
if (patch == null) {
|
||||||
|
result.appendHTML(true).html {
|
||||||
|
head {
|
||||||
|
title { +"VST Chip" }
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
script {
|
||||||
|
type = "application/javascript"
|
||||||
|
src = "/vst-chip-worklet-ui.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.appendHTML(true).html {
|
||||||
|
head {
|
||||||
|
title { +"VST Chip" }
|
||||||
|
meta {
|
||||||
|
httpEquiv = "refresh"
|
||||||
|
content = "0; url=/patch/$patch"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
+"Redirecting to patch $patch..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.toString()
|
||||||
|
}
|
||||||
128
src/jvmMain/kotlin/nl/astraeus/vst/chip/web/RequestHandler.kt
Normal file
128
src/jvmMain/kotlin/nl/astraeus/vst/chip/web/RequestHandler.kt
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
package nl.astraeus.vst.chip.web
|
||||||
|
|
||||||
|
import io.undertow.Handlers.websocket
|
||||||
|
import io.undertow.server.HttpHandler
|
||||||
|
import io.undertow.server.HttpServerExchange
|
||||||
|
import io.undertow.server.handlers.PathHandler
|
||||||
|
import io.undertow.server.handlers.resource.PathResourceManager
|
||||||
|
import io.undertow.server.handlers.resource.ResourceHandler
|
||||||
|
import io.undertow.server.session.Session
|
||||||
|
import io.undertow.server.session.SessionConfig
|
||||||
|
import io.undertow.server.session.SessionManager
|
||||||
|
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.chip.db.PatchDao
|
||||||
|
import nl.astraeus.vst.chip.db.PatchEntity
|
||||||
|
import nl.astraeus.vst.chip.db.transaction
|
||||||
|
import nl.astraeus.vst.chip.generateId
|
||||||
|
import java.nio.file.Paths
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object WebsocketConnectHandler : HttpHandler {
|
||||||
|
override fun handleRequest(exchange: HttpServerExchange) {
|
||||||
|
val sessionManager = exchange.getAttachment(SessionManager.ATTACHMENT_KEY)
|
||||||
|
val sessionConfig = exchange.getAttachment(SessionConfig.ATTACHMENT_KEY)
|
||||||
|
|
||||||
|
val httpSession: Session? = sessionManager.getSession(exchange, sessionConfig)
|
||||||
|
|
||||||
|
websocket(WebsocketHandler(httpSession)).handleRequest(exchange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object PatchHandler : HttpHandler {
|
||||||
|
override fun handleRequest(exchange: HttpServerExchange) {
|
||||||
|
if (exchange.requestPath.startsWith("/patch/")) {
|
||||||
|
val patchId = exchange.requestPath.substring(7)
|
||||||
|
val sessionManager = exchange.getAttachment(SessionManager.ATTACHMENT_KEY)
|
||||||
|
val sessionConfig = exchange.getAttachment(SessionConfig.ATTACHMENT_KEY)
|
||||||
|
var httpSession: Session? = sessionManager.getSession(exchange, sessionConfig)
|
||||||
|
|
||||||
|
if (httpSession == null) {
|
||||||
|
httpSession = sessionManager.createSession(exchange, sessionConfig)
|
||||||
|
}
|
||||||
|
httpSession?.setAttribute("html-session", VstSession(patchId))
|
||||||
|
|
||||||
|
exchange.responseSender.send(generateIndex(null))
|
||||||
|
} else {
|
||||||
|
val patchId = generateId()
|
||||||
|
|
||||||
|
exchange.responseSender.send(generateIndex(patchId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object RequestHandler : HttpHandler {
|
||||||
|
val resourceHandler = ResourceHandler(PathResourceManager(Paths.get("web")))
|
||||||
|
val pathHandler = PathHandler(resourceHandler)
|
||||||
|
|
||||||
|
init {
|
||||||
|
pathHandler.addExactPath("/", PatchHandler)
|
||||||
|
pathHandler.addExactPath("/index.html", PatchHandler)
|
||||||
|
pathHandler.addPrefixPath("/patch", PatchHandler)
|
||||||
|
pathHandler.addExactPath("/ws", WebsocketConnectHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleRequest(exchange: HttpServerExchange) {
|
||||||
|
pathHandler.handleRequest(exchange)
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/jvmMain/kotlin/nl/astraeus/vst/chip/web/Session.kt
Normal file
5
src/jvmMain/kotlin/nl/astraeus/vst/chip/web/Session.kt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package nl.astraeus.vst.chip.web
|
||||||
|
|
||||||
|
class VstSession(
|
||||||
|
val patchId: String
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user