12 Commits

Author SHA1 Message Date
7110188d33 Add project documentation and update .gitignore
Introduces development guidelines for the VST Chip synthesizer, including setup, build, and testing instructions. Adds a placeholder README for the data directory and updates `.gitignore` to include new project-specific build artifacts and paths.
2025-05-06 19:13:43 +02:00
2cfc8a8201 Update output directories and Kotlin version, add buildJS task
Modified outputDirectory paths in build scripts and upgraded the Kotlin multiplatform plugin to version 2.1.20. Added a new buildJS task to handle copying files from multiple directories into the web folder. These changes streamline the build process and ensure compatibility with updated tooling.
2025-05-06 18:59:15 +02:00
ce353d3113 Add MidiMessageHandler for MIDI event handling
Introduced `MidiMessageHandler` to process and handle MIDI messages with customizable handlers for specific byte patterns. This addition improves extensibility and keeps MIDI message processing modular and organized.
2025-03-28 13:47:00 +01:00
ff8a4dbf92 Update dependencies and clean up unused code
Upgraded `vst-ui-base` to version 2.0.0 and removed `kotlin-css-generator`. Cleaned up commented-out and unnecessary dependency blocks for better readability and maintenance.
2025-03-27 19:46:36 +01:00
dc50084e84 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	build.gradle.kts
2025-03-27 19:34:30 +01:00
7fe29916f7 Refactor MainView and enhance View management
Replaced `MainView` object with a `Views` singleton for better modularity and lazy initialization. Adjusted CSS structure, updated dependencies, and improved FM/AM modulation logic for greater flexibility. Additionally, upgraded Kotlin multiplatform version and added inline source mapping.
2025-03-27 19:33:43 +01:00
310f77fc3a Update Gradle config, dependencies, and Kotlin plugin version
Configured Gradle to use "corretto-21" JVM and bumped the Kotlin Multiplatform plugin to version 2.1.10. Updated the "midi-arrays" dependency to version 0.3.4 across relevant modules. These changes ensure compatibility and leverage the latest library improvements.
2025-03-17 18:16:47 +00:00
60a21bbd79 Update dependencies and refine MIDI handling.
Upgraded various dependencies, including `vst-ui-base` to 1.2.0 and build configurations to include `mavenLocal`. Refined MIDI handling by removing redundant logging to improve performance and clarity. Adjusted knob component value ranges for better user experience.
2024-12-26 14:23:16 +01:00
d58fb9c7b5 Refactor MIDI handling and update dependencies.
Streamlined MIDI message handling by introducing `MidiMessageHandler` and removed redundant code. Added better handler support for specific message types and parameters. Also upgraded Kotlin to version 2.1.0 and adjusted build configurations.
2024-12-21 20:42:19 +01:00
fbba6d1422 Refactor MIDI handling and improve audio processing
Replaced `uInt8ArrayOf` with simplified integer arrays for MIDI messages. Introduced `TimedMidiMessage` and buffer handling for better synchronization in audio processing. Updated Gradle dependencies and added timing-aware MIDI utilities.
2024-12-17 20:51:32 +01:00
4c00356dff Increase polyphony and comment out debug logs
Updated the polyphony level from 10 to 20 to enhance sound capability and commented out several debug logs for cleaner console output. Additionally, commented out a block of code related to sine wave modulation that appears unnecessary at this stage. The console log message for registering the processor was slightly modified for consistency.
2024-12-09 19:51:57 +01:00
29aac228e5 Update VST Worklet Base dependency version
Upgrade the nl.astraeus:vst-worklet-base dependency from version 1.0.0-SNAPSHOT to 1.0.1 in audio-worklet/build.gradle.kts. This change ensures compatibility with the updated library while bringing in any fixes or enhancements included in the new version.
2024-12-08 20:43:00 +01:00
18 changed files with 519 additions and 307 deletions

4
.gitignore vendored
View File

