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 ###
|
### Mac OS ###
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
web
|
/web
|
||||||
|
/web1
|
||||||
|
/web2
|
||||||
|
|
||||||
|
/data/*.db*
|
||||||
|
**/kotlin-js-store/*
|
||||||
.kotlin
|
.kotlin
|
||||||
.idea
|
.idea
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
<component name="ArtifactManager">
|
<component name="ArtifactManager">
|
||||||
<artifact type="jar" name="audio-worklet-js-1.0.0-SNAPSHOT">
|
<artifact type="jar" name="audio-worklet-js-1.0.0-SNAPSHOT">
|
||||||
<output-path>$PROJECT_DIR$/audio-worklet/build/libs</output-path>
|
<output-path>$PROJECT_DIR$/audio-worklet/build/libs</output-path>
|
||||||
<root id="archive" name="audio-worklet-js-1.0.0-SNAPSHOT.jar">
|
<root id="archive" name="audio-worklet-js-1.0.0-SNAPSHOT.jar" />
|
||||||
<element id="module-output" name="vst-chip.audio-worklet.jsMain" />
|
|
||||||
</root>
|
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</component>
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
<component name="ArtifactManager">
|
<component name="ArtifactManager">
|
||||||
<artifact type="jar" name="audio-worklet-jvm-1.0.0-SNAPSHOT">
|
<artifact type="jar" name="audio-worklet-jvm-1.0.0-SNAPSHOT">
|
||||||
<output-path>$PROJECT_DIR$/audio-worklet/build/libs</output-path>
|
<output-path>$PROJECT_DIR$/audio-worklet/build/libs</output-path>
|
||||||
<root id="archive" name="audio-worklet-jvm-1.0.0-SNAPSHOT.jar">
|
<root id="archive" name="audio-worklet-jvm-1.0.0-SNAPSHOT.jar" />
|
||||||
<element id="module-output" name="vst-chip.audio-worklet.jvmMain" />
|
|
||||||
</root>
|
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component name="ArtifactManager">
|
||||||
<artifact type="jar" name="common-js-1.0.0-SNAPSHOT">
|
<artifact type="jar" name="common-js-1.0.0-SNAPSHOT">
|
||||||
<output-path>$PROJECT_DIR$/common/build/libs</output-path>
|
<output-path>$PROJECT_DIR$/common/build/libs</output-path>
|
||||||
<root id="archive" name="common-js-1.0.0-SNAPSHOT.jar">
|
<root id="archive" name="common-js-1.0.0-SNAPSHOT.jar" />
|
||||||
<element id="module-output" name="vst-chip.common.jsMain" />
|
|
||||||
</root>
|
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component name="ArtifactManager">
|
||||||
<artifact type="jar" name="common-jvm-1.0.0-SNAPSHOT">
|
<artifact type="jar" name="common-jvm-1.0.0-SNAPSHOT">
|
||||||
<output-path>$PROJECT_DIR$/common/build/libs</output-path>
|
<output-path>$PROJECT_DIR$/common/build/libs</output-path>
|
||||||
<root id="archive" name="common-jvm-1.0.0-SNAPSHOT.jar">
|
<root id="archive" name="common-jvm-1.0.0-SNAPSHOT.jar" />
|
||||||
<element id="module-output" name="vst-chip.common.jvmMain" />
|
|
||||||
</root>
|
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component name="ArtifactManager">
|
||||||
<artifact type="jar" name="vst-chip-js-1.0.0-SNAPSHOT">
|
<artifact type="jar" name="vst-chip-js-1.0.0-SNAPSHOT">
|
||||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||||
<root id="archive" name="vst-chip-js-1.0.0-SNAPSHOT.jar">
|
<root id="archive" name="vst-chip-js-1.0.0-SNAPSHOT.jar" />
|
||||||
<element id="module-output" name="vst-chip.jsMain" />
|
|
||||||
</root>
|
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component name="ArtifactManager">
|
||||||
<artifact type="jar" name="vst-chip-jvm-1.0.0-SNAPSHOT">
|
<artifact type="jar" name="vst-chip-jvm-1.0.0-SNAPSHOT">
|
||||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||||
<root id="archive" name="vst-chip-jvm-1.0.0-SNAPSHOT.jar">
|
<root id="archive" name="vst-chip-jvm-1.0.0-SNAPSHOT.jar" />
|
||||||
<element id="module-output" name="vst-chip.jvmMain" />
|
|
||||||
</root>
|
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</component>
|
||||||
3
.idea/gradle.xml
generated
3
.idea/gradle.xml
generated
@@ -5,12 +5,11 @@
|
|||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
<option name="gradleHome" value="" />
|
<option name="gradleJvm" value="corretto-21" />
|
||||||
<option name="modules">
|
<option name="modules">
|
||||||
<set>
|
<set>
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
<option value="$PROJECT_DIR$/audio-worklet" />
|
<option value="$PROJECT_DIR$/audio-worklet" />
|
||||||
<option value="$PROJECT_DIR$/common" />
|
|
||||||
</set>
|
</set>
|
||||||
</option>
|
</option>
|
||||||
</GradleProjectSettings>
|
</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">
|
<component name="FrameworkDetectionExcludesConfiguration">
|
||||||
<file type="web" url="file://$PROJECT_DIR$" />
|
<file type="web" url="file://$PROJECT_DIR$" />
|
||||||
</component>
|
</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" />
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
</component>
|
</component>
|
||||||
<component name="accountSettings">
|
<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
|
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackOutput
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
@@ -27,24 +31,18 @@ kotlin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
distribution {
|
distribution {
|
||||||
outputDirectory.set(File("$projectDir/../web/"))
|
outputDirectory.set(File("$projectDir/../web2/"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jvm()
|
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
val commonMain by getting {
|
val commonMain by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":common"))
|
implementation("nl.astraeus:vst-worklet-base:1.0.1")
|
||||||
|
implementation("nl.astraeus:midi-arrays:0.3.4")
|
||||||
implementation("nl.astraeus:vst-worklet-base:1.0.0-SNAPSHOT")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val jsMain by getting {
|
|
||||||
dependencies {
|
|
||||||
implementation(project(":common"))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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
|
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.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.registerProcessor
|
||||||
import nl.astraeus.vst.sampleRate
|
import nl.astraeus.vst.sampleRate
|
||||||
import org.khronos.webgl.ArrayBuffer
|
|
||||||
import org.khronos.webgl.Float32Array
|
import org.khronos.webgl.Float32Array
|
||||||
import org.khronos.webgl.Int32Array
|
|
||||||
import org.khronos.webgl.Uint8Array
|
|
||||||
import org.khronos.webgl.get
|
import org.khronos.webgl.get
|
||||||
import org.khronos.webgl.set
|
import org.khronos.webgl.set
|
||||||
import org.w3c.dom.MessageEvent
|
import org.w3c.dom.MessageEvent
|
||||||
import kotlin.math.PI
|
import kotlin.math.PI
|
||||||
|
import kotlin.math.min
|
||||||
import kotlin.math.sin
|
import kotlin.math.sin
|
||||||
|
|
||||||
val POLYPHONICS = 10
|
val POLYPHONICS = 20
|
||||||
val PI2 = PI * 2
|
val PI2 = PI * 2
|
||||||
|
|
||||||
@ExperimentalJsExport
|
|
||||||
@JsExport
|
|
||||||
enum class NoteState {
|
|
||||||
ON,
|
|
||||||
RELEASED,
|
|
||||||
OFF
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExperimentalJsExport
|
@ExperimentalJsExport
|
||||||
@JsExport
|
@JsExport
|
||||||
class PlayingNote(
|
class PlayingNote(
|
||||||
val note: Int,
|
val note: Int,
|
||||||
var velocity: Int = 0
|
var velocity: Int = 0
|
||||||
) {
|
) {
|
||||||
|
val noteObj = Note.fromMidi(note)
|
||||||
|
|
||||||
fun retrigger(velocity: Int) {
|
fun retrigger(velocity: Int) {
|
||||||
this.velocity = velocity
|
this.velocity = velocity
|
||||||
state = NoteState.ON
|
|
||||||
sample = 0
|
sample = 0
|
||||||
attackSamples = 2500
|
noteStart = currentTime
|
||||||
releaseSamples = 10000
|
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 cycleOffset = 0.0
|
||||||
var sample = 0
|
var sample = 0
|
||||||
var attackSamples = 2500
|
|
||||||
var releaseSamples = 10000
|
|
||||||
var actualVolume = 0f
|
var actualVolume = 0f
|
||||||
|
val combDelayBuffer = Float32Array((sampleRate / noteObj.freq).toInt())
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class Waveform {
|
enum class Waveform {
|
||||||
@@ -56,152 +55,230 @@ enum class Waveform {
|
|||||||
SAWTOOTH
|
SAWTOOTH
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExperimentalJsExport
|
||||||
|
@JsExport
|
||||||
|
enum class RecordingState {
|
||||||
|
STOPPED,
|
||||||
|
WAITING_TO_START,
|
||||||
|
RECORDING
|
||||||
|
}
|
||||||
|
|
||||||
@ExperimentalJsExport
|
@ExperimentalJsExport
|
||||||
@JsExport
|
@JsExport
|
||||||
class VstChipProcessor : AudioWorkletProcessor() {
|
class VstChipProcessor : AudioWorkletProcessor() {
|
||||||
val notes = Array(POLYPHONICS) {
|
val midiMessageBuffer = SortedTimedMidiMessageList()
|
||||||
PlayingNote(
|
val midiMessageHandler = MidiMessageHandler()
|
||||||
0
|
val notes = Array<PlayingNote?>(POLYPHONICS) { null }
|
||||||
)
|
|
||||||
}
|
|
||||||
var waveform = Waveform.SINE.ordinal
|
var waveform = Waveform.SINE.ordinal
|
||||||
|
var volume = 0.75f
|
||||||
var dutyCycle = 0.5
|
var dutyCycle = 0.5
|
||||||
var fmFreq = 0.0
|
var fmFreq = 0.5
|
||||||
var fmAmp = 0.0
|
var fmAmp = 0.0
|
||||||
var amFreq = 0.0
|
var amFreq = 0.0
|
||||||
var amAmp = 0.0
|
var amAmp = 0.0
|
||||||
|
|
||||||
|
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 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 {
|
init {
|
||||||
this.port.onmessage = ::handleMessage
|
this.port.onmessage = ::handleMessage
|
||||||
Note.updateSampleRate(sampleRate)
|
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) {
|
private fun handleMessage(message: MessageEvent) {
|
||||||
//console.log("VstChipProcessor: Received message", message)
|
//console.log("VstChipProcessor: Received message:", currentTime)
|
||||||
|
|
||||||
val data = message.data
|
val data = message.data
|
||||||
|
|
||||||
when (data) {
|
try {
|
||||||
"test_on" -> {
|
when (data) {
|
||||||
playMidi(Int32Array(arrayOf(0x90, 60, 64)))
|
is String -> {
|
||||||
|
when {
|
||||||
|
data == "start_recording" -> {
|
||||||
|
port.postMessage(recordingBuffer)
|
||||||
|
if (recordingState == RecordingState.STOPPED) {
|
||||||
|
recordingState = RecordingState.WAITING_TO_START
|
||||||
|
recordingSample = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
data.startsWith("set_channel") -> {
|
||||||
"test_off" -> {
|
val parts = data.split('\n')
|
||||||
playMidi(Int32Array(arrayOf(0x90, 60, 0)))
|
if (parts.size == 2) {
|
||||||
}
|
midiMessageHandler.channel = parts[1].toByte()
|
||||||
is String -> {
|
|
||||||
}
|
println("Setting channel: ${midiMessageHandler.channel}")
|
||||||
is ArrayBuffer -> {
|
}
|
||||||
}
|
}
|
||||||
is Uint8Array -> {
|
|
||||||
val data32 = Int32Array(data.length)
|
data.startsWith("waveform") -> {
|
||||||
for (i in 0 until data.length) {
|
val parts = data.split('\n')
|
||||||
data32[i] = (data[i].toInt() and 0xff)
|
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 -> {
|
} catch (e: Exception) {
|
||||||
playMidi(data)
|
console.log(e.message, e)
|
||||||
}
|
|
||||||
else ->
|
|
||||||
console.error("Don't kow how to handle message", message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun playMidi(bytes: Int32Array) {
|
private fun playBuffer() {
|
||||||
if (bytes.length > 0) {
|
while (
|
||||||
//console.log("Received", bytes)
|
midiMessageBuffer.isNotEmpty() &&
|
||||||
when(bytes[0]) {
|
(midiMessageBuffer.nextTimestamp() ?: 0.0) < currentTime
|
||||||
0x90 -> {
|
) {
|
||||||
if (bytes.length == 3) {
|
val midi = midiMessageBuffer.read()
|
||||||
val note = bytes[1]
|
playMidi(midi.midi)
|
||||||
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 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) {
|
private fun noteOn(note: Int, velocity: Int) {
|
||||||
for (i in 0 until POLYPHONICS) {
|
for (i in 0 until POLYPHONICS) {
|
||||||
if (notes[i].note == note) {
|
if (notes[i]?.note == note) {
|
||||||
notes[i].retrigger(velocity)
|
notes[i]?.retrigger(velocity)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (i in 0 until POLYPHONICS) {
|
for (i in 0 until POLYPHONICS) {
|
||||||
if (notes[i].state == NoteState.OFF) {
|
if (notes[i] == null) {
|
||||||
notes[i] = PlayingNote(
|
notes[i] = PlayingNote(
|
||||||
note,
|
note,
|
||||||
velocity
|
velocity
|
||||||
)
|
)
|
||||||
notes[i].state = NoteState.ON
|
|
||||||
|
|
||||||
val n = Note.fromMidi(note)
|
|
||||||
//console.log("Playing note: ${n.sharp} (${n.freq})")
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -209,79 +286,168 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
|||||||
|
|
||||||
private fun noteOff(note: Int) {
|
private fun noteOff(note: Int) {
|
||||||
for (i in 0 until POLYPHONICS) {
|
for (i in 0 until POLYPHONICS) {
|
||||||
if (notes[i].note == note && notes[i].state == NoteState.ON) {
|
if (notes[i]?.note == note) {
|
||||||
notes[i].state = NoteState.RELEASED
|
notes[i]?.noteRelease = currentTime
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun process (
|
override fun process(
|
||||||
inputs: Array<Array<Float32Array>>,
|
inputs: Array<Array<Float32Array>>,
|
||||||
outputs: Array<Array<Float32Array>>,
|
outputs: Array<Array<Float32Array>>,
|
||||||
parameters: dynamic
|
parameters: dynamic
|
||||||
) : Boolean {
|
): Boolean {
|
||||||
val samples = outputs[0][0].length
|
val samples = outputs[0][0].length
|
||||||
|
|
||||||
val left = outputs[0][0]
|
val left = outputs[0][0]
|
||||||
val right = outputs[0][1]
|
val right = outputs[0][1]
|
||||||
|
|
||||||
|
var lowestNote = 200
|
||||||
for (note in notes) {
|
for (note in notes) {
|
||||||
if (note.state != NoteState.OFF) {
|
if (note != null) {
|
||||||
val sampleDelta = Note.fromMidi(note.note).sampleDelta
|
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) {
|
for (i in 0 until samples) {
|
||||||
var targetVolume = note.velocity / 127f
|
var targetVolume = note.velocity / 127f * 1f
|
||||||
if (note.state == NoteState.ON && note.sample < note.attackSamples) {
|
targetVolume *= ADSR.calculate(
|
||||||
note.attackSamples--
|
attack,
|
||||||
targetVolume *= ( 1f - (note.attackSamples / 2500f))
|
decay,
|
||||||
} else if (note.state == NoteState.RELEASED) {
|
sustain,
|
||||||
note.releaseSamples--
|
release,
|
||||||
targetVolume *= (note.releaseSamples / 10000f)
|
note.noteStart,
|
||||||
}
|
currentTime,
|
||||||
note.actualVolume += (targetVolume - note.actualVolume) * 0.0001f
|
note.noteRelease
|
||||||
|
).toFloat()
|
||||||
|
note.actualVolume += (targetVolume - note.actualVolume) * 0.01f
|
||||||
|
|
||||||
if (note.state == NoteState.RELEASED && note.actualVolume <= 0) {
|
if (note.noteRelease != null && note.actualVolume <= 0.01) {
|
||||||
note.state = NoteState.OFF
|
notes[index] = null
|
||||||
}
|
}
|
||||||
|
|
||||||
var cycleOffset = note.cycleOffset
|
var cycleOffset = note.cycleOffset
|
||||||
val fmModulation = sin(sampleLength * fmFreq * 10f * PI2 * note.sample).toFloat() * fmAmp * 5f
|
val fmMult = sin(currentTime * fmFreq * midiNote.freq * 2f * PI2) * fmAmp
|
||||||
val amModulation = 1f + (sin(sampleLength * amFreq * 10f * PI2 * note.sample) * amAmp).toFloat()
|
val fmModulation =
|
||||||
cycleOffset += 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) {
|
val waveValue: Float = when (waveform) {
|
||||||
0 -> {
|
0 -> {
|
||||||
sin(cycleOffset * PI2).toFloat()
|
sin(cycleOffset * PI2).toFloat()
|
||||||
}
|
}
|
||||||
|
|
||||||
1 -> {
|
1 -> {
|
||||||
if (cycleOffset < dutyCycle) { 1f } else { -1f }
|
if (cycleOffset < 0.5) {
|
||||||
|
1f
|
||||||
|
} else {
|
||||||
|
-1f
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
2 -> when {
|
2 -> when {
|
||||||
cycleOffset < 0.25 -> 4 * cycleOffset
|
cycleOffset < 0.25 -> 4 * cycleOffset
|
||||||
cycleOffset < 0.75 -> 2 - 4 * cycleOffset
|
cycleOffset < 0.75 -> 2 - 4 * cycleOffset
|
||||||
else -> 4 * cycleOffset - 4
|
else -> 4 * cycleOffset - 4
|
||||||
}.toFloat()
|
}.toFloat()
|
||||||
|
|
||||||
3 -> {
|
3 -> {
|
||||||
((cycleOffset * 2f) - 1f).toFloat()
|
((cycleOffset * 2f) - 1f).toFloat()
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
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
|
left[i] = left[i] + waveValue * note.actualVolume * volume * amModulation
|
||||||
right[i] = right[i] + waveValue * note.actualVolume * 0.3f * amModulation
|
right[i] = right[i] + waveValue * note.actualVolume * volume * amModulation
|
||||||
|
|
||||||
note.cycleOffset += sampleDelta
|
// comb filter delay
|
||||||
if (cycleOffset > 1f) {
|
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
|
note.cycleOffset -= 1f
|
||||||
|
if (note.note == lowestNote && recordingState == RecordingState.WAITING_TO_START) {
|
||||||
|
recordingState = RecordingState.RECORDING
|
||||||
|
recordingSample = 0
|
||||||
|
recordingStart = i
|
||||||
|
}
|
||||||
}
|
}
|
||||||
note.sample++
|
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
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -289,5 +455,5 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
|||||||
fun main() {
|
fun main() {
|
||||||
registerProcessor("vst-chip-processor", VstChipProcessor::class.js)
|
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 {
|
buildscript {
|
||||||
apply(from = "common.gradle.kts")
|
apply(from = "common.gradle.kts")
|
||||||
|
apply(from = "version.gradle.kts")
|
||||||
}
|
}
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
kotlin("multiplatform")
|
kotlin("multiplatform")
|
||||||
kotlin("plugin.serialization")
|
id("maven-publish")
|
||||||
id("maven-publish")
|
application
|
||||||
application
|
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
js {
|
js {
|
||||||
compilerOptions {
|
compilerOptions {
|
||||||
target.set("es2015")
|
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()
|
|
||||||
}
|
}
|
||||||
|
//useEsModules()
|
||||||
|
//useCommonJs()
|
||||||
|
|
||||||
sourceSets {
|
binaries.executable()
|
||||||
val commonMain by getting {
|
browser {
|
||||||
dependencies {
|
commonWebpackConfig {
|
||||||
implementation(project(":common"))
|
outputFileName = "vst-chip-worklet-ui.js"
|
||||||
//base
|
sourceMaps = true
|
||||||
api("nl.astraeus:kotlin-css-generator:1.0.7")
|
devtool = "inline-source-map"
|
||||||
}
|
}
|
||||||
}
|
|
||||||
val jsMain by getting {
|
|
||||||
dependencies {
|
|
||||||
//base
|
|
||||||
implementation("nl.astraeus:kotlin-komponent-js:1.2.2")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val jsTest by getting {
|
|
||||||
dependencies {
|
|
||||||
implementation(kotlin("test-js"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val jvmMain by getting {
|
|
||||||
dependencies {
|
|
||||||
//base
|
|
||||||
|
|
||||||
implementation("io.undertow:undertow-core:2.3.13.Final")
|
distribution {
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.11.0")
|
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"
|
group = "nl.astraeus"
|
||||||
version = "1.0.0-SNAPSHOT"
|
version = "0.1.0"
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
mavenLocal()
|
|
||||||
mavenCentral()
|
|
||||||
maven("https://reposilite.astraeus.nl/releases")
|
|
||||||
maven {
|
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 {
|
pluginManagement {
|
||||||
plugins {
|
plugins {
|
||||||
kotlin("multiplatform") version "2.0.0"
|
kotlin("multiplatform") version "2.1.20"
|
||||||
kotlin("plugin.serialization") version "2.0.0"
|
|
||||||
id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0"
|
|
||||||
}
|
}
|
||||||
repositories {
|
repositories {
|
||||||
gradlePluginPortal()
|
gradlePluginPortal()
|
||||||
|
|||||||
@@ -2,5 +2,4 @@ apply(from = "settings.common.gradle.kts")
|
|||||||
|
|
||||||
rootProject.name = "vst-chip"
|
rootProject.name = "vst-chip"
|
||||||
|
|
||||||
include(":common")
|
|
||||||
include(":audio-worklet")
|
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
|
package nl.astraeus.vst.chip
|
||||||
|
|
||||||
import kotlinx.browser.document
|
import kotlinx.browser.document
|
||||||
import kotlinx.browser.window
|
|
||||||
import nl.astraeus.komp.Komponent
|
import nl.astraeus.komp.Komponent
|
||||||
import nl.astraeus.vst.chip.midi.Broadcaster
|
import nl.astraeus.komp.UnsafeMode
|
||||||
import nl.astraeus.vst.chip.midi.MidiMessage
|
import nl.astraeus.vst.chip.logger.log
|
||||||
import nl.astraeus.vst.chip.midi.Midi
|
import nl.astraeus.vst.chip.midi.Midi
|
||||||
import nl.astraeus.vst.chip.view.MainView
|
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() {
|
fun main() {
|
||||||
Komponent.create(document.body!!, MainView)
|
Komponent.unsafeMode = UnsafeMode.UNSAFE_SVG_ONLY
|
||||||
|
Komponent.create(document.body!!, Views.mainView)
|
||||||
|
|
||||||
Midi.start()
|
Midi.start()
|
||||||
|
|
||||||
console.log("Performance", window.performance)
|
WebsocketClient.connect {
|
||||||
Broadcaster.getChannel(0).postMessage(
|
log.debug { "Connected to server" }
|
||||||
MidiMessage(
|
}
|
||||||
Uint8Array(arrayOf(0x80.toByte(), 60, 60)),
|
|
||||||
window.performance.now()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
window.setInterval({
|
|
||||||
Broadcaster.sync()
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
package nl.astraeus.vst.chip.audio
|
package nl.astraeus.vst.chip.audio
|
||||||
|
|
||||||
import nl.astraeus.vst.chip.AudioContext
|
|
||||||
|
|
||||||
object AudioContextHandler {
|
object AudioContextHandler {
|
||||||
val audioContext: dynamic = AudioContext()
|
val audioContext: dynamic = js("new AudioContext()")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package nl.astraeus.vst.chip.audio
|
package nl.astraeus.vst.chip.audio
|
||||||
|
|
||||||
|
import nl.astraeus.midi.message.TimedMidiMessage
|
||||||
import nl.astraeus.vst.chip.AudioWorkletNode
|
import nl.astraeus.vst.chip.AudioWorkletNode
|
||||||
import nl.astraeus.vst.chip.AudioWorkletNodeParameters
|
import nl.astraeus.vst.chip.AudioWorkletNodeParameters
|
||||||
import nl.astraeus.vst.chip.audio.AudioContextHandler.audioContext
|
import nl.astraeus.vst.chip.audio.AudioContextHandler.audioContext
|
||||||
@@ -53,8 +54,27 @@ abstract class AudioNode(
|
|||||||
|
|
||||||
abstract fun onMessage(message: MessageEvent)
|
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) {
|
open fun postMessage(msg: Any) {
|
||||||
|
if (port == null) {
|
||||||
|
console.log("postMessage port is NULL!")
|
||||||
|
}
|
||||||
port?.postMessage(msg)
|
port?.postMessage(msg)
|
||||||
|
//console.log("Posted message", audioContext.currentTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
// call from user gesture
|
// call from user gesture
|
||||||
@@ -80,6 +100,7 @@ abstract class AudioNode(
|
|||||||
port = node.port as? MessagePort
|
port = node.port as? MessagePort
|
||||||
|
|
||||||
created = true
|
created = true
|
||||||
|
console.log("Created node: ${audioContext.currentTime}")
|
||||||
|
|
||||||
done(node)
|
done(node)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,239 @@
|
|||||||
|
@file:OptIn(ExperimentalJsExport::class)
|
||||||
|
|
||||||
package nl.astraeus.vst.chip.audio
|
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
|
import org.w3c.dom.MessageEvent
|
||||||
|
|
||||||
object VstChipWorklet : AudioNode(
|
object VstChipWorklet : AudioNode(
|
||||||
"vst-chip-worklet.js",
|
"/vst-chip-worklet.js",
|
||||||
"vst-chip-processor"
|
"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) {
|
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
|
package nl.astraeus.vst.chip.midi
|
||||||
|
|
||||||
import kotlinx.browser.window
|
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.audio.VstChipWorklet
|
||||||
import nl.astraeus.vst.chip.view.MainView
|
|
||||||
import org.khronos.webgl.Uint8Array
|
import org.khronos.webgl.Uint8Array
|
||||||
import org.khronos.webgl.get
|
import org.khronos.webgl.get
|
||||||
|
|
||||||
@@ -37,7 +39,6 @@ external class MIDIOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
object Midi {
|
object Midi {
|
||||||
var inputChannel: Int = -1
|
|
||||||
var outputChannel: Int = -1
|
var outputChannel: Int = -1
|
||||||
|
|
||||||
var inputs = mutableListOf<MIDIInput>()
|
var inputs = mutableListOf<MIDIInput>()
|
||||||
@@ -67,7 +68,7 @@ object Midi {
|
|||||||
outputs.add(output)
|
outputs.add(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
MainView.requestUpdate()
|
Views.mainView.requestUpdate()
|
||||||
},
|
},
|
||||||
{ e ->
|
{ e ->
|
||||||
println("Failed to get MIDI access - $e")
|
println("Failed to get MIDI access - $e")
|
||||||
@@ -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?) {
|
fun setInput(input: MIDIInput?) {
|
||||||
console.log("Setting input", input)
|
console.log("Setting input", input)
|
||||||
currentInput?.close()
|
currentInput?.close()
|
||||||
@@ -92,9 +126,14 @@ object Midi {
|
|||||||
hex.append(data[index].toString(16))
|
hex.append(data[index].toString(16))
|
||||||
hex.append(" ")
|
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(
|
VstChipWorklet.postMessage(
|
||||||
message.data
|
timeMessage.data.buffer.toByteArray()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +149,7 @@ object Midi {
|
|||||||
currentOutput?.open()
|
currentOutput?.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun send(data: Uint8Array, timestamp: dynamic? = null) {
|
fun send(data: Uint8Array, timestamp: dynamic = null) {
|
||||||
currentOutput?.send(data, timestamp)
|
currentOutput?.send(data, timestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,26 @@
|
|||||||
|
@file:OptIn(ExperimentalJsExport::class)
|
||||||
|
|
||||||
package nl.astraeus.vst.chip.view
|
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.browser.window
|
||||||
import kotlinx.html.InputType
|
import kotlinx.html.InputType
|
||||||
|
import kotlinx.html.canvas
|
||||||
|
import kotlinx.html.classes
|
||||||
import kotlinx.html.div
|
import kotlinx.html.div
|
||||||
import kotlinx.html.h1
|
import kotlinx.html.h1
|
||||||
import kotlinx.html.input
|
import kotlinx.html.input
|
||||||
import kotlinx.html.js.onChangeFunction
|
import kotlinx.html.js.onChangeFunction
|
||||||
import kotlinx.html.js.onClickFunction
|
import kotlinx.html.js.onClickFunction
|
||||||
|
import kotlinx.html.js.onInputFunction
|
||||||
import kotlinx.html.option
|
import kotlinx.html.option
|
||||||
import kotlinx.html.select
|
import kotlinx.html.select
|
||||||
import kotlinx.html.span
|
import kotlinx.html.span
|
||||||
|
import nl.astraeus.css.properties.AlignItems
|
||||||
import nl.astraeus.css.properties.BoxSizing
|
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.FontWeight
|
||||||
|
import nl.astraeus.css.properties.JustifyContent
|
||||||
import nl.astraeus.css.properties.Position
|
import nl.astraeus.css.properties.Position
|
||||||
import nl.astraeus.css.properties.Transform
|
import nl.astraeus.css.properties.Transform
|
||||||
import nl.astraeus.css.properties.em
|
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.rem
|
||||||
import nl.astraeus.css.properties.vh
|
import nl.astraeus.css.properties.vh
|
||||||
import nl.astraeus.css.properties.vw
|
import nl.astraeus.css.properties.vw
|
||||||
|
import nl.astraeus.css.style.Style
|
||||||
import nl.astraeus.css.style.cls
|
import nl.astraeus.css.style.cls
|
||||||
import nl.astraeus.komp.HtmlBuilder
|
import nl.astraeus.komp.HtmlBuilder
|
||||||
import nl.astraeus.komp.Komponent
|
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
|
||||||
|
import nl.astraeus.vst.chip.audio.VstChipWorklet.midiChannel
|
||||||
import nl.astraeus.vst.chip.midi.Midi
|
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.HTMLInputElement
|
||||||
import org.w3c.dom.HTMLSelectElement
|
import org.w3c.dom.HTMLSelectElement
|
||||||
|
|
||||||
object MainView : Komponent() {
|
object WaveformView: Komponent() {
|
||||||
private var messages: MutableList<String> = ArrayList()
|
|
||||||
private var started = false
|
|
||||||
|
|
||||||
init {
|
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) {
|
fun addMessage(message: String) {
|
||||||
@@ -52,19 +115,25 @@ object MainView : Komponent() {
|
|||||||
requestUpdate()
|
requestUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun renderUpdate() {
|
||||||
|
println("Rendering MainView")
|
||||||
|
super.renderUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
override fun HtmlBuilder.render() {
|
override fun HtmlBuilder.render() {
|
||||||
div(MainViewCss.MainDivCss.name) {
|
div(MainDivCss.name) {
|
||||||
if (!started) {
|
if (!started) {
|
||||||
div(MainViewCss.StartSplashCss.name) {
|
div(StartSplashCss.name) {
|
||||||
div(MainViewCss.StartBoxCss.name) {
|
div(StartBoxCss.name) {
|
||||||
div(MainViewCss.StartButtonCss.name) {
|
div(StartButtonCss.name) {
|
||||||
+"START"
|
+"START"
|
||||||
onClickFunction = {
|
}
|
||||||
started = true
|
}
|
||||||
VstChipWorklet.create {
|
onClickFunction = {
|
||||||
requestUpdate()
|
VstChipWorklet.create {
|
||||||
}
|
started = true
|
||||||
}
|
requestUpdate()
|
||||||
|
WebsocketClient.send("LOAD\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,30 +147,22 @@ object MainView : Komponent() {
|
|||||||
select {
|
select {
|
||||||
option {
|
option {
|
||||||
+"None"
|
+"None"
|
||||||
value = ""
|
value = "none"
|
||||||
}
|
|
||||||
option {
|
|
||||||
+"Midi over Broadcast"
|
|
||||||
value = "midi-broadcast"
|
|
||||||
}
|
}
|
||||||
for (mi in Midi.inputs) {
|
for (mi in Midi.inputs) {
|
||||||
option {
|
option {
|
||||||
+mi.name
|
+mi.name
|
||||||
value = mi.id
|
value = mi.id
|
||||||
|
selected = mi.id == Midi.currentInput?.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeFunction = { event ->
|
onChangeFunction = { event ->
|
||||||
val target = event.target as HTMLSelectElement
|
val target = event.target as HTMLSelectElement
|
||||||
if (target.value == "") {
|
if (target.value == "none") {
|
||||||
Midi.setInput(null)
|
Midi.setInput(null)
|
||||||
} else {
|
} else {
|
||||||
val selected = Midi.inputs.find { it.id == target.value }
|
Midi.setInput(target.value)
|
||||||
if (selected != null) {
|
|
||||||
Midi.setInput(selected)
|
|
||||||
} else if (target.value == "midi-broadcast") {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,98 +171,269 @@ object MainView : Komponent() {
|
|||||||
+"channel:"
|
+"channel:"
|
||||||
input {
|
input {
|
||||||
type = InputType.number
|
type = InputType.number
|
||||||
value = Midi.inputChannel.toString()
|
value = VstChipWorklet.midiChannel.toString()
|
||||||
onChangeFunction = { event ->
|
onInputFunction = { event ->
|
||||||
val target = event.target as HTMLInputElement
|
val target = event.target as HTMLInputElement
|
||||||
Midi.inputChannel = target.value.toInt()
|
println("onInput channel: $target")
|
||||||
|
VstChipWorklet.midiChannel = target.value.toInt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
span {
|
span(ButtonBarCss.name) {
|
||||||
+"Midi output: "
|
+"SAVE"
|
||||||
select {
|
onClickFunction = {
|
||||||
option {
|
val patch = VstChipWorklet.save().copy(
|
||||||
+"None"
|
midiId = Midi.currentInput?.id ?: "",
|
||||||
value = ""
|
midiName = Midi.currentInput?.name ?: ""
|
||||||
}
|
)
|
||||||
option {
|
|
||||||
+"Midi over Broadcast"
|
|
||||||
value = "midi-broadcast"
|
|
||||||
}
|
|
||||||
for (mi in Midi.outputs) {
|
|
||||||
option {
|
|
||||||
+mi.name
|
|
||||||
value = mi.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeFunction = { event ->
|
WebsocketClient.send("SAVE\n${JSON.stringify(patch)}")
|
||||||
val target = event.target as HTMLSelectElement
|
|
||||||
if (target.value == "") {
|
|
||||||
Midi.setOutput(null)
|
|
||||||
} else {
|
|
||||||
val selected = Midi.outputs.find { it.id == target.value }
|
|
||||||
if (selected != null) {
|
|
||||||
Midi.setOutput(selected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
span {
|
span(ButtonBarCss.name) {
|
||||||
+"channel:"
|
+"STOP"
|
||||||
input {
|
onClickFunction = {
|
||||||
type = InputType.number
|
VstChipWorklet.postDirectlyToWorklet(
|
||||||
value = Midi.outputChannel.toString()
|
TimedMidiMessage(getCurrentTime(), (0xb0 + midiChannel).toByte(), 123, 0)
|
||||||
onChangeFunction = { event ->
|
.data.buffer.data
|
||||||
val target = event.target as HTMLInputElement
|
)
|
||||||
Midi.outputChannel = target.value.toInt()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div(MainViewCss.ButtonCss.name) {
|
div {
|
||||||
+"Send note on to output"
|
span(ButtonBarCss.name) {
|
||||||
onClickFunction = {
|
+"Sine"
|
||||||
val data = Uint8Array(
|
if (VstChipWorklet.waveform == 0) {
|
||||||
arrayOf(
|
classes += SelectedCss.name
|
||||||
0x90.toByte(),
|
}
|
||||||
0x3c.toByte(),
|
onClickFunction = {
|
||||||
0x70.toByte()
|
VstChipWorklet.waveform = 0
|
||||||
)
|
requestUpdate()
|
||||||
)
|
}
|
||||||
Midi.send(data, window.performance.now() + 1000)
|
}
|
||||||
Midi.send(data, window.performance.now() + 2000)
|
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) {
|
div(ControlsCss.name) {
|
||||||
+"Send note off to output"
|
include(
|
||||||
onClickFunction = {
|
ExpKnobComponent(
|
||||||
val data = Uint8Array(
|
value = VstChipWorklet.volume,
|
||||||
arrayOf(
|
label = "Volume",
|
||||||
0x90.toByte(),
|
minValue = 0.0,
|
||||||
0x3c.toByte(),
|
maxValue = 1.0,
|
||||||
0x0.toByte(),
|
step = 5.0 / 127.0,
|
||||||
)
|
width = 100,
|
||||||
)
|
height = 120,
|
||||||
Midi.send(data)
|
) { 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 MainDivCss : CssName()
|
||||||
object ActiveCss : CssName()
|
object ActiveCss : CssName()
|
||||||
object ButtonCss : CssName()
|
object ButtonCss : CssName()
|
||||||
|
object ButtonBarCss : CssName()
|
||||||
|
object SelectedCss : CssName()
|
||||||
object NoteBarCss : CssName()
|
object NoteBarCss : CssName()
|
||||||
object StartSplashCss : CssName()
|
object StartSplashCss : CssName()
|
||||||
object StartBoxCss : CssName()
|
object StartBoxCss : CssName()
|
||||||
object StartButtonCss : CssName()
|
object StartButtonCss : CssName()
|
||||||
|
object ControlsCss : CssName()
|
||||||
|
|
||||||
init {
|
private fun css() {
|
||||||
defineCss {
|
defineCss {
|
||||||
select("*") {
|
select("*") {
|
||||||
select("*:before") {
|
select("*:before") {
|
||||||
@@ -226,15 +458,18 @@ object MainView : Komponent() {
|
|||||||
//transition()
|
//transition()
|
||||||
noTextSelect()
|
noTextSelect()
|
||||||
}
|
}
|
||||||
|
select("input", "textarea") {
|
||||||
|
backgroundColor(Css.currentStyle.inputBackgroundColor)
|
||||||
|
color(Css.currentStyle.mainFontColor)
|
||||||
|
border("none")
|
||||||
|
}
|
||||||
select(cls(ButtonCss)) {
|
select(cls(ButtonCss)) {
|
||||||
margin(1.rem)
|
margin(1.rem)
|
||||||
padding(1.rem)
|
commonButton()
|
||||||
backgroundColor(Css.currentStyle.buttonBackgroundColor)
|
}
|
||||||
color(Css.currentStyle.mainFontColor)
|
select(cls(ButtonBarCss)) {
|
||||||
|
margin(1.rem, 0.px)
|
||||||
hover {
|
commonButton()
|
||||||
backgroundColor(Css.currentStyle.buttonBackgroundColor.hover())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
select(cls(ActiveCss)) {
|
select(cls(ActiveCss)) {
|
||||||
//backgroundColor(Css.currentStyle.selectedBackgroundColor)
|
//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')")
|
backgroundImage("url('https://upload.wikimedia.org/wikipedia/commons/9/9d/Caret_down_font_awesome_whitevariation.svg')")
|
||||||
background("right 0.8em center/1.4em")
|
background("right 0.8em center/1.4em")
|
||||||
backgroundColor(Css.currentStyle.inputBackgroundColor)
|
backgroundColor(Css.currentStyle.inputBackgroundColor)
|
||||||
//color(Css.currentStyle.entryFontColor)
|
color(Css.currentStyle.mainFontColor)
|
||||||
borderRadius(0.25.em)
|
borderRadius(0.25.em)
|
||||||
}
|
}
|
||||||
select(cls(StartSplashCss)) {
|
select(cls(StartSplashCss)) {
|
||||||
@@ -264,7 +499,7 @@ object MainView : Komponent() {
|
|||||||
width(100.vw)
|
width(100.vw)
|
||||||
height(100.vh)
|
height(100.vh)
|
||||||
zIndex(100)
|
zIndex(100)
|
||||||
backgroundColor(hsla(32, 0, 50, 0.6))
|
backgroundColor(hsla(32, 0, 5, 0.65))
|
||||||
|
|
||||||
select(cls(StartBoxCss)) {
|
select(cls(StartBoxCss)) {
|
||||||
position(Position.relative)
|
position(Position.relative)
|
||||||
@@ -272,7 +507,9 @@ object MainView : Komponent() {
|
|||||||
top(25.vh)
|
top(25.vh)
|
||||||
width(50.vw)
|
width(50.vw)
|
||||||
height(50.vh)
|
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)) {
|
select(cls(StartButtonCss)) {
|
||||||
position(Position.absolute)
|
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
|
package nl.astraeus.vst.chip
|
||||||
|
|
||||||
import io.undertow.Undertow
|
import nl.astraeus.vst.base.Settings
|
||||||
import io.undertow.UndertowOptions
|
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() {
|
fun main() {
|
||||||
|
Logger.level = LogLevel.DEBUG
|
||||||
|
|
||||||
Thread.setDefaultUncaughtExceptionHandler { _, e ->
|
Thread.setDefaultUncaughtExceptionHandler { _, e ->
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
|
||||||
val server = Undertow.builder()
|
Settings.port = 9005
|
||||||
.addHttpListener(Settings.port, "localhost")
|
Settings.jdbcStatsPort = 6005
|
||||||
.setIoThreads(4)
|
|
||||||
.setHandler(RequestHandler)
|
|
||||||
.setServerOption(UndertowOptions.SHUTDOWN_TIMEOUT, 1000)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
println("Starting server at port ${Settings.port}...")
|
Database.start()
|
||||||
server?.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