1 Commits

Author SHA1 Message Date
b3cd36b8f8 Remove dependency 2024-06-27 10:07:45 +02:00
42 changed files with 919 additions and 1851 deletions

6
.gitignore vendored
View File

@@ -41,11 +41,7 @@ bin/
### Mac OS ###
.DS_Store
/web
/web1
/web2
web
/data/*.db*
**/kotlin-js-store/*
.kotlin
.idea

View File

@@ -1,6 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="audio-worklet-js-1.0.0-SNAPSHOT">
<output-path>$PROJECT_DIR$/audio-worklet/build/libs</output-path>
<root id="archive" name="audio-worklet-js-1.0.0-SNAPSHOT.jar" />
<root id="archive" name="audio-worklet-js-1.0.0-SNAPSHOT.jar">
<element id="module-output" name="vst-chip.audio-worklet.jsMain" />
</root>
</artifact>
</component>

View File

@@ -1,6 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="audio-worklet-jvm-1.0.0-SNAPSHOT">
<output-path>$PROJECT_DIR$/audio-worklet/build/libs</output-path>
<root id="archive" name="audio-worklet-jvm-1.0.0-SNAPSHOT.jar" />
<root id="archive" name="audio-worklet-jvm-1.0.0-SNAPSHOT.jar">
<element id="module-output" name="vst-chip.audio-worklet.jvmMain" />
</root>
</artifact>
</component>

View File

@@ -1,6 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="common-js-1.0.0-SNAPSHOT">
<output-path>$PROJECT_DIR$/common/build/libs</output-path>
<root id="archive" name="common-js-1.0.0-SNAPSHOT.jar" />
<root id="archive" name="common-js-1.0.0-SNAPSHOT.jar">
<element id="module-output" name="vst-chip.common.jsMain" />
</root>
</artifact>
</component>

View File

@@ -1,6 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="common-jvm-1.0.0-SNAPSHOT">
<output-path>$PROJECT_DIR$/common/build/libs</output-path>
<root id="archive" name="common-jvm-1.0.0-SNAPSHOT.jar" />
<root id="archive" name="common-jvm-1.0.0-SNAPSHOT.jar">
<element id="module-output" name="vst-chip.common.jvmMain" />
</root>
</artifact>
</component>

View File

@@ -1,6 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="vst-chip-js-1.0.0-SNAPSHOT">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="vst-chip-js-1.0.0-SNAPSHOT.jar" />
<root id="archive" name="vst-chip-js-1.0.0-SNAPSHOT.jar">
<element id="module-output" name="vst-chip.jsMain" />
</root>
</artifact>
</component>

View File

@@ -1,6 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="vst-chip-jvm-1.0.0-SNAPSHOT">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="vst-chip-jvm-1.0.0-SNAPSHOT.jar" />
<root id="archive" name="vst-chip-jvm-1.0.0-SNAPSHOT.jar">
<element id="module-output" name="vst-chip.jvmMain" />
</root>
</artifact>
</component>

3
.idea/gradle.xml generated
View File

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

7
.idea/jsLibraryMappings.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<excludedPredefinedLibrary name="vst-chip/build/js/node_modules" />
<excludedPredefinedLibrary name="vst-chip/build/js/packages/vst-base-test/node_modules" />
</component>
</project>

2
.idea/misc.xml generated
View File

@@ -4,7 +4,7 @@
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="temurin-21" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
<component name="accountSettings">

View File

@@ -1,105 +0,0 @@
# 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

@@ -1,27 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Main [jvm]" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName"/>
<option name="externalProjectPath" value="$PROJECT_DIR$"/>
<option name="externalSystemIdString" value="GRADLE"/>
<option name="scriptParameters" value="-DmainClass=nl.astraeus.vst.chip.MainKt --quiet"/>
<option name="taskDescriptions">
<list/>
</option>
<option name="taskNames">
<list>
<option value="jvmRun"/>
</list>
</option>
<option name="vmOptions"/>
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<method v="2">
<option name="Gradle.BeforeRunTask" enabled="true" tasks="clean build" externalProjectPath="$PROJECT_DIR$"
vmOptions="" scriptParameters=""/>
</method>
</configuration>
</component>

View File

@@ -1,7 +1,3 @@
@file:OptIn(ExperimentalKotlinGradlePluginApi::class, ExperimentalDistributionDsl::class)
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalDistributionDsl
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackOutput
buildscript {
@@ -31,18 +27,22 @@ kotlin {
}
distribution {
outputDirectory.set(File("$projectDir/../web2/"))
outputDirectory.set(File("$projectDir/../web/"))
}
}
}
jvm()
sourceSets {
val commonMain by getting {
dependencies {
implementation("nl.astraeus:vst-worklet-base:1.0.1")
implementation("nl.astraeus:midi-arrays:0.3.4")
implementation(project(":common"))
}
}
val jsMain by getting {
dependencies {
implementation(project(":common"))
}
}
val jsMain by getting
}
}

View File

@@ -1,82 +0,0 @@
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

@@ -0,0 +1,45 @@
package nl.astraeus.vst
import org.khronos.webgl.Float32Array
import org.w3c.dom.MessagePort
enum class AutomationRate(
val rate: String
) {
A_RATE("a-rate"),
K_RATE("k-rate")
}
interface AudioParam {
var value: Double
var automationRate: AutomationRate
val defaultValue: Double
val minValue: Double
val maxValue: Double
}
interface AudioParamMap {
operator fun get(name: String): AudioParam
}
abstract external class AudioWorkletProcessor {
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/AudioWorkletNode/parameters) */
//val parameters: AudioParamMap;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/AudioWorkletNode/port) */
@JsName("port")
val port: MessagePort
@JsName("process")
open fun process(
inputs: Array<Array<Float32Array>>,
outputs: Array<Array<Float32Array>>,
parameters: dynamic
): Boolean {
definedExternally
}
}
external fun registerProcessor(name: String, processorCtor: JsClass<*>)
external val sampleRate: Int
external val currentTime: Double

View File