@@ -42,6 +42,10 @@ bin/
.DS_Store .DS_Store
/web /web
/web1
/web2
/data/*.db*
**/kotlin-js-store/*
.kotlin .kotlin
.idea .idea

2
.idea/gradle.xml generated
View File

@@ -5,7 +5,7 @@
<option name="linkedExternalProjectsSettings"> <option name="linkedExternalProjectsSettings">
<GradleProjectSettings> <GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleHome" value="" /> <option name="gradleJvm" value="corretto-21" />
<option name="modules"> <option name="modules">
<set> <set>
<option value="$PROJECT_DIR$" /> <option value="$PROJECT_DIR$" />

105
.junie/guidelines.md Normal file
View File

@@ -0,0 +1,105 @@
# VST Chip Synthesizer Development Guidelines
## Project Overview
VST Chip is a Kotlin multiplatform project that implements a chip-style synthesizer with a web interface. The project consists of:
- JVM backend that serves the web application
- JS frontend that runs in the browser
- Audio worklet processor for real-time audio processing
## Build/Configuration Instructions
### Prerequisites
- JDK 11 or higher
- Gradle 7.x or higher
- Node.js and npm (for JS development)
### Building the Project
1. **Full Build**:
```bash
./gradlew build
```
2. **JS Build Only**:
```bash
./gradlew buildJS
```
3. **Deployment**:
```bash
./gradlew deploy
```
This will build the project, copy web assets, and deploy to the configured server location.
### Configuration
- Server port: 9005 (configured in `Main.kt`)
- JDBC stats port: 6005
- Deployment directory: configured in `build.gradle.kts` as `vst-chip.midi-vst.com`
## Testing Information
- **Do not generate tests** for this project. The audio processing code is highly specialized and requires manual testing with audio
equipment.
- Manual testing should be performed using MIDI controllers and audio monitoring tools.
## Development Information
### Project Structure
- **src/commonMain**: Shared code between JS and JVM
- **src/jsMain**: Browser-specific code
- **src/jvmMain**: Server-specific code
- **audio-worklet**: Audio processing code that runs in a separate thread in the browser
### Key Components
1. **JVM Server** (`src/jvmMain/kotlin/nl/astraeus/vst/chip/Main.kt`):
- Undertow server that serves the web application
- Database initialization
- Logging setup
2. **JS Frontend** (`src/jsMain/kotlin/nl/astraeus/vst/chip/Main.kt`):
- UI implementation using Komponent library
- MIDI handling
- WebSocket client for server communication
3. **Audio Processor** (`audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/chip/ChipProcessor.kt`):
- Real-time audio synthesis
- MIDI message handling
- Sound generation with multiple waveforms (sine, square, triangle, sawtooth)
- Effects processing (FM, AM, ADSR envelope, delay, feedback)
### Development Workflow
1. Make changes to the code
2. Run `./gradlew buildJS` to build the JS part
3. Run the JVM application to test locally
4. Use `./gradlew deploy` to deploy to the server
### Audio Worklet Development
The audio worklet runs in a separate thread in the browser and handles real-time audio processing. When modifying the audio worklet code:
1. Understand that it runs in a separate context from the main JS code
2. Communication happens via message passing
3. Performance is critical - avoid garbage collection and heavy operations in the audio processing loop
### MIDI Implementation
The synthesizer responds to standard MIDI messages:
- Note On/Off (0x90/0x80)
- Control Change (0xb0) for various parameters
- Program Change (0xc9) for waveform selection
## Deployment
The project is configured to deploy to a specific server location. The deployment process:
1. Builds the project
2. Copies web assets
3. Creates a symbolic link for the latest version

View File

@@ -31,16 +31,16 @@ kotlin {
} }
distribution { distribution {
outputDirectory.set(File("$projectDir/../web/")) outputDirectory.set(File("$projectDir/../web2/"))
} }
} }
} }
jvm()
sourceSets { sourceSets {
val commonMain by getting { val commonMain by getting {
dependencies { dependencies {
implementation("nl.astraeus:vst-worklet-base:1.0.0-SNAPSHOT") implementation("nl.astraeus:vst-worklet-base:1.0.1")
implementation("nl.astraeus:midi-arrays:0.3.4")
} }
} }
val jsMain by getting val jsMain by getting

View File

@@ -0,0 +1,82 @@
package nl.astraeus.vst.midi
typealias MidiHandler = (Byte, Byte, Byte) -> Unit
val Byte.channel: Byte
get() {
return (this.toInt() and 0x0F).toByte()
}
val Byte.command: Byte
get() {
return ((this.toInt() and 0xF0) shr 4).toByte()
}
val Byte.channelCommand: Boolean
get() {
return this.command >= 8 && this.command <= 13
}
class MidiMessageHandler(
var channel: Byte = -1
) {
val singleByteHandlers = mutableMapOf<Byte, MidiHandler>()
val doubleByteHandlers = mutableMapOf<Byte, MutableMap<Byte, MidiHandler>>()
fun addHandler(
byte1: Byte,
byte2: Byte = 0,
handler: MidiHandler
) {
val b1 = if (byte1.channelCommand) {
(byte1.toInt() and 0xF0).toByte()
} else {
byte1
}
if (byte2 == 0.toByte()) {
singleByteHandlers[b1] = handler
} else {
val map = doubleByteHandlers.getOrPut(b1) {
mutableMapOf()
}
map[byte2] = handler
}
}
fun addHandler(
byte1: Int,
byte2: Int = 0,
handler: MidiHandler
) = addHandler(
byte1.toByte(),
byte2.toByte(),
handler
)
fun handle(
byte1: Byte,
byte2: Byte,
byte3: Byte
) {
if (
channel < 0 ||
!byte1.channelCommand ||
(byte1.channelCommand && channel == byte1.channel)
) {
val b1 = if (byte1.channelCommand) {
(byte1.toInt() and 0xF0).toByte()
} else {
byte1
}
if (doubleByteHandlers.containsKey(b1)) {
doubleByteHandlers[b1]?.get(byte2)?.invoke(byte1, byte2, byte3)
} else if (singleByteHandlers.containsKey(b1)) {
singleByteHandlers[b1]?.invoke(byte1, byte2, byte3)
} else {
println("Unhandled message: $byte1 $byte2 $byte3")
}
}
}
}

View File

@@ -2,14 +2,16 @@
package nl.astraeus.vst.chip package nl.astraeus.vst.chip
import nl.astraeus.midi.message.SortedTimedMidiMessageList
import nl.astraeus.midi.message.TimedMidiMessage
import nl.astraeus.tba.SlicedByteArray
import nl.astraeus.vst.ADSR import nl.astraeus.vst.ADSR
import nl.astraeus.vst.AudioWorkletProcessor import nl.astraeus.vst.AudioWorkletProcessor
import nl.astraeus.vst.currentTime import nl.astraeus.vst.currentTime
import nl.astraeus.vst.midi.MidiMessageHandler
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
import org.khronos.webgl.Int32Array
import org.khronos.webgl.Uint8Array
import org.khronos.webgl.get import org.khronos.webgl.get
import org.khronos.webgl.set import org.khronos.webgl.set
import org.w3c.dom.MessageEvent import org.w3c.dom.MessageEvent
@@ -17,7 +19,7 @@ import kotlin.math.PI
import kotlin.math.min import kotlin.math.min
import kotlin.math.sin import kotlin.math.sin
val POLYPHONICS = 10 val POLYPHONICS = 20
val PI2 = PI * 2 val PI2 = PI * 2
@ExperimentalJsExport @ExperimentalJsExport
@@ -64,13 +66,14 @@ enum class RecordingState {
@ExperimentalJsExport @ExperimentalJsExport
@JsExport @JsExport
class VstChipProcessor : AudioWorkletProcessor() { class VstChipProcessor : AudioWorkletProcessor() {
var midiChannel = 0 val midiMessageBuffer = SortedTimedMidiMessageList()
val midiMessageHandler = MidiMessageHandler()
val notes = Array<PlayingNote?>(POLYPHONICS) { null } val notes = Array<PlayingNote?>(POLYPHONICS) { null }
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
var fmFreq = 0.0 var fmFreq = 0.5
var fmAmp = 0.0 var fmAmp = 0.0
var amFreq = 0.0 var amFreq = 0.0
var amAmp = 0.0 var amAmp = 0.0
@@ -98,18 +101,94 @@ class VstChipProcessor : AudioWorkletProcessor() {
init { init {
this.port.onmessage = ::handleMessage this.port.onmessage = ::handleMessage
Note.updateSampleRate(sampleRate) Note.updateSampleRate(sampleRate)
with(midiMessageHandler) {
addHandler(0x90) { b1, b2, b3 ->
val note = b2.toInt() and 0xff
val velocity = b3.toInt() and 0xff
if (velocity > 0) {
console.log("Note on", note, velocity)
noteOn(note, velocity)
} else {
console.log("Note off", note)
noteOff(note)
}
}
addHandler(0x80) { b1, b2, b3 ->
val note = b2.toInt() and 0xff
console.log("Note off", note)
noteOff(note)
}
addHandler(0xc9) { b1, b2, b3 ->
waveform = b2.toInt() and 0xff
}
addHandler(0xb0, 7) { b1, b2, b3 ->
volume = b3 / 127f
}
addHandler(0xb0, 0x47) { b1, b2, b3 ->
dutyCycle = b3 / 127.0
}
addHandler(0xb0, 0x40) { b1, b2, b3 ->
fmFreq = b3 / 127.0
}
addHandler(0xb0, 0x41) { b1, b2, b3 ->
fmAmp = b3 / 127.0
}
addHandler(0xb0, 0x42) { b1, b2, b3 ->
amFreq = b3 / 127.0
}
addHandler(0xb0, 0x43) { b1, b2, b3 ->
amAmp = b3 / 127.0
}
addHandler(0xb0, 0x49) { b1, b2, b3 ->
attack = b3 / 127.0
}
addHandler(0xb0, 0x4b) { b1, b2, b3 ->
decay = b3 / 127.0
}
addHandler(0xb0, 0x46) { b1, b2, b3 ->
sustain = b3 / 127.0
}
addHandler(0xb0, 0x48) { b1, b2, b3 ->
release = b3 / 127.0
}
addHandler(0xb0, 0x4e) { b1, b2, b3 ->
delay = b3 / 127.0
}
addHandler(0xb0, 0x4f) { b1, b2, b3 ->
delayDepth = b3 / 127.0
}
addHandler(0xb0, 0x50) { b1, b2, b3 ->
feedback = b3 / 127.0
}
addHandler(0xb0, 123) { b1, b2, b3 ->
for (note in notes) {
note?.noteRelease = currentTime
}
}
addHandler(0xe0) { b1, b2, b3 ->
if (b2.toInt() and 0xff > 0) {
val lsb = b2.toInt() and 0xff
val msb = b3.toInt() and 0xff
amFreq = (((msb - 0x40) + (lsb / 127.0)) / 0x40) * 10.0
}
}
}
} }
private fun handleMessage(message: MessageEvent) { private fun handleMessage(message: MessageEvent) {
//console.log("VstChipProcessor: Received message:", message.data) //console.log("VstChipProcessor: Received message:", currentTime)
val data = message.data val data = message.data
try { try {
when (data) { when (data) {
is String -> { is String -> {
when (data) { when {
"start_recording" -> { data == "start_recording" -> {
port.postMessage(recordingBuffer) port.postMessage(recordingBuffer)
if (recordingState == RecordingState.STOPPED) { if (recordingState == RecordingState.STOPPED) {
recordingState = RecordingState.WAITING_TO_START recordingState = RecordingState.WAITING_TO_START
@@ -117,34 +196,43 @@ class VstChipProcessor : AudioWorkletProcessor() {
} }
} }
else -> data.startsWith("set_channel") -> {
if (data.startsWith("set_channel")) { val parts = data.split('\n')
val parts = data.split('\n') if (parts.size == 2) {
if (parts.size == 2) { midiMessageHandler.channel = parts[1].toByte()
midiChannel = parts[1].toInt()
println("Setting channel: $midiChannel") println("Setting channel: ${midiMessageHandler.channel}")
}
} else if (data.startsWith("waveform")) {
val parts = data.split('\n')
if (parts.size == 2) {
waveform = parts[1].toInt()
println("Setting waveform: $waveform")
}
} }
}
data.startsWith("waveform") -> {
val parts = data.split('\n')
if (parts.size == 2) {
waveform = parts[1].toInt()
println("Setting waveform: $waveform")
}
}
} }
} }
is Uint8Array -> { is ByteArray -> {
val data32 = Int32Array(data.length) val message1 = TimedMidiMessage(data)
for (i in 0 until data.length) { midiMessageBuffer.add(message1)
data32[i] = (data[i].toInt() and 0xff) playBuffer()
}
playMidi(data32)
} }
/*
is Uint8Array -> {
val data32 = Int32Array(data.length)
for (i in 0 until data.length) {
data32[i] = (data[i].toInt() and 0xff)
}
playMidi(data32)
}
is Int32Array -> { is Int32Array -> {
playMidi(data) playMidi(data)
} }
*/
else -> else ->
console.error("Don't kow how to handle message", message) console.error("Don't kow how to handle message", message)
@@ -154,135 +242,30 @@ class VstChipProcessor : AudioWorkletProcessor() {
} }
} }
private fun playMidi(bytes: Int32Array) { private fun playBuffer() {
console.log("playMidi", bytes) while (
if (bytes.length > 0) { midiMessageBuffer.isNotEmpty() &&
var cmdByte = bytes[0] (midiMessageBuffer.nextTimestamp() ?: 0.0) < currentTime
val channelCmd = ((cmdByte shr 4) and 0xf) != 0xf0 ) {
val channel = cmdByte and 0xf val midi = midiMessageBuffer.read()
println("Channel cmd: $channelCmd") playMidi(midi.midi)
if (channelCmd && channel != midiChannel) {
console.log("Wrong channel", midiChannel, bytes)
return
}
cmdByte = cmdByte and 0xf0
//console.log("Received", bytes)
when (cmdByte) {
0x90 -> {
if (bytes.length == 3) {
val note = bytes[1]
val velocity = bytes[2]
if (velocity > 0) {
noteOn(note, velocity)
} else {
noteOff(note)
}
}
}
0x80 -> {
if (bytes.length >= 2) {
val note = bytes[1]
noteOff(note)
}
}
0xc9 -> {
if (bytes.length >= 1) {
val waveform = bytes[1]
if (waveform < 4) {
this.waveform = waveform
}
}
}
0xb0 -> {
if (bytes.length == 3) {
val knob = bytes[1]
val value = bytes[2]
when (knob) {
7 -> {
volume = value / 127f
}
0x47 -> {
dutyCycle = value / 127.0
}
0x40 -> {
fmFreq = value / 127.0
}
0x41 -> {
fmAmp = value / 127.0
}
0x42 -> {
amFreq = value / 127.0
}
0x43 -> {
amAmp = value / 127.0
}
0x49 -> {
attack = value / 127.0
}
0x4b -> {
decay = value / 127.0
}
0x46 -> {
sustain = value / 127.0
}
0x48 -> {
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 -> {
for (note in notes) {
note?.noteRelease = currentTime
}
}
}
}
}
0xe0 -> {
if (bytes.length == 3) {
val lsb = bytes[1]
val msb = bytes[2]
amFreq = (((msb - 0x40) + (lsb / 127.0)) / 0x40) * 10.0
}
}
}
} }
} }
private fun playMidi(bytes: SlicedByteArray) {
var index = 0
while (index < bytes.size && bytes[index].toUByte() > 0u) {
val buffer = bytes.getBlob(index, 3)
playMidiFromBuffer(buffer)
index += 3
}
}
private fun playMidiFromBuffer(bytes: SlicedByteArray) {
midiMessageHandler.handle(bytes[0], bytes[1], bytes[2])
}
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) {
@@ -332,13 +315,15 @@ class VstChipProcessor : AudioWorkletProcessor() {
recordingStart = 0 recordingStart = 0
} }
playBuffer()
for ((index, note) in notes.withIndex()) { for ((index, note) in notes.withIndex()) {
if (note != null) { if (note != null) {
val midiNote = Note.fromMidi(note.note) val midiNote = Note.fromMidi(note.note)
val sampleDelta = midiNote.sampleDelta val sampleDelta = midiNote.sampleDelta
for (i in 0 until samples) { for (i in 0 until samples) {
var targetVolume = note.velocity / 127f * 10f var targetVolume = note.velocity / 127f * 1f
targetVolume *= ADSR.calculate( targetVolume *= ADSR.calculate(
attack, attack,
decay, decay,
@@ -355,8 +340,10 @@ class VstChipProcessor : AudioWorkletProcessor() {
} }
var cycleOffset = note.cycleOffset var cycleOffset = note.cycleOffset
val fmMult = sin(currentTime * fmFreq * midiNote.freq * 2f * PI2) * fmAmp
val fmModulation = val fmModulation =
sampleDelta + (sin(fmFreq * 1000f * PI2 * (note.sample / sampleRate.toDouble())).toFloat() * (100f * fmAmp * sampleDelta)) sampleDelta * fmMult //+ (sin(fmFreq * 1000f * PI2 * (note.sample / sampleRate.toDouble())).toFloat() * (100f * fmAmp * sampleDelta))
val amModulation = val amModulation =
1f + (sin(sampleLength * amFreq * 1000f * PI2 * note.sample) * amAmp).toFloat() 1f + (sin(sampleLength * amFreq * 1000f * PI2 * note.sample) * amAmp).toFloat()
@@ -401,7 +388,6 @@ class VstChipProcessor : AudioWorkletProcessor() {
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 // comb filter delay
val delaySampleIndex = val delaySampleIndex =
(note.sample + note.combDelayBuffer.length) % note.combDelayBuffer.length (note.sample + note.combDelayBuffer.length) % note.combDelayBuffer.length
@@ -428,10 +414,13 @@ class VstChipProcessor : AudioWorkletProcessor() {
} }
// if sin enable // if sin enable
/*
for (i in 0 until samples) { for (i in 0 until samples) {
left[i] = sin(left[i] * PI2).toFloat() left[i] = sin(left[i] * PI2).toFloat()
right[i] = sin(right[i] * PI2).toFloat() right[i] = sin(right[i] * PI2).toFloat()
} }
*/
val delaySamples = (delay * leftDelayBuffer.length).toInt() val delaySamples = (delay * leftDelayBuffer.length).toInt()
for (i in 0 until samples) { for (i in 0 until samples) {
@@ -466,5 +455,5 @@ class VstChipProcessor : AudioWorkletProcessor() {
fun main() { fun main() {
registerProcessor("vst-chip-processor", VstChipProcessor::class.js) registerProcessor("vst-chip-processor", VstChipProcessor::class.js)
println("VstChipProcessor registered!") console.log("'vst-chip-processor' registered!", currentTime)
} }

View File

@@ -12,8 +12,6 @@ import kotlin.math.round
* Time: 11:50 * Time: 11:50
*/ */
@ExperimentalJsExport
@JsExport
enum class Note( enum class Note(
val sharp: String, val sharp: String,
val flat: String val flat: String

View File

@@ -25,10 +25,11 @@ kotlin {
commonWebpackConfig { commonWebpackConfig {
outputFileName = "vst-chip-worklet-ui.js" outputFileName = "vst-chip-worklet-ui.js"
sourceMaps = true sourceMaps = true
devtool = "inline-source-map"
} }
distribution { distribution {
outputDirectory.set(File("$projectDir/web/")) outputDirectory.set(File("$projectDir/web1/"))
} }
} }
} }
@@ -55,43 +56,17 @@ kotlin {
sourceSets { sourceSets {
val commonMain by getting { val commonMain by getting {
dependencies { dependencies {
//base api("nl.astraeus:vst-ui-base:2.0.0")
implementation("nl.astraeus:kotlin-css-generator:1.0.10") implementation("nl.astraeus:midi-arrays:0.3.4")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.0")
implementation("nl.astraeus:vst-ui-base:1.1.2")
}
}
val jsMain by getting {
dependencies {
implementation("nl.astraeus:kotlin-komponent:1.2.4")
} }
} }
val jsMain by getting
val jsTest by getting { val jsTest by getting {
dependencies { dependencies {
implementation(kotlin("test-js")) implementation(kotlin("test-js"))
} }
} }
/* val wasmJsMain by getting { val jvmMain 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 {
dependencies {
//base
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.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")
}
}
} }
} }
@@ -99,6 +74,18 @@ application {
mainClass.set("nl.astraeus.vst.chip.MainKt") mainClass.set("nl.astraeus.vst.chip.MainKt")
} }
tasks.register<Copy>("buildJS") {
duplicatesStrategy = DuplicatesStrategy.INCLUDE
dependsOn("audio-worklet:jsBrowserDevelopmentExecutableDistribution")
dependsOn("jsBrowserDevelopmentExecutableDistribution")
from(layout.projectDirectory.dir("web1"))
into(layout.projectDirectory.dir("web"))
from(layout.projectDirectory.dir("web2"))
into(layout.projectDirectory.dir("web"))
}
/* Hardcoded deploy configuration */ /* Hardcoded deploy configuration */
val deployDirectory = "vst-chip.midi-vst.com" val deployDirectory = "vst-chip.midi-vst.com"

View File

@@ -3,10 +3,13 @@ version = "0.1.0"
allprojects { allprojects {
repositories { repositories {
mavenLocal() maven {
mavenCentral() url = uri("https://gitea.astraeus.nl/api/packages/rnentjes/maven")
}
maven { maven {
url = uri("https://gitea.astraeus.nl:8443/api/packages/rnentjes/maven") url = uri("https://gitea.astraeus.nl:8443/api/packages/rnentjes/maven")
} }
mavenCentral()
mavenLocal()
} }
} }

1
data/readme.md Normal file
View File

@@ -0,0 +1 @@
Data directory for the db

View File

@@ -1,6 +1,6 @@
pluginManagement { pluginManagement {
plugins { plugins {
kotlin("multiplatform") version "2.0.21" kotlin("multiplatform") version "2.1.20"
} }
repositories { repositories {
gradlePluginPortal() gradlePluginPortal()

View File

@@ -9,12 +9,20 @@ import nl.astraeus.vst.chip.view.MainView
import nl.astraeus.vst.chip.ws.WebsocketClient import nl.astraeus.vst.chip.ws.WebsocketClient
import nl.astraeus.vst.ui.css.CssSettings import nl.astraeus.vst.ui.css.CssSettings
fun main() { object Views {
CssSettings.shortId = false val mainView by lazy {
CssSettings.preFix = "vst-chip" MainView()
}
init {
CssSettings.shortId = false
CssSettings.preFix = "vst"
}
}
fun main() {
Komponent.unsafeMode = UnsafeMode.UNSAFE_SVG_ONLY Komponent.unsafeMode = UnsafeMode.UNSAFE_SVG_ONLY
Komponent.create(document.body!!, MainView) Komponent.create(document.body!!, Views.mainView)
Midi.start() Midi.start()

View File

@@ -1,10 +1,5 @@
package nl.astraeus.vst.chip.audio package nl.astraeus.vst.chip.audio
import nl.astraeus.vst.chip.AudioContext
object AudioContextHandler { object AudioContextHandler {
val audioContext: dynamic = AudioContext() val audioContext: dynamic = js("new AudioContext()")
}
}

View File

@@ -1,5 +1,6 @@
package nl.astraeus.vst.chip.audio package nl.astraeus.vst.chip.audio
import nl.astraeus.midi.message.TimedMidiMessage
import nl.astraeus.vst.chip.AudioWorkletNode import nl.astraeus.vst.chip.AudioWorkletNode
import nl.astraeus.vst.chip.AudioWorkletNodeParameters import nl.astraeus.vst.chip.AudioWorkletNodeParameters
import nl.astraeus.vst.chip.audio.AudioContextHandler.audioContext import nl.astraeus.vst.chip.audio.AudioContextHandler.audioContext
@@ -53,11 +54,27 @@ abstract class AudioNode(
abstract fun onMessage(message: MessageEvent) abstract fun onMessage(message: MessageEvent)
open fun postMessage(vararg data: Int) {
if (port == null) {
console.log("postMessage port is NULL!")
}
val array = ByteArray(data.size) { data[it].toByte() }
port?.postMessage(
TimedMidiMessage(
audioContext.currentTime,
*array
).data.buffer.toByteArray()
)
}
open fun postMessage(msg: Any) { open fun postMessage(msg: Any) {
if (port == null) { if (port == null) {
console.log("postMessage port is NULL!") console.log("postMessage port is NULL!")
} }
port?.postMessage(msg) port?.postMessage(msg)
//console.log("Posted message", audioContext.currentTime)
} }
// call from user gesture // call from user gesture
@@ -83,6 +100,7 @@ abstract class AudioNode(
port = node.port as? MessagePort port = node.port as? MessagePort
created = true created = true
console.log("Created node: ${audioContext.currentTime}")
done(node) done(node)
} }

View File

@@ -2,15 +2,12 @@
package nl.astraeus.vst.chip.audio package nl.astraeus.vst.chip.audio
import nl.astraeus.midi.message.TimedMidiMessage
import nl.astraeus.vst.chip.PatchDTO import nl.astraeus.vst.chip.PatchDTO
import nl.astraeus.vst.chip.view.MainView import nl.astraeus.vst.chip.Views
import nl.astraeus.vst.chip.view.WaveformView import nl.astraeus.vst.chip.view.WaveformView
import nl.astraeus.vst.ui.util.uInt8ArrayOf
import org.khronos.webgl.Float32Array import org.khronos.webgl.Float32Array
import org.khronos.webgl.Uint8Array
import org.khronos.webgl.get
import org.w3c.dom.MessageEvent import org.w3c.dom.MessageEvent
import kotlin.experimental.and
object VstChipWorklet : AudioNode( object VstChipWorklet : AudioNode(
"/vst-chip-worklet.js", "/vst-chip-worklet.js",
@@ -33,63 +30,63 @@ object VstChipWorklet : AudioNode(
set(value) { set(value) {
field = value field = value
super.postMessage( super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 7, (value * 127).toInt()) 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, 0x47, (value * 127).toInt()) 0xb0 + midiChannel, 0x47, (value * 127).toInt()
) )
} }
var fmModFreq = 0.0 var fmModFreq = 1.0
set(value) { set(value) {
field = value field = value
super.postMessage( super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x40, (value * 127).toInt()) 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, 0x41, (value * 127).toInt()) 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, 0x42, (value * 127).toInt()) 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, 0x43, (value * 127).toInt()) 0xb0 + midiChannel, 0x43, (value * 127).toInt()
) )
} }
var feedback = 0.0 var feedback = 0.0
set(value) { set(value) {
field = value field = value
super.postMessage( super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x50, (value * 127).toInt()) 0xb0 + midiChannel, 0x50, (value * 127).toInt()
) )
} }
var delay = 0.0 var delay = 0.0
set(value) { set(value) {
field = value field = value
super.postMessage( super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x4e, (value * 127).toInt()) 0xb0 + midiChannel, 0x4e, (value * 127).toInt()
) )
} }
var delayDepth = 0.0 var delayDepth = 0.0
set(value) { set(value) {
field = value field = value
super.postMessage( super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x4f, (value * 127).toInt()) 0xb0 + midiChannel, 0x4f, (value * 127).toInt()
) )
} }
@@ -97,28 +94,28 @@ object VstChipWorklet : AudioNode(
set(value) { set(value) {
field = value field = value
super.postMessage( super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x49, (value * 127).toInt()) 0xb0 + midiChannel, 0x49, (value * 127).toInt()
) )
} }
var decay = 0.2 var decay = 0.2
set(value) { set(value) {
field = value field = value
super.postMessage( super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x4b, (value * 127).toInt()) 0xb0 + midiChannel, 0x4b, (value * 127).toInt()
) )
} }
var sustain = 0.5 var sustain = 0.5
set(value) { set(value) {
field = value field = value
super.postMessage( super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x46, (value * 127).toInt()) 0xb0 + midiChannel, 0x46, (value * 127).toInt()
) )
} }
var release = 0.2 var release = 0.2
set(value) { set(value) {
field = value field = value
super.postMessage( super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x48, (value * 127).toInt()) 0xb0 + midiChannel, 0x48, (value * 127).toInt()
) )
} }
@@ -139,54 +136,64 @@ object VstChipWorklet : AudioNode(
} }
override fun postMessage(msg: Any) { override fun postMessage(msg: Any) {
if (msg is Uint8Array) { if (msg is ByteArray) {
if ( val tmm = TimedMidiMessage(msg)
msg.length == 3 val byte1 = tmm.midi[0]
&& (msg[0] and 0xf == midiChannel.toByte())
&& (msg[0] and 0xf0.toByte() == 0xb0.toByte())
) {
val knob = msg[1]
val value = msg[2]
handleIncomingMidi(knob, value) if (byte1.toInt() and 0xf0 == 0xb0) {
} else { handleIncomingMidi(tmm.midi[1], tmm.midi[2])
super.postMessage(msg)
} }
} else {
super.postMessage(msg)
} }
super.postMessage(msg)
}
override fun postMessage(vararg msg: Int) {
println("postMessage ${msg.size} bytes")
if (
msg.size == 3
&& (msg[0] and 0xf == midiChannel)
&& (msg[0] and 0xf0 == 0xb0)
) {
val knob = msg[1]
val value = msg[2]
handleIncomingMidi(knob.toByte(), value.toByte())
}
super.postMessage(msg)
} }
private fun handleIncomingMidi(knob: Byte, value: Byte) { private fun handleIncomingMidi(knob: Byte, value: Byte) {
println("Incoming knob: $knob, value: $value")
when (knob) { when (knob) {
0x46.toByte() -> { 0x46.toByte() -> {
volume = value / 127.0 volume = value / 127.0
MainView.requestUpdate() Views.mainView.requestUpdate()
} }
0x4a.toByte() -> { 0x4a.toByte() -> {
dutyCycle = value / 127.0 dutyCycle = value / 127.0
MainView.requestUpdate() Views.mainView.requestUpdate()
} }
0x40.toByte() -> { 0x40.toByte() -> {
fmModFreq = value / 127.0 fmModFreq = value / 127.0
MainView.requestUpdate() Views.mainView.requestUpdate()
} }
0x41.toByte() -> { 0x41.toByte() -> {
fmModAmp = value / 127.0 fmModAmp = value / 127.0
MainView.requestUpdate() Views.mainView.requestUpdate()
} }
0x42.toByte() -> { 0x42.toByte() -> {
amModFreq = value / 127.0 amModFreq = value / 127.0
MainView.requestUpdate() Views.mainView.requestUpdate()
} }
0x43.toByte() -> { 0x43.toByte() -> {
amModAmp = value / 127.0 amModAmp = value / 127.0
MainView.requestUpdate() Views.mainView.requestUpdate()
} }
} }
} }

