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
/web
/web1
/web2
/data/*.db*
**/kotlin-js-store/*
.kotlin
.idea

2
.idea/gradle.xml generated
View File

@@ -5,7 +5,7 @@
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleHome" value="" />
<option name="gradleJvm" value="corretto-21" />
<option name="modules">
<set>
<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 {
outputDirectory.set(File("$projectDir/../web/"))
outputDirectory.set(File("$projectDir/../web2/"))
}
}
}
jvm()
sourceSets {
val commonMain by getting {
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

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
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.AudioWorkletProcessor
import nl.astraeus.vst.currentTime
import nl.astraeus.vst.midi.MidiMessageHandler
import nl.astraeus.vst.registerProcessor
import nl.astraeus.vst.sampleRate
import org.khronos.webgl.Float32Array
import org.khronos.webgl.Int32Array
import org.khronos.webgl.Uint8Array
import org.khronos.webgl.get
import org.khronos.webgl.set
import org.w3c.dom.MessageEvent
@@ -17,7 +19,7 @@ import kotlin.math.PI
import kotlin.math.min
import kotlin.math.sin
val POLYPHONICS = 10
val POLYPHONICS = 20
val PI2 = PI * 2
@ExperimentalJsExport
@@ -64,13 +66,14 @@ enum class RecordingState {
@ExperimentalJsExport
@JsExport
class VstChipProcessor : AudioWorkletProcessor() {
var midiChannel = 0
val midiMessageBuffer = SortedTimedMidiMessageList()
val midiMessageHandler = MidiMessageHandler()
val notes = Array<PlayingNote?>(POLYPHONICS) { null }
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
@@ -98,18 +101,94 @@ class VstChipProcessor : AudioWorkletProcessor() {
init {
this.port.onmessage = ::handleMessage
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) {
//console.log("VstChipProcessor: Received message:", message.data)
//console.log("VstChipProcessor: Received message:", currentTime)
val data = message.data
try {
when (data) {
is String -> {
when (data) {
"start_recording" -> {
when {
data == "start_recording" -> {
port.postMessage(recordingBuffer)
if (recordingState == RecordingState.STOPPED) {
recordingState = RecordingState.WAITING_TO_START
@@ -117,34 +196,43 @@ class VstChipProcessor : AudioWorkletProcessor() {
}
}
else ->
if (data.startsWith("set_channel")) {
val parts = data.split('\n')
if (parts.size == 2) {
midiChannel = parts[1].toInt()
println("Setting channel: $midiChannel")
}
} else if (data.startsWith("waveform")) {
val parts = data.split('\n')
if (parts.size == 2) {
waveform = parts[1].toInt()
println("Setting waveform: $waveform")
}
data.startsWith("set_channel") -> {
val parts = data.split('\n')
if (parts.size == 2) {
midiMessageHandler.channel = parts[1].toByte()
println("Setting channel: ${midiMessageHandler.channel}")
}
}
data.startsWith("waveform") -> {
val parts = data.split('\n')
if (parts.size == 2) {
waveform = parts[1].toInt()
println("Setting waveform: $waveform")
}
}
}
}
is Uint8Array -> {
val data32 = Int32Array(data.length)
for (i in 0 until data.length) {
data32[i] = (data[i].toInt() and 0xff)
}
playMidi(data32)
is ByteArray -> {
val message1 = TimedMidiMessage(data)
midiMessageBuffer.add(message1)
playBuffer()
}
/*
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 -> {
playMidi(data)
}
is Int32Array -> {
playMidi(data)
}
*/
else ->
console.error("Don't kow how to handle message", message)
@@ -154,135 +242,30 @@ class VstChipProcessor : AudioWorkletProcessor() {
}
}
private fun playMidi(bytes: Int32Array) {
console.log("playMidi", bytes)
if (bytes.length > 0) {
var cmdByte = bytes[0]
val channelCmd = ((cmdByte shr 4) and 0xf) != 0xf0
val channel = cmdByte and 0xf
println("Channel cmd: $channelCmd")
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 playBuffer() {
while (
midiMessageBuffer.isNotEmpty() &&
(midiMessageBuffer.nextTimestamp() ?: 0.0) < currentTime
) {
val midi = midiMessageBuffer.read()
playMidi(midi.midi)
}
}
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) {
for (i in 0 until POLYPHONICS) {
if (notes[i]?.note == note) {
@@ -332,13 +315,15 @@ class VstChipProcessor : AudioWorkletProcessor() {
recordingStart = 0
}
playBuffer()
for ((index, note) in notes.withIndex()) {
if (note != null) {
val midiNote = Note.fromMidi(note.note)
val sampleDelta = midiNote.sampleDelta
for (i in 0 until samples) {
var targetVolume = note.velocity / 127f * 10f
var targetVolume = note.velocity / 127f * 1f
targetVolume *= ADSR.calculate(
attack,
decay,
@@ -355,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()
@@ -401,7 +388,6 @@ class VstChipProcessor : AudioWorkletProcessor() {
left[i] = left[i] + waveValue * note.actualVolume * volume * amModulation
right[i] = right[i] + waveValue * note.actualVolume * volume * amModulation
// comb filter delay
val delaySampleIndex =
(note.sample + note.combDelayBuffer.length) % note.combDelayBuffer.length
@@ -428,10 +414,13 @@ class VstChipProcessor : AudioWorkletProcessor() {
}
// if sin enable
/*
for (i in 0 until samples) {
left[i] = sin(left[i] * PI2).toFloat()
right[i] = sin(right[i] * PI2).toFloat()
}
*/
val delaySamples = (delay * leftDelayBuffer.length).toInt()
for (i in 0 until samples) {
@@ -466,5 +455,5 @@ class VstChipProcessor : AudioWorkletProcessor() {
fun main() {
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
*/
@ExperimentalJsExport
@JsExport
enum class Note(
val sharp: String,
val flat: String

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,43 +56,17 @@ kotlin {
sourceSets {
val commonMain by getting {
dependencies {
//base
implementation("nl.astraeus:kotlin-css-generator:1.0.10")
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")
api("nl.astraeus:vst-ui-base:2.0.0")
implementation("nl.astraeus:midi-arrays:0.3.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("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
}
}
@@ -99,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"

View File

@@ -3,10 +3,13 @@ version = "0.1.0"
allprojects {
repositories {
mavenLocal()
mavenCentral()
maven {
url = uri("https://gitea.astraeus.nl/api/packages/rnentjes/maven")
}
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 {
plugins {
kotlin("multiplatform") version "2.0.21"
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

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

View File

@@ -1,5 +1,6 @@
package nl.astraeus.vst.chip.audio
import nl.astraeus.midi.message.TimedMidiMessage
import nl.astraeus.vst.chip.AudioWorkletNode
import nl.astraeus.vst.chip.AudioWorkletNodeParameters
import nl.astraeus.vst.chip.audio.AudioContextHandler.audioContext
@@ -53,11 +54,27 @@ abstract class AudioNode(
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) {
if (port == null) {
console.log("postMessage port is NULL!")
}
port?.postMessage(msg)
//console.log("Posted message", audioContext.currentTime)
}
// call from user gesture
@@ -83,6 +100,7 @@ abstract class AudioNode(
port = node.port as? MessagePort
created = true
console.log("Created node: ${audioContext.currentTime}")
done(node)
}

View File

@@ -2,15 +2,12 @@
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 nl.astraeus.vst.ui.util.uInt8ArrayOf
import org.khronos.webgl.Float32Array
import org.khronos.webgl.Uint8Array
import org.khronos.webgl.get
import org.w3c.dom.MessageEvent
import kotlin.experimental.and
object VstChipWorklet : AudioNode(
"/vst-chip-worklet.js",
@@ -33,63 +30,63 @@ object VstChipWorklet : AudioNode(
set(value) {
field = value
super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 7, (value * 127).toInt())
0xb0 + midiChannel, 7, (value * 127).toInt()
)
}
var dutyCycle = 0.5
set(value) {
field = value
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) {
field = value
super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x40, (value * 127).toInt())
0xb0 + midiChannel, 0x40, (value * 127).toInt()
)
}
var fmModAmp = 0.0
set(value) {
field = value
super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x41, (value * 127).toInt())
0xb0 + midiChannel, 0x41, (value * 127).toInt()
)
}
var amModFreq = 0.0
set(value) {
field = value
super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x42, (value * 127).toInt())
0xb0 + midiChannel, 0x42, (value * 127).toInt()
)
}
var amModAmp = 0.0
set(value) {
field = value
super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x43, (value * 127).toInt())
0xb0 + midiChannel, 0x43, (value * 127).toInt()
)
}
var feedback = 0.0
set(value) {
field = value
super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x50, (value * 127).toInt())
0xb0 + midiChannel, 0x50, (value * 127).toInt()
)
}
var delay = 0.0
set(value) {
field = value
super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x4e, (value * 127).toInt())
0xb0 + midiChannel, 0x4e, (value * 127).toInt()
)
}
var delayDepth = 0.0
set(value) {
field = value
super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x4f, (value * 127).toInt())
0xb0 + midiChannel, 0x4f, (value * 127).toInt()
)
}
@@ -97,28 +94,28 @@ object VstChipWorklet : AudioNode(
set(value) {
field = value
super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x49, (value * 127).toInt())
0xb0 + midiChannel, 0x49, (value * 127).toInt()
)
}
var decay = 0.2
set(value) {
field = value
super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x4b, (value * 127).toInt())
0xb0 + midiChannel, 0x4b, (value * 127).toInt()
)
}
var sustain = 0.5
set(value) {
field = value
super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x46, (value * 127).toInt())
0xb0 + midiChannel, 0x46, (value * 127).toInt()
)
}
var release = 0.2
set(value) {
field = value
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) {
if (msg is Uint8Array) {
if (
msg.length == 3
&& (msg[0] and 0xf == midiChannel.toByte())
&& (msg[0] and 0xf0.toByte() == 0xb0.toByte())
) {
val knob = msg[1]
val value = msg[2]
if (msg is ByteArray) {
val tmm = TimedMidiMessage(msg)
val byte1 = tmm.midi[0]
handleIncomingMidi(knob, value)
} else {
super.postMessage(msg)
if (byte1.toInt() and 0xf0 == 0xb0) {
handleIncomingMidi(tmm.midi[1], tmm.midi[2])
}
} 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) {
println("Incoming knob: $knob, value: $value")
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

@@ -1,8 +1,10 @@
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
@@ -66,7 +68,7 @@ object Midi {
outputs.add(output)
}
MainView.requestUpdate()
Views.mainView.requestUpdate()
},
{ e ->
println("Failed to get MIDI access - $e")
@@ -124,9 +126,14 @@ object Midi {
hex.append(data[index].toString(16))
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(
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.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
@@ -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.CssName
import nl.astraeus.vst.ui.css.hover
import nl.astraeus.vst.ui.util.uInt8ArrayOf
import org.khronos.webgl.get
import org.w3c.dom.CanvasRenderingContext2D
import org.w3c.dom.HTMLCanvasElement
@@ -60,7 +62,7 @@ object WaveformView: Komponent() {
}
fun onAnimationFrame(time: Double) {
if (MainView.started) {
if (Views.mainView.started) {
VstChipWorklet.postMessage("start_recording")
}
@@ -95,10 +97,9 @@ object WaveformView: Komponent() {
}
}
}
}
object MainView : Komponent(), CssName {
class MainView : Komponent() {
private var messages: MutableList<String> = ArrayList()
var started = false
@@ -114,6 +115,11 @@ object MainView : Komponent(), CssName {
requestUpdate()
}
override fun renderUpdate() {
println("Rendering MainView")
super.renderUpdate()
}
override fun HtmlBuilder.render() {
div(MainDivCss.name) {
if (!started) {
@@ -190,7 +196,8 @@ object MainView : Komponent(), CssName {
+"STOP"
onClickFunction = {
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(
value = VstChipWorklet.volume,
label = "Volume",
minValue = 0.005,
minValue = 0.0,
maxValue = 1.0,
step = 5.0 / 127.0,
width = 100,
@@ -268,8 +275,8 @@ object MainView : Komponent(), CssName {
ExpKnobComponent(
value = VstChipWorklet.fmModFreq,
label = "FM Freq",
minValue = 0.005,
maxValue = 1.0,
minValue = 0.0,
maxValue = 2.0,
step = 5.0 / 127.0,
width = 100,
height = 120,
@@ -281,7 +288,7 @@ object MainView : Komponent(), CssName {
ExpKnobComponent(
value = VstChipWorklet.fmModAmp,
label = "FM Ampl",
minValue = 0.005,
minValue = 0.0,
maxValue = 1.0,
step = 5.0 / 127.0,
width = 100,
@@ -294,7 +301,7 @@ object MainView : Komponent(), CssName {
ExpKnobComponent(
value = VstChipWorklet.amModFreq,
label = "AM Freq",
minValue = 0.005,
minValue = 0.0,
maxValue = 1.0,
step = 5.0 / 127.0,
width = 100,
@@ -307,7 +314,7 @@ object MainView : Komponent(), CssName {
ExpKnobComponent(
value = VstChipWorklet.amModAmp,
label = "AM Ampl",
minValue = 0.005,
minValue = 0.0,
maxValue = 1.0,
step = 5.0 / 127.0,
width = 100,
@@ -320,7 +327,7 @@ object MainView : Komponent(), CssName {
ExpKnobComponent(
value = VstChipWorklet.feedback,
label = "Feedback",
minValue = 0.005,
minValue = 0.0,
maxValue = 1.0,
step = 5.0 / 127.0,
width = 100,
@@ -333,7 +340,7 @@ object MainView : Komponent(), CssName {
ExpKnobComponent(
value = VstChipWorklet.delay,
label = "Delay",
minValue = 0.005,
minValue = 0.0,
maxValue = 1.0,
step = 5.0 / 127.0,
width = 100,
@@ -346,7 +353,7 @@ object MainView : Komponent(), CssName {
ExpKnobComponent(
value = VstChipWorklet.delayDepth,
label = "Delay depth",
minValue = 0.005,
minValue = 0.0,
maxValue = 1.0,
step = 5.0 / 127.0,
width = 100,
@@ -361,26 +368,26 @@ object MainView : Komponent(), CssName {
ExpKnobComponent(
value = VstChipWorklet.attack,
label = "Attack",
minValue = 0.005,
maxValue = 1.0,
step = 5.0 / 127.0,
minValue = 0.0,
maxValue = 5.0,
step = 25.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.attack = value
VstChipWorklet.attack = value / 5.0
}
)
include(
ExpKnobComponent(
value = VstChipWorklet.decay,
label = "Decay",
minValue = 0.005,
maxValue = 1.0,
step = 5.0 / 127.0,
minValue = 0.0,
maxValue = 5.0,
step = 25.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.decay = value
VstChipWorklet.decay = value / 5.0
}
)
include(
@@ -400,13 +407,13 @@ object MainView : Komponent(), CssName {
ExpKnobComponent(
value = VstChipWorklet.release,
label = "Release",
minValue = 0.005,
maxValue = 1.0,
step = 5.0 / 127.0,
minValue = 0.0,
maxValue = 5.0,
step = 25.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.release = value
VstChipWorklet.release = value / 5.0
}
)
}
@@ -414,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") {
@@ -524,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()
}
}
}