Compare commits
32 Commits
incrementa
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 7110188d33 | |||
| 2cfc8a8201 | |||
| ce353d3113 | |||
| ff8a4dbf92 | |||
| dc50084e84 | |||
| 7fe29916f7 | |||
| 310f77fc3a | |||
| 60a21bbd79 | |||
| d58fb9c7b5 | |||
| fbba6d1422 | |||
| 4c00356dff | |||
| 29aac228e5 | |||
| 52c7495f43 | |||
| dc3a940942 | |||
| 4f5b30c52a | |||
| ee76d4c4a3 | |||
| b20b2266ba | |||
| 31f2d8060c | |||
| 9d67e742f3 | |||
| 92e68cdc47 | |||
| b412dd9b4e | |||
| f2269c8865 | |||
| 6554fd746a | |||
| 976328ed69 | |||
| 194857d687 | |||
| f22a800c93 | |||
| ccc7e9a4e9 | |||
| b02c7733b0 | |||
| 0cfd6f31d5 | |||
| 05764ec588 | |||
| 0281d2751f | |||
| f7e088bb67 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -41,7 +41,11 @@ bin/
|
||||
### Mac OS ###
|
||||
.DS_Store
|
||||
|
||||
web
|
||||
/web
|
||||
/web1
|
||||
/web2
|
||||
|
||||
/data/*.db*
|
||||
**/kotlin-js-store/*
|
||||
.kotlin
|
||||
.idea
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<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">
|
||||
<element id="module-output" name="vst-chip.audio-worklet.jsMain" />
|
||||
</root>
|
||||
<root id="archive" name="audio-worklet-js-1.0.0-SNAPSHOT.jar" />
|
||||
</artifact>
|
||||
</component>
|
||||
@@ -1,8 +1,6 @@
|
||||
<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">
|
||||
<element id="module-output" name="vst-chip.audio-worklet.jvmMain" />
|
||||
</root>
|
||||
<root id="archive" name="audio-worklet-jvm-1.0.0-SNAPSHOT.jar" />
|
||||
</artifact>
|
||||
</component>
|
||||
4
.idea/artifacts/common_js_1_0_0_SNAPSHOT.xml
generated
4
.idea/artifacts/common_js_1_0_0_SNAPSHOT.xml
generated
@@ -1,8 +1,6 @@
|
||||
<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">
|
||||
<element id="module-output" name="vst-chip.common.jsMain" />
|
||||
</root>
|
||||
<root id="archive" name="common-js-1.0.0-SNAPSHOT.jar" />
|
||||
</artifact>
|
||||
</component>
|
||||
4
.idea/artifacts/common_jvm_1_0_0_SNAPSHOT.xml
generated
4
.idea/artifacts/common_jvm_1_0_0_SNAPSHOT.xml
generated
@@ -1,8 +1,6 @@
|
||||
<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">
|
||||
<element id="module-output" name="vst-chip.common.jvmMain" />
|
||||
</root>
|
||||
<root id="archive" name="common-jvm-1.0.0-SNAPSHOT.jar" />
|
||||
</artifact>
|
||||
</component>
|
||||
4
.idea/artifacts/vst_chip_js_1_0_0_SNAPSHOT.xml
generated
4
.idea/artifacts/vst_chip_js_1_0_0_SNAPSHOT.xml
generated
@@ -1,8 +1,6 @@
|
||||
<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">
|
||||
<element id="module-output" name="vst-chip.jsMain" />
|
||||
</root>
|
||||
<root id="archive" name="vst-chip-js-1.0.0-SNAPSHOT.jar" />
|
||||
</artifact>
|
||||
</component>
|
||||
4
.idea/artifacts/vst_chip_jvm_1_0_0_SNAPSHOT.xml
generated
4
.idea/artifacts/vst_chip_jvm_1_0_0_SNAPSHOT.xml
generated
@@ -1,8 +1,6 @@
|
||||
<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">
|
||||
<element id="module-output" name="vst-chip.jvmMain" />
|
||||
</root>
|
||||
<root id="archive" name="vst-chip-jvm-1.0.0-SNAPSHOT.jar" />
|
||||
</artifact>
|
||||
</component>
|
||||
3
.idea/gradle.xml
generated
3
.idea/gradle.xml
generated
@@ -5,12 +5,11 @@
|
||||
<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$" />
|
||||
<option value="$PROJECT_DIR$/audio-worklet" />
|
||||
<option value="$PROJECT_DIR$/common" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
|
||||
7
.idea/jsLibraryMappings.xml
generated
7
.idea/jsLibraryMappings.xml
generated
@@ -1,7 +0,0 @@
|
||||
<?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
2
.idea/misc.xml
generated
@@ -4,7 +4,7 @@
|
||||
<component name="FrameworkDetectionExcludesConfiguration">
|
||||
<file type="web" url="file://$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="temurin-21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
<component name="accountSettings">
|
||||
|
||||
105
.junie/guidelines.md
Normal file
105
.junie/guidelines.md
Normal 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
|
||||
27
.run/Main [jvm].run.xml
Normal file
27
.run/Main [jvm].run.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<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>
|
||||
@@ -1,3 +1,7 @@
|
||||
@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 {
|
||||
@@ -27,24 +31,18 @@ kotlin {
|
||||
}
|
||||
|
||||
distribution {
|
||||
outputDirectory.set(File("$projectDir/../web/"))
|
||||
outputDirectory.set(File("$projectDir/../web2/"))
|
||||
}
|
||||
}
|
||||
}
|
||||
jvm()
|
||||
|
||||
sourceSets {
|
||||
val commonMain by getting {
|
||||
dependencies {
|
||||
implementation(project(":common"))
|
||||
|
||||
implementation("nl.astraeus:vst-worklet-base:1.0.0-SNAPSHOT")
|
||||
}
|
||||
}
|
||||
val jsMain by getting {
|
||||
dependencies {
|
||||
implementation(project(":common"))
|
||||
implementation("nl.astraeus:vst-worklet-base:1.0.1")
|
||||
implementation("nl.astraeus:midi-arrays:0.3.4")
|
||||
}
|
||||
}
|
||||
val jsMain by getting
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,51 +2,50 @@
|
||||
|
||||
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.Note
|
||||
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.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 = 10
|
||||
val POLYPHONICS = 20
|
||||
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
|
||||
attackSamples = 2500
|
||||
releaseSamples = 10000
|
||||
noteStart = currentTime
|
||||
noteRelease = null
|
||||
for (i in 0 until combDelayBuffer.length) {
|
||||
combDelayBuffer[i] = 0f
|
||||
}
|
||||
}
|
||||
|
||||
var state = NoteState.OFF
|
||||
var noteStart = currentTime
|
||||
var noteRelease: Double? = null
|
||||
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 {
|
||||
@@ -56,152 +55,230 @@ enum class Waveform {
|
||||
SAWTOOTH
|
||||
}
|
||||
|
||||
@ExperimentalJsExport
|
||||
@JsExport
|
||||
enum class RecordingState {
|
||||
STOPPED,
|
||||
WAITING_TO_START,
|
||||
RECORDING
|
||||
}
|
||||
|
||||
@ExperimentalJsExport
|
||||
@JsExport
|
||||
class VstChipProcessor : AudioWorkletProcessor() {
|
||||
val notes = Array(POLYPHONICS) {
|
||||
PlayingNote(
|
||||
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
|
||||
|
||||
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", message)
|
||||
//console.log("VstChipProcessor: Received message:", currentTime)
|
||||
|
||||
val data = message.data
|
||||
|
||||
when (data) {
|
||||
"test_on" -> {
|
||||
playMidi(Int32Array(arrayOf(0x90, 60, 64)))
|
||||
try {
|
||||
when (data) {
|
||||
is String -> {
|
||||
when {
|
||||
data == "start_recording" -> {
|
||||
port.postMessage(recordingBuffer)
|
||||
if (recordingState == RecordingState.STOPPED) {
|
||||
recordingState = RecordingState.WAITING_TO_START
|
||||
recordingSample = 0
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
"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)
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
*/
|
||||
|
||||
else ->
|
||||
console.error("Don't kow how to handle message", message)
|
||||
}
|
||||
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 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
notes[i].retrigger(velocity)
|
||||
if (notes[i]?.note == note) {
|
||||
notes[i]?.retrigger(velocity)
|
||||
return
|
||||
}
|
||||
}
|
||||
for (i in 0 until POLYPHONICS) {
|
||||
if (notes[i].state == NoteState.OFF) {
|
||||
if (notes[i] == null) {
|
||||
notes[i] = PlayingNote(
|
||||
note,
|
||||
velocity
|
||||
)
|
||||
notes[i].state = NoteState.ON
|
||||
|
||||
val n = Note.fromMidi(note)
|
||||
//console.log("Playing note: ${n.sharp} (${n.freq})")
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -209,79 +286,168 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
||||
|
||||
private fun noteOff(note: Int) {
|
||||
for (i in 0 until POLYPHONICS) {
|
||||
if (notes[i].note == note && notes[i].state == NoteState.ON) {
|
||||
notes[i].state = NoteState.RELEASED
|
||||
if (notes[i]?.note == note) {
|
||||
notes[i]?.noteRelease = currentTime
|
||||
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.state != NoteState.OFF) {
|
||||
val sampleDelta = Note.fromMidi(note.note).sampleDelta
|
||||
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
|
||||
|
||||
for (i in 0 until samples) {
|
||||
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
|
||||
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
|
||||
|
||||
if (note.state == NoteState.RELEASED && note.actualVolume <= 0) {
|
||||
note.state = NoteState.OFF
|
||||
if (note.noteRelease != null && note.actualVolume <= 0.01) {
|
||||
notes[index] = null
|
||||
}
|
||||
|
||||
var cycleOffset = note.cycleOffset
|
||||
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 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 waveValue: Float = when (waveform) {
|
||||
0 -> {
|
||||
sin(cycleOffset * PI2).toFloat()
|
||||
}
|
||||
|
||||
1 -> {
|
||||
if (cycleOffset < dutyCycle) { 1f } else { -1f }
|
||||
if (cycleOffset < 0.5) {
|
||||
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 * 0.3f * amModulation
|
||||
right[i] = right[i] + waveValue * note.actualVolume * 0.3f * amModulation
|
||||
left[i] = left[i] + waveValue * note.actualVolume * volume * amModulation
|
||||
right[i] = right[i] + waveValue * note.actualVolume * volume * amModulation
|
||||
|
||||
note.cycleOffset += sampleDelta
|
||||
if (cycleOffset > 1f) {
|
||||
// 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 -= 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
|
||||
}
|
||||
}
|
||||
@@ -289,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)
|
||||
}
|
||||
|
||||
194
audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/chip/Note.kt
Normal file
194
audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/chip/Note.kt
Normal file
@@ -0,0 +1,194 @@
|
||||
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
|
||||
208
build.gradle.kts
208
build.gradle.kts
@@ -1,66 +1,168 @@
|
||||
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackOutput
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
|
||||
buildscript {
|
||||
apply(from = "common.gradle.kts")
|
||||
apply(from = "common.gradle.kts")
|
||||
apply(from = "version.gradle.kts")
|
||||
}
|
||||
|
||||
plugins {
|
||||
kotlin("multiplatform")
|
||||
kotlin("plugin.serialization")
|
||||
id("maven-publish")
|
||||
application
|
||||
kotlin("multiplatform")
|
||||
id("maven-publish")
|
||||
application
|
||||
}
|
||||
|
||||
kotlin {
|
||||
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/"))
|
||||
}
|
||||
}
|
||||
}
|
||||
jvm{
|
||||
withJava()
|
||||
js {
|
||||
compilerOptions {
|
||||
target.set("es2015")
|
||||
}
|
||||
//useEsModules()
|
||||
//useCommonJs()
|
||||
|
||||
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
|
||||
binaries.executable()
|
||||
browser {
|
||||
commonWebpackConfig {
|
||||
outputFileName = "vst-chip-worklet-ui.js"
|
||||
sourceMaps = true
|
||||
devtool = "inline-source-map"
|
||||
}
|
||||
|
||||
implementation("io.undertow:undertow-core:2.3.13.Final")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.11.0")
|
||||
}
|
||||
}
|
||||
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")
|
||||
}
|
||||
|
||||
if (Files.exists(symlink)) {
|
||||
println("Symbolic link already exists: $symlink")
|
||||
} else {
|
||||
Files.createSymbolicLink(symlink, targetDir)
|
||||
println("Symbolic link created: $symlink -> $targetDir")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
group = "nl.astraeus"
|
||||
version = "1.0.0-SNAPSHOT"
|
||||
version = "0.1.0"
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
maven("https://reposilite.astraeus.nl/releases")
|
||||
maven {
|
||||
url = uri("https://nexus.astraeus.nl/nexus/content/groups/public")
|
||||
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,32 +0,0 @@
|
||||
@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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
apply(from = "../settings.common.gradle.kts")
|
||||
@@ -1,192 +0,0 @@
|
||||
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
|
||||
1
data/readme.md
Normal file
1
data/readme.md
Normal file
@@ -0,0 +1 @@
|
||||
Data directory for the db
|
||||
@@ -1,8 +1,6 @@
|
||||
pluginManagement {
|
||||
plugins {
|
||||
kotlin("multiplatform") version "2.0.0"
|
||||
kotlin("plugin.serialization") version "2.0.0"
|
||||
id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0"
|
||||
kotlin("multiplatform") version "2.1.20"
|
||||
}
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
|
||||
@@ -2,5 +2,4 @@ apply(from = "settings.common.gradle.kts")
|
||||
|
||||
rootProject.name = "vst-chip"
|
||||
|
||||
include(":common")
|
||||
include(":audio-worklet")
|
||||
|
||||
44
src/commonMain/kotlin/nl/astraeus/vst/chip/PatchDTO.kt
Normal file
44
src/commonMain/kotlin/nl/astraeus/vst/chip/PatchDTO.kt
Normal file
@@ -0,0 +1,44 @@
|
||||
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,
|
||||
)
|
||||
53
src/commonMain/kotlin/nl/astraeus/vst/chip/logger/Logger.kt
Normal file
53
src/commonMain/kotlin/nl/astraeus/vst/chip/logger/Logger.kt
Normal file
@@ -0,0 +1,53 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,32 @@
|
||||
package nl.astraeus.vst.chip
|
||||
|
||||
import kotlinx.browser.document
|
||||
import kotlinx.browser.window
|
||||
import nl.astraeus.komp.Komponent
|
||||
import nl.astraeus.vst.chip.midi.Broadcaster
|
||||
import nl.astraeus.vst.chip.midi.MidiMessage
|
||||
import nl.astraeus.komp.UnsafeMode
|
||||
import nl.astraeus.vst.chip.logger.log
|
||||
import nl.astraeus.vst.chip.midi.Midi
|
||||
import nl.astraeus.vst.chip.view.MainView
|
||||
import org.khronos.webgl.Uint8Array
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
Komponent.create(document.body!!, MainView)
|
||||
Komponent.unsafeMode = UnsafeMode.UNSAFE_SVG_ONLY
|
||||
Komponent.create(document.body!!, Views.mainView)
|
||||
|
||||
Midi.start()
|
||||
|
||||
console.log("Performance", window.performance)
|
||||
Broadcaster.getChannel(0).postMessage(
|
||||
MidiMessage(
|
||||
Uint8Array(arrayOf(0x80.toByte(), 60, 60)),
|
||||
window.performance.now()
|
||||
)
|
||||
)
|
||||
|
||||
window.setInterval({
|
||||
Broadcaster.sync()
|
||||
}, 1000)
|
||||
|
||||
WebsocketClient.connect {
|
||||
log.debug { "Connected to server" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()")
|
||||
}
|
||||
|
||||
@@ -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,8 +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
|
||||
@@ -80,6 +100,7 @@ abstract class AudioNode(
|
||||
port = node.port as? MessagePort
|
||||
|
||||
created = true
|
||||
console.log("Created node: ${audioContext.currentTime}")
|
||||
|
||||
done(node)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,239 @@
|
||||
@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)
|
||||
//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
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -37,7 +39,6 @@ external class MIDIOutput {
|
||||
}
|
||||
|
||||
object Midi {
|
||||
var inputChannel: Int = -1
|
||||
var outputChannel: Int = -1
|
||||
|
||||
var inputs = mutableListOf<MIDIInput>()
|
||||
@@ -67,7 +68,7 @@ object Midi {
|
||||
outputs.add(output)
|
||||
}
|
||||
|
||||
MainView.requestUpdate()
|
||||
Views.mainView.requestUpdate()
|
||||
},
|
||||
{ e ->
|
||||
println("Failed to get MIDI access - $e")
|
||||
@@ -75,6 +76,39 @@ 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()
|
||||
@@ -92,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()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -110,7 +149,7 @@ object Midi {
|
||||
currentOutput?.open()
|
||||
}
|
||||
|
||||
fun send(data: Uint8Array, timestamp: dynamic? = null) {
|
||||
fun send(data: Uint8Array, timestamp: dynamic = null) {
|
||||
currentOutput?.send(data, timestamp)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
@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
|
||||
@@ -27,21 +30,81 @@ 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 org.khronos.webgl.Uint8Array
|
||||
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.w3c.dom.HTMLInputElement
|
||||
import org.w3c.dom.HTMLSelectElement
|
||||
|
||||
object MainView : Komponent() {
|
||||
private var messages: MutableList<String> = ArrayList()
|
||||
private var started = false
|
||||
object WaveformView: Komponent() {
|
||||
|
||||
init {
|
||||
MainViewCss
|
||||
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() {
|
||||
private var messages: MutableList<String> = ArrayList()
|
||||
var started = false
|
||||
|
||||
init {
|
||||
css()
|
||||
}
|
||||
|
||||
fun addMessage(message: String) {
|
||||
@@ -52,19 +115,25 @@ object MainView : Komponent() {
|
||||
requestUpdate()
|
||||
}
|
||||
|
||||
override fun renderUpdate() {
|
||||
println("Rendering MainView")
|
||||
super.renderUpdate()
|
||||
}
|
||||
|
||||
override fun HtmlBuilder.render() {
|
||||
div(MainViewCss.MainDivCss.name) {
|
||||
div(MainDivCss.name) {
|
||||
if (!started) {
|
||||
div(MainViewCss.StartSplashCss.name) {
|
||||
div(MainViewCss.StartBoxCss.name) {
|
||||
div(MainViewCss.StartButtonCss.name) {
|
||||
div(StartSplashCss.name) {
|
||||
div(StartBoxCss.name) {
|
||||
div(StartButtonCss.name) {
|
||||
+"START"
|
||||
onClickFunction = {
|
||||
started = true
|
||||
VstChipWorklet.create {
|
||||
requestUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
onClickFunction = {
|
||||
VstChipWorklet.create {
|
||||
started = true
|
||||
requestUpdate()
|
||||
WebsocketClient.send("LOAD\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,30 +147,22 @@ object MainView : Komponent() {
|
||||
select {
|
||||
option {
|
||||
+"None"
|
||||
value = ""
|
||||
}
|
||||
option {
|
||||
+"Midi over Broadcast"
|
||||
value = "midi-broadcast"
|
||||
value = "none"
|
||||
}
|
||||
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 == "") {
|
||||
if (target.value == "none") {
|
||||
Midi.setInput(null)
|
||||
} else {
|
||||
val selected = Midi.inputs.find { it.id == target.value }
|
||||
if (selected != null) {
|
||||
Midi.setInput(selected)
|
||||
} else if (target.value == "midi-broadcast") {
|
||||
//
|
||||
}
|
||||
Midi.setInput(target.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -110,98 +171,269 @@ object MainView : Komponent() {
|
||||
+"channel:"
|
||||
input {
|
||||
type = InputType.number
|
||||
value = Midi.inputChannel.toString()
|
||||
onChangeFunction = { event ->
|
||||
value = VstChipWorklet.midiChannel.toString()
|
||||
onInputFunction = { event ->
|
||||
val target = event.target as HTMLInputElement
|
||||
Midi.inputChannel = target.value.toInt()
|
||||
println("onInput channel: $target")
|
||||
VstChipWorklet.midiChannel = target.value.toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
div {
|
||||
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
|
||||
}
|
||||
}
|
||||
span(ButtonBarCss.name) {
|
||||
+"SAVE"
|
||||
onClickFunction = {
|
||||
val patch = VstChipWorklet.save().copy(
|
||||
midiId = Midi.currentInput?.id ?: "",
|
||||
midiName = Midi.currentInput?.name ?: ""
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
WebsocketClient.send("SAVE\n${JSON.stringify(patch)}")
|
||||
}
|
||||
}
|
||||
span {
|
||||
+"channel:"
|
||||
input {
|
||||
type = InputType.number
|
||||
value = Midi.outputChannel.toString()
|
||||
onChangeFunction = { event ->
|
||||
val target = event.target as HTMLInputElement
|
||||
Midi.outputChannel = target.value.toInt()
|
||||
}
|
||||
span(ButtonBarCss.name) {
|
||||
+"STOP"
|
||||
onClickFunction = {
|
||||
VstChipWorklet.postDirectlyToWorklet(
|
||||
TimedMidiMessage(getCurrentTime(), (0xb0 + midiChannel).toByte(), 123, 0)
|
||||
.data.buffer.data
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
object MainViewCss : CssId("main") {
|
||||
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()
|
||||
|
||||
init {
|
||||
private fun css() {
|
||||
defineCss {
|
||||
select("*") {
|
||||
select("*:before") {
|
||||
@@ -226,15 +458,18 @@ object MainView : Komponent() {
|
||||
//transition()
|
||||
noTextSelect()
|
||||
}
|
||||
select("input", "textarea") {
|
||||
backgroundColor(Css.currentStyle.inputBackgroundColor)
|
||||
color(Css.currentStyle.mainFontColor)
|
||||
border("none")
|
||||
}
|
||||
select(cls(ButtonCss)) {
|
||||
margin(1.rem)
|
||||
padding(1.rem)
|
||||
backgroundColor(Css.currentStyle.buttonBackgroundColor)
|
||||
color(Css.currentStyle.mainFontColor)
|
||||
|
||||
hover {
|
||||
backgroundColor(Css.currentStyle.buttonBackgroundColor.hover())
|
||||
}
|
||||
commonButton()
|
||||
}
|
||||
select(cls(ButtonBarCss)) {
|
||||
margin(1.rem, 0.px)
|
||||
commonButton()
|
||||
}
|
||||
select(cls(ActiveCss)) {
|
||||
//backgroundColor(Css.currentStyle.selectedBackgroundColor)
|
||||
@@ -254,7 +489,7 @@ object 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.entryFontColor)
|
||||
color(Css.currentStyle.mainFontColor)
|
||||
borderRadius(0.25.em)
|
||||
}
|
||||
select(cls(StartSplashCss)) {
|
||||
@@ -264,7 +499,7 @@ object MainView : Komponent() {
|
||||
width(100.vw)
|
||||
height(100.vh)
|
||||
zIndex(100)
|
||||
backgroundColor(hsla(32, 0, 50, 0.6))
|
||||
backgroundColor(hsla(32, 0, 5, 0.65))
|
||||
|
||||
select(cls(StartBoxCss)) {
|
||||
position(Position.relative)
|
||||
@@ -272,7 +507,9 @@ object MainView : Komponent() {
|
||||
top(25.vh)
|
||||
width(50.vw)
|
||||
height(50.vh)
|
||||
backgroundColor(hsla(0, 0, 50, 0.25))
|
||||
backgroundColor(hsla(239, 50, 10, 1.0))
|
||||
borderColor(Css.currentStyle.mainFontColor)
|
||||
borderWidth(2.px)
|
||||
|
||||
select(cls(StartButtonCss)) {
|
||||
position(Position.absolute)
|
||||
@@ -285,6 +522,31 @@ object 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
||||
122
src/jsMain/kotlin/nl/astraeus/vst/chip/ws/WebsocketClient.kt
Normal file
122
src/jsMain/kotlin/nl/astraeus/vst/chip/ws/WebsocketClient.kt
Normal file
@@ -0,0 +1,122 @@
|
||||
@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)
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
package nl.astraeus.vst.chip
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
package nl.astraeus.vst.chip
|
||||
|
||||
import io.undertow.Undertow
|
||||
import io.undertow.UndertowOptions
|
||||
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
|
||||
|
||||
fun main() {
|
||||
Logger.level = LogLevel.DEBUG
|
||||
|
||||
Thread.setDefaultUncaughtExceptionHandler { _, e ->
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
val server = Undertow.builder()
|
||||
.addHttpListener(Settings.port, "localhost")
|
||||
.setIoThreads(4)
|
||||
.setHandler(RequestHandler)
|
||||
.setServerOption(UndertowOptions.SHUTDOWN_TIMEOUT, 1000)
|
||||
.build()
|
||||
Settings.port = 9005
|
||||
Settings.jdbcStatsPort = 6005
|
||||
|
||||
println("Starting server at port ${Settings.port}...")
|
||||
server?.start()
|
||||
Database.start()
|
||||
|
||||
UndertowServer.start(
|
||||
"Vst Chip",
|
||||
"/vst-chip-worklet-ui.js"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
20
version.gradle.kts
Normal file
20
version.gradle.kts
Normal file
@@ -0,0 +1,20 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user