6 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
13 changed files with 270 additions and 80 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

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,7 +31,7 @@ kotlin {
} }
distribution { distribution {
outputDirectory.set(File("$projectDir/../web/")) outputDirectory.set(File("$projectDir/../web2/"))
} }
} }
} }

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

@@ -73,7 +73,7 @@ class VstChipProcessor : AudioWorkletProcessor() {
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
@@ -340,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()

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,44 +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:vst-ui-base:1.2.0")
implementation("nl.astraeus:midi-arrays:0.3.4") implementation("nl.astraeus:midi-arrays:0.3.4")
} }
} }
val jsMain by getting { val jsMain by getting
dependencies {
implementation("nl.astraeus:kotlin-komponent:1.2.4")
}
}
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("org.jetbrains.kotlinx:kotlinx-datetime:0.6.0")
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")
}
}
} }
} }
@@ -100,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"

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.1.10" 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

@@ -4,7 +4,7 @@ package nl.astraeus.vst.chip.audio
import nl.astraeus.midi.message.TimedMidiMessage 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 org.khronos.webgl.Float32Array import org.khronos.webgl.Float32Array
import org.w3c.dom.MessageEvent import org.w3c.dom.MessageEvent
@@ -40,7 +40,7 @@ object VstChipWorklet : AudioNode(
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(
@@ -168,32 +168,32 @@ object VstChipWorklet : AudioNode(
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

@@ -2,9 +2,9 @@ package nl.astraeus.vst.chip.midi
import kotlinx.browser.window import kotlinx.browser.window
import nl.astraeus.midi.message.TimedMidiMessage 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.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
@@ -68,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")

View File

@@ -37,6 +37,7 @@ 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.TimedMidiMessage
import nl.astraeus.midi.message.getCurrentTime 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
@@ -61,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")
} }
@@ -98,7 +99,7 @@ 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
@@ -275,7 +276,7 @@ object MainView : Komponent(), CssName {
value = VstChipWorklet.fmModFreq, value = VstChipWorklet.fmModFreq,
label = "FM Freq", label = "FM Freq",
minValue = 0.0, 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,
@@ -420,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") {
@@ -530,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()
} }
} }
} }