commit 68b7ffffa8181fab4afb2899a86a419e98cdb6e1 Author: rnentjes Date: Sun Jun 16 20:40:05 2024 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6625dfe --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store + +web diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/artifacts/audio_worklet_js_1_0_0_SNAPSHOT.xml b/.idea/artifacts/audio_worklet_js_1_0_0_SNAPSHOT.xml new file mode 100644 index 0000000..48401c3 --- /dev/null +++ b/.idea/artifacts/audio_worklet_js_1_0_0_SNAPSHOT.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/audio-worklet/build/libs + + + + + \ No newline at end of file diff --git a/.idea/artifacts/audio_worklet_jvm_1_0_0_SNAPSHOT.xml b/.idea/artifacts/audio_worklet_jvm_1_0_0_SNAPSHOT.xml new file mode 100644 index 0000000..06b6fe8 --- /dev/null +++ b/.idea/artifacts/audio_worklet_jvm_1_0_0_SNAPSHOT.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/audio-worklet/build/libs + + + + + \ No newline at end of file diff --git a/.idea/artifacts/common_js_1_0_0_SNAPSHOT.xml b/.idea/artifacts/common_js_1_0_0_SNAPSHOT.xml new file mode 100644 index 0000000..d3d29c0 --- /dev/null +++ b/.idea/artifacts/common_js_1_0_0_SNAPSHOT.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/common/build/libs + + + + + \ No newline at end of file diff --git a/.idea/artifacts/common_jvm_1_0_0_SNAPSHOT.xml b/.idea/artifacts/common_jvm_1_0_0_SNAPSHOT.xml new file mode 100644 index 0000000..1d6f5d0 --- /dev/null +++ b/.idea/artifacts/common_jvm_1_0_0_SNAPSHOT.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/common/build/libs + + + + + \ No newline at end of file diff --git a/.idea/artifacts/vst_chip_js_1_0_0_SNAPSHOT.xml b/.idea/artifacts/vst_chip_js_1_0_0_SNAPSHOT.xml new file mode 100644 index 0000000..8ad05f0 --- /dev/null +++ b/.idea/artifacts/vst_chip_js_1_0_0_SNAPSHOT.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/build/libs + + + + + \ No newline at end of file diff --git a/.idea/artifacts/vst_chip_jvm_1_0_0_SNAPSHOT.xml b/.idea/artifacts/vst_chip_jvm_1_0_0_SNAPSHOT.xml new file mode 100644 index 0000000..718198c --- /dev/null +++ b/.idea/artifacts/vst_chip_jvm_1_0_0_SNAPSHOT.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/build/libs + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..181281b --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/jsLibraryMappings.xml b/.idea/jsLibraryMappings.xml new file mode 100644 index 0000000..7160600 --- /dev/null +++ b/.idea/jsLibraryMappings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..c7fb6ee --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/audio-worklet/build.gradle.kts b/audio-worklet/build.gradle.kts new file mode 100644 index 0000000..fbd9ae0 --- /dev/null +++ b/audio-worklet/build.gradle.kts @@ -0,0 +1,48 @@ +import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackOutput + +buildscript { + apply(from = "../common.gradle.kts") +} + +plugins { + kotlin("multiplatform") +} + +kotlin { + js { + compilerOptions { + target.set("es2015") + } + binaries.executable() + + browser { + commonWebpackConfig { + outputFileName = "vst-chip-worklet.js" + sourceMaps = true + } + + webpackTask { + output.libraryTarget = KotlinWebpackOutput.Target.VAR + output.library = "vstChipWorklet" + } + + distribution { + outputDirectory.set(File("$projectDir/../web/")) + } + } + } + jvm() + + sourceSets { + val commonMain by getting { + dependencies { + implementation(project(":common")) + } + } + val jsMain by getting { + dependencies { + implementation(project(":common")) + } + } + } +} diff --git a/audio-worklet/settings.gradle.kts b/audio-worklet/settings.gradle.kts new file mode 100644 index 0000000..8dba1e4 --- /dev/null +++ b/audio-worklet/settings.gradle.kts @@ -0,0 +1,5 @@ +apply(from = "../settings.common.gradle.kts") + +include(":common") + +project(":common").projectDir = file("../common") diff --git a/audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/Externals.kt b/audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/Externals.kt new file mode 100644 index 0000000..ccbe28c --- /dev/null +++ b/audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/Externals.kt @@ -0,0 +1,43 @@ +package nl.astraeus.vst + +import org.khronos.webgl.Float32Array +import org.w3c.dom.MessagePort + +enum class AutomationRate( + val rate: String +) { + A_RATE("a-rate"), + K_RATE("k-rate") +} + +interface AudioParam { + var value: Double + var automationRate: AutomationRate + val defaultValue: Double + val minValue: Double + val maxValue: Double +} + +interface AudioParamMap { + operator fun get(name: String): AudioParam +} + +abstract external class AudioWorkletProcessor { + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/AudioWorkletNode/parameters) */ + //val parameters: AudioParamMap; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/AudioWorkletNode/port) */ + @JsName("port") + val port: MessagePort + + @JsName("process") + open fun process ( + inputs: Array>, + outputs: Array>, + parameters: dynamic + ) : Boolean { definedExternally } + +} + +external fun registerProcessor(name: String, processorCtor: JsClass<*>) +external val sampleRate: Int +external val currentTime: Double diff --git a/audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/chip/ChipProcessor.kt b/audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/chip/ChipProcessor.kt new file mode 100644 index 0000000..c169aa8 --- /dev/null +++ b/audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/chip/ChipProcessor.kt @@ -0,0 +1,191 @@ +@file:OptIn(ExperimentalJsExport::class) + +package nl.astraeus.vst.chip + +import nl.astraeus.vst.AudioWorkletProcessor +import nl.astraeus.vst.Note +import nl.astraeus.vst.registerProcessor +import nl.astraeus.vst.sampleRate +import org.khronos.webgl.ArrayBuffer +import org.khronos.webgl.Float32Array +import org.khronos.webgl.Int32Array +import org.khronos.webgl.get +import org.khronos.webgl.set +import org.w3c.dom.MessageEvent +import kotlin.math.PI +import kotlin.math.sin + +val POLYPHONICS = 10 +val PI2 = PI * 2 + +@ExperimentalJsExport +@JsExport +enum class NoteState { + ON, + RELEASED, + OFF +} + +@ExperimentalJsExport +@JsExport +class PlayingNote( + val note: Int, + var velocity: Int = 0 +) { + fun retrigger(velocity: Int) { + this.velocity = velocity + state = NoteState.ON + sample = 0 + attackSamples = 2500 + releaseSamples = 10000 + } + + var state = NoteState.OFF + var cycleOffset = 0.0 + var sample = 0 + var attackSamples = 2500 + var releaseSamples = 10000 + var actualVolume = 0f +} + +@ExperimentalJsExport +@JsExport +class VstChipProcessor : AudioWorkletProcessor() { + val notes = Array(POLYPHONICS) { + PlayingNote( + 0 + ) + } + + init { + this.port.onmessage = ::handleMessage + Note.updateSampleRate(sampleRate) + } + + private fun handleMessage(message: MessageEvent) { + //console.log("VstChipProcessor: Received message", message) + + val data = message.data + + when (data) { + "test_on" -> { + playMidi(Int32Array(arrayOf(0x90, 60, 64))) + + } + "test_off" -> { + playMidi(Int32Array(arrayOf(0x90, 60, 0))) + } + is String -> { + } + is ArrayBuffer -> { + } + is Int32Array -> { + playMidi(data) + } + else -> + console.error("Don't kow how to handle message", message) + } + } + + private fun playMidi(bytes: Int32Array) { + if (bytes.length > 0) { + when(bytes[0]) { + 0x90 -> { + if (bytes.length == 3) { + val note = bytes[1] + val velocity = bytes[2] + + if (velocity > 0) { + noteOn(note, velocity) + } else { + noteOff(note) + } + } + } + 0x90 -> { + + } + } + } + } + + private fun noteOn(note: Int, velocity: Int) { + for (i in 0 until POLYPHONICS) { + if (notes[i].note == note) { + notes[i].retrigger(velocity) + //console.log("Note retriggered", notes[i]) + return + } + } + for (i in 0 until POLYPHONICS) { + if (notes[i].state == NoteState.OFF) { + notes[i] = PlayingNote( + note, + velocity + ) + notes[i].state = NoteState.ON + console.log("Playing note", notes[i]) + break + } + } + } + + private fun noteOff(note: Int) { + for (i in 0 until POLYPHONICS) { + if (notes[i].note == note && notes[i].state == NoteState.ON) { + notes[i].state = NoteState.RELEASED + //console.log("Released note", notes[i]) + break + } + } + } + + override fun process ( + inputs: Array>, + outputs: Array>, + parameters: dynamic + ) : Boolean { + val samples = outputs[0][0].length + + val left = outputs[0][0] + val right = outputs[0][1] + + for (note in notes) { + if (note.state != NoteState.OFF) { + val sampleDelta = Note.fromMidi(note.note).sampleDelta + + for (i in 0 until samples) { + var targetVolume = note.velocity / 127f + if (note.state == NoteState.ON && note.sample < note.attackSamples) { + note.attackSamples-- + targetVolume *= ( 1f - (note.attackSamples / 2500f)) + } else if (note.state == NoteState.RELEASED) { + note.releaseSamples-- + targetVolume *= (note.releaseSamples / 10000f) + } + note.actualVolume += (targetVolume - note.actualVolume) * 0.01f + + if (note.state == NoteState.RELEASED && note.actualVolume <= 0) { + note.state = NoteState.OFF + } + + left[i] = left[i] + sin(note.cycleOffset * PI2).toFloat() * note.actualVolume + right[i] = right[i] + sin(note.cycleOffset * PI2).toFloat() * note.actualVolume + + note.cycleOffset += sampleDelta + if (note.cycleOffset > 1f) { + note.cycleOffset -= 1f + } + } + } + } + + return true + } +} + +fun main() { + registerProcessor("vst-chip-processor", VstChipProcessor::class.js) + + println("VstChipProcessor registered!") +} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..d9de1d9 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,67 @@ +import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackOutput + +buildscript { + apply(from = "common.gradle.kts") +} + +plugins { + kotlin("multiplatform") + kotlin("plugin.serialization") + id("maven-publish") + application +} + +kotlin { + js { + compilerOptions { + target.set("es2015") + } + //useEsModules() + //useCommonJs() + + binaries.executable() + browser { + commonWebpackConfig { + outputFileName = "vst-chip-worklet-ui.js" + //cssSupport.enabled = true + sourceMaps = true + } + + distribution { + outputDirectory.set(File("$projectDir/web/")) + } + } + } + jvm{ + withJava() + } + + sourceSets { + val commonMain by getting { + dependencies { + implementation(project(":common")) + //base + api("nl.astraeus:kotlin-css-generator:1.0.7") + } + } + val jsMain by getting { + dependencies { + //base + implementation("nl.astraeus:kotlin-komponent-js:1.2.2-SNAPSHOT") + } + } + val jsTest by getting { + dependencies { + implementation(kotlin("test-js")) + } + } + val jvmMain by getting { + dependencies { + //base + + implementation("io.undertow:undertow-core:2.3.13.Final") + implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.11.0") + } + } + } +} diff --git a/common.gradle.kts b/common.gradle.kts new file mode 100644 index 0000000..34228df --- /dev/null +++ b/common.gradle.kts @@ -0,0 +1,13 @@ +group = "nl.astraeus" +version = "1.0.0-SNAPSHOT" + +allprojects { + repositories { + mavenLocal() + mavenCentral() + maven("https://reposilite.astraeus.nl/releases") + maven { + url = uri("https://nexus.astraeus.nl/nexus/content/groups/public") + } + } +} diff --git a/common/build.gradle.kts b/common/build.gradle.kts new file mode 100644 index 0000000..4e2f549 --- /dev/null +++ b/common/build.gradle.kts @@ -0,0 +1,32 @@ +@file:OptIn(ExperimentalKotlinGradlePluginApi::class) + +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi + +buildscript { + apply(from = "../common.gradle.kts") +} + +plugins { + kotlin("multiplatform") +} + +kotlin { + js { + compilerOptions { + target.set("es2015") + } + browser() + } + jvm() + + sourceSets { + val commonMain by getting { + dependencies { + } + } + val jsMain by getting { + dependencies { + } + } + } +} diff --git a/common/settings.gradle.kts b/common/settings.gradle.kts new file mode 100644 index 0000000..fb2502c --- /dev/null +++ b/common/settings.gradle.kts @@ -0,0 +1 @@ +apply(from = "../settings.common.gradle.kts") diff --git a/common/src/commonMain/kotlin/nl/astraeus/vst/Note.kt b/common/src/commonMain/kotlin/nl/astraeus/vst/Note.kt new file mode 100644 index 0000000..9bdea86 --- /dev/null +++ b/common/src/commonMain/kotlin/nl/astraeus/vst/Note.kt @@ -0,0 +1,186 @@ +package nl.astraeus.vst + +import kotlin.math.max +import kotlin.math.min +import kotlin.math.pow + +/** + * User: rnentjes + * Date: 14-11-15 + * Time: 11:50 + */ + +enum class Note( + val sharp: String, + val flat: String +) { + NONE("---", "---"), + NO02("",""), + NO03("",""), + NO04("",""), + NO05("",""), + NO06("",""), + NO07("",""), + NO08("",""), + NO09("",""), + NO10("",""), + NO11("",""), + NO12("",""), + C0("C-0","C-0"), + C0s("C#0","Db0"), + D0("D-0","D-0"), + D0s("D#0","Eb0"), + E0("E-0","E-0"), + F0("F-0","F-0"), + F0s("F#0","Gb0"), + G0("G-0","G-0"), + G0s("G#0","Ab0"), + A0("A-0","A-0"), + A0s("A#0","Bb0"), + B0("B-0","B-0"), + C1("C-1","C-1"), + C1s("C#1","Db1"), + D1("D-1","D-1"), + D1s("D#1","Eb1"), + E1("E-1","E-1"), + F1("F-1","F-1"), + F1s("F#1","Gb1"), + G1("G-1","G-1"), + G1s("G#1","Ab1"), + A1("A-1","A-1"), + A1s("A#1","Bb1"), + B1("B-1","B-1"), + C2("C-2","C-2"), + C2s("C#2","Db2"), + D2("D-2","D-2"), + D2s("D#2","Eb2"), + E2("E-2","E-2"), + F2("F-2","F-2"), + F2s("F#2","Gb2"), + G2("G-2","G-2"), + G2s("G#2","Ab2"), + A2("A-2","A-2"), + A2s("A#2","Bb2"), + B2("B-2","B-2"), + C3("C-3","C-3"), + C3s("C#3","Db3"), + D3("D-3","D-3"), + D3s("D#3","Eb3"), + E3("E-3","E-3"), + F3("F-3","F-3"), + F3s("F#3","Gb3"), + G3("G-3","G-3"), + G3s("G#3","Ab3"), + A3("A-3","A-3"), + A3s("A#3","Bb3"), + B3("B-3","B-3"), + C4("C-4","C-4"), + C4s("C#4","Db4"), + D4("D-4","D-4"), + D4s("D#4","Eb4"), + E4("E-4","E-4"), + F4("F-4","F-4"), + F4s("F#4","Gb4"), + G4("G-4","G-4"), + G4s("G#4","Ab4"), + A4("A-4","A-4"), + A4s("A#4","Bb4"), + B4("B-4","B-4"), + C5("C-5","C-5"), + C5s("C#5","Db5"), + D5("D-5","D-5"), + D5s("D#5","Eb5"), + E5("E-5","E-5"), + F5("F-5","F-5"), + F5s("F#5","Gb5"), + G5("G-5","G-5"), + G5s("G#5","Ab5"), + A5("A-5","A-5"), + A5s("A#5","Bb5"), + B5("B-5","B-5"), + C6("C-6","C-6"), + C6s("C#6","Db6"), + D6("D-6","D-6"), + D6s("D#6","Eb6"), + E6("E-6","E-6"), + F6("F-6","F-6"), + F6s("F#6","Gb6"), + G6("G-6","G-6"), + G6s("G#6","Ab6"), + A6("A-6","A-6"), + A6s("A#6","Bb6"), + B6("B-6","B-6"), + C7("C-7","C-7"), + C7s("C#7","Db7"), + D7("D-7","D-7"), + D7s("D#7","Eb7"), + E7("E-7","E-7"), + F7("F-7","F-7"), + F7s("F#7","Gb7"), + G7("G-7","G-7"), + G7s("G#7","Ab7"), + A7("A-7","A-7"), + A7s("A#7","Bb7"), + B7("B-7","B-7"), + C8("C-8","C-8"), + C8s("C#8","Db8"), + D8("D-8","D-8"), + D8s("D#8","Eb8"), + E8("E-8","E-8"), + F8("F-8","F-8"), + F8s("F#8","Gb8"), + G8("G-8","G-8"), + G8s("G#8","Ab8"), + A8("A-8","A-8"), + A8s("A#8","Bb8"), + B8("B-8","B-8"), + C9("C-9","C-9"), + C9s("C#9","Db9"), + D9("D-9","D-9"), + D9s("D#9","Eb9"), + E9("E-9","E-9"), + F9("F-9","F-9"), + F9s("F#9","Gb9"), + G9("G-9","G-9"), + // out of midi range + //G9s("G#9","Ab9"), + //A9("A-9","A-9"), + //A9s("A#9","Bb9"), + //B9("B-9","B-9"), + UP("^^^","^^^"), + END("XXX","XXX"), + ; + + // 69 = A4.ordinal + val freq: Double = 440.0 * 2.0.pow((ordinal - 69)/12.0) + val cycleLength: Double = 1.0 / freq + var sampleDelta: Double = 0.0 + + fun transpose(semiNotes: Int): Note = if (ordinal >= C0.ordinal && ordinal <= G9.ordinal) { + var result = this.ordinal + semiNotes + + result = min(result, G9.ordinal) + result = max(result, C0.ordinal) + + entries.firstOrNull { it.ordinal == result } ?: this + } else { + this + } + + companion object { + var sampleRate: Int = 44100 + + fun fromMidi(midi: Int): Note { + // todo: add check + return entries[midi] + } + + fun updateSampleRate(rate: Int) { + println("Setting sample rate to $rate") + sampleRate = rate + for (note in Note.entries) { + note.sampleDelta = (1.0 / sampleRate.toDouble()) / note.cycleLength + } + } + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..7fc6f1f --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +kotlin.code.style=official diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..03a8119 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sat Jun 15 15:16:05 CEST 2024 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.common.gradle.kts b/settings.common.gradle.kts new file mode 100644 index 0000000..08e8c93 --- /dev/null +++ b/settings.common.gradle.kts @@ -0,0 +1,12 @@ +pluginManagement { + plugins { + kotlin("multiplatform") version "2.0.0" + kotlin("plugin.serialization") version "2.0.0" + id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0" + } + repositories { + gradlePluginPortal() + mavenCentral() + maven { setUrl("https://plugins.gradle.org/m2/") } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..ab45cf9 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,6 @@ +apply(from = "settings.common.gradle.kts") + +rootProject.name = "vst-chip" + +include(":common") +include(":audio-worklet") diff --git a/src/jsMain/kotlin/nl/astraeus/vst/chip/Externals.kt b/src/jsMain/kotlin/nl/astraeus/vst/chip/Externals.kt new file mode 100644 index 0000000..c476f6b --- /dev/null +++ b/src/jsMain/kotlin/nl/astraeus/vst/chip/Externals.kt @@ -0,0 +1,18 @@ +package nl.astraeus.vst.chip + +external class AudioContext { + var sampleRate: Int +} + +external class AudioWorkletNode( + audioContext: dynamic, + name: String, + options: dynamic +) + +class AudioWorkletNodeParameters( + @JsName("numberOfInputs") + val numberOfInputs: Int, + @JsName("outputChannelCount") + val outputChannelCount: Array +) diff --git a/src/jsMain/kotlin/nl/astraeus/vst/chip/Main.kt b/src/jsMain/kotlin/nl/astraeus/vst/chip/Main.kt new file mode 100644 index 0000000..f2fb097 --- /dev/null +++ b/src/jsMain/kotlin/nl/astraeus/vst/chip/Main.kt @@ -0,0 +1,12 @@ +package nl.astraeus.vst.chip + +import kotlinx.browser.document +import nl.astraeus.komp.Komponent +import nl.astraeus.vst.chip.channel.Broadcaster +import nl.astraeus.vst.chip.view.MainView + +fun main() { + Komponent.create(document.body!!, MainView) + + Broadcaster.start() +} diff --git a/src/jsMain/kotlin/nl/astraeus/vst/chip/audio/AudioContextHandler.kt b/src/jsMain/kotlin/nl/astraeus/vst/chip/audio/AudioContextHandler.kt new file mode 100644 index 0000000..3db72b1 --- /dev/null +++ b/src/jsMain/kotlin/nl/astraeus/vst/chip/audio/AudioContextHandler.kt @@ -0,0 +1,10 @@ +package nl.astraeus.vst.chip.audio + +import nl.astraeus.vst.chip.AudioContext + +object AudioContextHandler { + val audioContext: dynamic = AudioContext() + + + +} \ No newline at end of file diff --git a/src/jsMain/kotlin/nl/astraeus/vst/chip/audio/AudioModule.kt b/src/jsMain/kotlin/nl/astraeus/vst/chip/audio/AudioModule.kt new file mode 100644 index 0000000..95cbe95 --- /dev/null +++ b/src/jsMain/kotlin/nl/astraeus/vst/chip/audio/AudioModule.kt @@ -0,0 +1,88 @@ +package nl.astraeus.vst.chip.audio + +import nl.astraeus.vst.chip.AudioWorkletNode +import nl.astraeus.vst.chip.AudioWorkletNodeParameters +import nl.astraeus.vst.chip.audio.AudioContextHandler.audioContext +import org.w3c.dom.MessageEvent +import org.w3c.dom.MessagePort + +enum class ModuleStatus { + INIT, + LOADING, + READY +} + +class AudioModule( + val jsFile: String +) { + var status = ModuleStatus.INIT + var module: dynamic = null + + fun load(action: () -> Unit = {}) { + if (module == null && status == ModuleStatus.INIT) { + status = ModuleStatus.LOADING + + module = audioContext.audioWorklet.addModule( + jsFile + ) + module.then { + status = ModuleStatus.READY + action() + } + } else if (status == ModuleStatus.READY) { + action() + } else { + console.log("Module not yet loaded") + } + } + +} + +abstract class AudioNode( + val jsFile: String, + val processorName: String, + val numberOfInputs: Int = 0, + val outputChannelCount: Array = arrayOf(2), + val destination: dynamic = null, + val outputIndex: Int = 0, + val inputIndex: Int = 0 +) { + val module = AudioModule(jsFile) + var created = false + var node: dynamic = null + var port: MessagePort? = null + + abstract fun onMessage(message: MessageEvent) + + open fun postMessage(msg: Any) { + port?.postMessage(msg) + } + + // call from user gesture + fun create(done: (node: dynamic) -> Unit) { + module.load { + node = AudioWorkletNode( + audioContext, + processorName, + AudioWorkletNodeParameters( + numberOfInputs, + outputChannelCount + ) + ) + + if (destination == null) { + node.connect(audioContext.destination) + } else { + node.connect(destination, outputIndex, inputIndex) + } + + node.port.onmessage = ::onMessage + + port = node.port as? MessagePort + + created = true + + done(node) + } + } +} diff --git a/src/jsMain/kotlin/nl/astraeus/vst/chip/audio/VstChipWorklet.kt b/src/jsMain/kotlin/nl/astraeus/vst/chip/audio/VstChipWorklet.kt new file mode 100644 index 0000000..b07cd70 --- /dev/null +++ b/src/jsMain/kotlin/nl/astraeus/vst/chip/audio/VstChipWorklet.kt @@ -0,0 +1,14 @@ +package nl.astraeus.vst.chip.audio + +import org.w3c.dom.MessageEvent + +object VstChipWorklet : AudioNode( + "vst-chip-worklet.js", + "vst-chip-processor" +) { + + override fun onMessage(message: MessageEvent) { + console.log("Message from worklet: ", message) + } + +} diff --git a/src/jsMain/kotlin/nl/astraeus/vst/chip/channel/Broadcaster.kt b/src/jsMain/kotlin/nl/astraeus/vst/chip/channel/Broadcaster.kt new file mode 100644 index 0000000..08bbc35 --- /dev/null +++ b/src/jsMain/kotlin/nl/astraeus/vst/chip/channel/Broadcaster.kt @@ -0,0 +1,23 @@ +package nl.astraeus.vst.chip.channel + +import nl.astraeus.vst.chip.view.MainView +import org.w3c.dom.BroadcastChannel +import org.w3c.dom.MessageEvent +import kotlin.js.Date + +object Broadcaster { + + val channel = BroadcastChannel("audio-worklet") + + fun start() { + channel.onmessage = ::onMessage + } + + fun onMessage(event: MessageEvent) { + MainView.addMessage("Received message ${event.data} time ${Date().getTime()}") + } + + fun send(message: String) { + channel.postMessage(message) + } +} diff --git a/src/jsMain/kotlin/nl/astraeus/vst/chip/view/MainView.kt b/src/jsMain/kotlin/nl/astraeus/vst/chip/view/MainView.kt new file mode 100644 index 0000000..5015dad --- /dev/null +++ b/src/jsMain/kotlin/nl/astraeus/vst/chip/view/MainView.kt @@ -0,0 +1,184 @@ +package nl.astraeus.vst.chip.view + +import daw.style.Css +import daw.style.Css.defineCss +import daw.style.Css.noTextSelect +import daw.style.CssId +import daw.style.CssName +import daw.style.hover +import kotlinx.html.FlowContent +import kotlinx.html.a +import kotlinx.html.classes +import kotlinx.html.div +import kotlinx.html.h1 +import kotlinx.html.hr +import kotlinx.html.js.onClickFunction +import kotlinx.html.js.onMouseDownFunction +import kotlinx.html.js.onMouseUpFunction +import kotlinx.html.span +import nl.astraeus.css.properties.BoxSizing +import nl.astraeus.css.properties.FontWeight +import nl.astraeus.css.properties.prc +import nl.astraeus.css.properties.px +import nl.astraeus.css.properties.rem +import nl.astraeus.css.style.cls +import nl.astraeus.komp.HtmlBuilder +import nl.astraeus.komp.Komponent +import nl.astraeus.vst.Note +import nl.astraeus.vst.chip.audio.VstChipWorklet +import nl.astraeus.vst.chip.channel.Broadcaster +import org.khronos.webgl.Int32Array + +object MainView : Komponent() { + private var messages: MutableList = ArrayList() + + init { + MainViewCss + } + + fun addMessage(message: String) { + messages.add(message) + while (messages.size > 10) { + messages.removeAt(0) + } + requestUpdate() + } + + override fun HtmlBuilder.render() { + div { + h1 { + +"VST Chip" + } + div { + +"Hello, World!" + } + div { + if (VstChipWorklet.created) { + +"Worklet created" + } else { + a { + href = "#" + +"Create worklet" + onClickFunction = { + VstChipWorklet.create { + requestUpdate() + } + } + } + } + } + div { + a { + href = "#" + +"Send broadcast test message" + onClickFunction = { + Broadcaster.send("Test message") + } + } + } + div { + a { + href = "#" + +"Note on" + onClickFunction = { + VstChipWorklet.postMessage("test_on") + } + } + a { + href = "#" + +"Note off" + onClickFunction = { + VstChipWorklet.postMessage("test_off") + } + } + } + + hr {} + + repeat(9) { + div(classes = MainViewCss.NoteBarCss.name) { + for (index in it*12+12..it*12+23) { + notePlayer(Note.entries[index]) + } + } + } + + hr {} + + for (message in messages) { + div { + +message + } + } + } + } + + private fun FlowContent.notePlayer(note: Note) { + span { + a(classes = MainViewCss.ButtonCss.name) { + href = "#" + +note.sharp + onMouseDownFunction = { + VstChipWorklet.postMessage(Int32Array(arrayOf(0x90, note.ordinal, 32))) + } + onMouseUpFunction = { + VstChipWorklet.postMessage(Int32Array(arrayOf(0x90, note.ordinal, 0))) + } + /* + onMouseOutFunction = { + VstChipWorklet.postMessage(Int32Array(arrayOf(0x90, note.ordinal, 0))) + } +*/ + } + } + } + + object MainViewCss : CssId("main") { + object ActiveCss : CssName() + object ButtonCss : CssName() + object NoteBarCss : CssName() + + init { + defineCss { + select("*") { + select("*:before") { + select("*:after") { + boxSizing(BoxSizing.borderBox) + } + } + } + select("html", "body") { + margin(0.px) + padding(0.px) + height(100.prc) + + color(Css.currentStyle.mainFontColor) + backgroundColor(Css.currentStyle.mainBackgroundColor) + + fontFamily("JetbrainsMono, monospace") + fontSize(14.px) + fontWeight(FontWeight.bold) + + //transition() + noTextSelect() + } + select(cls(ButtonCss)) { + margin(1.rem) + padding(1.rem) + backgroundColor(Css.currentStyle.buttonBackgroundColor) + + hover { + backgroundColor(Css.currentStyle.buttonBackgroundColor.hover()) + } + } + select(cls(ActiveCss)) { + backgroundColor(Css.currentStyle.selectedBackgroundColor) + } + select(cls(NoteBarCss)) { + minHeight(4.rem) + } + } + } + } + +} diff --git a/src/jsMain/kotlin/nl/astraeus/vst/chip/view/css/Css.kt b/src/jsMain/kotlin/nl/astraeus/vst/chip/view/css/Css.kt new file mode 100644 index 0000000..3ddfece --- /dev/null +++ b/src/jsMain/kotlin/nl/astraeus/vst/chip/view/css/Css.kt @@ -0,0 +1,137 @@ +package daw.style + +import kotlinx.browser.document +import nl.astraeus.css.properties.Color +import nl.astraeus.css.properties.UserSelect +import nl.astraeus.css.properties.hsl +import nl.astraeus.css.properties.hsla +import nl.astraeus.css.style +import nl.astraeus.css.style.ConditionalStyle +import nl.astraeus.css.style.Style + +class StyleDefinition( + val mainFontColorNumber: Int = 32, + val mainFontColor: Color = hsla(mainFontColorNumber, 70, 55, 1.0), + val mainBackgroundColor: Color, + val selectedBackgroundColor: Color = hsl((Css.mainFontColorNumber + 180) % 360, 40, 30), + val mainBorderColor: Color = hsla((Css.mainFontColorNumber + 270) % 360, 0, 20, 0.5), + val headerFontColor: Color = hsl(Css.mainFontColorNumber, 50, 95), + val entryFontColor: Color = hsl(Css.mainFontColorNumber, 70, 55), + val successColor: Color = hsl(120, 70, 55), + val highlightFontColor : Color = hsl(Css.mainFontColorNumber, 70, 65), + val menuFontColor : Color = hsl(Css.mainFontColorNumber, 50, 100), + val menuBackgroundColor : Color = hsl(Css.mainFontColorNumber, 50, 16), + val backgroundComponentColor : Color = hsl(0, 0, 16), + val inputBackgroundColor : Color = hsl(0, 0, 20), + val inputBorderColor : Color = hsl(Css.mainFontColorNumber, 50, 50), + val buttonFontColor : Color = hsl(Css.mainFontColorNumber, 50, 95), + val buttonBackgroundColor : Color = hsl(Css.mainFontColorNumber, 70, 55), + val tabColor: Color = mainFontColor.darken(20), + val tabSelectedColor: Color = mainFontColor.lighten(10), + val tabHoverColor: Color = tabSelectedColor.lighten(20), +) + +object NoTextSelectCls : CssName("no-text-select") +object SelectedCls : CssName("selected") +object ActiveCls : CssName("active") + +fun Color.hover(): Color = if (Css.currentStyle == Css.darkStyle) { + this.lighten(15) +} else { + this.darken(15) +} + +object Css { + var minified = false + var mainFontColorNumber = 32 + var dynamicStyles = mutableMapOf Unit>() + + fun CssId.defineCss(conditionalStyle: ConditionalStyle.() -> Unit) { + check(!dynamicStyles.containsKey(this)) { + "CssId with name ${this.name} already defined!" + } + + updateCss(conditionalStyle) + } + + private fun CssId.updateCss(conditionalStyle: ConditionalStyle.() -> Unit) { + val elementId = this.description() + var dynamicStyleElement = document.getElementById(elementId) + + dynamicStyles[this] = conditionalStyle + + if (dynamicStyleElement == null) { + dynamicStyleElement = document.createElement("style") + dynamicStyleElement.id = elementId + + document.head?.append(dynamicStyleElement) + } + + val css = style(conditionalStyle) + + dynamicStyleElement.innerHTML = css.generateCss(minified = false) + } + + val darkStyle = StyleDefinition( + mainFontColorNumber = mainFontColorNumber, + mainBackgroundColor = hsl(0, 0, 10) + ) + + val lightStyle = StyleDefinition( + mainFontColorNumber = (mainFontColorNumber + 120) % 360, + mainBackgroundColor = hsl(0, 0, 98), + mainBorderColor = hsla((Css.mainFontColorNumber + 270) % 360, 0, 20, 0.15), + headerFontColor = hsl(mainFontColorNumber, 50, 5), + backgroundComponentColor = hsl(0, 0, 84), + ) + + var currentStyle: StyleDefinition = darkStyle + + fun updateStyle() { + for ((cssId, dynStyle) in dynamicStyles) { + cssId.apply { + updateCss(dynStyle) + } + } + } + + fun switchLayout() { + currentStyle = if (currentStyle == darkStyle) { + lightStyle + } else { + darkStyle + } + + updateStyle() + } + + fun Style.transition() { + transition("all 0.5s ease") + } + + fun Style.noTextSelect() { + plain("-webkit-touch-callout", "none") + plain("-webkit-user-select", "none") + plain("-moz-user-select", "none") + plain("-ms-user-select", "none") + + userSelect(UserSelect.none) + + select("::selection") { + background("none") + } + } + + object GenericCss : CssId("generic") { + init { + fun generateStyle(): String { + val css = style { + + } + + return css.generateCss(minified = minified) + } + } + } + +} diff --git a/src/jsMain/kotlin/nl/astraeus/vst/chip/view/css/CssName.kt b/src/jsMain/kotlin/nl/astraeus/vst/chip/view/css/CssName.kt new file mode 100644 index 0000000..9c69b18 --- /dev/null +++ b/src/jsMain/kotlin/nl/astraeus/vst/chip/view/css/CssName.kt @@ -0,0 +1,65 @@ +package daw.style + +import nl.astraeus.css.style.DescriptionProvider +import nl.astraeus.css.style.cls + +private val CAPITAL_LETTER = Regex("[A-Z]") + +fun String.hyphenize(): String = + replace(CAPITAL_LETTER) { + "-${it.value.lowercase()}" + } + +private val shortId = false +private var nextCssId = 1 + +private fun nextShortId(): String { + var id = nextCssId++ + val result = StringBuilder() + + while(id > 0) { + val ch = ((id % 26) + 'a'.code).toChar() + result.append(ch) + + id /= 26 + } + + return result.toString() +} + +open class CssName(name: String? = null) : DescriptionProvider { + val name: String = if (shortId) { + nextShortId() + } else if (name != null) { + "daw-$name" + } else { + "daw${this::class.simpleName?.hyphenize() ?: this::class}" + } + + override fun description() = name +} + +fun CssName.cls() : DescriptionProvider = cls(this) + +open class CssId(name: String) : DescriptionProvider { + val name: String = if (shortId) { + nextShortId() + } else { + "daw-$name-css" + } + override fun description() = name + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is CssId) return false + + if (name != other.name) return false + + return true + } + + override fun hashCode(): Int { + return name.hashCode() + } + +} diff --git a/src/jvmMain/kotlin/nl/astraeus/vst/chip/Index.kt b/src/jvmMain/kotlin/nl/astraeus/vst/chip/Index.kt new file mode 100644 index 0000000..42da567 --- /dev/null +++ b/src/jvmMain/kotlin/nl/astraeus/vst/chip/Index.kt @@ -0,0 +1,2 @@ +package nl.astraeus.vst.chip + diff --git a/src/jvmMain/kotlin/nl/astraeus/vst/chip/Main.kt b/src/jvmMain/kotlin/nl/astraeus/vst/chip/Main.kt new file mode 100644 index 0000000..b67c6e0 --- /dev/null +++ b/src/jvmMain/kotlin/nl/astraeus/vst/chip/Main.kt @@ -0,0 +1,20 @@ +package nl.astraeus.vst.chip + +import io.undertow.Undertow +import io.undertow.UndertowOptions + +fun main() { + Thread.setDefaultUncaughtExceptionHandler { _, e -> + e.printStackTrace() + } + + val server = Undertow.builder() + .addHttpListener(Settings.port, "0.0.0.0") + .setIoThreads(4) + .setHandler(RequestHandler) + .setServerOption(UndertowOptions.SHUTDOWN_TIMEOUT, 1000) + .build() + + println("Starting server at port ${Settings.port}...") + server?.start() +} diff --git a/src/jvmMain/kotlin/nl/astraeus/vst/chip/RequestHandler.kt b/src/jvmMain/kotlin/nl/astraeus/vst/chip/RequestHandler.kt new file mode 100644 index 0000000..b3120c5 --- /dev/null +++ b/src/jvmMain/kotlin/nl/astraeus/vst/chip/RequestHandler.kt @@ -0,0 +1,16 @@ +package nl.astraeus.vst.chip + +import io.undertow.server.HttpHandler +import io.undertow.server.HttpServerExchange +import io.undertow.server.handlers.resource.PathResourceManager +import io.undertow.server.handlers.resource.ResourceHandler +import java.nio.file.Paths + +object RequestHandler : HttpHandler { + val resourceHandler = ResourceHandler(PathResourceManager(Paths.get("web"))) + + override fun handleRequest(exchange: HttpServerExchange) { + resourceHandler.handleRequest(exchange) + } + +} diff --git a/src/jvmMain/kotlin/nl/astraeus/vst/chip/Settings.kt b/src/jvmMain/kotlin/nl/astraeus/vst/chip/Settings.kt new file mode 100644 index 0000000..9a76c92 --- /dev/null +++ b/src/jvmMain/kotlin/nl/astraeus/vst/chip/Settings.kt @@ -0,0 +1,50 @@ +package nl.astraeus.vst.chip + +import java.io.File +import java.io.FileInputStream +import java.util.* + +object Settings { + var runningAsRoot: Boolean = false + var port = 9000 + var sslPort = 8443 + var connectionTimeout = 30000 + + var jdbcDriver = "nl.astraeus.jdbc.Driver" + var jdbcConnectionUrl = "jdbc:stat:webServerPort=6001:jdbc:sqlite:data/srp.db" + var jdbcUser = "sa" + var jdbcPassword = "" + + var adminUser = "rnentjes" + var adminPassword = "9/SG_Bd}9gWz~?j\\A.U]n9]OO" + + fun getPropertiesFromFile(filename: String): Properties? { + val propertiesFile = File(filename) + return if (propertiesFile.exists()) { + val properties = Properties() + FileInputStream(propertiesFile).use { + properties.load(it) + } + properties + } else { + null + } + } + + fun readProperties(args: Array) { + val filename = if (args.isNotEmpty()) args[0] else "srp.properties" + val properties = getPropertiesFromFile(filename) ?: return // return if properties couldn't be loaded + + runningAsRoot = properties.getProperty("runningAsRoot", runningAsRoot.toString()).toBoolean() + port = properties.getProperty("port", port.toString()).toInt() + sslPort = properties.getProperty("sslPort", sslPort.toString()).toInt() + connectionTimeout = properties.getProperty("connectionTimeout", connectionTimeout.toString()).toInt() + jdbcDriver = properties.getProperty("jdbcDriver", jdbcDriver) + jdbcConnectionUrl = properties.getProperty("jdbcConnectionUrl", jdbcConnectionUrl) + jdbcUser = properties.getProperty("jdbcUser", jdbcUser) + jdbcPassword = properties.getProperty("jdbcPassword", jdbcPassword) + + adminUser = properties.getProperty("adminUser", adminUser) + adminPassword = properties.getProperty("adminPassword", adminPassword) + } +} diff --git a/src/jvmMain/resources/index.html b/src/jvmMain/resources/index.html new file mode 100644 index 0000000..22e3c6e --- /dev/null +++ b/src/jvmMain/resources/index.html @@ -0,0 +1,6 @@ + + + + + +