View File

@@ -1,8 +1,10 @@
package nl.astraeus.vst.chip.midi package nl.astraeus.vst.chip.midi
import kotlinx.browser.window import kotlinx.browser.window
import nl.astraeus.midi.message.TimedMidiMessage
import nl.astraeus.vst.chip.Views
import nl.astraeus.vst.chip.audio.AudioContextHandler
import nl.astraeus.vst.chip.audio.VstChipWorklet import nl.astraeus.vst.chip.audio.VstChipWorklet
import nl.astraeus.vst.chip.view.MainView
import org.khronos.webgl.Uint8Array import org.khronos.webgl.Uint8Array
import org.khronos.webgl.get import org.khronos.webgl.get
@@ -66,7 +68,7 @@ object Midi {
outputs.add(output) outputs.add(output)
} }
MainView.requestUpdate() Views.mainView.requestUpdate()
}, },
{ e -> { e ->
println("Failed to get MIDI access - $e") println("Failed to get MIDI access - $e")
@@ -124,9 +126,14 @@ object Midi {
hex.append(data[index].toString(16)) hex.append(data[index].toString(16))
hex.append(" ") hex.append(" ")
} }
console.log("Midi message:", hex) console.log("Midi message:", hex, message)
val midiData = ByteArray(message.data.length) { data[it].toByte() }
val timeMessage = TimedMidiMessage(
AudioContextHandler.audioContext.currentTime,
*midiData
)
VstChipWorklet.postMessage( VstChipWorklet.postMessage(
message.data timeMessage.data.buffer.toByteArray()
) )
} }

