Playing with settings

This commit is contained in:
2024-08-12 20:36:30 +02:00
parent f2269c8865
commit b412dd9b4e
9 changed files with 248 additions and 74 deletions

View File

@@ -21,17 +21,20 @@ import kotlin.math.sin
val POLYPHONICS = 10 val POLYPHONICS = 10
val PI2 = PI * 2 val PI2 = PI * 2
@ExperimentalJsExport
@JsExport
class PlayingNote( class PlayingNote(
val note: Int, val note: Int,
var velocity: Int = 0 var velocity: Int = 0
) { ) {
val noteObj = Note.fromMidi(note)
fun retrigger(velocity: Int) { fun retrigger(velocity: Int) {
this.velocity = velocity this.velocity = velocity
sample = 0 sample = 0
noteStart = currentTime noteStart = currentTime
noteRelease = null noteRelease = null
for (i in 0 until combDelayBuffer.length) {
combDelayBuffer[i] = 0f
}
} }
var noteStart = currentTime var noteStart = currentTime
@@ -39,6 +42,7 @@ class PlayingNote(
var cycleOffset = 0.0 var cycleOffset = 0.0
var sample = 0 var sample = 0
var actualVolume = 0f var actualVolume = 0f
val combDelayBuffer = Float32Array((sampleRate / noteObj.freq).toInt())
} }
enum class Waveform { enum class Waveform {
@@ -82,6 +86,14 @@ class VstChipProcessor : AudioWorkletProcessor() {
var recordingSample = 0 var recordingSample = 0
var recordingStart = 0 var recordingStart = 0
val rightDelayBuffer = Float32Array(sampleRate)
val leftDelayBuffer = Float32Array(sampleRate)
var delayIndex = 0
var delay = 0.0
var delayDepth = 0.0
var feedback = 0.0
init { init {
this.port.onmessage = ::handleMessage this.port.onmessage = ::handleMessage
Note.updateSampleRate(sampleRate) Note.updateSampleRate(sampleRate)
@@ -95,7 +107,7 @@ class VstChipProcessor : AudioWorkletProcessor() {
try { try {
when (data) { when (data) {
is String -> { is String -> {
when(data) { when (data) {
"start_recording" -> { "start_recording" -> {
port.postMessage(recordingBuffer) port.postMessage(recordingBuffer)
if (recordingState == RecordingState.STOPPED) { if (recordingState == RecordingState.STOPPED) {
@@ -103,6 +115,7 @@ class VstChipProcessor : AudioWorkletProcessor() {
recordingSample = 0 recordingSample = 0
} }
} }
else -> else ->
if (data.startsWith("set_channel")) { if (data.startsWith("set_channel")) {
val parts = data.split('\n') val parts = data.split('\n')
@@ -113,7 +126,7 @@ class VstChipProcessor : AudioWorkletProcessor() {
} else if (data.startsWith("waveform")) { } else if (data.startsWith("waveform")) {
val parts = data.split('\n') val parts = data.split('\n')
if (parts.size == 2) { if (parts.size == 2) {
waveform =parts[1].toInt() waveform = parts[1].toInt()
println("Setting waveform: $waveform") println("Setting waveform: $waveform")
} }
} }
@@ -135,7 +148,7 @@ class VstChipProcessor : AudioWorkletProcessor() {
else -> else ->
console.error("Don't kow how to handle message", message) console.error("Don't kow how to handle message", message)
} }
} catch(e: Exception) { } catch (e: Exception) {
console.log(e.message, e) console.log(e.message, e)
} }
} }
@@ -155,7 +168,7 @@ class VstChipProcessor : AudioWorkletProcessor() {
cmdByte = cmdByte and 0xf0 cmdByte = cmdByte and 0xf0
//console.log("Received", bytes) //console.log("Received", bytes)
when(cmdByte) { when (cmdByte) {
0x90 -> { 0x90 -> {
if (bytes.length == 3) { if (bytes.length == 3) {
val note = bytes[1] val note = bytes[1]
@@ -201,18 +214,19 @@ class VstChipProcessor : AudioWorkletProcessor() {
dutyCycle = value / 127.0 dutyCycle = value / 127.0
} }
0x4a -> { 0x40 -> {
fmFreq = value / 127.0 fmFreq = value / 127.0
} }
0x4b -> { 0x41 -> {
fmAmp = value / 127.0 fmAmp = value / 127.0
} }
0x4c -> { 0x42 -> {
amFreq = value / 127.0 amFreq = value / 127.0
} }
0x4d -> {
0x43 -> {
amAmp = value / 127.0 amAmp = value / 127.0
} }
@@ -232,6 +246,21 @@ class VstChipProcessor : AudioWorkletProcessor() {
release = value / 127.0 release = value / 127.0
} }
0x4e -> {
delay = value / 127.0
println("Setting delay $delay")
}
0x4f -> {
delayDepth = value / 127.0
println("Setting delayDepth $delayDepth")
}
0x50 -> {
feedback = value / 127.0
println("Setting feedback $delayDepth")
}
123 -> { 123 -> {
for (note in notes) { for (note in notes) {
note?.noteRelease = currentTime note?.noteRelease = currentTime
@@ -280,11 +309,11 @@ class VstChipProcessor : AudioWorkletProcessor() {
} }
} }
override fun process ( override fun process(
inputs: Array<Array<Float32Array>>, inputs: Array<Array<Float32Array>>,
outputs: Array<Array<Float32Array>>, outputs: Array<Array<Float32Array>>,
parameters: dynamic parameters: dynamic
) : Boolean { ): Boolean {
val samples = outputs[0][0].length val samples = outputs[0][0].length
val left = outputs[0][0] val left = outputs[0][0]
@@ -304,10 +333,11 @@ class VstChipProcessor : AudioWorkletProcessor() {
for ((index, note) in notes.withIndex()) { for ((index, note) in notes.withIndex()) {
if (note != null) { if (note != null) {
val sampleDelta = Note.fromMidi(note.note).sampleDelta val midiNote = Note.fromMidi(note.note)
val sampleDelta = midiNote.sampleDelta
for (i in 0 until samples) { for (i in 0 until samples) {
var targetVolume = note.velocity / 127f var targetVolume = note.velocity / 127f * 10f
targetVolume *= ADSR.calculate( targetVolume *= ADSR.calculate(
attack, attack,
decay, decay,
@@ -324,38 +354,64 @@ class VstChipProcessor : AudioWorkletProcessor() {
} }
var cycleOffset = note.cycleOffset var cycleOffset = note.cycleOffset
val fmModulation = sampleDelta * sin( fmFreq * 20f * PI2 * (note.sample / sampleRate.toDouble())).toFloat() * fmAmp val fmModulation =
val amModulation = 1f + (sin(sampleLength * amFreq * 10f * PI2 * note.sample) * amAmp).toFloat() sampleDelta + (sin(fmFreq * 1000f * PI2 * (note.sample / sampleRate.toDouble())).toFloat() * (100f * fmAmp * sampleDelta))
val amModulation =
1f + (sin(sampleLength * amFreq * 1000f * PI2 * note.sample) * amAmp).toFloat()
cycleOffset = if (cycleOffset < dutyCycle) { cycleOffset = if (cycleOffset < dutyCycle) {
cycleOffset / dutyCycle / 2.0 cycleOffset / dutyCycle / 2.0
} else { } else {
0.5 + ((cycleOffset -dutyCycle) / (1.0 - dutyCycle) / 2.0) 0.5 + ((cycleOffset - dutyCycle) / (1.0 - dutyCycle) / 2.0)
} }
val waveValue: Float = when (waveform) { val waveValue: Float = when (waveform) {
0 -> { 0 -> {
sin(cycleOffset * PI2).toFloat() sin(cycleOffset * PI2).toFloat()
} }
1 -> { 1 -> {
if (cycleOffset < 0.5) { 1f } else { -1f } if (cycleOffset < 0.5) {
1f
} else {
-1f
} }
}
2 -> when { 2 -> when {
cycleOffset < 0.25 -> 4 * cycleOffset cycleOffset < 0.25 -> 4 * cycleOffset
cycleOffset < 0.75 -> 2 - 4 * cycleOffset cycleOffset < 0.75 -> 2 - 4 * cycleOffset
else -> 4 * cycleOffset - 4 else -> 4 * cycleOffset - 4
}.toFloat() }.toFloat()
3 -> { 3 -> {
((cycleOffset * 2f) - 1f).toFloat() ((cycleOffset * 2f) - 1f).toFloat()
} }
else -> { else -> {
if (cycleOffset < 0.5) { 1f } else { -1f } if (cycleOffset < 0.5) {
1f
} else {
-1f
}
} }
} }
left[i] = left[i] + waveValue * note.actualVolume * volume * amModulation left[i] = left[i] + waveValue * note.actualVolume * volume * amModulation
right[i] = right[i] + waveValue * note.actualVolume * volume * amModulation right[i] = right[i] + waveValue * note.actualVolume * volume * amModulation
// comb filter delay
val delaySampleIndex =
(note.sample + note.combDelayBuffer.length) % note.combDelayBuffer.length
left[i] = left[i] + (note.combDelayBuffer[delaySampleIndex] * feedback.toFloat())
right[i] = right[i] + (note.combDelayBuffer[delaySampleIndex] * feedback.toFloat())
note.combDelayBuffer[delaySampleIndex] = (left[i] + right[i]) / 2f
// end - comb filter delay
note.cycleOffset += sampleDelta + fmModulation note.cycleOffset += sampleDelta + fmModulation
if (note.cycleOffset > 1f) { if (note.cycleOffset > 1f) {
note.cycleOffset -= 1f note.cycleOffset -= 1f
@@ -370,6 +426,26 @@ class VstChipProcessor : AudioWorkletProcessor() {
} }
} }
// if sin enable
for (i in 0 until samples) {
left[i] = sin(left[i] * PI2).toFloat()
right[i] = sin(right[i] * PI2).toFloat()
}
val delaySamples = (delay * leftDelayBuffer.length).toInt()
for (i in 0 until samples) {
if (delaySamples > 0) {
val delaySampleIndex = (delayIndex + sampleRate - delaySamples) % sampleRate
left[i] = left[i] + (leftDelayBuffer[delaySampleIndex] * delayDepth.toFloat())
right[i] = right[i] + (rightDelayBuffer[delaySampleIndex] * delayDepth.toFloat())
}
leftDelayBuffer[delayIndex] = left[i]
rightDelayBuffer[delayIndex++] = right[i]
delayIndex %= sampleRate
}
if (recordingState == RecordingState.RECORDING) { if (recordingState == RecordingState.RECORDING) {
for (i in recordingStart until samples) { for (i in recordingStart until samples) {
recordingBuffer[recordingSample] = (left[i] + right[i]) / 2f recordingBuffer[recordingSample] = (left[i] + right[i]) / 2f

View File

@@ -28,6 +28,22 @@ kotlin {
} }
} }
} }
/*
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
binaries.executable()
browser{
distribution {
outputDirectory.set(File("$projectDir/web/"))
}
}
mavenPublication {
groupId = group as String
pom { name = "${project.name}-wasm-js" }
}
}
*/
jvm{ jvm{
withJava() withJava()
} }
@@ -37,15 +53,14 @@ kotlin {
dependencies { dependencies {
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.9-SNAPSHOT")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.0")
} }
} }
val jsMain by getting { val jsMain by getting {
dependencies { dependencies {
//base implementation("nl.astraeus:kotlin-komponent:1.2.4-SNAPSHOT")
implementation("nl.astraeus:kotlin-komponent-js:1.2.2") implementation("nl.astraeus:vst-ui-base:1.0.1-SNAPSHOT")
implementation("nl.astraeus:vst-ui-base:1.0.0-SNAPSHOT")
} }
} }
val jsTest by getting { val jsTest by getting {
@@ -53,6 +68,12 @@ kotlin {
implementation(kotlin("test-js")) implementation(kotlin("test-js"))
} }
} }
/* val wasmJsMain by getting {
dependencies {
implementation("nl.astraeus:kotlin-komponent:1.2.4-SNAPSHOT")
implementation("nl.astraeus:vst-ui-base:1.0.1-SNAPSHOT")
}
}*/
val jvmMain by getting { val jvmMain by getting {
dependencies { dependencies {
//base //base

View File

@@ -5,9 +5,5 @@ allprojects {
repositories { repositories {
mavenLocal() mavenLocal()
mavenCentral() mavenCentral()
maven("https://reposilite.astraeus.nl/releases")
maven {
url = uri("https://nexus.astraeus.nl/nexus/content/groups/public")
}
} }
} }

View File

@@ -1,6 +1,7 @@
@file:OptIn(ExperimentalKotlinGradlePluginApi::class) @file:OptIn(ExperimentalKotlinGradlePluginApi::class)
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
buildscript { buildscript {
apply(from = "../common.gradle.kts") apply(from = "../common.gradle.kts")
@@ -17,16 +18,19 @@ kotlin {
} }
browser() browser()
} }
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
browser()
mavenPublication {
groupId = group as String
pom { name = "${project.name}-wasm-js" }
}
}
jvm() jvm()
sourceSets { sourceSets {
val commonMain by getting { val commonMain by getting
dependencies { val jsMain by getting
} val wasmJsMain by getting
}
val jsMain by getting {
dependencies {
}
}
} }
} }

View File

@@ -1,6 +1,6 @@
pluginManagement { pluginManagement {
plugins { plugins {
kotlin("multiplatform") version "2.0.20-Beta1" kotlin("multiplatform") version "2.0.20-RC"
id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0" id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0"
} }
repositories { repositories {

View File

@@ -1,7 +1,11 @@
package nl.astraeus.vst.chip package nl.astraeus.vst.chip
import kotlin.js.ExperimentalJsExport
import kotlin.js.JsExport
import kotlin.js.JsName import kotlin.js.JsName
@ExperimentalJsExport
@JsExport
data class PatchDTO( data class PatchDTO(
@JsName("waveform") @JsName("waveform")
val waveform: Int = 0, val waveform: Int = 0,
@@ -31,4 +35,10 @@ data class PatchDTO(
var sustain: Double = 0.5, var sustain: Double = 0.5,
@JsName("release") @JsName("release")
var release: Double = 0.2, var release: Double = 0.2,
@JsName("delay")
var delay: Double = 0.0,
@JsName("delayDepth")
var delayDepth: Double = 0.0,
@JsName("feedback")
var feedback: Double = 0.0,
) )

View File

@@ -45,28 +45,49 @@ object VstChipWorklet : AudioNode(
set(value) { set(value) {
field = value field = value
super.postMessage( super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x4a, (value * 127).toInt()) uInt8ArrayOf(0xb0 + midiChannel, 0x40, (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, 0x4b, (value * 127).toInt()) uInt8ArrayOf(0xb0 + midiChannel, 0x41, (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, 0x4c, (value * 127).toInt()) uInt8ArrayOf(0xb0 + midiChannel, 0x42, (value * 127).toInt())
) )
} }
var amModAmp = 0.0 var amModAmp = 0.0
set(value) { set(value) {
field = value field = value
super.postMessage( super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x4d, (value * 127).toInt()) uInt8ArrayOf(0xb0 + midiChannel, 0x43, (value * 127).toInt())
)
}
var feedback = 0.0
set(value) {
field = value
super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x50, (value * 127).toInt())
)
}
var delay = 0.0
set(value) {
field = value
super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x4e, (value * 127).toInt())
)
}
var delayDepth = 0.0
set(value) {
field = value
super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x4f, (value * 127).toInt())
) )
} }
@@ -146,22 +167,22 @@ object VstChipWorklet : AudioNode(
MainView.requestUpdate() MainView.requestUpdate()
} }
0x4b.toByte() -> { 0x40.toByte() -> {
fmModFreq = value / 127.0 fmModFreq = value / 127.0
MainView.requestUpdate() MainView.requestUpdate()
} }
0x4c.toByte() -> { 0x41.toByte() -> {
fmModAmp = value / 127.0 fmModAmp = value / 127.0
MainView.requestUpdate() MainView.requestUpdate()
} }
0x47.toByte() -> { 0x42.toByte() -> {
amModFreq = value / 127.0 amModFreq = value / 127.0
MainView.requestUpdate() MainView.requestUpdate()
} }
0x48.toByte() -> { 0x43.toByte() -> {
amModAmp = value / 127.0 amModAmp = value / 127.0
MainView.requestUpdate() MainView.requestUpdate()
} }
@@ -181,6 +202,9 @@ object VstChipWorklet : AudioNode(
decay = patch.decay decay = patch.decay
sustain = patch.sustain sustain = patch.sustain
release = patch.release release = patch.release
delay = patch.delay
delayDepth = patch.delayDepth
feedback = patch.feedback
} }
fun save(): PatchDTO { fun save(): PatchDTO {
@@ -196,7 +220,10 @@ object VstChipWorklet : AudioNode(
attack = attack, attack = attack,
decay = decay, decay = decay,
sustain = sustain, sustain = sustain,
release = release release = release,
delay = delay,
delayDepth = delayDepth,
feedback = feedback
) )
} }

View File

@@ -37,6 +37,7 @@ import nl.astraeus.vst.chip.audio.VstChipWorklet
import nl.astraeus.vst.chip.audio.VstChipWorklet.midiChannel 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.chip.ws.WebsocketClient
import nl.astraeus.vst.ui.components.ExpKnobComponent
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
@@ -236,12 +237,12 @@ object MainView : Komponent(), CssName {
} }
div(ControlsCss.name) { div(ControlsCss.name) {
include( include(
KnobComponent( ExpKnobComponent(
value = VstChipWorklet.volume, value = VstChipWorklet.volume,
label = "Volume", label = "Volume",
minValue = 0.0, minValue = 0.005,
maxValue = 1.0, maxValue = 1.0,
step = 2.0 / 127.0, step = 5.0 / 127.0,
width = 100, width = 100,
height = 120, height = 120,
) { value -> ) { value ->
@@ -262,12 +263,12 @@ object MainView : Komponent(), CssName {
} }
) )
include( include(
KnobComponent( ExpKnobComponent(
value = VstChipWorklet.fmModFreq, value = VstChipWorklet.fmModFreq,
label = "FM Freq", label = "FM Freq",
minValue = 0.0, minValue = 0.005,
maxValue = 1.0, maxValue = 1.0,
step = 2.0 / 127.0, step = 5.0 / 127.0,
width = 100, width = 100,
height = 120, height = 120,
) { value -> ) { value ->
@@ -275,12 +276,12 @@ object MainView : Komponent(), CssName {
} }
) )
include( include(
KnobComponent( ExpKnobComponent(
value = VstChipWorklet.fmModAmp, value = VstChipWorklet.fmModAmp,
label = "FM Ampl", label = "FM Ampl",
minValue = 0.0, minValue = 0.005,
maxValue = 1.0, maxValue = 1.0,
step = 2.0 / 127.0, step = 5.0 / 127.0,
width = 100, width = 100,
height = 120, height = 120,
) { value -> ) { value ->
@@ -288,12 +289,12 @@ object MainView : Komponent(), CssName {
} }
) )
include( include(
KnobComponent( ExpKnobComponent(
value = VstChipWorklet.amModFreq, value = VstChipWorklet.amModFreq,
label = "AM Freq", label = "AM Freq",
minValue = 0.0, minValue = 0.005,
maxValue = 1.0, maxValue = 1.0,
step = 2.0 / 127.0, step = 5.0 / 127.0,
width = 100, width = 100,
height = 120, height = 120,
) { value -> ) { value ->
@@ -301,27 +302,66 @@ object MainView : Komponent(), CssName {
} }
) )
include( include(
KnobComponent( ExpKnobComponent(
value = VstChipWorklet.amModAmp, value = VstChipWorklet.amModAmp,
label = "AM Ampl", label = "AM Ampl",
minValue = 0.0, minValue = 0.005,
maxValue = 1.0, maxValue = 1.0,
step = 2.0 / 127.0, step = 5.0 / 127.0,
width = 100, width = 100,
height = 120, height = 120,
) { value -> ) { value ->
VstChipWorklet.amModAmp = value VstChipWorklet.amModAmp = value
} }
) )
include(
ExpKnobComponent(
value = VstChipWorklet.feedback,
label = "Feedback",
minValue = 0.005,
maxValue = 1.0,
step = 5.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.feedback = value
}
)
include(
ExpKnobComponent(
value = VstChipWorklet.delay,
label = "Delay",
minValue = 0.005,
maxValue = 1.0,
step = 5.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.delay = value
}
)
include(
ExpKnobComponent(
value = VstChipWorklet.delayDepth,
label = "Delay depth",
minValue = 0.005,
maxValue = 1.0,
step = 5.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.delayDepth = value
}
)
} }
div(ControlsCss.name) { div(ControlsCss.name) {
include( include(
KnobComponent( ExpKnobComponent(
value = VstChipWorklet.attack, value = VstChipWorklet.attack,
label = "Attack", label = "Attack",
minValue = 0.0, minValue = 0.005,
maxValue = 1.0, maxValue = 1.0,
step = 2.0 / 127.0, step = 5.0 / 127.0,
width = 100, width = 100,
height = 120, height = 120,
) { value -> ) { value ->
@@ -329,12 +369,12 @@ object MainView : Komponent(), CssName {
} }
) )
include( include(
KnobComponent( ExpKnobComponent(
value = VstChipWorklet.decay, value = VstChipWorklet.decay,
label = "Decay", label = "Decay",
minValue = 0.0, minValue = 0.005,
maxValue = 1.0, maxValue = 1.0,
step = 2.0 / 127.0, step = 5.0 / 127.0,
width = 100, width = 100,
height = 120, height = 120,
) { value -> ) { value ->
@@ -355,12 +395,12 @@ object MainView : Komponent(), CssName {
} }
) )
include( include(
KnobComponent( ExpKnobComponent(
value = VstChipWorklet.release, value = VstChipWorklet.release,
label = "Release", label = "Release",
minValue = 0.0, minValue = 0.005,
maxValue = 1.0, maxValue = 1.0,
step = 2.0 / 127.0, step = 5.0 / 127.0,
width = 100, width = 100,
height = 120, height = 120,
) { value -> ) { value ->

View File

@@ -17,7 +17,7 @@ object WebsocketClient {
close() close()
websocket = if (window.location.hostname.contains("localhost") || window.location.hostname.contains("192.168")) { websocket = if (window.location.hostname.contains("localhost") || window.location.hostname.contains("192.168")) {
WebSocket("ws://${window.location.hostname}:9000/ws") WebSocket("ws://${window.location.hostname}:${window.location.port}/ws")
} else { } else {
WebSocket("wss://${window.location.hostname}/ws") WebSocket("wss://${window.location.hostname}/ws")
} }