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
/web
/web1
/web2
/data/*.db*
**/kotlin-js-store/*
.kotlin
.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 {
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 volume = 0.75f
var dutyCycle = 0.5
var fmFreq = 0.0
var fmFreq = 0.5
var fmAmp = 0.0
var amFreq = 0.0
var amAmp = 0.0
@@ -340,8 +340,10 @@ class VstChipProcessor : AudioWorkletProcessor() {
}
var cycleOffset = note.cycleOffset
val fmMult = sin(currentTime * fmFreq * midiNote.freq * 2f * PI2) * fmAmp
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 =
1f + (sin(sampleLength * amFreq * 1000f * PI2 * note.sample) * amAmp).toFloat()

View File

@@ -25,10 +25,11 @@ kotlin {
commonWebpackConfig {
outputFileName = "vst-chip-worklet-ui.js"
sourceMaps = true
devtool = "inline-source-map"
}
distribution {
outputDirectory.set(File("$projectDir/web/"))
outputDirectory.set(File("$projectDir/web1/"))
}
}
}
@@ -55,44 +56,17 @@ kotlin {
sourceSets {
val commonMain by getting {
dependencies {
//base
implementation("nl.astraeus:kotlin-css-generator:1.0.10")
implementation("nl.astraeus:vst-ui-base:1.2.0")
api("nl.astraeus:vst-ui-base:2.0.0")
implementation("nl.astraeus:midi-arrays:0.3.4")
}
}
val jsMain by getting {
dependencies {
implementation("nl.astraeus:kotlin-komponent:1.2.4")
}
}
val jsMain by getting
val jsTest by getting {
dependencies {
implementation(kotlin("test-js"))
}
}
/* val wasmJsMain by getting {
dependencies {
implementation("nl.astraeus:kotlin-komponent:1.2.4-SNAPSHOT")
implementation("nl.astraeus:vst-ui-base:1.0.1-SNAPSHOT")
}
}*/
val jvmMain by getting {
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")
}
}
val jvmMain by getting
}
}
@@ -100,6 +74,18 @@ application {
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 */
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 {
plugins {
kotlin("multiplatform") version "2.1.10"
kotlin("multiplatform") version "2.1.20"
}
repositories {
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.ui.css.CssSettings
fun main() {
CssSettings.shortId = false
CssSettings.preFix = "vst-chip"
object Views {
val mainView by lazy {
MainView()
}
init {
CssSettings.shortId = false
CssSettings.preFix = "vst"
}
}
fun main() {
Komponent.unsafeMode = UnsafeMode.UNSAFE_SVG_ONLY
Komponent.create(document.body!!, MainView)
Komponent.create(document.body!!, Views.mainView)
Midi.start()

View File

@@ -4,7 +4,7 @@ package nl.astraeus.vst.chip.audio
import nl.astraeus.midi.message.TimedMidiMessage
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 org.khronos.webgl.Float32Array
import org.w3c.dom.MessageEvent
@@ -40,7 +40,7 @@ object VstChipWorklet : AudioNode(
0xb0 + midiChannel, 0x47, (value * 127).toInt()
)
}
var fmModFreq = 0.0
var fmModFreq = 1.0
set(value) {
field = value
super.postMessage(
@@ -168,32 +168,32 @@ object VstChipWorklet : AudioNode(
when (knob) {
0x46.toByte() -> {
volume = value / 127.0
MainView.requestUpdate()
Views.mainView.requestUpdate()
}
0x4a.toByte() -> {
dutyCycle = value / 127.0
MainView.requestUpdate()
Views.mainView.requestUpdate()
}
0x40.toByte() -> {
fmModFreq = value / 127.0
MainView.requestUpdate()
Views.mainView.requestUpdate()
}
0x41.toByte() -> {
fmModAmp = value / 127.0
MainView.requestUpdate()
Views.mainView.requestUpdate()
}
0x42.toByte() -> {
amModFreq = value / 127.0
MainView.requestUpdate()
Views.mainView.requestUpdate()
}
0x43.toByte() -> {
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 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.view.MainView
import org.khronos.webgl.Uint8Array
import org.khronos.webgl.get
@@ -68,7 +68,7 @@ object Midi {
outputs.add(output)
}
MainView.requestUpdate()
Views.mainView.requestUpdate()
},
{ 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.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.midiChannel
import nl.astraeus.vst.chip.midi.Midi
@@ -61,7 +62,7 @@ object WaveformView: Komponent() {
}
fun onAnimationFrame(time: Double) {
if (MainView.started) {
if (Views.mainView.started) {
VstChipWorklet.postMessage("start_recording")
}
@@ -98,7 +99,7 @@ object WaveformView: Komponent() {
}
}
object MainView : Komponent(), CssName {
class MainView : Komponent() {
private var messages: MutableList<String> = ArrayList()
var started = false
@@ -275,7 +276,7 @@ object MainView : Komponent(), CssName {
value = VstChipWorklet.fmModFreq,
label = "FM Freq",
minValue = 0.0,
maxValue = 1.0,
maxValue = 2.0,
step = 5.0 / 127.0,
width = 100,
height = 120,
@@ -420,18 +421,19 @@ object MainView : Komponent(), CssName {
}
}
object MainDivCss : CssName
object ActiveCss : CssName
object ButtonCss : CssName
object ButtonBarCss : CssName
object SelectedCss : CssName
object NoteBarCss : CssName
object StartSplashCss : CssName
object StartBoxCss : CssName
object StartButtonCss : CssName
object ControlsCss : CssName
companion object MainViewCss : CssName() {
object MainDivCss : CssName()
object ActiveCss : CssName()
object ButtonCss : CssName()
object ButtonBarCss : CssName()
object SelectedCss : CssName()
object NoteBarCss : CssName()
object StartSplashCss : CssName()
object StartBoxCss : CssName()
object StartButtonCss : CssName()
object ControlsCss : CssName()
private fun css() {
private fun css() {
defineCss {
select("*") {
select("*:before") {
@@ -530,23 +532,23 @@ object MainView : Komponent(), CssName {
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 nl.astraeus.vst.chip.PatchDTO
import nl.astraeus.vst.chip.Views
import nl.astraeus.vst.chip.audio.VstChipWorklet
import nl.astraeus.vst.chip.midi.Midi
import nl.astraeus.vst.chip.view.MainView
import org.w3c.dom.MessageEvent
import org.w3c.dom.WebSocket
import org.w3c.dom.events.Event
@@ -90,7 +90,7 @@ object WebsocketClient {
Midi.setInput(patch.midiId, patch.midiName)
VstChipWorklet.load(patch)
MainView.requestUpdate()
Views.mainView.requestUpdate()
}
}
}