@@ -2,50 +2,51 @@
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.Note
import nl.astraeus.vst.registerProcessor
import nl.astraeus.vst.sampleRate
import org.khronos.webgl.ArrayBuffer
import org.khronos.webgl.Float32Array
import org.khronos.webgl.Int32Array
import org.khronos.webgl.Uint8Array
import org.khronos.webgl.get
import org.khronos.webgl.set
import org.w3c.dom.MessageEvent
import kotlin.math.PI
import kotlin.math.min
import kotlin.math.sin
val POLYPHONICS = 20
val POLYPHONICS = 10
val PI2 = PI * 2
@ExperimentalJsExport
@JsExport
enum class NoteState {
ON,
RELEASED,
OFF
}
@ExperimentalJsExport
@JsExport
class PlayingNote(
val note: Int,
var velocity: Int = 0
) {
val noteObj = Note.fromMidi(note)
fun retrigger(velocity: Int) {
this.velocity = velocity
state = NoteState.ON
sample = 0
noteStart = currentTime
noteRelease = null
for (i in 0 until combDelayBuffer.length) {
combDelayBuffer[i] = 0f
}
attackSamples = 2500
releaseSamples = 10000
}
var noteStart = currentTime
var noteRelease: Double? = null
var state = NoteState.OFF
var cycleOffset = 0.0
var sample = 0
var attackSamples = 2500
var releaseSamples = 10000
var actualVolume = 0f
val combDelayBuffer = Float32Array((sampleRate / noteObj.freq).toInt())
}
enum class Waveform {
@@ -55,230 +56,152 @@ enum class Waveform {
SAWTOOTH
}
@ExperimentalJsExport
@JsExport
enum class RecordingState {
STOPPED,
WAITING_TO_START,
RECORDING
}
@ExperimentalJsExport
@JsExport
class VstChipProcessor : AudioWorkletProcessor() {
val midiMessageBuffer = SortedTimedMidiMessageList()
val midiMessageHandler = MidiMessageHandler()
val notes = Array<PlayingNote?>(POLYPHONICS) { null }
val notes = Array(POLYPHONICS) {
PlayingNote(
0
)
}
var waveform = Waveform.SINE.ordinal
var volume = 0.75f
var dutyCycle = 0.5
var fmFreq = 0.5
var fmFreq = 0.0
var fmAmp = 0.0
var amFreq = 0.0
var amAmp = 0.0
var attack = 0.1
var decay = 0.2
var sustain = 0.5
var release = 0.2
val recordingBuffer = Float32Array(sampleRate / 60)
var recordingState = RecordingState.STOPPED
var recordingSample = 0
var recordingStart = 0
val sampleLength = 1 / sampleRate.toDouble()
val rightDelayBuffer = Float32Array(sampleRate)
val leftDelayBuffer = Float32Array(sampleRate)
var delayIndex = 0
var delay = 0.0
var delayDepth = 0.0
var feedback = 0.0
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:", currentTime)
//console.log("VstChipProcessor: Received message", message)
val data = message.data
try {
when (data) {
is String -> {
when {
data == "start_recording" -> {
port.postMessage(recordingBuffer)
if (recordingState == RecordingState.STOPPED) {
recordingState = RecordingState.WAITING_TO_START
recordingSample = 0
}
when (data) {
"test_on" -> {
playMidi(Int32Array(arrayOf(0x90, 60, 64)))
}
"test_off" -> {
playMidi(Int32Array(arrayOf(0x90, 60, 0)))
}
is String -> {
}
is ArrayBuffer -> {
}
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)
}
else ->
console.error("Don't kow how to handle message", message)
}
}
private fun playMidi(bytes: Int32Array) {
if (bytes.length > 0) {
//console.log("Received", bytes)
when(bytes[0]) {
0x90 -> {
if (bytes.length == 3) {
val note = bytes[1]
val velocity = bytes[2]
if (velocity > 0) {
noteOn(note, velocity)
} else {
noteOff(note)
}
}
}
data.startsWith("set_channel") -> {
val parts = data.split('\n')
if (parts.size == 2) {
midiMessageHandler.channel = parts[1].toByte()
0x80 -> {
if (bytes.length >= 2) {
val note = bytes[1]
println("Setting channel: ${midiMessageHandler.channel}")
}
noteOff(note)
}
}
0xc9 -> {
if (bytes.length >= 1) {
val waveform = bytes[1]
if (waveform < 4) {
this.waveform = waveform
}
}
}
data.startsWith("waveform") -> {
val parts = data.split('\n')
if (parts.size == 2) {
waveform = parts[1].toInt()
println("Setting waveform: $waveform")
0xb0 -> {
if (bytes.length == 3) {
val knob = bytes[1]
val value = bytes[2]
when (knob) {
0x4a -> {
dutyCycle = value / 127.0
}
0x4b -> {
fmFreq = value / 127.0
}
0x4c -> {
fmAmp = value / 127.0
}
0x47 -> {
amFreq = value / 127.0
}
0x48 -> {
amAmp = value / 127.0
}
}
}
}
is ByteArray -> {
val message1 = TimedMidiMessage(data)
midiMessageBuffer.add(message1)
playBuffer()
0xe0 -> {
if (bytes.length == 3) {
val lsb = bytes[1]
val msb = bytes[2]
amFreq = (((msb - 0x40) + (lsb / 127.0)) / 0x40) * 10.0
}
}
/*
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)
}
*/
else ->
console.error("Don't kow how to handle message", message)
}
} catch (e: Exception) {
console.log(e.message, e)
}
}
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) {
notes[i]?.retrigger(velocity)
if (notes[i].note == note) {
notes[i].retrigger(velocity)
return
}
}
for (i in 0 until POLYPHONICS) {
if (notes[i] == null) {
if (notes[i].state == NoteState.OFF) {
notes[i] = PlayingNote(
note,
velocity
)
notes[i].state = NoteState.ON
val n = Note.fromMidi(note)
//console.log("Playing note: ${n.sharp} (${n.freq})")
break
}
}
@@ -286,168 +209,79 @@ class VstChipProcessor : AudioWorkletProcessor() {
private fun noteOff(note: Int) {
for (i in 0 until POLYPHONICS) {
if (notes[i]?.note == note) {
notes[i]?.noteRelease = currentTime
if (notes[i].note == note && notes[i].state == NoteState.ON) {
notes[i].state = NoteState.RELEASED
break
}
}
}
override fun process(
inputs: Array<Array<Float32Array>>,
outputs: Array<Array<Float32Array>>,
parameters: dynamic
): Boolean {
override fun process (
inputs: Array<Array<Float32Array>>,
outputs: Array<Array<Float32Array>>,
parameters: dynamic
) : Boolean {
val samples = outputs[0][0].length
val left = outputs[0][0]
val right = outputs[0][1]
var lowestNote = 200
for (note in notes) {
if (note != null) {
lowestNote = min(lowestNote, note.note)
}
}
if (lowestNote == 200 && recordingState == RecordingState.WAITING_TO_START) {
recordingState = RecordingState.RECORDING
recordingSample = 0
recordingStart = 0
}
playBuffer()
for ((index, note) in notes.withIndex()) {
if (note != null) {
val midiNote = Note.fromMidi(note.note)
val sampleDelta = midiNote.sampleDelta
if (note.state != NoteState.OFF) {
val sampleDelta = Note.fromMidi(note.note).sampleDelta
for (i in 0 until samples) {
var targetVolume = note.velocity / 127f * 1f
targetVolume *= ADSR.calculate(
attack,
decay,
sustain,
release,
note.noteStart,
currentTime,
note.noteRelease
).toFloat()
note.actualVolume += (targetVolume - note.actualVolume) * 0.01f
var targetVolume = note.velocity / 127f
if (note.state == NoteState.ON && note.sample < note.attackSamples) {
note.attackSamples--
targetVolume *= ( 1f - (note.attackSamples / 2500f))
} else if (note.state == NoteState.RELEASED) {
note.releaseSamples--
targetVolume *= (note.releaseSamples / 10000f)
}
note.actualVolume += (targetVolume - note.actualVolume) * 0.0001f
if (note.noteRelease != null && note.actualVolume <= 0.01) {
notes[index] = null
if (note.state == NoteState.RELEASED && note.actualVolume <= 0) {
note.state = NoteState.OFF
}
var cycleOffset = note.cycleOffset
val fmMult = sin(currentTime * fmFreq * midiNote.freq * 2f * PI2) * fmAmp
val fmModulation =
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()
cycleOffset = if (cycleOffset < dutyCycle) {
cycleOffset / dutyCycle / 2.0
} else {
0.5 + ((cycleOffset - dutyCycle) / (1.0 - dutyCycle) / 2.0)
}
val fmModulation = sin(sampleLength * fmFreq * 10f * PI2 * note.sample).toFloat() * fmAmp * 5f
val amModulation = 1f + (sin(sampleLength * amFreq * 10f * PI2 * note.sample) * amAmp).toFloat()
cycleOffset += fmModulation
val waveValue: Float = when (waveform) {
0 -> {
sin(cycleOffset * PI2).toFloat()
}
1 -> {
if (cycleOffset < 0.5) {
1f
} else {
-1f
}
if (cycleOffset < dutyCycle) { 1f } else { -1f }
}
2 -> when {
cycleOffset < 0.25 -> 4 * cycleOffset
cycleOffset < 0.75 -> 2 - 4 * cycleOffset
else -> 4 * cycleOffset - 4
}.toFloat()
3 -> {
((cycleOffset * 2f) - 1f).toFloat()
}
else -> {
if (cycleOffset < 0.5) {
1f
} else {
-1f
}
if (cycleOffset < 0.5) { 1f } else { -1f }
}
}
left[i] = left[i] + waveValue * note.actualVolume * volume * amModulation
right[i] = right[i] + waveValue * note.actualVolume * volume * amModulation
left[i] = left[i] + waveValue * note.actualVolume * 0.3f * amModulation
right[i] = right[i] + waveValue * note.actualVolume * 0.3f * amModulation
// comb filter delay
val delaySampleIndex =
(note.sample + note.combDelayBuffer.length) % note.combDelayBuffer.length
left[i] = left[i] + (note.combDelayBuffer[delaySampleIndex] * feedback.toFloat())
right[i] = right[i] + (note.combDelayBuffer[delaySampleIndex] * feedback.toFloat())
note.combDelayBuffer[delaySampleIndex] = (left[i] + right[i]) / 2f
// end - comb filter delay
note.cycleOffset += sampleDelta + fmModulation
if (note.cycleOffset > 1f) {
note.cycleOffset += sampleDelta
if (cycleOffset > 1f) {
note.cycleOffset -= 1f
if (note.note == lowestNote && recordingState == RecordingState.WAITING_TO_START) {
recordingState = RecordingState.RECORDING
recordingSample = 0
recordingStart = i
}
}
note.sample++
}
}
}
// 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) {
if (delaySamples > 0) {
val delaySampleIndex = (delayIndex + sampleRate - delaySamples) % sampleRate
left[i] = left[i] + (leftDelayBuffer[delaySampleIndex] * delayDepth.toFloat())
right[i] = right[i] + (rightDelayBuffer[delaySampleIndex] * delayDepth.toFloat())
}
leftDelayBuffer[delayIndex] = left[i]
rightDelayBuffer[delayIndex++] = right[i]
delayIndex %= sampleRate
}
if (recordingState == RecordingState.RECORDING) {
for (i in recordingStart until samples) {
recordingBuffer[recordingSample] = (left[i] + right[i]) / 2f
if (recordingSample < recordingBuffer.length - 1) {
recordingSample++
} else {
recordingState = RecordingState.STOPPED
}
}
recordingStart = 0
}
return true
}
}
@@ -455,5 +289,5 @@ class VstChipProcessor : AudioWorkletProcessor() {
fun main() {
registerProcessor("vst-chip-processor", VstChipProcessor::class.js)
console.log("'vst-chip-processor' registered!", currentTime)
println("VstChipProcessor registered!")
}

View File

@@ -1,194 +0,0 @@
package nl.astraeus.vst.chip
import nl.astraeus.vst.chip.Note.entries
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
import kotlin.math.round
/**
* User: rnentjes
* Date: 14-11-15
* Time: 11:50
*/
enum class Note(
val sharp: String,
val flat: String
) {
NO01("C--", "C--"),
NO02("C#-", "Db-"),
NO03("D--", "D--"),
NO04("D#-", "Eb-"),
NO05("E--", "E--"),
NO06("F--", "F--"),
NO07("F#-", "Gb-"),
NO08("G--", "G--"),
NO09("G#-", "Ab-"),
NO10("A--", "A--"),
NO11("A#-", "Bb-"),
NO12("B--", "B--"),
C0("C-0", "C-0"),
C0s("C#0", "Db0"),
D0("D-0", "D-0"),
D0s("D#0", "Eb0"),
E0("E-0", "E-0"),
F0("F-0", "F-0"),
F0s("F#0", "Gb0"),
G0("G-0", "G-0"),
G0s("G#0", "Ab0"),
A0("A-0", "A-0"),
A0s("A#0", "Bb0"),
B0("B-0", "B-0"),
C1("C-1", "C-1"),
C1s("C#1", "Db1"),
D1("D-1", "D-1"),
D1s("D#1", "Eb1"),
E1("E-1", "E-1"),
F1("F-1", "F-1"),
F1s("F#1", "Gb1"),
G1("G-1", "G-1"),
G1s("G#1", "Ab1"),
A1("A-1", "A-1"),
A1s("A#1", "Bb1"),
B1("B-1", "B-1"),
C2("C-2", "C-2"),
C2s("C#2", "Db2"),
D2("D-2", "D-2"),
D2s("D#2", "Eb2"),
E2("E-2", "E-2"),
F2("F-2", "F-2"),
F2s("F#2", "Gb2"),
G2("G-2", "G-2"),
G2s("G#2", "Ab2"),
A2("A-2", "A-2"),
A2s("A#2", "Bb2"),
B2("B-2", "B-2"),
C3("C-3", "C-3"),
C3s("C#3", "Db3"),
D3("D-3", "D-3"),
D3s("D#3", "Eb3"),
E3("E-3", "E-3"),
F3("F-3", "F-3"),
F3s("F#3", "Gb3"),
G3("G-3", "G-3"),
G3s("G#3", "Ab3"),
A3("A-3", "A-3"),
A3s("A#3", "Bb3"),
B3("B-3", "B-3"),
C4("C-4", "C-4"),
C4s("C#4", "Db4"),
D4("D-4", "D-4"),
D4s("D#4", "Eb4"),
E4("E-4", "E-4"),
F4("F-4", "F-4"),
F4s("F#4", "Gb4"),
G4("G-4", "G-4"),
G4s("G#4", "Ab4"),
A4("A-4", "A-4"),
A4s("A#4", "Bb4"),
B4("B-4", "B-4"),
C5("C-5", "C-5"),
C5s("C#5", "Db5"),
D5("D-5", "D-5"),
D5s("D#5", "Eb5"),
E5("E-5", "E-5"),
F5("F-5", "F-5"),
F5s("F#5", "Gb5"),
G5("G-5", "G-5"),
G5s("G#5", "Ab5"),
A5("A-5", "A-5"),
A5s("A#5", "Bb5"),
B5("B-5", "B-5"),
C6("C-6", "C-6"),
C6s("C#6", "Db6"),
D6("D-6", "D-6"),
D6s("D#6", "Eb6"),
E6("E-6", "E-6"),
F6("F-6", "F-6"),
F6s("F#6", "Gb6"),
G6("G-6", "G-6"),
G6s("G#6", "Ab6"),
A6("A-6", "A-6"),
A6s("A#6", "Bb6"),
B6("B-6", "B-6"),
C7("C-7", "C-7"),
C7s("C#7", "Db7"),
D7("D-7", "D-7"),
D7s("D#7", "Eb7"),
E7("E-7", "E-7"),
F7("F-7", "F-7"),
F7s("F#7", "Gb7"),
G7("G-7", "G-7"),
G7s("G#7", "Ab7"),
A7("A-7", "A-7"),
A7s("A#7", "Bb7"),
B7("B-7", "B-7"),
C8("C-8", "C-8"),
C8s("C#8", "Db8"),
D8("D-8", "D-8"),
D8s("D#8", "Eb8"),
E8("E-8", "E-8"),
F8("F-8", "F-8"),
F8s("F#8", "Gb8"),
G8("G-8", "G-8"),
G8s("G#8", "Ab8"),
A8("A-8", "A-8"),
A8s("A#8", "Bb8"),
B8("B-8", "B-8"),
C9("C-9", "C-9"),
C9s("C#9", "Db9"),
D9("D-9", "D-9"),
D9s("D#9", "Eb9"),
E9("E-9", "E-9"),
F9("F-9", "F-9"),
F9s("F#9", "Gb9"),
G9("G-9", "G-9"),
// out of midi range
//G9s("G#9","Ab9"),
//A9("A-9","A-9"),
//A9s("A#9","Bb9"),
//B9("B-9","B-9"),
NONE("---", "---"),
UP("^^^", "^^^"),
END("XXX", "XXX"),
;
// 69 = A4.ordinal
val freq: Double = round(440.0 * 2.0.pow((ordinal - 69) / 12.0)) // * 10000.0) / 10000.0
val cycleLength: Double = 1.0 / freq
var sampleDelta: Double = 0.0
fun transpose(semiNotes: Int): Note = if (ordinal >= C0.ordinal && ordinal <= G9.ordinal) {
var result = this.ordinal + semiNotes
result = min(result, G9.ordinal)
result = max(result, C0.ordinal)
fromMidi(result)
} else {
this
}
companion object {
fun fromMidi(midi: Int): Note {
// todo: add check
return entries[midi]
}
fun updateSampleRate(rate: Int) {
println("Setting sample rate to $rate")
for (note in Note.entries) {
note.sampleDelta = (1.0 / rate.toDouble()) / note.cycleLength
}
}
}
}
// freq = 10Hz
// cycleLength = 0.1
// sampleRate = 48000
// sampleDelta = 4800
// (1.0 / freq) * sampleRate

View File

@@ -1,168 +1,66 @@
import java.nio.file.Files
import java.nio.file.Paths
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackOutput
buildscript {
apply(from = "common.gradle.kts")
apply(from = "version.gradle.kts")
apply(from = "common.gradle.kts")
}
plugins {
kotlin("multiplatform")
id("maven-publish")
application
kotlin("multiplatform")
kotlin("plugin.serialization")
id("maven-publish")
application
}
kotlin {
js {
compilerOptions {
target.set("es2015")
js {
compilerOptions {
target.set("es2015")
}
//useEsModules()
//useCommonJs()
binaries.executable()
browser {
commonWebpackConfig {
outputFileName = "vst-chip-worklet-ui.js"
sourceMaps = true
}
distribution {
outputDirectory.set(File("$projectDir/web/"))
}
}
}
//useEsModules()
//useCommonJs()
binaries.executable()
browser {
commonWebpackConfig {
outputFileName = "vst-chip-worklet-ui.js"
sourceMaps = true
devtool = "inline-source-map"
}
distribution {
outputDirectory.set(File("$projectDir/web1/"))
}
}
}
/*
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
binaries.executable()
browser{
distribution {
outputDirectory.set(File("$projectDir/web/"))
}
}
mavenPublication {
groupId = group as String
pom { name = "${project.name}-wasm-js" }
}
}
*/
jvm {
withJava()
}
sourceSets {
val commonMain by getting {
dependencies {
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 jvmMain by getting
}
}
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"
tasks.register<Copy>("unzipDistribution") {
mustRunAfter("removeSymbolicLink")
val zipDir = layout.projectDirectory.dir("build/distributions")
val zipFile = zipDir.file("${project.name}-${project.version}.zip")
val outputDir = file("/home/rnentjes/www/${deployDirectory}")
from(zipTree(zipFile))
into(outputDir)
}
tasks.register("createSymbolicLink") {
mustRunAfter("unzipDistribution")
doLast {
val targetDir =
Paths.get("/home/rnentjes/www/${deployDirectory}/${project.name}-${project.version}") // Directory to link to
val symlink =
Paths.get("/home/rnentjes/www/${deployDirectory}/${project.name}") // Path for the symbolic link
if (!Files.exists(targetDir)) {
throw IllegalArgumentException("Target directory does not exist: $targetDir")
jvm{
withJava()
}
if (Files.exists(symlink)) {
println("Symbolic link already exists: $symlink")
} else {
Files.createSymbolicLink(symlink, targetDir)
println("Symbolic link created: $symlink -> $targetDir")
sourceSets {
val commonMain by getting {
dependencies {
implementation(project(":common"))
//base
api("nl.astraeus:kotlin-css-generator:1.0.7")
}
}
val jsMain by getting {
dependencies {
//base
implementation("nl.astraeus:kotlin-komponent-js:1.2.2")
}
}
val jsTest by getting {
dependencies {
implementation(kotlin("test-js"))
}
}
val jvmMain by getting {
dependencies {
//base
implementation("io.undertow:undertow-core:2.3.13.Final")
implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.11.0")
}
}
}
}
}
tasks.register<Copy>("copyWeb") {
val webDir = layout.projectDirectory.dir("web")
val outputDir = file("/home/rnentjes/www/${deployDirectory}/web")
from(webDir)
into(outputDir)
}
tasks.named<Task>("build") {
dependsOn("generateVersionProperties")
}
tasks.named("kotlinUpgradeYarnLock") {
mustRunAfter("clean")
}
tasks.named("build") {
mustRunAfter("kotlinUpgradeYarnLock")
}
tasks.named("build") {
mustRunAfter("kotlinUpgradeYarnLock")
}
tasks.named("copyWeb") {
mustRunAfter("build")
}
tasks.register("removeSymbolicLink") {
mustRunAfter("build")
doLast {
delete(layout.projectDirectory.file("/home/rnentjes/www/${deployDirectory}/${project.name}"))
}
}
tasks.register("deploy") {
dependsOn("clean")
dependsOn("kotlinUpgradeYarnLock")
dependsOn("build")
dependsOn("copyWeb")
dependsOn("removeSymbolicLink")
dependsOn("unzipDistribution")
dependsOn("createSymbolicLink")
}

View File

@@ -1,15 +1,13 @@
group = "nl.astraeus"
version = "0.1.0"
version = "1.0.0-SNAPSHOT"
allprojects {
repositories {
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()
mavenCentral()
maven("https://reposilite.astraeus.nl/releases")
maven {
url = uri("https://nexus.astraeus.nl/nexus/content/groups/public")
}
}
}

32
common/build.gradle.kts Normal file
View File

@@ -0,0 +1,32 @@
@file:OptIn(ExperimentalKotlinGradlePluginApi::class)
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
buildscript {
apply(from = "../common.gradle.kts")
}
plugins {
kotlin("multiplatform")
}
kotlin {
js {
compilerOptions {
target.set("es2015")
}
browser()
}
jvm()
sourceSets {
val commonMain by getting {
dependencies {
}
}
val jsMain by getting {
dependencies {
}
}
}
}

View File

@@ -0,0 +1 @@
apply(from = "../settings.common.gradle.kts")

View File

@@ -0,0 +1,192 @@
package nl.astraeus.vst
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
import kotlin.math.round
/**
* User: rnentjes
* Date: 14-11-15
* Time: 11:50
*/
enum class Note(
val sharp: String,
val flat: String
) {
NO01("C--","C--"),
NO02("C#-","Db-"),
NO03("D--","D--"),
NO04("D#-","Eb-"),
NO05("E--","E--"),
NO06("F--","F--"),
NO07("F#-","Gb-"),
NO08("G--","G--"),
NO09("G#-","Ab-"),
NO10("A--","A--"),
NO11("A#-","Bb-"),
NO12("B--","B--"),
C0("C-0","C-0"),
C0s("C#0","Db0"),
D0("D-0","D-0"),
D0s("D#0","Eb0"),
E0("E-0","E-0"),
F0("F-0","F-0"),
F0s("F#0","Gb0"),
G0("G-0","G-0"),
G0s("G#0","Ab0"),
A0("A-0","A-0"),
A0s("A#0","Bb0"),
B0("B-0","B-0"),
C1("C-1","C-1"),
C1s("C#1","Db1"),
D1("D-1","D-1"),
D1s("D#1","Eb1"),
E1("E-1","E-1"),
F1("F-1","F-1"),
F1s("F#1","Gb1"),
G1("G-1","G-1"),
G1s("G#1","Ab1"),
A1("A-1","A-1"),
A1s("A#1","Bb1"),
B1("B-1","B-1"),
C2("C-2","C-2"),
C2s("C#2","Db2"),
D2("D-2","D-2"),
D2s("D#2","Eb2"),
E2("E-2","E-2"),
F2("F-2","F-2"),
F2s("F#2","Gb2"),
G2("G-2","G-2"),
G2s("G#2","Ab2"),
A2("A-2","A-2"),
A2s("A#2","Bb2"),
B2("B-2","B-2"),
C3("C-3","C-3"),
C3s("C#3","Db3"),
D3("D-3","D-3"),
D3s("D#3","Eb3"),
E3("E-3","E-3"),
F3("F-3","F-3"),
F3s("F#3","Gb3"),
G3("G-3","G-3"),
G3s("G#3","Ab3"),
A3("A-3","A-3"),
A3s("A#3","Bb3"),
B3("B-3","B-3"),
C4("C-4","C-4"),
C4s("C#4","Db4"),
D4("D-4","D-4"),
D4s("D#4","Eb4"),
E4("E-4","E-4"),
F4("F-4","F-4"),
F4s("F#4","Gb4"),
G4("G-4","G-4"),
G4s("G#4","Ab4"),
A4("A-4","A-4"),
A4s("A#4","Bb4"),
B4("B-4","B-4"),
C5("C-5","C-5"),
C5s("C#5","Db5"),
D5("D-5","D-5"),
D5s("D#5","Eb5"),
E5("E-5","E-5"),
F5("F-5","F-5"),
F5s("F#5","Gb5"),
G5("G-5","G-5"),
G5s("G#5","Ab5"),
A5("A-5","A-5"),
A5s("A#5","Bb5"),
B5("B-5","B-5"),
C6("C-6","C-6"),
C6s("C#6","Db6"),
D6("D-6","D-6"),
D6s("D#6","Eb6"),
E6("E-6","E-6"),
F6("F-6","F-6"),
F6s("F#6","Gb6"),
G6("G-6","G-6"),
G6s("G#6","Ab6"),
A6("A-6","A-6"),
A6s("A#6","Bb6"),
B6("B-6","B-6"),
C7("C-7","C-7"),
C7s("C#7","Db7"),
D7("D-7","D-7"),
D7s("D#7","Eb7"),
E7("E-7","E-7"),
F7("F-7","F-7"),
F7s("F#7","Gb7"),
G7("G-7","G-7"),
G7s("G#7","Ab7"),
A7("A-7","A-7"),
A7s("A#7","Bb7"),
B7("B-7","B-7"),
C8("C-8","C-8"),
C8s("C#8","Db8"),
D8("D-8","D-8"),
D8s("D#8","Eb8"),
E8("E-8","E-8"),
F8("F-8","F-8"),
F8s("F#8","Gb8"),
G8("G-8","G-8"),
G8s("G#8","Ab8"),
A8("A-8","A-8"),
A8s("A#8","Bb8"),
B8("B-8","B-8"),
C9("C-9","C-9"),
C9s("C#9","Db9"),
D9("D-9","D-9"),
D9s("D#9","Eb9"),
E9("E-9","E-9"),
F9("F-9","F-9"),
F9s("F#9","Gb9"),
G9("G-9","G-9"),
// out of midi range
//G9s("G#9","Ab9"),
//A9("A-9","A-9"),
//A9s("A#9","Bb9"),
//B9("B-9","B-9"),
NONE("---", "---"),
UP("^^^","^^^"),
END("XXX","XXX"),
;
// 69 = A4.ordinal
val freq: Double = round(440.0 * 2.0.pow((ordinal - 69)/12.0)) // * 10000.0) / 10000.0
val cycleLength: Double = 1.0 / freq
var sampleDelta: Double = 0.0
fun transpose(semiNotes: Int): Note = if (ordinal >= C0.ordinal && ordinal <= G9.ordinal) {
var result = this.ordinal + semiNotes
result = min(result, G9.ordinal)
result = max(result, C0.ordinal)
fromMidi(result)
} else {
this
}
companion object {
fun fromMidi(midi: Int): Note {
// todo: add check
return entries[midi]
}
fun updateSampleRate(rate: Int) {
println("Setting sample rate to $rate")
for (note in Note.entries) {
note.sampleDelta = (1.0 / rate.toDouble()) / note.cycleLength
}
}
}
}
// freq = 10Hz
// cycleLength = 0.1
// sampleRate = 48000
// sampleDelta = 4800
// (1.0 / freq) * sampleRate

View File

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

View File

@@ -1,6 +1,8 @@
pluginManagement {
plugins {
kotlin("multiplatform") version "2.1.20"
kotlin("multiplatform") version "2.0.0"
kotlin("plugin.serialization") version "2.0.0"
id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0"
}
repositories {
gradlePluginPortal()

View File

@@ -2,4 +2,5 @@ apply(from = "settings.common.gradle.kts")
rootProject.name = "vst-chip"
include(":common")
include(":audio-worklet")

View File

@@ -1,44 +0,0 @@
package nl.astraeus.vst.chip
import kotlin.js.ExperimentalJsExport
import kotlin.js.JsExport
import kotlin.js.JsName
@ExperimentalJsExport
@JsExport
data class PatchDTO(
@JsName("waveform")
val waveform: Int = 0,
@JsName("midiId")
val midiId: String = "",
@JsName("midiName")
val midiName: String = "",
@JsName("midiChannel")
var midiChannel: Int = 0,
@JsName("volume")
var volume: Double = 0.75,
@JsName("dutyCycle")
var dutyCycle: Double = 0.5,
@JsName("fmModFreq")
var fmModFreq: Double = 0.0,
@JsName("fmModAmp")
var fmModAmp: Double = 0.0,
@JsName("amModFreq")
var amModFreq: Double = 0.0,
@JsName("amModAmp")
var amModAmp: Double = 0.0,
@JsName("attack")
var attack: Double = 0.1,
@JsName("decay")
var decay: Double = 0.2,
@JsName("sustain")
var sustain: Double = 0.5,
@JsName("release")
var release: Double = 0.2,
@JsName("delay")
var delay: Double = 0.0,
@JsName("delayDepth")
var delayDepth: Double = 0.0,
@JsName("feedback")
var feedback: Double = 0.0,
)

View File

@@ -1,53 +0,0 @@
package nl.astraeus.vst.chip.logger
val log = Logger
enum class LogLevel {
TRACE,
DEBUG,
INFO,
WARN,
ERROR,
FATAL
}
object Logger {
var level: LogLevel = LogLevel.INFO
fun trace(message: () -> String?) {
if (level.ordinal <= LogLevel.TRACE.ordinal) {
println("TRACE: ${message()}")
}
}
fun debug(message: () -> String?) {
if (level.ordinal <= LogLevel.DEBUG.ordinal) {
println("DEBUG: ${message()}")
}
}
fun info(message: () -> String?) {
if (level.ordinal <= LogLevel.INFO.ordinal) {
println("INFO: ${message()}")
}
}
fun warn(e: Throwable? = null, message: () -> String?) {
if (level.ordinal <= LogLevel.WARN.ordinal) {
println("WARN: ${message()}")
e?.printStackTrace()
}
}
fun error(e: Throwable? = null, message: () -> String?) {
if (level.ordinal <= LogLevel.ERROR.ordinal) {
println("ERROR: ${message()}")
e?.printStackTrace()
}
}
fun fatal(e: Throwable, message: () -> String?) {
println("FATAL: ${message()}")
e.printStackTrace()
}
}

View File

@@ -1,32 +1,29 @@
package nl.astraeus.vst.chip
import kotlinx.browser.document
import kotlinx.browser.window
import nl.astraeus.komp.Komponent
import nl.astraeus.komp.UnsafeMode
import nl.astraeus.vst.chip.logger.log
import nl.astraeus.vst.chip.midi.Broadcaster
import nl.astraeus.vst.chip.midi.MidiMessage
import nl.astraeus.vst.chip.midi.Midi
import nl.astraeus.vst.chip.view.MainView
import nl.astraeus.vst.chip.ws.WebsocketClient
import nl.astraeus.vst.ui.css.CssSettings
object Views {
val mainView by lazy {
MainView()
}
init {
CssSettings.shortId = false
CssSettings.preFix = "vst"
}
}
import org.khronos.webgl.Uint8Array
fun main() {
Komponent.unsafeMode = UnsafeMode.UNSAFE_SVG_ONLY
Komponent.create(document.body!!, Views.mainView)
Komponent.create(document.body!!, MainView)
Midi.start()
WebsocketClient.connect {
log.debug { "Connected to server" }
}
console.log("Performance", window.performance)
Broadcaster.getChannel(0).postMessage(
MidiMessage(
Uint8Array(arrayOf(0x80.toByte(), 60, 60)),
window.performance.now()
)
)
window.setInterval({
Broadcaster.sync()
}, 1000)
}

View File

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

View File

@@ -1,6 +1,5 @@
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
@@ -54,27 +53,8 @@ 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
@@ -100,7 +80,6 @@ abstract class AudioNode(
port = node.port as? MessagePort
created = true
console.log("Created node: ${audioContext.currentTime}")
done(node)
}

View File

@@ -1,239 +1,14 @@
@file:OptIn(ExperimentalJsExport::class)
package nl.astraeus.vst.chip.audio
import nl.astraeus.midi.message.TimedMidiMessage
import nl.astraeus.vst.chip.PatchDTO
import nl.astraeus.vst.chip.Views
import nl.astraeus.vst.chip.view.WaveformView
import org.khronos.webgl.Float32Array
import org.w3c.dom.MessageEvent
object VstChipWorklet : AudioNode(
"/vst-chip-worklet.js",
"vst-chip-worklet.js",
"vst-chip-processor"
) {
var waveform: Int = 0
set(value) {
field = value
postMessage("waveform\n$value")
}
var midiChannel = 0
set(value) {
check(value in 0..15) {
"Midi channel must be between 0 and 15."
}
field = value
postMessage("set_channel\n${midiChannel}")
}
var volume = 0.75
set(value) {
field = value
super.postMessage(
0xb0 + midiChannel, 7, (value * 127).toInt()
)
}
var dutyCycle = 0.5
set(value) {
field = value
super.postMessage(
0xb0 + midiChannel, 0x47, (value * 127).toInt()
)
}
var fmModFreq = 1.0
set(value) {
field = value
super.postMessage(
0xb0 + midiChannel, 0x40, (value * 127).toInt()
)
}
var fmModAmp = 0.0
set(value) {
field = value
super.postMessage(
0xb0 + midiChannel, 0x41, (value * 127).toInt()
)
}
var amModFreq = 0.0
set(value) {
field = value
super.postMessage(
0xb0 + midiChannel, 0x42, (value * 127).toInt()
)
}
var amModAmp = 0.0
set(value) {
field = value
super.postMessage(
0xb0 + midiChannel, 0x43, (value * 127).toInt()
)
}
var feedback = 0.0
set(value) {
field = value
super.postMessage(
0xb0 + midiChannel, 0x50, (value * 127).toInt()
)
}
var delay = 0.0
set(value) {
field = value
super.postMessage(
0xb0 + midiChannel, 0x4e, (value * 127).toInt()
)
}
var delayDepth = 0.0
set(value) {
field = value
super.postMessage(
0xb0 + midiChannel, 0x4f, (value * 127).toInt()
)
}
var attack = 0.1
set(value) {
field = value
super.postMessage(
0xb0 + midiChannel, 0x49, (value * 127).toInt()
)
}
var decay = 0.2
set(value) {
field = value
super.postMessage(
0xb0 + midiChannel, 0x4b, (value * 127).toInt()
)
}
var sustain = 0.5
set(value) {
field = value
super.postMessage(
0xb0 + midiChannel, 0x46, (value * 127).toInt()
)
}
var release = 0.2
set(value) {
field = value
super.postMessage(
0xb0 + midiChannel, 0x48, (value * 127).toInt()
)
}
var recording: Float32Array? = null
override fun onMessage(message: MessageEvent) {
//console.log("Message from worklet: ", message)
val data = message.data
if (data is Float32Array) {
this.recording = data
WaveformView.requestUpdate()
}
}
fun postDirectlyToWorklet(msg: Any) {
super.postMessage(msg)
}
override fun postMessage(msg: Any) {
if (msg is ByteArray) {
val tmm = TimedMidiMessage(msg)
val byte1 = tmm.midi[0]
if (byte1.toInt() and 0xf0 == 0xb0) {
handleIncomingMidi(tmm.midi[1], tmm.midi[2])
}
}
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
Views.mainView.requestUpdate()
}
0x4a.toByte() -> {
dutyCycle = value / 127.0
Views.mainView.requestUpdate()
}
0x40.toByte() -> {
fmModFreq = value / 127.0
Views.mainView.requestUpdate()
}
0x41.toByte() -> {
fmModAmp = value / 127.0
Views.mainView.requestUpdate()
}
0x42.toByte() -> {
amModFreq = value / 127.0
Views.mainView.requestUpdate()
}
0x43.toByte() -> {
amModAmp = value / 127.0
Views.mainView.requestUpdate()
}
}
}
fun load(patch: PatchDTO) {
waveform = patch.waveform
midiChannel = patch.midiChannel
volume = patch.volume
dutyCycle = patch.dutyCycle
fmModFreq = patch.fmModFreq
fmModAmp = patch.fmModAmp
amModFreq = patch.amModFreq
amModAmp = patch.amModAmp
attack = patch.attack
decay = patch.decay
sustain = patch.sustain
release = patch.release
delay = patch.delay
delayDepth = patch.delayDepth
feedback = patch.feedback
}
fun save(): PatchDTO {
return PatchDTO(
waveform = waveform,
midiChannel = midiChannel,
volume = volume,
dutyCycle = dutyCycle,
fmModFreq = fmModFreq,
fmModAmp = fmModAmp,
amModFreq = amModFreq,
amModAmp = amModAmp,
attack = attack,
decay = decay,
sustain = sustain,
release = release,
delay = delay,
delayDepth = delayDepth,
feedback = feedback
)
console.log("Message from worklet: ", message)
}
}

View File

@@ -1,10 +1,8 @@
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
@@ -39,6 +37,7 @@ external class MIDIOutput {
}
object Midi {
var inputChannel: Int = -1
var outputChannel: Int = -1
var inputs = mutableListOf<MIDIInput>()
@@ -68,7 +67,7 @@ object Midi {
outputs.add(output)
}
Views.mainView.requestUpdate()
MainView.requestUpdate()
},
{ e ->
println("Failed to get MIDI access - $e")
@@ -76,39 +75,6 @@ object Midi {
)
}
fun setInput(id: String, name: String = "") {
var selected = inputs.find { it.id == id }
if (selected == null) {
var maxMatchChar = 0
inputs.forEach {
val matchChars = matchChars(it.name, name)
if (matchChars > maxMatchChar) {
selected = it
maxMatchChar = matchChars
}
}
}
setInput(selected)
}
private fun matchChars(str1: String, str2: String): Int {
var result = 0
if (str1.length > str2.length) {
for (ch in str1.toCharArray()) {
if (str2.contains(ch)) {
result++
}
}
} else {
for (ch in str2.toCharArray()) {
if (str1.contains(ch)) {
result++
}
}
}
return result
}
fun setInput(input: MIDIInput?) {
console.log("Setting input", input)
currentInput?.close()
@@ -126,14 +92,9 @@ object Midi {
hex.append(data[index].toString(16))
hex.append(" ")
}
console.log("Midi message:", hex, message)
val midiData = ByteArray(message.data.length) { data[it].toByte() }
val timeMessage = TimedMidiMessage(
AudioContextHandler.audioContext.currentTime,
*midiData
)
console.log("Midi message:", hex)
VstChipWorklet.postMessage(
timeMessage.data.buffer.toByteArray()
message.data
)
}
@@ -149,7 +110,7 @@ object Midi {
currentOutput?.open()
}
fun send(data: Uint8Array, timestamp: dynamic = null) {
fun send(data: Uint8Array, timestamp: dynamic? = null) {
currentOutput?.send(data, timestamp)
}

View File

@@ -1,26 +1,23 @@
@file:OptIn(ExperimentalJsExport::class)
package nl.astraeus.vst.chip.view
import daw.style.Css
import daw.style.Css.defineCss
import daw.style.Css.noTextSelect
import daw.style.CssId
import daw.style.CssName
import daw.style.hover
import kotlinx.browser.window
import kotlinx.html.InputType
import kotlinx.html.canvas
import kotlinx.html.classes
import kotlinx.html.div
import kotlinx.html.h1
import kotlinx.html.input
import kotlinx.html.js.onChangeFunction
import kotlinx.html.js.onClickFunction
import kotlinx.html.js.onInputFunction
import kotlinx.html.option
import kotlinx.html.select
import kotlinx.html.span
import nl.astraeus.css.properties.AlignItems
import nl.astraeus.css.properties.BoxSizing
import nl.astraeus.css.properties.Display
import nl.astraeus.css.properties.FlexDirection
import nl.astraeus.css.properties.FontWeight
import nl.astraeus.css.properties.JustifyContent
import nl.astraeus.css.properties.Position
import nl.astraeus.css.properties.Transform
import nl.astraeus.css.properties.em
@@ -30,81 +27,21 @@ import nl.astraeus.css.properties.px
import nl.astraeus.css.properties.rem
import nl.astraeus.css.properties.vh
import nl.astraeus.css.properties.vw
import nl.astraeus.css.style.Style
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
import nl.astraeus.vst.chip.ws.WebsocketClient
import nl.astraeus.vst.ui.components.ExpKnobComponent
import nl.astraeus.vst.ui.components.KnobComponent
import nl.astraeus.vst.ui.css.Css
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 org.khronos.webgl.get
import org.w3c.dom.CanvasRenderingContext2D
import org.w3c.dom.HTMLCanvasElement
import org.khronos.webgl.Uint8Array
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.HTMLSelectElement
object WaveformView: Komponent() {
init {
window.requestAnimationFrame(::onAnimationFrame)
}
fun onAnimationFrame(time: Double) {
if (Views.mainView.started) {
VstChipWorklet.postMessage("start_recording")
}
window.requestAnimationFrame(::onAnimationFrame)
}
override fun HtmlBuilder.render() {
div {
if (VstChipWorklet.recording != null) {
canvas {
width = "1000"
height = "400"
val ctx = (currentElement() as? HTMLCanvasElement)?.getContext("2d") as? CanvasRenderingContext2D
val data = VstChipWorklet.recording
if (ctx != null && data != null) {
val width = ctx.canvas.width.toDouble()
val height = ctx.canvas.height.toDouble()
val halfHeight = height / 2.0
ctx.lineWidth = 2.0
ctx.clearRect(0.0, 0.0, width, height)
val step = 1000.0 / data.length
ctx.beginPath()
ctx.strokeStyle = "rgba(0, 255, 255, 0.5)"
ctx.moveTo(0.0, halfHeight)
for (i in 0 until data.length) {
ctx.lineTo(i * step, halfHeight - data[i] * halfHeight)
}
ctx.stroke()
}
}
}
}
}
}
class MainView : Komponent() {
object MainView : Komponent() {
private var messages: MutableList<String> = ArrayList()
var started = false
private var started = false
init {
css()
MainViewCss
}
fun addMessage(message: String) {
@@ -115,25 +52,19 @@ class MainView : Komponent() {
requestUpdate()
}
override fun renderUpdate() {
println("Rendering MainView")
super.renderUpdate()
}
override fun HtmlBuilder.render() {
div(MainDivCss.name) {
div(MainViewCss.MainDivCss.name) {
if (!started) {
div(StartSplashCss.name) {
div(StartBoxCss.name) {
div(StartButtonCss.name) {
div(MainViewCss.StartSplashCss.name) {
div(MainViewCss.StartBoxCss.name) {
div(MainViewCss.StartButtonCss.name) {
+"START"
}
}
onClickFunction = {
VstChipWorklet.create {
started = true
requestUpdate()
WebsocketClient.send("LOAD\n")
onClickFunction = {
started = true
VstChipWorklet.create {
requestUpdate()
}
}
}
}
}
@@ -147,22 +78,30 @@ class MainView : Komponent() {
select {
option {
+"None"
value = "none"
value = ""
}
option {
+"Midi over Broadcast"
value = "midi-broadcast"
}
for (mi in Midi.inputs) {
option {
+mi.name
value = mi.id
selected = mi.id == Midi.currentInput?.id
}
}
onChangeFunction = { event ->
val target = event.target as HTMLSelectElement
if (target.value == "none") {
if (target.value == "") {
Midi.setInput(null)
} else {
Midi.setInput(target.value)
val selected = Midi.inputs.find { it.id == target.value }
if (selected != null) {
Midi.setInput(selected)
} else if (target.value == "midi-broadcast") {
//
}
}
}
}
@@ -171,269 +110,98 @@ class MainView : Komponent() {
+"channel:"
input {
type = InputType.number
value = VstChipWorklet.midiChannel.toString()
onInputFunction = { event ->
value = Midi.inputChannel.toString()
onChangeFunction = { event ->
val target = event.target as HTMLInputElement
println("onInput channel: $target")
VstChipWorklet.midiChannel = target.value.toInt()
Midi.inputChannel = target.value.toInt()
}
}
}
}
div {
span(ButtonBarCss.name) {
+"SAVE"
onClickFunction = {
val patch = VstChipWorklet.save().copy(
midiId = Midi.currentInput?.id ?: "",
midiName = Midi.currentInput?.name ?: ""
)
span {
+"Midi output: "
select {
option {
+"None"
value = ""
}
option {
+"Midi over Broadcast"
value = "midi-broadcast"
}
for (mi in Midi.outputs) {
option {
+mi.name
value = mi.id
}
}
WebsocketClient.send("SAVE\n${JSON.stringify(patch)}")
onChangeFunction = { event ->
val target = event.target as HTMLSelectElement
if (target.value == "") {
Midi.setOutput(null)
} else {
val selected = Midi.outputs.find { it.id == target.value }
if (selected != null) {
Midi.setOutput(selected)
}
}
}
}
}
span(ButtonBarCss.name) {
+"STOP"
onClickFunction = {
VstChipWorklet.postDirectlyToWorklet(
TimedMidiMessage(getCurrentTime(), (0xb0 + midiChannel).toByte(), 123, 0)
.data.buffer.data
span {
+"channel:"
input {
type = InputType.number
value = Midi.outputChannel.toString()
onChangeFunction = { event ->
val target = event.target as HTMLInputElement
Midi.outputChannel = target.value.toInt()
}
}
}
}
div(MainViewCss.ButtonCss.name) {
+"Send note on to output"
onClickFunction = {
val data = Uint8Array(
arrayOf(
0x90.toByte(),
0x3c.toByte(),
0x70.toByte()
)
}
)
Midi.send(data, window.performance.now() + 1000)
Midi.send(data, window.performance.now() + 2000)
}
}
div {
span(ButtonBarCss.name) {
+"Sine"
if (VstChipWorklet.waveform == 0) {
classes += SelectedCss.name
}
onClickFunction = {
VstChipWorklet.waveform = 0
requestUpdate()
}
}
span(ButtonBarCss.name) {
+"Square"
if (VstChipWorklet.waveform == 1) {
classes += SelectedCss.name
}
onClickFunction = {
VstChipWorklet.waveform = 1
requestUpdate()
}
}
span(ButtonBarCss.name) {
+"Triangle"
if (VstChipWorklet.waveform == 2) {
classes += SelectedCss.name
}
onClickFunction = {
VstChipWorklet.waveform = 2
requestUpdate()
}
}
span(ButtonBarCss.name) {
+"Sawtooth"
if (VstChipWorklet.waveform == 3) {
classes += SelectedCss.name
}
onClickFunction = {
VstChipWorklet.waveform = 3
requestUpdate()
}
div(MainViewCss.ButtonCss.name) {
+"Send note off to output"
onClickFunction = {
val data = Uint8Array(
arrayOf(
0x90.toByte(),
0x3c.toByte(),
0x0.toByte(),
)
)
Midi.send(data)
}
}
div(ControlsCss.name) {
include(
ExpKnobComponent(
value = VstChipWorklet.volume,
label = "Volume",
minValue = 0.0,
maxValue = 1.0,
step = 5.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.volume = value
}
)
include(
KnobComponent(
value = VstChipWorklet.dutyCycle,
label = "Duty cycle",
minValue = 0.0,
maxValue = 1.0,
step = 2.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.dutyCycle = value
}
)
include(
ExpKnobComponent(
value = VstChipWorklet.fmModFreq,
label = "FM Freq",
minValue = 0.0,
maxValue = 2.0,
step = 5.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.fmModFreq = value
}
)
include(
ExpKnobComponent(
value = VstChipWorklet.fmModAmp,
label = "FM Ampl",
minValue = 0.0,
maxValue = 1.0,
step = 5.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.fmModAmp = value
}
)
include(
ExpKnobComponent(
value = VstChipWorklet.amModFreq,
label = "AM Freq",
minValue = 0.0,
maxValue = 1.0,
step = 5.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.amModFreq = value
}
)
include(
ExpKnobComponent(
value = VstChipWorklet.amModAmp,
label = "AM Ampl",
minValue = 0.0,
maxValue = 1.0,
step = 5.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.amModAmp = value
}
)
include(
ExpKnobComponent(
value = VstChipWorklet.feedback,
label = "Feedback",
minValue = 0.0,
maxValue = 1.0,
step = 5.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.feedback = value
}
)
include(
ExpKnobComponent(
value = VstChipWorklet.delay,
label = "Delay",
minValue = 0.0,
maxValue = 1.0,
step = 5.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.delay = value
}
)
include(
ExpKnobComponent(
value = VstChipWorklet.delayDepth,
label = "Delay depth",
minValue = 0.0,
maxValue = 1.0,
step = 5.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.delayDepth = value
}
)
}
div(ControlsCss.name) {
include(
ExpKnobComponent(
value = VstChipWorklet.attack,
label = "Attack",
minValue = 0.0,
maxValue = 5.0,
step = 25.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.attack = value / 5.0
}
)
include(
ExpKnobComponent(
value = VstChipWorklet.decay,
label = "Decay",
minValue = 0.0,
maxValue = 5.0,
step = 25.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.decay = value / 5.0
}
)
include(
KnobComponent(
value = VstChipWorklet.sustain,
label = "Sustain",
minValue = 0.0,
maxValue = 1.0,
step = 2.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.sustain = value
}
)
include(
ExpKnobComponent(
value = VstChipWorklet.release,
label = "Release",
minValue = 0.0,
maxValue = 5.0,
step = 25.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.release = value / 5.0
}
)
}
include(WaveformView)
}
}
companion object MainViewCss : CssName() {
object MainViewCss : CssId("main") {
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() {
init {
defineCss {
select("*") {
select("*:before") {
@@ -458,18 +226,15 @@ class MainView : Komponent() {
//transition()
noTextSelect()
}
select("input", "textarea") {
backgroundColor(Css.currentStyle.inputBackgroundColor)
color(Css.currentStyle.mainFontColor)
border("none")
}
select(cls(ButtonCss)) {
margin(1.rem)
commonButton()
}
select(cls(ButtonBarCss)) {
margin(1.rem, 0.px)
commonButton()
padding(1.rem)
backgroundColor(Css.currentStyle.buttonBackgroundColor)
color(Css.currentStyle.mainFontColor)
hover {
backgroundColor(Css.currentStyle.buttonBackgroundColor.hover())
}
}
select(cls(ActiveCss)) {
//backgroundColor(Css.currentStyle.selectedBackgroundColor)
@@ -489,7 +254,7 @@ class MainView : Komponent() {
backgroundImage("url('https://upload.wikimedia.org/wikipedia/commons/9/9d/Caret_down_font_awesome_whitevariation.svg')")
background("right 0.8em center/1.4em")
backgroundColor(Css.currentStyle.inputBackgroundColor)
color(Css.currentStyle.mainFontColor)
//color(Css.currentStyle.entryFontColor)
borderRadius(0.25.em)
}
select(cls(StartSplashCss)) {
@@ -499,7 +264,7 @@ class MainView : Komponent() {
width(100.vw)
height(100.vh)
zIndex(100)
backgroundColor(hsla(32, 0, 5, 0.65))
backgroundColor(hsla(32, 0, 50, 0.6))
select(cls(StartBoxCss)) {
position(Position.relative)
@@ -507,9 +272,7 @@ class MainView : Komponent() {
top(25.vh)
width(50.vw)
height(50.vh)
backgroundColor(hsla(239, 50, 10, 1.0))
borderColor(Css.currentStyle.mainFontColor)
borderWidth(2.px)
backgroundColor(hsla(0, 0, 50, 0.25))
select(cls(StartButtonCss)) {
position(Position.absolute)
@@ -522,31 +285,6 @@ class MainView : Komponent() {
}
}
}
select(ControlsCss.cls()) {
display(Display.flex)
flexDirection(FlexDirection.row)
justifyContent(JustifyContent.flexStart)
alignItems(AlignItems.center)
margin(1.rem)
padding(1.rem)
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())
}
}
}

View File

@@ -0,0 +1,115 @@
package daw.style
import kotlinx.browser.document
import nl.astraeus.css.properties.*
import nl.astraeus.css.style
import nl.astraeus.css.style.ConditionalStyle
import nl.astraeus.css.style.Style
class StyleDefinition(
val mainFontColor: Color = hsla(178, 70, 55, 1.0),
val mainBackgroundColor: Color = hsl(239, 50, 10),
//val entryFontColor: Color = hsl(Css.mainFontColorNumber, 70, 55),
val inputBackgroundColor : Color = mainBackgroundColor.lighten(15),
val buttonBackgroundColor : Color = mainBackgroundColor.lighten(15),
val buttonBorderColor : Color = mainFontColor.changeAlpha(0.25),
val buttonBorderWidth : Measurement = 2.px,
)
object NoTextSelectCls : CssName("no-text-select")
object SelectedCls : CssName("selected")
object ActiveCls : CssName("active")
fun Color.hover(): Color = if (Css.currentStyle == Css.darkStyle) {
this.lighten(15)
} else {
this.darken(15)
}
object Css {
var minified = false
var dynamicStyles = mutableMapOf<CssId, ConditionalStyle.() -> Unit>()
fun CssId.defineCss(conditionalStyle: ConditionalStyle.() -> Unit) {
check(!dynamicStyles.containsKey(this)) {
"CssId with name ${this.name} already defined!"
}
updateCss(conditionalStyle)
}
private fun CssId.updateCss(conditionalStyle: ConditionalStyle.() -> Unit) {
val elementId = this.description()
var dynamicStyleElement = document.getElementById(elementId)
dynamicStyles[this] = conditionalStyle
if (dynamicStyleElement == null) {
dynamicStyleElement = document.createElement("style")
dynamicStyleElement.id = elementId
document.head?.append(dynamicStyleElement)
}
val css = style(conditionalStyle)
dynamicStyleElement.innerHTML = css.generateCss(minified = false)
}
val darkStyle = StyleDefinition(
)
val lightStyle = StyleDefinition(
mainBackgroundColor = hsl(239+180, 50, 15),
)
var currentStyle: StyleDefinition = darkStyle
fun updateStyle() {
for ((cssId, dynStyle) in dynamicStyles) {
cssId.apply {
updateCss(dynStyle)
}
}
}
fun switchLayout() {
currentStyle = if (currentStyle == darkStyle) {
lightStyle
} else {
darkStyle
}
updateStyle()
}
fun Style.transition() {
transition("all 0.5s ease")
}
fun Style.noTextSelect() {
plain("-webkit-touch-callout", "none")
plain("-webkit-user-select", "none")
plain("-moz-user-select", "none")
plain("-ms-user-select", "none")
userSelect(UserSelect.none)
select("::selection") {
background("none")
}
}
object GenericCss : CssId("generic") {
init {
fun generateStyle(): String {
val css = style {
}
return css.generateCss(minified = minified)
}
}
}
}

View File

@@ -0,0 +1,65 @@
package daw.style
import nl.astraeus.css.style.DescriptionProvider
import nl.astraeus.css.style.cls
private val CAPITAL_LETTER = Regex("[A-Z]")
fun String.hyphenize(): String =
replace(CAPITAL_LETTER) {
"-${it.value.lowercase()}"
}
private val shortId = false
private var nextCssId = 1
private fun nextShortId(): String {
var id = nextCssId++
val result = StringBuilder()
while(id > 0) {
val ch = ((id % 26) + 'a'.code).toChar()
result.append(ch)
id /= 26
}
return result.toString()
}
open class CssName(name: String? = null) : DescriptionProvider {
val name: String = if (shortId) {
nextShortId()
} else if (name != null) {
"daw-$name"
} else {
"daw${this::class.simpleName?.hyphenize() ?: this::class}"
}
override fun description() = name
}
fun CssName.cls() : DescriptionProvider = cls(this)
open class CssId(name: String) : DescriptionProvider {
val name: String = if (shortId) {
nextShortId()
} else {
"daw-$name-css"
}
override fun description() = name
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is CssId) return false
if (name != other.name) return false
return true
}
override fun hashCode(): Int {
return name.hashCode()
}
}

View File

@@ -1,122 +0,0 @@
@file:OptIn(ExperimentalJsExport::class)
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 org.w3c.dom.MessageEvent
import org.w3c.dom.WebSocket
import org.w3c.dom.events.Event
object WebsocketClient {
var websocket: WebSocket? = null
var interval: Int = 0
fun connect(onConnect: () -> Unit) {
close()
websocket = if (window.location.hostname.contains("localhost") || window.location.hostname.contains("192.168")) {
WebSocket("ws://${window.location.hostname}:${window.location.port}/ws")
} else {
WebSocket("wss://${window.location.hostname}/ws")
}
websocket?.also { ws ->
ws.onopen = {
onOpen(ws, it)
onConnect()
}
ws.onmessage = { onMessage(ws, it) }
ws.onclose = { onClose(ws, it) }
ws.onerror = { onError(ws, it) }
}
}
fun close() {
websocket?.close(-1, "Application closed socket.")
}
fun onOpen(
ws: WebSocket,
event: Event
) {
interval = window.setInterval({
val actualWs = websocket
if (actualWs == null) {
window.clearInterval(interval)
console.log("Connection to the server was lost!\\nPlease try again later.")
reconnect()
}
}, 10000)
}
fun reconnect() {
val actualWs = websocket
if (actualWs != null) {
if (actualWs.readyState == WebSocket.OPEN) {
console.log("Connection to the server was lost!\\nPlease try again later.")
} else {
window.setTimeout({
reconnect()
}, 1000)
}
} else {
connect {}
window.setTimeout({
reconnect()
}, 1000)
}
}
fun onMessage(
ws: WebSocket,
event: Event
) {
if (event is MessageEvent) {
val data = event.data
if (data is String) {
console.log("Received message: $data")
if (data.startsWith("LOAD")) {
val patchJson = data.substring(5)
val patch = JSON.parse<PatchDTO>(patchJson)
Midi.setInput(patch.midiId, patch.midiName)
VstChipWorklet.load(patch)
Views.mainView.requestUpdate()
}
}
}
}
fun onClose(
ws: WebSocket,
event: Event
): dynamic {
websocket = null
return "dynamic"
}
fun onError(
ws: WebSocket,
event: Event
): dynamic {
console.log("Error websocket!", ws, event)
websocket = null
return "dynamic"
}
fun send(message: String) {
websocket?.send(message)
}
}

View File

@@ -0,0 +1,2 @@
package nl.astraeus.vst.chip

View File

@@ -1,25 +1,20 @@
package nl.astraeus.vst.chip
import nl.astraeus.vst.base.Settings
import nl.astraeus.vst.base.db.Database
import nl.astraeus.vst.base.web.UndertowServer
import nl.astraeus.vst.chip.logger.LogLevel
import nl.astraeus.vst.chip.logger.Logger
import io.undertow.Undertow
import io.undertow.UndertowOptions
fun main() {
Logger.level = LogLevel.DEBUG
Thread.setDefaultUncaughtExceptionHandler { _, e ->
e.printStackTrace()
}
Settings.port = 9005
Settings.jdbcStatsPort = 6005
val server = Undertow.builder()
.addHttpListener(Settings.port, "localhost")
.setIoThreads(4)
.setHandler(RequestHandler)
.setServerOption(UndertowOptions.SHUTDOWN_TIMEOUT, 1000)
.build()
Database.start()
UndertowServer.start(
"Vst Chip",
"/vst-chip-worklet-ui.js"
)
println("Starting server at port ${Settings.port}...")
server?.start()
}

View File

@@ -0,0 +1,15 @@
package nl.astraeus.vst.chip
import io.undertow.server.HttpHandler
import io.undertow.server.HttpServerExchange
import io.undertow.server.handlers.resource.PathResourceManager
import io.undertow.server.handlers.resource.ResourceHandler
import java.nio.file.Paths
object RequestHandler : HttpHandler {
val resourceHandler = ResourceHandler(PathResourceManager(Paths.get("web")))
override fun handleRequest(exchange: HttpServerExchange) {
resourceHandler.handleRequest(exchange)
}
}

View File

@@ -0,0 +1,50 @@
package nl.astraeus.vst.chip
import java.io.File
import java.io.FileInputStream
import java.util.*
object Settings {
var runningAsRoot: Boolean = false
var port = 9000
var sslPort = 8443
var connectionTimeout = 30000
var jdbcDriver = "nl.astraeus.jdbc.Driver"
var jdbcConnectionUrl = "jdbc:stat:webServerPort=6001:jdbc:sqlite:data/srp.db"
var jdbcUser = "sa"
var jdbcPassword = ""
var adminUser = "rnentjes"
var adminPassword = "9/SG_Bd}9gWz~?j\\A.U]n9]OO"
fun getPropertiesFromFile(filename: String): Properties? {
val propertiesFile = File(filename)
return if (propertiesFile.exists()) {
val properties = Properties()
FileInputStream(propertiesFile).use {
properties.load(it)
}
properties
} else {
null
}
}
fun readProperties(args: Array<String>) {
val filename = if (args.isNotEmpty()) args[0] else "srp.properties"
val properties = getPropertiesFromFile(filename) ?: return // return if properties couldn't be loaded
runningAsRoot = properties.getProperty("runningAsRoot", runningAsRoot.toString()).toBoolean()
port = properties.getProperty("port", port.toString()).toInt()
sslPort = properties.getProperty("sslPort", sslPort.toString()).toInt()
connectionTimeout = properties.getProperty("connectionTimeout", connectionTimeout.toString()).toInt()
jdbcDriver = properties.getProperty("jdbcDriver", jdbcDriver)
jdbcConnectionUrl = properties.getProperty("jdbcConnectionUrl", jdbcConnectionUrl)
jdbcUser = properties.getProperty("jdbcUser", jdbcUser)
jdbcPassword = properties.getProperty("jdbcPassword", jdbcPassword)
adminUser = properties.getProperty("adminUser", adminUser)
adminPassword = properties.getProperty("adminPassword", adminPassword)
}
}

View File

@@ -1,20 +0,0 @@
import java.util.*
tasks.register("generateVersionProperties") {
doLast {
val versionDir = layout.buildDirectory.dir("processedResources/jvm/main")
val versionFile = versionDir.get().file("version.properties").asFile
versionDir.get().asFile.mkdirs()
val properties = Properties().apply {
setProperty("group", project.group.toString())
setProperty("name", project.name.toString())
setProperty("version", project.version.toString())
setProperty("buildTime", Date().toString())
}
versionFile.writer().use { writer ->
properties.store(writer, "Version information")
}
}
}