View File

@@ -35,6 +35,9 @@ import nl.astraeus.css.style.cls
import nl.astraeus.komp.HtmlBuilder 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.midi.message.TimedMidiMessage
import nl.astraeus.midi.message.getCurrentTime
import nl.astraeus.vst.chip.Views
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.audio.VstChipWorklet.midiChannel
import nl.astraeus.vst.chip.midi.Midi import nl.astraeus.vst.chip.midi.Midi
@@ -46,7 +49,6 @@ import nl.astraeus.vst.ui.css.Css.defineCss
import nl.astraeus.vst.ui.css.Css.noTextSelect import nl.astraeus.vst.ui.css.Css.noTextSelect
import nl.astraeus.vst.ui.css.CssName import nl.astraeus.vst.ui.css.CssName
import nl.astraeus.vst.ui.css.hover import nl.astraeus.vst.ui.css.hover
import nl.astraeus.vst.ui.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
@@ -60,7 +62,7 @@ object WaveformView: Komponent() {
} }
fun onAnimationFrame(time: Double) { fun onAnimationFrame(time: Double) {
if (MainView.started) { if (Views.mainView.started) {
VstChipWorklet.postMessage("start_recording") VstChipWorklet.postMessage("start_recording")
} }
@@ -95,10 +97,9 @@ object WaveformView: Komponent() {
} }
} }
} }
} }
object MainView : Komponent(), CssName { class MainView : Komponent() {
private var messages: MutableList<String> = ArrayList() private var messages: MutableList<String> = ArrayList()
var started = false var started = false
@@ -114,6 +115,11 @@ object MainView : Komponent(), CssName {
requestUpdate() requestUpdate()
} }
override fun renderUpdate() {
println("Rendering MainView")
super.renderUpdate()
}
override fun HtmlBuilder.render() { override fun HtmlBuilder.render() {
div(MainDivCss.name) { div(MainDivCss.name) {
if (!started) { if (!started) {
@@ -190,7 +196,8 @@ object MainView : Komponent(), CssName {
+"STOP" +"STOP"
onClickFunction = { onClickFunction = {
VstChipWorklet.postDirectlyToWorklet( VstChipWorklet.postDirectlyToWorklet(
uInt8ArrayOf(0xb0 + midiChannel, 123, 0) TimedMidiMessage(getCurrentTime(), (0xb0 + midiChannel).toByte(), 123, 0)
.data.buffer.data
) )
} }
} }
@@ -242,7 +249,7 @@ object MainView : Komponent(), CssName {
ExpKnobComponent( ExpKnobComponent(
value = VstChipWorklet.volume, value = VstChipWorklet.volume,
label = "Volume", label = "Volume",
minValue = 0.005, minValue = 0.0,
maxValue = 1.0, maxValue = 1.0,
step = 5.0 / 127.0, step = 5.0 / 127.0,
width = 100, width = 100,
@@ -268,8 +275,8 @@ object MainView : Komponent(), CssName {
ExpKnobComponent( ExpKnobComponent(
value = VstChipWorklet.fmModFreq, value = VstChipWorklet.fmModFreq,
label = "FM Freq", label = "FM Freq",
minValue = 0.005, minValue = 0.0,
maxValue = 1.0, maxValue = 2.0,
step = 5.0 / 127.0, step = 5.0 / 127.0,
width = 100, width = 100,
height = 120, height = 120,
@@ -281,7 +288,7 @@ object MainView : Komponent(), CssName {
ExpKnobComponent( ExpKnobComponent(
value = VstChipWorklet.fmModAmp, value = VstChipWorklet.fmModAmp,
label = "FM Ampl", label = "FM Ampl",
minValue = 0.005, minValue = 0.0,
maxValue = 1.0, maxValue = 1.0,
step = 5.0 / 127.0, step = 5.0 / 127.0,
width = 100, width = 100,
@@ -294,7 +301,7 @@ object MainView : Komponent(), CssName {
ExpKnobComponent( ExpKnobComponent(
value = VstChipWorklet.amModFreq, value = VstChipWorklet.amModFreq,
label = "AM Freq", label = "AM Freq",
minValue = 0.005, minValue = 0.0,
maxValue = 1.0, maxValue = 1.0,
step = 5.0 / 127.0, step = 5.0 / 127.0,
width = 100, width = 100,
@@ -307,7 +314,7 @@ object MainView : Komponent(), CssName {
ExpKnobComponent( ExpKnobComponent(
value = VstChipWorklet.amModAmp, value = VstChipWorklet.amModAmp,
label = "AM Ampl", label = "AM Ampl",
minValue = 0.005, minValue = 0.0,
maxValue = 1.0, maxValue = 1.0,
step = 5.0 / 127.0, step = 5.0 / 127.0,
width = 100, width = 100,
@@ -320,7 +327,7 @@ object MainView : Komponent(), CssName {
ExpKnobComponent( ExpKnobComponent(
value = VstChipWorklet.feedback, value = VstChipWorklet.feedback,
label = "Feedback", label = "Feedback",
minValue = 0.005, minValue = 0.0,
maxValue = 1.0, maxValue = 1.0,
step = 5.0 / 127.0, step = 5.0 / 127.0,
width = 100, width = 100,
@@ -333,7 +340,7 @@ object MainView : Komponent(), CssName {
ExpKnobComponent( ExpKnobComponent(
value = VstChipWorklet.delay, value = VstChipWorklet.delay,
label = "Delay", label = "Delay",
minValue = 0.005, minValue = 0.0,
maxValue = 1.0, maxValue = 1.0,
step = 5.0 / 127.0, step = 5.0 / 127.0,
width = 100, width = 100,
@@ -346,7 +353,7 @@ object MainView : Komponent(), CssName {
ExpKnobComponent( ExpKnobComponent(
value = VstChipWorklet.delayDepth, value = VstChipWorklet.delayDepth,
label = "Delay depth", label = "Delay depth",
minValue = 0.005, minValue = 0.0,
maxValue = 1.0, maxValue = 1.0,
step = 5.0 / 127.0, step = 5.0 / 127.0,
width = 100, width = 100,
@@ -361,26 +368,26 @@ object MainView : Komponent(), CssName {
ExpKnobComponent( ExpKnobComponent(
value = VstChipWorklet.attack, value = VstChipWorklet.attack,
label = "Attack", label = "Attack",
minValue = 0.005, minValue = 0.0,
maxValue = 1.0, maxValue = 5.0,
step = 5.0 / 127.0, step = 25.0 / 127.0,
width = 100, width = 100,
height = 120, height = 120,
) { value -> ) { value ->
VstChipWorklet.attack = value VstChipWorklet.attack = value / 5.0
} }
) )
include( include(
ExpKnobComponent( ExpKnobComponent(
value = VstChipWorklet.decay, value = VstChipWorklet.decay,
label = "Decay", label = "Decay",
minValue = 0.005, minValue = 0.0,
maxValue = 1.0, maxValue = 5.0,
step = 5.0 / 127.0, step = 25.0 / 127.0,
width = 100, width = 100,
height = 120, height = 120,
) { value -> ) { value ->
VstChipWorklet.decay = value VstChipWorklet.decay = value / 5.0
} }
) )
include( include(
@@ -400,13 +407,13 @@ object MainView : Komponent(), CssName {
ExpKnobComponent( ExpKnobComponent(
value = VstChipWorklet.release, value = VstChipWorklet.release,
label = "Release", label = "Release",
minValue = 0.005, minValue = 0.0,
maxValue = 1.0, maxValue = 5.0,
step = 5.0 / 127.0, step = 25.0 / 127.0,
width = 100, width = 100,
height = 120, height = 120,
) { value -> ) { value ->
VstChipWorklet.release = value VstChipWorklet.release = value / 5.0
} }
) )
} }
@@ -414,18 +421,19 @@ object MainView : Komponent(), CssName {
} }
} }
object MainDivCss : CssName companion object MainViewCss : CssName() {
object ActiveCss : CssName object MainDivCss : CssName()
object ButtonCss : CssName object ActiveCss : CssName()
object ButtonBarCss : CssName object ButtonCss : CssName()
object SelectedCss : CssName object ButtonBarCss : CssName()
object NoteBarCss : CssName object SelectedCss : CssName()
object StartSplashCss : CssName object NoteBarCss : CssName()
object StartBoxCss : CssName object StartSplashCss : CssName()
object StartButtonCss : CssName object StartBoxCss : CssName()
object ControlsCss : CssName object StartButtonCss : CssName()
object ControlsCss : CssName()
private fun css() { private fun css() {
defineCss { defineCss {
select("*") { select("*") {
select("*:before") { select("*:before") {
@@ -524,23 +532,23 @@ object MainView : Komponent(), CssName {
backgroundColor(Css.currentStyle.mainBackgroundColor) backgroundColor(Css.currentStyle.mainBackgroundColor)
} }
} }
}
private fun Style.commonButton() {
display(Display.inlineBlock)
padding(1.rem)
backgroundColor(Css.currentStyle.buttonBackgroundColor)
borderColor(Css.currentStyle.buttonBorderColor)
borderWidth(Css.currentStyle.buttonBorderWidth)
color(Css.currentStyle.mainFontColor)
hover {
backgroundColor(Css.currentStyle.buttonBackgroundColor.hover())
} }
and(SelectedCss.cls()) {
backgroundColor(Css.currentStyle.buttonBackgroundColor.hover().hover().hover()) private fun Style.commonButton() {
display(Display.inlineBlock)
padding(1.rem)
backgroundColor(Css.currentStyle.buttonBackgroundColor)
borderColor(Css.currentStyle.buttonBorderColor)
borderWidth(Css.currentStyle.buttonBorderWidth)
color(Css.currentStyle.mainFontColor)
hover {
backgroundColor(Css.currentStyle.buttonBackgroundColor.hover())
}
and(SelectedCss.cls()) {
backgroundColor(Css.currentStyle.buttonBackgroundColor.hover().hover().hover())
}
} }
} }
} }

View File

@@ -4,9 +4,9 @@ package nl.astraeus.vst.chip.ws
import kotlinx.browser.window import kotlinx.browser.window
import nl.astraeus.vst.chip.PatchDTO import nl.astraeus.vst.chip.PatchDTO
import nl.astraeus.vst.chip.Views
import nl.astraeus.vst.chip.audio.VstChipWorklet import nl.astraeus.vst.chip.audio.VstChipWorklet
import nl.astraeus.vst.chip.midi.Midi import nl.astraeus.vst.chip.midi.Midi
import nl.astraeus.vst.chip.view.MainView
import org.w3c.dom.MessageEvent import org.w3c.dom.MessageEvent
import org.w3c.dom.WebSocket import org.w3c.dom.WebSocket
import org.w3c.dom.events.Event import org.w3c.dom.events.Event
@@ -90,7 +90,7 @@ object WebsocketClient {
Midi.setInput(patch.midiId, patch.midiName) Midi.setInput(patch.midiId, patch.midiName)
VstChipWorklet.load(patch) VstChipWorklet.load(patch)
MainView.requestUpdate() Views.mainView.requestUpdate()
} }
} }
} }