Compare commits
1 Commits
master
...
incrementa
| Author | SHA1 | Date | |
|---|---|---|---|
| b3cd36b8f8 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -41,11 +41,7 @@ bin/
|
|||||||
### Mac OS ###
|
### Mac OS ###
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
/web
|
web
|
||||||
/web1
|
|
||||||
/web2
|
|
||||||
|
|
||||||
/data/*.db*
|
|
||||||
**/kotlin-js-store/*
|
|
||||||
.kotlin
|
.kotlin
|
||||||
.idea
|
.idea
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<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,6 +1,8 @@
|
|||||||
<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,6 +1,8 @@
|
|||||||
<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,6 +1,8 @@
|
|||||||
<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,6 +1,8 @@
|
|||||||
<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,6 +1,8 @@
|
|||||||
<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,11 +5,12 @@
|
|||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
<option name="gradleJvm" value="corretto-21" />
|
<option name="gradleHome" value="" />
|
||||||
<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
Normal file
7
.idea/jsLibraryMappings.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="JavaScriptLibraryMappings">
|
||||||
|
<excludedPredefinedLibrary name="vst-chip/build/js/node_modules" />
|
||||||
|
<excludedPredefinedLibrary name="vst-chip/build/js/packages/vst-base-test/node_modules" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
2
.idea/misc.xml
generated
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_21" default="true" project-jdk-name="temurin-21" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
</component>
|
</component>
|
||||||
<component name="accountSettings">
|
<component name="accountSettings">
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
# VST Chip Synthesizer Development Guidelines
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
VST Chip is a Kotlin multiplatform project that implements a chip-style synthesizer with a web interface. The project consists of:
|
|
||||||
|
|
||||||
- JVM backend that serves the web application
|
|
||||||
- JS frontend that runs in the browser
|
|
||||||
- Audio worklet processor for real-time audio processing
|
|
||||||
|
|
||||||
## Build/Configuration Instructions
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- JDK 11 or higher
|
|
||||||
- Gradle 7.x or higher
|
|
||||||
- Node.js and npm (for JS development)
|
|
||||||
|
|
||||||
### Building the Project
|
|
||||||
|
|
||||||
1. **Full Build**:
|
|
||||||
```bash
|
|
||||||
./gradlew build
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **JS Build Only**:
|
|
||||||
```bash
|
|
||||||
./gradlew buildJS
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Deployment**:
|
|
||||||
```bash
|
|
||||||
./gradlew deploy
|
|
||||||
```
|
|
||||||
This will build the project, copy web assets, and deploy to the configured server location.
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
|
|
||||||
- Server port: 9005 (configured in `Main.kt`)
|
|
||||||
- JDBC stats port: 6005
|
|
||||||
- Deployment directory: configured in `build.gradle.kts` as `vst-chip.midi-vst.com`
|
|
||||||
|
|
||||||
## Testing Information
|
|
||||||
|
|
||||||
- **Do not generate tests** for this project. The audio processing code is highly specialized and requires manual testing with audio
|
|
||||||
equipment.
|
|
||||||
- Manual testing should be performed using MIDI controllers and audio monitoring tools.
|
|
||||||
|
|
||||||
## Development Information
|
|
||||||
|
|
||||||
### Project Structure
|
|
||||||
|
|
||||||
- **src/commonMain**: Shared code between JS and JVM
|
|
||||||
- **src/jsMain**: Browser-specific code
|
|
||||||
- **src/jvmMain**: Server-specific code
|
|
||||||
- **audio-worklet**: Audio processing code that runs in a separate thread in the browser
|
|
||||||
|
|
||||||
### Key Components
|
|
||||||
|
|
||||||
1. **JVM Server** (`src/jvmMain/kotlin/nl/astraeus/vst/chip/Main.kt`):
|
|
||||||
- Undertow server that serves the web application
|
|
||||||
- Database initialization
|
|
||||||
- Logging setup
|
|
||||||
|
|
||||||
2. **JS Frontend** (`src/jsMain/kotlin/nl/astraeus/vst/chip/Main.kt`):
|
|
||||||
- UI implementation using Komponent library
|
|
||||||
- MIDI handling
|
|
||||||
- WebSocket client for server communication
|
|
||||||
|
|
||||||
3. **Audio Processor** (`audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/chip/ChipProcessor.kt`):
|
|
||||||
- Real-time audio synthesis
|
|
||||||
- MIDI message handling
|
|
||||||
- Sound generation with multiple waveforms (sine, square, triangle, sawtooth)
|
|
||||||
- Effects processing (FM, AM, ADSR envelope, delay, feedback)
|
|
||||||
|
|
||||||
### Development Workflow
|
|
||||||
|
|
||||||
1. Make changes to the code
|
|
||||||
2. Run `./gradlew buildJS` to build the JS part
|
|
||||||
3. Run the JVM application to test locally
|
|
||||||
4. Use `./gradlew deploy` to deploy to the server
|
|
||||||
|
|
||||||
### Audio Worklet Development
|
|
||||||
|
|
||||||
The audio worklet runs in a separate thread in the browser and handles real-time audio processing. When modifying the audio worklet code:
|
|
||||||
|
|
||||||
1. Understand that it runs in a separate context from the main JS code
|
|
||||||
2. Communication happens via message passing
|
|
||||||
3. Performance is critical - avoid garbage collection and heavy operations in the audio processing loop
|
|
||||||
|
|
||||||
### MIDI Implementation
|
|
||||||
|
|
||||||
The synthesizer responds to standard MIDI messages:
|
|
||||||
|
|
||||||
- Note On/Off (0x90/0x80)
|
|
||||||
- Control Change (0xb0) for various parameters
|
|
||||||
- Program Change (0xc9) for waveform selection
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
The project is configured to deploy to a specific server location. The deployment process:
|
|
||||||
|
|
||||||
1. Builds the project
|
|
||||||
2. Copies web assets
|
|
||||||
3. Creates a symbolic link for the latest version
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration default="false" name="JS" type="JavascriptDebugType" engineId="98ca6316-2f89-46d9-a9e5-fa9e2b0625b3"
|
|
||||||
uri="http://localhost:9005">
|
|
||||||
<method v="2">
|
|
||||||
<option name="Gradle.BeforeRunTask" enabled="false" tasks="buildJS" externalProjectPath="$PROJECT_DIR$" vmOptions=""
|
|
||||||
scriptParameters=""/>
|
|
||||||
</method>
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration default="false" name="Main [jvm]" type="GradleRunConfiguration" factoryName="Gradle">
|
|
||||||
<ExternalSystemSettings>
|
|
||||||
<option name="executionName"/>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$"/>
|
|
||||||
<option name="externalSystemIdString" value="GRADLE"/>
|
|
||||||
<option name="scriptParameters" value="-DmainClass=nl.astraeus.vst.chip.MainKt --quiet"/>
|
|
||||||
<option name="taskDescriptions">
|
|
||||||
<list/>
|
|
||||||
</option>
|
|
||||||
<option name="taskNames">
|
|
||||||
<list>
|
|
||||||
<option value="jvmRun"/>
|
|
||||||
</list>
|
|
||||||
</option>
|
|
||||||
<option name="vmOptions"/>
|
|
||||||
</ExternalSystemSettings>
|
|
||||||
<ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
|
|
||||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
|
||||||
<ExternalSystemDebugDisabled>false</ExternalSystemDebugDisabled>
|
|
||||||
<DebugAllEnabled>false</DebugAllEnabled>
|
|
||||||
<RunAsTest>false</RunAsTest>
|
|
||||||
<GradleProfilingDisabled>false</GradleProfilingDisabled>
|
|
||||||
<GradleCoverageDisabled>false</GradleCoverageDisabled>
|
|
||||||
<method v="2"/>
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
||||||
11
LICENSE.txt
11
LICENSE.txt
@@ -1,11 +0,0 @@
|
|||||||
MIT NonCommercial License
|
|
||||||
|
|
||||||
Copyright (c) 2025 H.Nentjes
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to use, copy, modify, merge, publish, and distribute the Software, subject to the following conditions:
|
|
||||||
|
|
||||||
NonCommercial Use Only. The Software may not be used, in whole or in part, for any commercial purpose. Commercial purpose means using the Software in a way that is intended for or directed toward commercial advantage or monetary compensation.
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
@@ -1,7 +1,3 @@
|
|||||||
@file:OptIn(ExperimentalKotlinGradlePluginApi::class, ExperimentalDistributionDsl::class)
|
|
||||||
|
|
||||||
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
|
|
||||||
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalDistributionDsl
|
|
||||||
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackOutput
|
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackOutput
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
@@ -31,18 +27,22 @@ kotlin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
distribution {
|
distribution {
|
||||||
outputDirectory.set(File("$projectDir/../web2/"))
|
outputDirectory.set(File("$projectDir/../web/"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
jvm()
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
val commonMain by getting {
|
val commonMain by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("nl.astraeus:vst-worklet-base:1.0.1")
|
implementation(project(":common"))
|
||||||
implementation("nl.astraeus:midi-arrays:0.3.6")
|
}
|
||||||
|
}
|
||||||
|
val jsMain by getting {
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":common"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val jsMain by getting
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
package nl.astraeus.vst.midi
|
|
||||||
|
|
||||||
typealias MidiHandler = (Byte, Byte, Byte) -> Unit
|
|
||||||
|
|
||||||
val Byte.channel: Byte
|
|
||||||
get() {
|
|
||||||
return (this.toInt() and 0x0F).toByte()
|
|
||||||
}
|
|
||||||
|
|
||||||
val Byte.command: Byte
|
|
||||||
get() {
|
|
||||||
return ((this.toInt() and 0xF0) shr 4).toByte()
|
|
||||||
}
|
|
||||||
|
|
||||||
val Byte.channelCommand: Boolean
|
|
||||||
get() {
|
|
||||||
return this.command >= 8 && this.command <= 13
|
|
||||||
}
|
|
||||||
|
|
||||||
class MidiMessageHandler(
|
|
||||||
var channel: Byte = -1
|
|
||||||
) {
|
|
||||||
val singleByteHandlers = mutableMapOf<Byte, MidiHandler>()
|
|
||||||
val doubleByteHandlers = mutableMapOf<Byte, MutableMap<Byte, MidiHandler>>()
|
|
||||||
|
|
||||||
fun addHandler(
|
|
||||||
byte1: Byte,
|
|
||||||
byte2: Byte = 0,
|
|
||||||
handler: MidiHandler
|
|
||||||
) {
|
|
||||||
val b1 = if (byte1.channelCommand) {
|
|
||||||
(byte1.toInt() and 0xF0).toByte()
|
|
||||||
} else {
|
|
||||||
byte1
|
|
||||||
}
|
|
||||||
if (byte2 == 0.toByte()) {
|
|
||||||
singleByteHandlers[b1] = handler
|
|
||||||
} else {
|
|
||||||
val map = doubleByteHandlers.getOrPut(b1) {
|
|
||||||
mutableMapOf()
|
|
||||||
}
|
|
||||||
map[byte2] = handler
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addHandler(
|
|
||||||
byte1: Int,
|
|
||||||
byte2: Int = 0,
|
|
||||||
handler: MidiHandler
|
|
||||||
) = addHandler(
|
|
||||||
byte1.toByte(),
|
|
||||||
byte2.toByte(),
|
|
||||||
handler
|
|
||||||
)
|
|
||||||
|
|
||||||
fun handle(
|
|
||||||
byte1: Byte,
|
|
||||||
byte2: Byte,
|
|
||||||
byte3: Byte
|
|
||||||
) {
|
|
||||||
if (
|
|
||||||
channel < 0 ||
|
|
||||||
!byte1.channelCommand ||
|
|
||||||
(byte1.channelCommand && channel == byte1.channel)
|
|
||||||
) {
|
|
||||||
val b1 = if (byte1.channelCommand) {
|
|
||||||
(byte1.toInt() and 0xF0).toByte()
|
|
||||||
} else {
|
|
||||||
byte1
|
|
||||||
}
|
|
||||||
|
|
||||||
if (doubleByteHandlers.containsKey(b1)) {
|
|
||||||
doubleByteHandlers[b1]?.get(byte2)?.invoke(byte1, byte2, byte3)
|
|
||||||
} else if (singleByteHandlers.containsKey(b1)) {
|
|
||||||
singleByteHandlers[b1]?.invoke(byte1, byte2, byte3)
|
|
||||||
} else {
|
|
||||||
println("Unhandled message: $byte1 $byte2 $byte3")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
45
audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/Externals.kt
Normal file
45
audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/Externals.kt
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package nl.astraeus.vst
|
||||||
|
|
||||||
|
import org.khronos.webgl.Float32Array
|
||||||
|
import org.w3c.dom.MessagePort
|
||||||
|
|
||||||
|
enum class AutomationRate(
|
||||||
|
val rate: String
|
||||||
|
) {
|
||||||
|
A_RATE("a-rate"),
|
||||||
|
K_RATE("k-rate")
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AudioParam {
|
||||||
|
var value: Double
|
||||||
|
var automationRate: AutomationRate
|
||||||
|
val defaultValue: Double
|
||||||
|
val minValue: Double
|
||||||
|
val maxValue: Double
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AudioParamMap {
|
||||||
|
operator fun get(name: String): AudioParam
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract external class AudioWorkletProcessor {
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/AudioWorkletNode/parameters) */
|
||||||
|
//val parameters: AudioParamMap;
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/AudioWorkletNode/port) */
|
||||||
|
@JsName("port")
|
||||||
|
val port: MessagePort
|
||||||
|
|
||||||
|
@JsName("process")
|
||||||
|
open fun process(
|
||||||
|
inputs: Array<Array<Float32Array>>,
|
||||||
|
outputs: Array<Array<Float32Array>>,
|
||||||
|
parameters: dynamic
|
||||||
|
): Boolean {
|
||||||
|
definedExternally
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
external fun registerProcessor(name: String, processorCtor: JsClass<*>)
|
||||||
|
external val sampleRate: Int
|
||||||
|
external val currentTime: Double
|
||||||
@@ -2,46 +2,51 @@
|
|||||||
|
|
||||||
package nl.astraeus.vst.chip
|
package nl.astraeus.vst.chip
|
||||||
|
|
||||||
import nl.astraeus.midi.message.SortedTimedMidiMessageList
|
import nl.astraeus.vst.AudioWorkletProcessor
|
||||||
import nl.astraeus.midi.message.TimedMidiMessage
|
import nl.astraeus.vst.Note
|
||||||
import nl.astraeus.tba.SlicedByteArray
|
import nl.astraeus.vst.registerProcessor
|
||||||
import nl.astraeus.vst.*
|
import nl.astraeus.vst.sampleRate
|
||||||
import nl.astraeus.vst.midi.MidiMessageHandler
|
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 = 20
|
val POLYPHONICS = 10
|
||||||
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
|
||||||
noteStart = currentTime
|
attackSamples = 2500
|
||||||
noteRelease = null
|
releaseSamples = 10000
|
||||||
for (i in 0 until combDelayBuffer.length) {
|
|
||||||
combDelayBuffer[i] = 0f
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var noteStart = currentTime
|
var state = NoteState.OFF
|
||||||
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 {
|
||||||
@@ -51,230 +56,152 @@ 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 midiMessageBuffer = SortedTimedMidiMessageList()
|
val notes = Array(POLYPHONICS) {
|
||||||
val midiMessageHandler = MidiMessageHandler()
|
PlayingNote(
|
||||||
val notes = Array<PlayingNote?>(POLYPHONICS) { null }
|
0
|
||||||
|
)
|
||||||
|
}
|
||||||
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.5
|
var fmFreq = 0.0
|
||||||
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:", currentTime)
|
//console.log("VstChipProcessor: Received message", message)
|
||||||
|
|
||||||
val data = message.data
|
val data = message.data
|
||||||
|
|
||||||
try {
|
when (data) {
|
||||||
when (data) {
|
"test_on" -> {
|
||||||
is String -> {
|
playMidi(Int32Array(arrayOf(0x90, 60, 64)))
|
||||||
when {
|
|
||||||
data == "start_recording" -> {
|
}
|
||||||
port.postMessage(recordingBuffer)
|
"test_off" -> {
|
||||||
if (recordingState == RecordingState.STOPPED) {
|
playMidi(Int32Array(arrayOf(0x90, 60, 0)))
|
||||||
recordingState = RecordingState.WAITING_TO_START
|
}
|
||||||
recordingSample = 0
|
is String -> {
|
||||||
}
|
}
|
||||||
|
is ArrayBuffer -> {
|
||||||
|
}
|
||||||
|
is Uint8Array -> {
|
||||||
|
val data32 = Int32Array(data.length)
|
||||||
|
for (i in 0 until data.length) {
|
||||||
|
data32[i] = (data[i].toInt() and 0xff)
|
||||||
|
}
|
||||||
|
playMidi(data32)
|
||||||
|
}
|
||||||
|
is Int32Array -> {
|
||||||
|
playMidi(data)
|
||||||
|
}
|
||||||
|
else ->
|
||||||
|
console.error("Don't kow how to handle message", message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun playMidi(bytes: Int32Array) {
|
||||||
|
if (bytes.length > 0) {
|
||||||
|
//console.log("Received", bytes)
|
||||||
|
when(bytes[0]) {
|
||||||
|
0x90 -> {
|
||||||
|
if (bytes.length == 3) {
|
||||||
|
val note = bytes[1]
|
||||||
|
val velocity = bytes[2]
|
||||||
|
|
||||||
|
if (velocity > 0) {
|
||||||
|
noteOn(note, velocity)
|
||||||
|
} else {
|
||||||
|
noteOff(note)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data.startsWith("set_channel") -> {
|
0x80 -> {
|
||||||
val parts = data.split('\n')
|
if (bytes.length >= 2) {
|
||||||
if (parts.size == 2) {
|
val note = bytes[1]
|
||||||
midiMessageHandler.channel = parts[1].toByte()
|
|
||||||
|
|
||||||
println("Setting channel: ${midiMessageHandler.channel}")
|
noteOff(note)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
0xc9 -> {
|
||||||
|
if (bytes.length >= 1) {
|
||||||
|
val waveform = bytes[1]
|
||||||
|
|
||||||
|
if (waveform < 4) {
|
||||||
|
this.waveform = waveform
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data.startsWith("waveform") -> {
|
0xb0 -> {
|
||||||
val parts = data.split('\n')
|
if (bytes.length == 3) {
|
||||||
if (parts.size == 2) {
|
val knob = bytes[1]
|
||||||
waveform = parts[1].toInt()
|
val value = bytes[2]
|
||||||
println("Setting waveform: $waveform")
|
|
||||||
|
when (knob) {
|
||||||
|
0x4a -> {
|
||||||
|
dutyCycle = value / 127.0
|
||||||
|
}
|
||||||
|
|
||||||
|
0x4b -> {
|
||||||
|
fmFreq = value / 127.0
|
||||||
|
}
|
||||||
|
|
||||||
|
0x4c -> {
|
||||||
|
fmAmp = value / 127.0
|
||||||
|
}
|
||||||
|
|
||||||
|
0x47 -> {
|
||||||
|
amFreq = value / 127.0
|
||||||
|
}
|
||||||
|
|
||||||
|
0x48 -> {
|
||||||
|
amAmp = value / 127.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is ByteArray -> {
|
0xe0 -> {
|
||||||
val message1 = TimedMidiMessage(data)
|
if (bytes.length == 3) {
|
||||||
midiMessageBuffer.add(message1)
|
val lsb = bytes[1]
|
||||||
playBuffer()
|
val msb = bytes[2]
|
||||||
|
|
||||||
|
amFreq = (((msb - 0x40) + (lsb / 127.0)) / 0x40) * 10.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
is Uint8Array -> {
|
|
||||||
val data32 = Int32Array(data.length)
|
|
||||||
for (i in 0 until data.length) {
|
|
||||||
data32[i] = (data[i].toInt() and 0xff)
|
|
||||||
}
|
|
||||||
playMidi(data32)
|
|
||||||
}
|
|
||||||
|
|
||||||
is Int32Array -> {
|
|
||||||
playMidi(data)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
else ->
|
|
||||||
console.error("Don't kow how to handle message", message)
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
console.log(e.message, e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun playBuffer() {
|
|
||||||
while (
|
|
||||||
midiMessageBuffer.isNotEmpty() &&
|
|
||||||
(midiMessageBuffer.nextTimestamp() ?: 0.0) < currentTime
|
|
||||||
) {
|
|
||||||
val midi = midiMessageBuffer.read()
|
|
||||||
playMidi(midi.midi)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun playMidi(bytes: SlicedByteArray) {
|
|
||||||
var index = 0
|
|
||||||
|
|
||||||
while (index < bytes.size && bytes[index].toUByte() > 0u) {
|
|
||||||
val buffer = bytes.getBlob(index, 3)
|
|
||||||
playMidiFromBuffer(buffer)
|
|
||||||
index += 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun playMidiFromBuffer(bytes: SlicedByteArray) {
|
|
||||||
midiMessageHandler.handle(bytes[0], bytes[1], bytes[2])
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun noteOn(note: Int, velocity: Int) {
|
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] == null) {
|
if (notes[i].state == NoteState.OFF) {
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -282,168 +209,79 @@ 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) {
|
if (notes[i].note == note && notes[i].state == NoteState.ON) {
|
||||||
notes[i]?.noteRelease = currentTime
|
notes[i].state = NoteState.RELEASED
|
||||||
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 != null) {
|
if (note.state != NoteState.OFF) {
|
||||||
lowestNote = min(lowestNote, note.note)
|
val sampleDelta = Note.fromMidi(note.note).sampleDelta
|
||||||
}
|
|
||||||
}
|
|
||||||
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 * 1f
|
var targetVolume = note.velocity / 127f
|
||||||
targetVolume *= ADSR.calculate(
|
if (note.state == NoteState.ON && note.sample < note.attackSamples) {
|
||||||
attack,
|
note.attackSamples--
|
||||||
decay,
|
targetVolume *= ( 1f - (note.attackSamples / 2500f))
|
||||||
sustain,
|
} else if (note.state == NoteState.RELEASED) {
|
||||||
release,
|
note.releaseSamples--
|
||||||
note.noteStart,
|
targetVolume *= (note.releaseSamples / 10000f)
|
||||||
currentTime,
|
}
|
||||||
note.noteRelease
|
note.actualVolume += (targetVolume - note.actualVolume) * 0.0001f
|
||||||
).toFloat()
|
|
||||||
note.actualVolume += (targetVolume - note.actualVolume) * 0.01f
|
|
||||||
|
|
||||||
if (note.noteRelease != null && note.actualVolume <= 0.01) {
|
if (note.state == NoteState.RELEASED && note.actualVolume <= 0) {
|
||||||
notes[index] = null
|
note.state = NoteState.OFF
|
||||||
}
|
}
|
||||||
|
|
||||||
var cycleOffset = note.cycleOffset
|
var cycleOffset = note.cycleOffset
|
||||||
val fmMult = sin(currentTime * fmFreq * midiNote.freq * 2f * PI2) * fmAmp
|
val fmModulation = sin(sampleLength * fmFreq * 10f * PI2 * note.sample).toFloat() * fmAmp * 5f
|
||||||
val fmModulation =
|
val amModulation = 1f + (sin(sampleLength * amFreq * 10f * PI2 * note.sample) * amAmp).toFloat()
|
||||||
sampleDelta * fmMult //+ (sin(fmFreq * 1000f * PI2 * (note.sample / sampleRate.toDouble())).toFloat() * (100f * fmAmp * sampleDelta))
|
cycleOffset += fmModulation
|
||||||
|
|
||||||
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 < 0.5) {
|
if (cycleOffset < dutyCycle) { 1f } else { -1f }
|
||||||
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) {
|
if (cycleOffset < 0.5) { 1f } else { -1f }
|
||||||
1f
|
|
||||||
} else {
|
|
||||||
-1f
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
left[i] = left[i] + waveValue * note.actualVolume * volume * amModulation
|
left[i] = left[i] + waveValue * note.actualVolume * 0.3f * amModulation
|
||||||
right[i] = right[i] + waveValue * note.actualVolume * volume * amModulation
|
right[i] = right[i] + waveValue * note.actualVolume * 0.3f * amModulation
|
||||||
|
|
||||||
// comb filter delay
|
note.cycleOffset += sampleDelta
|
||||||
val delaySampleIndex =
|
if (cycleOffset > 1f) {
|
||||||
(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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -451,5 +289,5 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
|||||||
fun main() {
|
fun main() {
|
||||||
registerProcessor("vst-chip-processor", VstChipProcessor::class.js)
|
registerProcessor("vst-chip-processor", VstChipProcessor::class.js)
|
||||||
|
|
||||||
console.log("'vst-chip-processor' registered!", currentTime)
|
println("VstChipProcessor registered!")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,194 +0,0 @@
|
|||||||
package nl.astraeus.vst.chip
|
|
||||||
|
|
||||||
import nl.astraeus.vst.chip.Note.entries
|
|
||||||
import kotlin.math.max
|
|
||||||
import kotlin.math.min
|
|
||||||
import kotlin.math.pow
|
|
||||||
import kotlin.math.round
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User: rnentjes
|
|
||||||
* Date: 14-11-15
|
|
||||||
* Time: 11:50
|
|
||||||
*/
|
|
||||||
|
|
||||||
enum class Note(
|
|
||||||
val sharp: String,
|
|
||||||
val flat: String
|
|
||||||
) {
|
|
||||||
NO01("C--", "C--"),
|
|
||||||
NO02("C#-", "Db-"),
|
|
||||||
NO03("D--", "D--"),
|
|
||||||
NO04("D#-", "Eb-"),
|
|
||||||
NO05("E--", "E--"),
|
|
||||||
NO06("F--", "F--"),
|
|
||||||
NO07("F#-", "Gb-"),
|
|
||||||
NO08("G--", "G--"),
|
|
||||||
NO09("G#-", "Ab-"),
|
|
||||||
NO10("A--", "A--"),
|
|
||||||
NO11("A#-", "Bb-"),
|
|
||||||
NO12("B--", "B--"),
|
|
||||||
C0("C-0", "C-0"),
|
|
||||||
C0s("C#0", "Db0"),
|
|
||||||
D0("D-0", "D-0"),
|
|
||||||
D0s("D#0", "Eb0"),
|
|
||||||
E0("E-0", "E-0"),
|
|
||||||
F0("F-0", "F-0"),
|
|
||||||
F0s("F#0", "Gb0"),
|
|
||||||
G0("G-0", "G-0"),
|
|
||||||
G0s("G#0", "Ab0"),
|
|
||||||
A0("A-0", "A-0"),
|
|
||||||
A0s("A#0", "Bb0"),
|
|
||||||
B0("B-0", "B-0"),
|
|
||||||
C1("C-1", "C-1"),
|
|
||||||
C1s("C#1", "Db1"),
|
|
||||||
D1("D-1", "D-1"),
|
|
||||||
D1s("D#1", "Eb1"),
|
|
||||||
E1("E-1", "E-1"),
|
|
||||||
F1("F-1", "F-1"),
|
|
||||||
F1s("F#1", "Gb1"),
|
|
||||||
G1("G-1", "G-1"),
|
|
||||||
G1s("G#1", "Ab1"),
|
|
||||||
A1("A-1", "A-1"),
|
|
||||||
A1s("A#1", "Bb1"),
|
|
||||||
B1("B-1", "B-1"),
|
|
||||||
C2("C-2", "C-2"),
|
|
||||||
C2s("C#2", "Db2"),
|
|
||||||
D2("D-2", "D-2"),
|
|
||||||
D2s("D#2", "Eb2"),
|
|
||||||
E2("E-2", "E-2"),
|
|
||||||
F2("F-2", "F-2"),
|
|
||||||
F2s("F#2", "Gb2"),
|
|
||||||
G2("G-2", "G-2"),
|
|
||||||
G2s("G#2", "Ab2"),
|
|
||||||
A2("A-2", "A-2"),
|
|
||||||
A2s("A#2", "Bb2"),
|
|
||||||
B2("B-2", "B-2"),
|
|
||||||
C3("C-3", "C-3"),
|
|
||||||
C3s("C#3", "Db3"),
|
|
||||||
D3("D-3", "D-3"),
|
|
||||||
D3s("D#3", "Eb3"),
|
|
||||||
E3("E-3", "E-3"),
|
|
||||||
F3("F-3", "F-3"),
|
|
||||||
F3s("F#3", "Gb3"),
|
|
||||||
G3("G-3", "G-3"),
|
|
||||||
G3s("G#3", "Ab3"),
|
|
||||||
A3("A-3", "A-3"),
|
|
||||||
A3s("A#3", "Bb3"),
|
|
||||||
B3("B-3", "B-3"),
|
|
||||||
C4("C-4", "C-4"),
|
|
||||||
C4s("C#4", "Db4"),
|
|
||||||
D4("D-4", "D-4"),
|
|
||||||
D4s("D#4", "Eb4"),
|
|
||||||
E4("E-4", "E-4"),
|
|
||||||
F4("F-4", "F-4"),
|
|
||||||
F4s("F#4", "Gb4"),
|
|
||||||
G4("G-4", "G-4"),
|
|
||||||
G4s("G#4", "Ab4"),
|
|
||||||
A4("A-4", "A-4"),
|
|
||||||
A4s("A#4", "Bb4"),
|
|
||||||
B4("B-4", "B-4"),
|
|
||||||
C5("C-5", "C-5"),
|
|
||||||
C5s("C#5", "Db5"),
|
|
||||||
D5("D-5", "D-5"),
|
|
||||||
D5s("D#5", "Eb5"),
|
|
||||||
E5("E-5", "E-5"),
|
|
||||||
F5("F-5", "F-5"),
|
|
||||||
F5s("F#5", "Gb5"),
|
|
||||||
G5("G-5", "G-5"),
|
|
||||||
G5s("G#5", "Ab5"),
|
|
||||||
A5("A-5", "A-5"),
|
|
||||||
A5s("A#5", "Bb5"),
|
|
||||||
B5("B-5", "B-5"),
|
|
||||||
C6("C-6", "C-6"),
|
|
||||||
C6s("C#6", "Db6"),
|
|
||||||
D6("D-6", "D-6"),
|
|
||||||
D6s("D#6", "Eb6"),
|
|
||||||
E6("E-6", "E-6"),
|
|
||||||
F6("F-6", "F-6"),
|
|
||||||
F6s("F#6", "Gb6"),
|
|
||||||
G6("G-6", "G-6"),
|
|
||||||
G6s("G#6", "Ab6"),
|
|
||||||
A6("A-6", "A-6"),
|
|
||||||
A6s("A#6", "Bb6"),
|
|
||||||
B6("B-6", "B-6"),
|
|
||||||
C7("C-7", "C-7"),
|
|
||||||
C7s("C#7", "Db7"),
|
|
||||||
D7("D-7", "D-7"),
|
|
||||||
D7s("D#7", "Eb7"),
|
|
||||||
E7("E-7", "E-7"),
|
|
||||||
F7("F-7", "F-7"),
|
|
||||||
F7s("F#7", "Gb7"),
|
|
||||||
G7("G-7", "G-7"),
|
|
||||||
G7s("G#7", "Ab7"),
|
|
||||||
A7("A-7", "A-7"),
|
|
||||||
A7s("A#7", "Bb7"),
|
|
||||||
B7("B-7", "B-7"),
|
|
||||||
C8("C-8", "C-8"),
|
|
||||||
C8s("C#8", "Db8"),
|
|
||||||
D8("D-8", "D-8"),
|
|
||||||
D8s("D#8", "Eb8"),
|
|
||||||
E8("E-8", "E-8"),
|
|
||||||
F8("F-8", "F-8"),
|
|
||||||
F8s("F#8", "Gb8"),
|
|
||||||
G8("G-8", "G-8"),
|
|
||||||
G8s("G#8", "Ab8"),
|
|
||||||
A8("A-8", "A-8"),
|
|
||||||
A8s("A#8", "Bb8"),
|
|
||||||
B8("B-8", "B-8"),
|
|
||||||
C9("C-9", "C-9"),
|
|
||||||
C9s("C#9", "Db9"),
|
|
||||||
D9("D-9", "D-9"),
|
|
||||||
D9s("D#9", "Eb9"),
|
|
||||||
E9("E-9", "E-9"),
|
|
||||||
F9("F-9", "F-9"),
|
|
||||||
F9s("F#9", "Gb9"),
|
|
||||||
G9("G-9", "G-9"),
|
|
||||||
|
|
||||||
// out of midi range
|
|
||||||
//G9s("G#9","Ab9"),
|
|
||||||
//A9("A-9","A-9"),
|
|
||||||
//A9s("A#9","Bb9"),
|
|
||||||
//B9("B-9","B-9"),
|
|
||||||
NONE("---", "---"),
|
|
||||||
UP("^^^", "^^^"),
|
|
||||||
END("XXX", "XXX"),
|
|
||||||
;
|
|
||||||
|
|
||||||
// 69 = A4.ordinal
|
|
||||||
val freq: Double = round(440.0 * 2.0.pow((ordinal - 69) / 12.0)) // * 10000.0) / 10000.0
|
|
||||||
val cycleLength: Double = 1.0 / freq
|
|
||||||
var sampleDelta: Double = 0.0
|
|
||||||
|
|
||||||
fun transpose(semiNotes: Int): Note = if (ordinal >= C0.ordinal && ordinal <= G9.ordinal) {
|
|
||||||
var result = this.ordinal + semiNotes
|
|
||||||
|
|
||||||
result = min(result, G9.ordinal)
|
|
||||||
result = max(result, C0.ordinal)
|
|
||||||
|
|
||||||
fromMidi(result)
|
|
||||||
} else {
|
|
||||||
this
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fromMidi(midi: Int): Note {
|
|
||||||
// todo: add check
|
|
||||||
return entries[midi]
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateSampleRate(rate: Int) {
|
|
||||||
println("Setting sample rate to $rate")
|
|
||||||
for (note in Note.entries) {
|
|
||||||
note.sampleDelta = (1.0 / rate.toDouble()) / note.cycleLength
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// freq = 10Hz
|
|
||||||
// cycleLength = 0.1
|
|
||||||
// sampleRate = 48000
|
|
||||||
// sampleDelta = 4800
|
|
||||||
|
|
||||||
// (1.0 / freq) * sampleRate
|
|
||||||
222
build.gradle.kts
222
build.gradle.kts
@@ -1,182 +1,66 @@
|
|||||||
import org.jetbrains.kotlin.gradle.dsl.JsSourceMapEmbedMode
|
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")
|
||||||
id("maven-publish")
|
kotlin("plugin.serialization")
|
||||||
application
|
id("maven-publish")
|
||||||
|
application
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
js {
|
js {
|
||||||
compilerOptions {
|
compilerOptions {
|
||||||
target.set("es2015")
|
target.set("es2015")
|
||||||
sourceMap.set(true)
|
}
|
||||||
sourceMapEmbedSources.set(JsSourceMapEmbedMode.SOURCE_MAP_SOURCE_CONTENT_ALWAYS)
|
//useEsModules()
|
||||||
|
//useCommonJs()
|
||||||
|
|
||||||
|
binaries.executable()
|
||||||
|
browser {
|
||||||
|
commonWebpackConfig {
|
||||||
|
outputFileName = "vst-chip-worklet-ui.js"
|
||||||
|
sourceMaps = true
|
||||||
|
}
|
||||||
|
|
||||||
|
distribution {
|
||||||
|
outputDirectory.set(File("$projectDir/web/"))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
//useEsModules()
|
jvm{
|
||||||
//useCommonJs()
|
withJava()
|
||||||
|
|
||||||
binaries.executable()
|
|
||||||
browser {
|
|
||||||
commonWebpackConfig {
|
|
||||||
outputFileName = "vst-chip-worklet-ui.js"
|
|
||||||
sourceMaps = true
|
|
||||||
devtool = "inline-source-map"
|
|
||||||
}
|
|
||||||
|
|
||||||
distribution {
|
|
||||||
outputDirectory.set(File("$projectDir/web1"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
@OptIn(ExperimentalWasmDsl::class)
|
|
||||||
wasmJs {
|
|
||||||
binaries.executable()
|
|
||||||
browser{
|
|
||||||
distribution {
|
|
||||||
outputDirectory.set(File("$projectDir/web/"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mavenPublication {
|
|
||||||
groupId = group as String
|
|
||||||
pom { name = "${project.name}-wasm-js" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
jvm {
|
|
||||||
withJava()
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
val commonMain by getting {
|
|
||||||
dependencies {
|
|
||||||
api("nl.astraeus:vst-ui-base:2.2.3")
|
|
||||||
implementation("nl.astraeus:midi-arrays:0.3.6")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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"))
|
|
||||||
}
|
|
||||||
tasks.register<Copy>("buildJSProd") {
|
|
||||||
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
|
||||||
dependsOn("audio-worklet:jsBrowserDistribution")
|
|
||||||
dependsOn("jsBrowserDistribution")
|
|
||||||
|
|
||||||
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)) {
|
sourceSets {
|
||||||
println("Symbolic link already exists: $symlink")
|
val commonMain by getting {
|
||||||
} else {
|
dependencies {
|
||||||
Files.createSymbolicLink(symlink, targetDir)
|
implementation(project(":common"))
|
||||||
println("Symbolic link created: $symlink -> $targetDir")
|
//base
|
||||||
|
api("nl.astraeus:kotlin-css-generator:1.0.7")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val jsMain by getting {
|
||||||
|
dependencies {
|
||||||
|
//base
|
||||||
|
implementation("nl.astraeus:kotlin-komponent-js:1.2.2")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val jsTest by getting {
|
||||||
|
dependencies {
|
||||||
|
implementation(kotlin("test-js"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val jvmMain by getting {
|
||||||
|
dependencies {
|
||||||
|
//base
|
||||||
|
|
||||||
|
implementation("io.undertow:undertow-core:2.3.13.Final")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.11.0")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register<Copy>("copyWeb") {
|
|
||||||
val webDir = layout.projectDirectory.dir("web")
|
|
||||||
val outputDir = file("/home/rnentjes/www/${deployDirectory}/web")
|
|
||||||
|
|
||||||
from(webDir)
|
|
||||||
into(outputDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.named<Task>("build") {
|
|
||||||
dependsOn("generateVersionProperties")
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.named("kotlinUpgradeYarnLock") {
|
|
||||||
mustRunAfter("clean")
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.named("build") {
|
|
||||||
mustRunAfter("kotlinUpgradeYarnLock")
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.named("build") {
|
|
||||||
mustRunAfter("kotlinUpgradeYarnLock")
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.named("copyWeb") {
|
|
||||||
mustRunAfter("build")
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register("removeSymbolicLink") {
|
|
||||||
mustRunAfter("build")
|
|
||||||
doLast {
|
|
||||||
delete(layout.projectDirectory.file("/home/rnentjes/www/${deployDirectory}/${project.name}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register("deploy") {
|
|
||||||
dependsOn("clean")
|
|
||||||
dependsOn("kotlinUpgradeYarnLock")
|
|
||||||
dependsOn("build")
|
|
||||||
dependsOn("copyWeb")
|
|
||||||
dependsOn("removeSymbolicLink")
|
|
||||||
dependsOn("unzipDistribution")
|
|
||||||
dependsOn("createSymbolicLink")
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
group = "nl.astraeus"
|
group = "nl.astraeus"
|
||||||
version = "0.2.0"
|
version = "1.0.0-SNAPSHOT"
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
maven {
|
|
||||||
url = uri("https://gitea.astraeus.nl/api/packages/rnentjes/maven")
|
|
||||||
}
|
|
||||||
maven {
|
|
||||||
url = uri("https://gitea.astraeus.nl:8443/api/packages/rnentjes/maven")
|
|
||||||
}
|
|
||||||
mavenCentral()
|
|
||||||
mavenLocal()
|
mavenLocal()
|
||||||
|
mavenCentral()
|
||||||
|
maven("https://reposilite.astraeus.nl/releases")
|
||||||
|
maven {
|
||||||
|
url = uri("https://nexus.astraeus.nl/nexus/content/groups/public")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
32
common/build.gradle.kts
Normal file
32
common/build.gradle.kts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
@file:OptIn(ExperimentalKotlinGradlePluginApi::class)
|
||||||
|
|
||||||
|
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
apply(from = "../common.gradle.kts")
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
kotlin("multiplatform")
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
js {
|
||||||
|
compilerOptions {
|
||||||
|
target.set("es2015")
|
||||||
|
}
|
||||||
|
browser()
|
||||||
|
}
|
||||||
|
jvm()
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
val commonMain by getting {
|
||||||
|
dependencies {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val jsMain by getting {
|
||||||
|
dependencies {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
common/settings.gradle.kts
Normal file
1
common/settings.gradle.kts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
apply(from = "../settings.common.gradle.kts")
|
||||||
192
common/src/commonMain/kotlin/nl/astraeus/vst/Note.kt
Normal file
192
common/src/commonMain/kotlin/nl/astraeus/vst/Note.kt
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
package nl.astraeus.vst
|
||||||
|
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
import kotlin.math.pow
|
||||||
|
import kotlin.math.round
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User: rnentjes
|
||||||
|
* Date: 14-11-15
|
||||||
|
* Time: 11:50
|
||||||
|
*/
|
||||||
|
|
||||||
|
enum class Note(
|
||||||
|
val sharp: String,
|
||||||
|
val flat: String
|
||||||
|
) {
|
||||||
|
NO01("C--","C--"),
|
||||||
|
NO02("C#-","Db-"),
|
||||||
|
NO03("D--","D--"),
|
||||||
|
NO04("D#-","Eb-"),
|
||||||
|
NO05("E--","E--"),
|
||||||
|
NO06("F--","F--"),
|
||||||
|
NO07("F#-","Gb-"),
|
||||||
|
NO08("G--","G--"),
|
||||||
|
NO09("G#-","Ab-"),
|
||||||
|
NO10("A--","A--"),
|
||||||
|
NO11("A#-","Bb-"),
|
||||||
|
NO12("B--","B--"),
|
||||||
|
C0("C-0","C-0"),
|
||||||
|
C0s("C#0","Db0"),
|
||||||
|
D0("D-0","D-0"),
|
||||||
|
D0s("D#0","Eb0"),
|
||||||
|
E0("E-0","E-0"),
|
||||||
|
F0("F-0","F-0"),
|
||||||
|
F0s("F#0","Gb0"),
|
||||||
|
G0("G-0","G-0"),
|
||||||
|
G0s("G#0","Ab0"),
|
||||||
|
A0("A-0","A-0"),
|
||||||
|
A0s("A#0","Bb0"),
|
||||||
|
B0("B-0","B-0"),
|
||||||
|
C1("C-1","C-1"),
|
||||||
|
C1s("C#1","Db1"),
|
||||||
|
D1("D-1","D-1"),
|
||||||
|
D1s("D#1","Eb1"),
|
||||||
|
E1("E-1","E-1"),
|
||||||
|
F1("F-1","F-1"),
|
||||||
|
F1s("F#1","Gb1"),
|
||||||
|
G1("G-1","G-1"),
|
||||||
|
G1s("G#1","Ab1"),
|
||||||
|
A1("A-1","A-1"),
|
||||||
|
A1s("A#1","Bb1"),
|
||||||
|
B1("B-1","B-1"),
|
||||||
|
C2("C-2","C-2"),
|
||||||
|
C2s("C#2","Db2"),
|
||||||
|
D2("D-2","D-2"),
|
||||||
|
D2s("D#2","Eb2"),
|
||||||
|
E2("E-2","E-2"),
|
||||||
|
F2("F-2","F-2"),
|
||||||
|
F2s("F#2","Gb2"),
|
||||||
|
G2("G-2","G-2"),
|
||||||
|
G2s("G#2","Ab2"),
|
||||||
|
A2("A-2","A-2"),
|
||||||
|
A2s("A#2","Bb2"),
|
||||||
|
B2("B-2","B-2"),
|
||||||
|
C3("C-3","C-3"),
|
||||||
|
C3s("C#3","Db3"),
|
||||||
|
D3("D-3","D-3"),
|
||||||
|
D3s("D#3","Eb3"),
|
||||||
|
E3("E-3","E-3"),
|
||||||
|
F3("F-3","F-3"),
|
||||||
|
F3s("F#3","Gb3"),
|
||||||
|
G3("G-3","G-3"),
|
||||||
|
G3s("G#3","Ab3"),
|
||||||
|
A3("A-3","A-3"),
|
||||||
|
A3s("A#3","Bb3"),
|
||||||
|
B3("B-3","B-3"),
|
||||||
|
C4("C-4","C-4"),
|
||||||
|
C4s("C#4","Db4"),
|
||||||
|
D4("D-4","D-4"),
|
||||||
|
D4s("D#4","Eb4"),
|
||||||
|
E4("E-4","E-4"),
|
||||||
|
F4("F-4","F-4"),
|
||||||
|
F4s("F#4","Gb4"),
|
||||||
|
G4("G-4","G-4"),
|
||||||
|
G4s("G#4","Ab4"),
|
||||||
|
A4("A-4","A-4"),
|
||||||
|
A4s("A#4","Bb4"),
|
||||||
|
B4("B-4","B-4"),
|
||||||
|
C5("C-5","C-5"),
|
||||||
|
C5s("C#5","Db5"),
|
||||||
|
D5("D-5","D-5"),
|
||||||
|
D5s("D#5","Eb5"),
|
||||||
|
E5("E-5","E-5"),
|
||||||
|
F5("F-5","F-5"),
|
||||||
|
F5s("F#5","Gb5"),
|
||||||
|
G5("G-5","G-5"),
|
||||||
|
G5s("G#5","Ab5"),
|
||||||
|
A5("A-5","A-5"),
|
||||||
|
A5s("A#5","Bb5"),
|
||||||
|
B5("B-5","B-5"),
|
||||||
|
C6("C-6","C-6"),
|
||||||
|
C6s("C#6","Db6"),
|
||||||
|
D6("D-6","D-6"),
|
||||||
|
D6s("D#6","Eb6"),
|
||||||
|
E6("E-6","E-6"),
|
||||||
|
F6("F-6","F-6"),
|
||||||
|
F6s("F#6","Gb6"),
|
||||||
|
G6("G-6","G-6"),
|
||||||
|
G6s("G#6","Ab6"),
|
||||||
|
A6("A-6","A-6"),
|
||||||
|
A6s("A#6","Bb6"),
|
||||||
|
B6("B-6","B-6"),
|
||||||
|
C7("C-7","C-7"),
|
||||||
|
C7s("C#7","Db7"),
|
||||||
|
D7("D-7","D-7"),
|
||||||
|
D7s("D#7","Eb7"),
|
||||||
|
E7("E-7","E-7"),
|
||||||
|
F7("F-7","F-7"),
|
||||||
|
F7s("F#7","Gb7"),
|
||||||
|
G7("G-7","G-7"),
|
||||||
|
G7s("G#7","Ab7"),
|
||||||
|
A7("A-7","A-7"),
|
||||||
|
A7s("A#7","Bb7"),
|
||||||
|
B7("B-7","B-7"),
|
||||||
|
C8("C-8","C-8"),
|
||||||
|
C8s("C#8","Db8"),
|
||||||
|
D8("D-8","D-8"),
|
||||||
|
D8s("D#8","Eb8"),
|
||||||
|
E8("E-8","E-8"),
|
||||||
|
F8("F-8","F-8"),
|
||||||
|
F8s("F#8","Gb8"),
|
||||||
|
G8("G-8","G-8"),
|
||||||
|
G8s("G#8","Ab8"),
|
||||||
|
A8("A-8","A-8"),
|
||||||
|
A8s("A#8","Bb8"),
|
||||||
|
B8("B-8","B-8"),
|
||||||
|
C9("C-9","C-9"),
|
||||||
|
C9s("C#9","Db9"),
|
||||||
|
D9("D-9","D-9"),
|
||||||
|
D9s("D#9","Eb9"),
|
||||||
|
E9("E-9","E-9"),
|
||||||
|
F9("F-9","F-9"),
|
||||||
|
F9s("F#9","Gb9"),
|
||||||
|
G9("G-9","G-9"),
|
||||||
|
// out of midi range
|
||||||
|
//G9s("G#9","Ab9"),
|
||||||
|
//A9("A-9","A-9"),
|
||||||
|
//A9s("A#9","Bb9"),
|
||||||
|
//B9("B-9","B-9"),
|
||||||
|
NONE("---", "---"),
|
||||||
|
UP("^^^","^^^"),
|
||||||
|
END("XXX","XXX"),
|
||||||
|
;
|
||||||
|
|
||||||
|
// 69 = A4.ordinal
|
||||||
|
val freq: Double = round(440.0 * 2.0.pow((ordinal - 69)/12.0)) // * 10000.0) / 10000.0
|
||||||
|
val cycleLength: Double = 1.0 / freq
|
||||||
|
var sampleDelta: Double = 0.0
|
||||||
|
|
||||||
|
fun transpose(semiNotes: Int): Note = if (ordinal >= C0.ordinal && ordinal <= G9.ordinal) {
|
||||||
|
var result = this.ordinal + semiNotes
|
||||||
|
|
||||||
|
result = min(result, G9.ordinal)
|
||||||
|
result = max(result, C0.ordinal)
|
||||||
|
|
||||||
|
fromMidi(result)
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromMidi(midi: Int): Note {
|
||||||
|
// todo: add check
|
||||||
|
return entries[midi]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateSampleRate(rate: Int) {
|
||||||
|
println("Setting sample rate to $rate")
|
||||||
|
for (note in Note.entries) {
|
||||||
|
note.sampleDelta = (1.0 / rate.toDouble()) / note.cycleLength
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// freq = 10Hz
|
||||||
|
// cycleLength = 0.1
|
||||||
|
// sampleRate = 48000
|
||||||
|
// sampleDelta = 4800
|
||||||
|
|
||||||
|
// (1.0 / freq) * sampleRate
|
||||||
@@ -1 +0,0 @@
|
|||||||
Data directory for the db
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
pluginManagement {
|
pluginManagement {
|
||||||
plugins {
|
plugins {
|
||||||
kotlin("multiplatform") version "2.3.0"
|
kotlin("multiplatform") version "2.0.0"
|
||||||
|
kotlin("plugin.serialization") version "2.0.0"
|
||||||
|
id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0"
|
||||||
}
|
}
|
||||||
repositories {
|
repositories {
|
||||||
gradlePluginPortal()
|
gradlePluginPortal()
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ apply(from = "settings.common.gradle.kts")
|
|||||||
|
|
||||||
rootProject.name = "vst-chip"
|
rootProject.name = "vst-chip"
|
||||||
|
|
||||||
|
include(":common")
|
||||||
include(":audio-worklet")
|
include(":audio-worklet")
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
package nl.astraeus.vst.chip
|
|
||||||
|
|
||||||
import kotlin.js.ExperimentalJsExport
|
|
||||||
import kotlin.js.JsExport
|
|
||||||
import kotlin.js.JsName
|
|
||||||
|
|
||||||
@ExperimentalJsExport
|
|
||||||
@JsExport
|
|
||||||
data class PatchDTO(
|
|
||||||
@JsName("waveform")
|
|
||||||
val waveform: Int = 0,
|
|
||||||
@JsName("midiId")
|
|
||||||
val midiId: String = "",
|
|
||||||
@JsName("midiName")
|
|
||||||
val midiName: String = "",
|
|
||||||
@JsName("midiChannel")
|
|
||||||
var midiChannel: Int = 0,
|
|
||||||
@JsName("volume")
|
|
||||||
var volume: Double = 0.75,
|
|
||||||
@JsName("dutyCycle")
|
|
||||||
var dutyCycle: Double = 0.5,
|
|
||||||
@JsName("fmModFreq")
|
|
||||||
var fmModFreq: Double = 0.0,
|
|
||||||
@JsName("fmModAmp")
|
|
||||||
var fmModAmp: Double = 0.0,
|
|
||||||
@JsName("amModFreq")
|
|
||||||
var amModFreq: Double = 0.0,
|
|
||||||
@JsName("amModAmp")
|
|
||||||
var amModAmp: Double = 0.0,
|
|
||||||
@JsName("attack")
|
|
||||||
var attack: Double = 0.1,
|
|
||||||
@JsName("decay")
|
|
||||||
var decay: Double = 0.2,
|
|
||||||
@JsName("sustain")
|
|
||||||
var sustain: Double = 0.5,
|
|
||||||
@JsName("release")
|
|
||||||
var release: Double = 0.2,
|
|
||||||
@JsName("delay")
|
|
||||||
var delay: Double = 0.0,
|
|
||||||
@JsName("delayDepth")
|
|
||||||
var delayDepth: Double = 0.0,
|
|
||||||
@JsName("feedback")
|
|
||||||
var feedback: Double = 0.0,
|
|
||||||
)
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
package nl.astraeus.vst.chip.logger
|
|
||||||
|
|
||||||
val log = Logger
|
|
||||||
|
|
||||||
enum class LogLevel {
|
|
||||||
TRACE,
|
|
||||||
DEBUG,
|
|
||||||
INFO,
|
|
||||||
WARN,
|
|
||||||
ERROR,
|
|
||||||
FATAL
|
|
||||||
}
|
|
||||||
|
|
||||||
object Logger {
|
|
||||||
var level: LogLevel = LogLevel.INFO
|
|
||||||
|
|
||||||
fun trace(message: () -> String?) {
|
|
||||||
if (level.ordinal <= LogLevel.TRACE.ordinal) {
|
|
||||||
println("TRACE: ${message()}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun debug(message: () -> String?) {
|
|
||||||
if (level.ordinal <= LogLevel.DEBUG.ordinal) {
|
|
||||||
println("DEBUG: ${message()}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun info(message: () -> String?) {
|
|
||||||
if (level.ordinal <= LogLevel.INFO.ordinal) {
|
|
||||||
println("INFO: ${message()}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun warn(e: Throwable? = null, message: () -> String?) {
|
|
||||||
if (level.ordinal <= LogLevel.WARN.ordinal) {
|
|
||||||
println("WARN: ${message()}")
|
|
||||||
e?.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun error(e: Throwable? = null, message: () -> String?) {
|
|
||||||
if (level.ordinal <= LogLevel.ERROR.ordinal) {
|
|
||||||
println("ERROR: ${message()}")
|
|
||||||
e?.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fatal(e: Throwable, message: () -> String?) {
|
|
||||||
println("FATAL: ${message()}")
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +1,29 @@
|
|||||||
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.komp.UnsafeMode
|
import nl.astraeus.vst.chip.midi.Broadcaster
|
||||||
import nl.astraeus.vst.chip.logger.log
|
import nl.astraeus.vst.chip.midi.MidiMessage
|
||||||
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 nl.astraeus.vst.chip.ws.WebsocketClient
|
import org.khronos.webgl.Uint8Array
|
||||||
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.unsafeMode = UnsafeMode.UNSAFE_SVG_ONLY
|
Komponent.create(document.body!!, MainView)
|
||||||
Komponent.create(document.body!!, Views.mainView)
|
|
||||||
|
|
||||||
Midi.start()
|
Midi.start()
|
||||||
|
|
||||||
WebsocketClient.connect {
|
console.log("Performance", window.performance)
|
||||||
log.debug { "Connected to server" }
|
Broadcaster.getChannel(0).postMessage(
|
||||||
}
|
MidiMessage(
|
||||||
|
Uint8Array(arrayOf(0x80.toByte(), 60, 60)),
|
||||||
|
window.performance.now()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
window.setInterval({
|
||||||
|
Broadcaster.sync()
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
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 = js("new AudioContext()")
|
val audioContext: dynamic = AudioContext()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
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
|
||||||
@@ -54,27 +53,8 @@ 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
|
||||||
@@ -100,7 +80,6 @@ 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,239 +1,14 @@
|
|||||||
@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,10 +1,8 @@
|
|||||||
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
|
||||||
|
|
||||||
@@ -39,6 +37,7 @@ 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>()
|
||||||
@@ -68,7 +67,7 @@ object Midi {
|
|||||||
outputs.add(output)
|
outputs.add(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
Views.mainView.requestUpdate()
|
MainView.requestUpdate()
|
||||||
},
|
},
|
||||||
{ e ->
|
{ e ->
|
||||||
println("Failed to get MIDI access - $e")
|
println("Failed to get MIDI access - $e")
|
||||||
@@ -76,39 +75,6 @@ object Midi {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setInput(id: String, name: String = "") {
|
|
||||||
var selected = inputs.find { it.id == id }
|
|
||||||
if (selected == null) {
|
|
||||||
var maxMatchChar = 0
|
|
||||||
inputs.forEach {
|
|
||||||
val matchChars = matchChars(it.name, name)
|
|
||||||
if (matchChars > maxMatchChar) {
|
|
||||||
selected = it
|
|
||||||
maxMatchChar = matchChars
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setInput(selected)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun matchChars(str1: String, str2: String): Int {
|
|
||||||
var result = 0
|
|
||||||
if (str1.length > str2.length) {
|
|
||||||
for (ch in str1.toCharArray()) {
|
|
||||||
if (str2.contains(ch)) {
|
|
||||||
result++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (ch in str2.toCharArray()) {
|
|
||||||
if (str1.contains(ch)) {
|
|
||||||
result++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setInput(input: MIDIInput?) {
|
fun setInput(input: MIDIInput?) {
|
||||||
console.log("Setting input", input)
|
console.log("Setting input", input)
|
||||||
currentInput?.close()
|
currentInput?.close()
|
||||||
@@ -126,14 +92,9 @@ object Midi {
|
|||||||
hex.append(data[index].toString(16))
|
hex.append(data[index].toString(16))
|
||||||
hex.append(" ")
|
hex.append(" ")
|
||||||
}
|
}
|
||||||
console.log("Midi message:", hex, message)
|
console.log("Midi message:", hex)
|
||||||
val midiData = ByteArray(message.data.length) { data[it].toByte() }
|
|
||||||
val timeMessage = TimedMidiMessage(
|
|
||||||
AudioContextHandler.audioContext.currentTime,
|
|
||||||
*midiData
|
|
||||||
)
|
|
||||||
VstChipWorklet.postMessage(
|
VstChipWorklet.postMessage(
|
||||||
timeMessage.data.buffer.toByteArray()
|
message.data
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +110,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,88 +1,47 @@
|
|||||||
@file:OptIn(ExperimentalJsExport::class)
|
|
||||||
|
|
||||||
package nl.astraeus.vst.chip.view
|
package nl.astraeus.vst.chip.view
|
||||||
|
|
||||||
import kotlinx.browser.document
|
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.*
|
import kotlinx.html.InputType
|
||||||
|
import kotlinx.html.div
|
||||||
|
import kotlinx.html.h1
|
||||||
|
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 nl.astraeus.css.properties.*
|
import kotlinx.html.select
|
||||||
import nl.astraeus.css.style.Style
|
import kotlinx.html.span
|
||||||
|
import nl.astraeus.css.properties.BoxSizing
|
||||||
|
import nl.astraeus.css.properties.FontWeight
|
||||||
|
import nl.astraeus.css.properties.Position
|
||||||
|
import nl.astraeus.css.properties.Transform
|
||||||
|
import nl.astraeus.css.properties.em
|
||||||
|
import nl.astraeus.css.properties.hsla
|
||||||
|
import nl.astraeus.css.properties.prc
|
||||||
|
import nl.astraeus.css.properties.px
|
||||||
|
import nl.astraeus.css.properties.rem
|
||||||
|
import nl.astraeus.css.properties.vh
|
||||||
|
import nl.astraeus.css.properties.vw
|
||||||
import nl.astraeus.css.style.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 nl.astraeus.vst.chip.ws.WebsocketClient
|
import org.khronos.webgl.Uint8Array
|
||||||
import nl.astraeus.vst.ui.components.ExpKnobComponent
|
import org.w3c.dom.HTMLInputElement
|
||||||
import nl.astraeus.vst.ui.components.KnobComponent
|
import org.w3c.dom.HTMLSelectElement
|
||||||
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.*
|
|
||||||
|
|
||||||
object WaveformView : Komponent() {
|
object MainView : Komponent() {
|
||||||
|
|
||||||
init {
|
|
||||||
window.requestAnimationFrame(::onAnimationFrame)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onAnimationFrame(time: Double) {
|
|
||||||
if (Views.mainView.started) {
|
|
||||||
VstChipWorklet.postMessage("start_recording")
|
|
||||||
}
|
|
||||||
|
|
||||||
window.requestAnimationFrame(::onAnimationFrame)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun HtmlBuilder.render() {
|
|
||||||
div {
|
|
||||||
if (VstChipWorklet.recording != null) {
|
|
||||||
canvas {
|
|
||||||
width = "1000"
|
|
||||||
height = "400"
|
|
||||||
val ctx = (currentElement() as? HTMLCanvasElement)?.getContext("2d") as? CanvasRenderingContext2D
|
|
||||||
val data = VstChipWorklet.recording
|
|
||||||
if (ctx != null && data != null) {
|
|
||||||
val width = ctx.canvas.width.toDouble()
|
|
||||||
val height = ctx.canvas.height.toDouble()
|
|
||||||
val halfHeight = height / 2.0
|
|
||||||
|
|
||||||
ctx.lineWidth = 2.0
|
|
||||||
ctx.clearRect(0.0, 0.0, width, height)
|
|
||||||
val step = 1000.0 / data.length
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.strokeStyle = "rgba(0, 255, 255, 0.5)"
|
|
||||||
ctx.moveTo(0.0, halfHeight)
|
|
||||||
for (i in 0 until data.length) {
|
|
||||||
ctx.lineTo(i * step, halfHeight - data[i] * halfHeight)
|
|
||||||
}
|
|
||||||
ctx.stroke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MainView : Komponent() {
|
|
||||||
private var messages: MutableList<String> = ArrayList()
|
private var messages: MutableList<String> = ArrayList()
|
||||||
var started = false
|
private var started = false
|
||||||
var firstRender = true
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
css()
|
MainViewCss
|
||||||
window.addEventListener("resize", { requestUpdate() }, Any())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addMessage(message: String) {
|
fun addMessage(message: String) {
|
||||||
@@ -93,352 +52,156 @@ class MainView : Komponent() {
|
|||||||
requestUpdate()
|
requestUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun renderUpdate() {
|
|
||||||
println("Rendering MainView")
|
|
||||||
super.renderUpdate()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun HtmlBuilder.render() {
|
override fun HtmlBuilder.render() {
|
||||||
div(MainDivCss.name) {
|
div(MainViewCss.MainDivCss.name) {
|
||||||
id = "fixed-container"
|
if (!started) {
|
||||||
style = scaleContainer(currentElement(), 1920, 1080)
|
div(MainViewCss.StartSplashCss.name) {
|
||||||
|
div(MainViewCss.StartBoxCss.name) {
|
||||||
div(InnerDivCss.name) {
|
div(MainViewCss.StartButtonCss.name) {
|
||||||
if (!started) {
|
+"START"
|
||||||
div(StartSplashCss.name) {
|
onClickFunction = {
|
||||||
div(StartBoxCss.name) {
|
|
||||||
div(StartButtonCss.name) {
|
|
||||||
id = "start-button"
|
|
||||||
+"START"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onClickFunction = {
|
|
||||||
document.getElementById("start-button")?.classList?.add(HiddenCss.name)
|
|
||||||
VstChipWorklet.create {
|
|
||||||
started = true
|
started = true
|
||||||
requestUpdate()
|
VstChipWorklet.create {
|
||||||
WebsocketClient.send("LOAD\n")
|
requestUpdate()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
h1 {
|
}
|
||||||
+"VST Chip"
|
h1 {
|
||||||
}
|
+"VST Chip"
|
||||||
div {
|
}
|
||||||
span {
|
div {
|
||||||
+"Midi input: "
|
span {
|
||||||
select {
|
+"Midi input: "
|
||||||
|
select {
|
||||||
|
option {
|
||||||
|
+"None"
|
||||||
|
value = ""
|
||||||
|
}
|
||||||
|
option {
|
||||||
|
+"Midi over Broadcast"
|
||||||
|
value = "midi-broadcast"
|
||||||
|
}
|
||||||
|
for (mi in Midi.inputs) {
|
||||||
option {
|
option {
|
||||||
+"None"
|
+mi.name
|
||||||
value = "none"
|
value = mi.id
|
||||||
}
|
|
||||||
for (mi in Midi.inputs) {
|
|
||||||
option {
|
|
||||||
+mi.name
|
|
||||||
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 == "none") {
|
if (target.value == "") {
|
||||||
Midi.setInput(null)
|
Midi.setInput(null)
|
||||||
} else {
|
} else {
|
||||||
Midi.setInput(target.value)
|
val selected = Midi.inputs.find { it.id == target.value }
|
||||||
|
if (selected != null) {
|
||||||
|
Midi.setInput(selected)
|
||||||
|
} else if (target.value == "midi-broadcast") {
|
||||||
|
//
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
span {
|
}
|
||||||
+"channel:"
|
span {
|
||||||
input {
|
+"channel:"
|
||||||
type = InputType.number
|
input {
|
||||||
value = VstChipWorklet.midiChannel.toString()
|
type = InputType.number
|
||||||
onInputFunction = { event ->
|
value = Midi.inputChannel.toString()
|
||||||
val target = event.target as HTMLInputElement
|
onChangeFunction = { event ->
|
||||||
println("onInput channel: $target")
|
val target = event.target as HTMLInputElement
|
||||||
VstChipWorklet.midiChannel = target.value.toInt()
|
Midi.inputChannel = target.value.toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
span {
|
||||||
|
+"Midi output: "
|
||||||
|
select {
|
||||||
|
option {
|
||||||
|
+"None"
|
||||||
|
value = ""
|
||||||
|
}
|
||||||
|
option {
|
||||||
|
+"Midi over Broadcast"
|
||||||
|
value = "midi-broadcast"
|
||||||
|
}
|
||||||
|
for (mi in Midi.outputs) {
|
||||||
|
option {
|
||||||
|
+mi.name
|
||||||
|
value = mi.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeFunction = { event ->
|
||||||
|
val target = event.target as HTMLSelectElement
|
||||||
|
if (target.value == "") {
|
||||||
|
Midi.setOutput(null)
|
||||||
|
} else {
|
||||||
|
val selected = Midi.outputs.find { it.id == target.value }
|
||||||
|
if (selected != null) {
|
||||||
|
Midi.setOutput(selected)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div {
|
span {
|
||||||
span(ButtonBarCss.name) {
|
+"channel:"
|
||||||
+"SAVE"
|
input {
|
||||||
onClickFunction = {
|
type = InputType.number
|
||||||
val patch = VstChipWorklet.save().copy(
|
value = Midi.outputChannel.toString()
|
||||||
midiId = Midi.currentInput?.id ?: "",
|
onChangeFunction = { event ->
|
||||||
midiName = Midi.currentInput?.name ?: ""
|
val target = event.target as HTMLInputElement
|
||||||
)
|
Midi.outputChannel = target.value.toInt()
|
||||||
|
|
||||||
WebsocketClient.send("SAVE\n${JSON.stringify(patch)}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
span(ButtonBarCss.name) {
|
|
||||||
+"STOP"
|
|
||||||
onClickFunction = {
|
|
||||||
VstChipWorklet.postDirectlyToWorklet(
|
|
||||||
TimedMidiMessage(getCurrentTime(), (0xb0 + midiChannel).toByte(), 123, 0)
|
|
||||||
.data.buffer.data
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div {
|
}
|
||||||
span(ButtonBarCss.name) {
|
div(MainViewCss.ButtonCss.name) {
|
||||||
+"Sine"
|
+"Send note on to output"
|
||||||
if (VstChipWorklet.waveform == 0) {
|
onClickFunction = {
|
||||||
classes += SelectedCss.name
|
val data = Uint8Array(
|
||||||
}
|
arrayOf(
|
||||||
onClickFunction = {
|
0x90.toByte(),
|
||||||
VstChipWorklet.waveform = 0
|
0x3c.toByte(),
|
||||||
requestUpdate()
|
0x70.toByte()
|
||||||
}
|
)
|
||||||
}
|
)
|
||||||
span(ButtonBarCss.name) {
|
Midi.send(data, window.performance.now() + 1000)
|
||||||
+"Square"
|
Midi.send(data, window.performance.now() + 2000)
|
||||||
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(ControlsCss.name) {
|
}
|
||||||
include(
|
div(MainViewCss.ButtonCss.name) {
|
||||||
ExpKnobComponent(
|
+"Send note off to output"
|
||||||
value = VstChipWorklet.volume,
|
onClickFunction = {
|
||||||
label = "Volume",
|
val data = Uint8Array(
|
||||||
minValue = 0.0,
|
arrayOf(
|
||||||
maxValue = 1.0,
|
0x90.toByte(),
|
||||||
step = 5.0 / 127.0,
|
0x3c.toByte(),
|
||||||
width = 100,
|
0x0.toByte(),
|
||||||
height = 120,
|
)
|
||||||
) { value ->
|
|
||||||
VstChipWorklet.volume = value
|
|
||||||
}
|
|
||||||
)
|
|
||||||
include(
|
|
||||||
KnobComponent(
|
|
||||||
value = VstChipWorklet.dutyCycle,
|
|
||||||
label = "Duty cycle",
|
|
||||||
minValue = 0.0,
|
|
||||||
maxValue = 1.0,
|
|
||||||
step = 2.0 / 127.0,
|
|
||||||
width = 100,
|
|
||||||
height = 120,
|
|
||||||
) { value ->
|
|
||||||
VstChipWorklet.dutyCycle = value
|
|
||||||
}
|
|
||||||
)
|
|
||||||
include(
|
|
||||||
ExpKnobComponent(
|
|
||||||
value = VstChipWorklet.fmModFreq,
|
|
||||||
label = "FM Freq",
|
|
||||||
minValue = 0.0,
|
|
||||||
maxValue = 2.0,
|
|
||||||
step = 5.0 / 127.0,
|
|
||||||
width = 100,
|
|
||||||
height = 120,
|
|
||||||
) { value ->
|
|
||||||
VstChipWorklet.fmModFreq = value
|
|
||||||
}
|
|
||||||
)
|
|
||||||
include(
|
|
||||||
ExpKnobComponent(
|
|
||||||
value = VstChipWorklet.fmModAmp,
|
|
||||||
label = "FM Ampl",
|
|
||||||
minValue = 0.0,
|
|
||||||
maxValue = 1.0,
|
|
||||||
step = 5.0 / 127.0,
|
|
||||||
width = 100,
|
|
||||||
height = 120,
|
|
||||||
) { value ->
|
|
||||||
VstChipWorklet.fmModAmp = value
|
|
||||||
}
|
|
||||||
)
|
|
||||||
include(
|
|
||||||
ExpKnobComponent(
|
|
||||||
value = VstChipWorklet.amModFreq,
|
|
||||||
label = "AM Freq",
|
|
||||||
minValue = 0.0,
|
|
||||||
maxValue = 1.0,
|
|
||||||
step = 5.0 / 127.0,
|
|
||||||
width = 100,
|
|
||||||
height = 120,
|
|
||||||
) { value ->
|
|
||||||
VstChipWorklet.amModFreq = value
|
|
||||||
}
|
|
||||||
)
|
|
||||||
include(
|
|
||||||
ExpKnobComponent(
|
|
||||||
value = VstChipWorklet.amModAmp,
|
|
||||||
label = "AM Ampl",
|
|
||||||
minValue = 0.0,
|
|
||||||
maxValue = 1.0,
|
|
||||||
step = 5.0 / 127.0,
|
|
||||||
width = 100,
|
|
||||||
height = 120,
|
|
||||||
) { value ->
|
|
||||||
VstChipWorklet.amModAmp = value
|
|
||||||
}
|
|
||||||
)
|
|
||||||
include(
|
|
||||||
ExpKnobComponent(
|
|
||||||
value = VstChipWorklet.feedback,
|
|
||||||
label = "Feedback",
|
|
||||||
minValue = 0.0,
|
|
||||||
maxValue = 1.0,
|
|
||||||
step = 5.0 / 127.0,
|
|
||||||
width = 100,
|
|
||||||
height = 120,
|
|
||||||
) { value ->
|
|
||||||
VstChipWorklet.feedback = value
|
|
||||||
}
|
|
||||||
)
|
|
||||||
include(
|
|
||||||
ExpKnobComponent(
|
|
||||||
value = VstChipWorklet.delay,
|
|
||||||
label = "Delay",
|
|
||||||
minValue = 0.0,
|
|
||||||
maxValue = 1.0,
|
|
||||||
step = 5.0 / 127.0,
|
|
||||||
width = 100,
|
|
||||||
height = 120,
|
|
||||||
) { value ->
|
|
||||||
VstChipWorklet.delay = value
|
|
||||||
}
|
|
||||||
)
|
|
||||||
include(
|
|
||||||
ExpKnobComponent(
|
|
||||||
value = VstChipWorklet.delayDepth,
|
|
||||||
label = "Delay depth",
|
|
||||||
minValue = 0.0,
|
|
||||||
maxValue = 1.0,
|
|
||||||
step = 5.0 / 127.0,
|
|
||||||
width = 100,
|
|
||||||
height = 120,
|
|
||||||
) { value ->
|
|
||||||
VstChipWorklet.delayDepth = value
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
Midi.send(data)
|
||||||
}
|
}
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun scaleContainer(
|
object MainViewCss : CssId("main") {
|
||||||
container: Element,
|
|
||||||
containerWidth: Int,
|
|
||||||
containerHeight: Int,
|
|
||||||
): String {
|
|
||||||
val vpWidth = window.innerWidth
|
|
||||||
val vpHeight = window.innerHeight
|
|
||||||
|
|
||||||
val scaleX: Double = vpWidth / containerWidth.toDouble()
|
|
||||||
val scaleY: Double = vpHeight / containerHeight.toDouble()
|
|
||||||
|
|
||||||
val scale = if (scaleX < scaleY) scaleX else scaleY
|
|
||||||
val left = (vpWidth - containerWidth * scale) / 2
|
|
||||||
val top = 0 // (vpHeight - containerHeight * scale) / 2
|
|
||||||
|
|
||||||
return "transform: translate(${left}px, ${top}px) scale($scale);"
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object MainViewCss : CssName() {
|
|
||||||
object MainDivCss : CssName()
|
object MainDivCss : CssName()
|
||||||
object InnerDivCss : 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 HiddenCss : CssName()
|
|
||||||
object ControlsCss : CssName()
|
|
||||||
|
|
||||||
private fun css() {
|
init {
|
||||||
defineCss {
|
defineCss {
|
||||||
select("*") {
|
select("*") {
|
||||||
select("*:before") {
|
select("*:before") {
|
||||||
@@ -450,12 +213,10 @@ class MainView : Komponent() {
|
|||||||
select("html", "body") {
|
select("html", "body") {
|
||||||
margin(0.px)
|
margin(0.px)
|
||||||
padding(0.px)
|
padding(0.px)
|
||||||
width(100.prc)
|
|
||||||
height(100.prc)
|
height(100.prc)
|
||||||
overflow(Overflow.hidden)
|
|
||||||
}
|
}
|
||||||
select("html", "body") {
|
select("html", "body") {
|
||||||
backgroundColor(Css.currentStyle.mainBackgroundColor.darken(10))
|
backgroundColor(Css.currentStyle.mainBackgroundColor)
|
||||||
color(Css.currentStyle.mainFontColor)
|
color(Css.currentStyle.mainFontColor)
|
||||||
|
|
||||||
fontFamily("JetbrainsMono, monospace")
|
fontFamily("JetbrainsMono, monospace")
|
||||||
@@ -465,18 +226,15 @@ class 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)
|
||||||
commonButton()
|
padding(1.rem)
|
||||||
}
|
backgroundColor(Css.currentStyle.buttonBackgroundColor)
|
||||||
select(cls(ButtonBarCss)) {
|
color(Css.currentStyle.mainFontColor)
|
||||||
margin(1.rem, 0.px)
|
|
||||||
commonButton()
|
hover {
|
||||||
|
backgroundColor(Css.currentStyle.buttonBackgroundColor.hover())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
select(cls(ActiveCss)) {
|
select(cls(ActiveCss)) {
|
||||||
//backgroundColor(Css.currentStyle.selectedBackgroundColor)
|
//backgroundColor(Css.currentStyle.selectedBackgroundColor)
|
||||||
@@ -485,17 +243,7 @@ class MainView : Komponent() {
|
|||||||
minHeight(4.rem)
|
minHeight(4.rem)
|
||||||
}
|
}
|
||||||
select(cls(MainDivCss)) {
|
select(cls(MainDivCss)) {
|
||||||
backgroundColor(Css.currentStyle.mainBackgroundColor)
|
margin(1.rem)
|
||||||
width(1920.px)
|
|
||||||
height(1080.px)
|
|
||||||
position(Position.absolute)
|
|
||||||
top(0.px)
|
|
||||||
left(0.px)
|
|
||||||
transformOrigin("0 0")
|
|
||||||
|
|
||||||
select(cls(InnerDivCss)) {
|
|
||||||
padding(1.rem)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
select("select") {
|
select("select") {
|
||||||
plain("appearance", "none")
|
plain("appearance", "none")
|
||||||
@@ -506,27 +254,25 @@ class 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.mainFontColor)
|
//color(Css.currentStyle.entryFontColor)
|
||||||
borderRadius(0.25.em)
|
borderRadius(0.25.em)
|
||||||
}
|
}
|
||||||
select(cls(StartSplashCss)) {
|
select(cls(StartSplashCss)) {
|
||||||
position(Position.fixed)
|
position(Position.fixed)
|
||||||
left(0.px)
|
left(0.px)
|
||||||
top(0.px)
|
top(0.px)
|
||||||
width(1920.px)
|
width(100.vw)
|
||||||
height(1080.px)
|
height(100.vh)
|
||||||
zIndex(100)
|
zIndex(100)
|
||||||
backgroundColor(hsla(32, 0, 5, 0.65))
|
backgroundColor(hsla(32, 0, 50, 0.6))
|
||||||
|
|
||||||
select(cls(StartBoxCss)) {
|
select(cls(StartBoxCss)) {
|
||||||
position(Position.relative)
|
position(Position.relative)
|
||||||
left(100.px)
|
left(25.vw)
|
||||||
top(100.px)
|
top(25.vh)
|
||||||
width(1720.px)
|
width(50.vw)
|
||||||
height(880.px)
|
height(50.vh)
|
||||||
backgroundColor(hsla(239, 50, 10, 1.0))
|
backgroundColor(hsla(0, 0, 50, 0.25))
|
||||||
borderColor(Css.currentStyle.mainFontColor)
|
|
||||||
borderWidth(2.px)
|
|
||||||
|
|
||||||
select(cls(StartButtonCss)) {
|
select(cls(StartButtonCss)) {
|
||||||
position(Position.absolute)
|
position(Position.absolute)
|
||||||
@@ -539,34 +285,6 @@ class MainView : Komponent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
select(ControlsCss.cls()) {
|
|
||||||
display(Display.flex)
|
|
||||||
flexDirection(FlexDirection.row)
|
|
||||||
justifyContent(JustifyContent.flexStart)
|
|
||||||
alignItems(AlignItems.center)
|
|
||||||
margin(1.rem)
|
|
||||||
padding(1.rem)
|
|
||||||
backgroundColor(Css.currentStyle.mainBackgroundColor)
|
|
||||||
}
|
|
||||||
select(HiddenCss.cls()) {
|
|
||||||
display(Display.none)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
115
src/jsMain/kotlin/nl/astraeus/vst/chip/view/css/Css.kt
Normal file
115
src/jsMain/kotlin/nl/astraeus/vst/chip/view/css/Css.kt
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package daw.style
|
||||||
|
|
||||||
|
import kotlinx.browser.document
|
||||||
|
import nl.astraeus.css.properties.*
|
||||||
|
import nl.astraeus.css.style
|
||||||
|
import nl.astraeus.css.style.ConditionalStyle
|
||||||
|
import nl.astraeus.css.style.Style
|
||||||
|
|
||||||
|
class StyleDefinition(
|
||||||
|
val mainFontColor: Color = hsla(178, 70, 55, 1.0),
|
||||||
|
val mainBackgroundColor: Color = hsl(239, 50, 10),
|
||||||
|
//val entryFontColor: Color = hsl(Css.mainFontColorNumber, 70, 55),
|
||||||
|
val inputBackgroundColor : Color = mainBackgroundColor.lighten(15),
|
||||||
|
val buttonBackgroundColor : Color = mainBackgroundColor.lighten(15),
|
||||||
|
val buttonBorderColor : Color = mainFontColor.changeAlpha(0.25),
|
||||||
|
val buttonBorderWidth : Measurement = 2.px,
|
||||||
|
)
|
||||||
|
|
||||||
|
object NoTextSelectCls : CssName("no-text-select")
|
||||||
|
object SelectedCls : CssName("selected")
|
||||||
|
object ActiveCls : CssName("active")
|
||||||
|
|
||||||
|
fun Color.hover(): Color = if (Css.currentStyle == Css.darkStyle) {
|
||||||
|
this.lighten(15)
|
||||||
|
} else {
|
||||||
|
this.darken(15)
|
||||||
|
}
|
||||||
|
|
||||||
|
object Css {
|
||||||
|
var minified = false
|
||||||
|
var dynamicStyles = mutableMapOf<CssId, ConditionalStyle.() -> Unit>()
|
||||||
|
|
||||||
|
fun CssId.defineCss(conditionalStyle: ConditionalStyle.() -> Unit) {
|
||||||
|
check(!dynamicStyles.containsKey(this)) {
|
||||||
|
"CssId with name ${this.name} already defined!"
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCss(conditionalStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CssId.updateCss(conditionalStyle: ConditionalStyle.() -> Unit) {
|
||||||
|
val elementId = this.description()
|
||||||
|
var dynamicStyleElement = document.getElementById(elementId)
|
||||||
|
|
||||||
|
dynamicStyles[this] = conditionalStyle
|
||||||
|
|
||||||
|
if (dynamicStyleElement == null) {
|
||||||
|
dynamicStyleElement = document.createElement("style")
|
||||||
|
dynamicStyleElement.id = elementId
|
||||||
|
|
||||||
|
document.head?.append(dynamicStyleElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
val css = style(conditionalStyle)
|
||||||
|
|
||||||
|
dynamicStyleElement.innerHTML = css.generateCss(minified = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
val darkStyle = StyleDefinition(
|
||||||
|
)
|
||||||
|
|
||||||
|
val lightStyle = StyleDefinition(
|
||||||
|
mainBackgroundColor = hsl(239+180, 50, 15),
|
||||||
|
)
|
||||||
|
|
||||||
|
var currentStyle: StyleDefinition = darkStyle
|
||||||
|
|
||||||
|
fun updateStyle() {
|
||||||
|
for ((cssId, dynStyle) in dynamicStyles) {
|
||||||
|
cssId.apply {
|
||||||
|
updateCss(dynStyle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun switchLayout() {
|
||||||
|
currentStyle = if (currentStyle == darkStyle) {
|
||||||
|
lightStyle
|
||||||
|
} else {
|
||||||
|
darkStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStyle()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Style.transition() {
|
||||||
|
transition("all 0.5s ease")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Style.noTextSelect() {
|
||||||
|
plain("-webkit-touch-callout", "none")
|
||||||
|
plain("-webkit-user-select", "none")
|
||||||
|
plain("-moz-user-select", "none")
|
||||||
|
plain("-ms-user-select", "none")
|
||||||
|
|
||||||
|
userSelect(UserSelect.none)
|
||||||
|
|
||||||
|
select("::selection") {
|
||||||
|
background("none")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object GenericCss : CssId("generic") {
|
||||||
|
init {
|
||||||
|
fun generateStyle(): String {
|
||||||
|
val css = style {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return css.generateCss(minified = minified)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
65
src/jsMain/kotlin/nl/astraeus/vst/chip/view/css/CssName.kt
Normal file
65
src/jsMain/kotlin/nl/astraeus/vst/chip/view/css/CssName.kt
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package daw.style
|
||||||
|
|
||||||
|
import nl.astraeus.css.style.DescriptionProvider
|
||||||
|
import nl.astraeus.css.style.cls
|
||||||
|
|
||||||
|
private val CAPITAL_LETTER = Regex("[A-Z]")
|
||||||
|
|
||||||
|
fun String.hyphenize(): String =
|
||||||
|
replace(CAPITAL_LETTER) {
|
||||||
|
"-${it.value.lowercase()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val shortId = false
|
||||||
|
private var nextCssId = 1
|
||||||
|
|
||||||
|
private fun nextShortId(): String {
|
||||||
|
var id = nextCssId++
|
||||||
|
val result = StringBuilder()
|
||||||
|
|
||||||
|
while(id > 0) {
|
||||||
|
val ch = ((id % 26) + 'a'.code).toChar()
|
||||||
|
result.append(ch)
|
||||||
|
|
||||||
|
id /= 26
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
open class CssName(name: String? = null) : DescriptionProvider {
|
||||||
|
val name: String = if (shortId) {
|
||||||
|
nextShortId()
|
||||||
|
} else if (name != null) {
|
||||||
|
"daw-$name"
|
||||||
|
} else {
|
||||||
|
"daw${this::class.simpleName?.hyphenize() ?: this::class}"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun description() = name
|
||||||
|
}
|
||||||
|
|
||||||
|
fun CssName.cls() : DescriptionProvider = cls(this)
|
||||||
|
|
||||||
|
open class CssId(name: String) : DescriptionProvider {
|
||||||
|
val name: String = if (shortId) {
|
||||||
|
nextShortId()
|
||||||
|
} else {
|
||||||
|
"daw-$name-css"
|
||||||
|
}
|
||||||
|
override fun description() = name
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is CssId) return false
|
||||||
|
|
||||||
|
if (name != other.name) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return name.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
@file:OptIn(ExperimentalJsExport::class)
|
|
||||||
|
|
||||||
package nl.astraeus.vst.chip.ws
|
|
||||||
|
|
||||||
import kotlinx.browser.window
|
|
||||||
import nl.astraeus.vst.chip.PatchDTO
|
|
||||||
import nl.astraeus.vst.chip.Views
|
|
||||||
import nl.astraeus.vst.chip.audio.VstChipWorklet
|
|
||||||
import nl.astraeus.vst.chip.midi.Midi
|
|
||||||
import org.w3c.dom.MessageEvent
|
|
||||||
import org.w3c.dom.WebSocket
|
|
||||||
import org.w3c.dom.events.Event
|
|
||||||
|
|
||||||
object WebsocketClient {
|
|
||||||
var websocket: WebSocket? = null
|
|
||||||
var interval: Int = 0
|
|
||||||
|
|
||||||
fun connect(onConnect: () -> Unit) {
|
|
||||||
close()
|
|
||||||
|
|
||||||
websocket = if (window.location.hostname.contains("localhost") || window.location.hostname.contains("192.168")) {
|
|
||||||
WebSocket("ws://${window.location.hostname}:${window.location.port}/ws")
|
|
||||||
} else {
|
|
||||||
WebSocket("wss://${window.location.hostname}/ws")
|
|
||||||
}
|
|
||||||
|
|
||||||
websocket?.also { ws ->
|
|
||||||
ws.onopen = {
|
|
||||||
onOpen(ws, it)
|
|
||||||
onConnect()
|
|
||||||
}
|
|
||||||
ws.onmessage = { onMessage(ws, it) }
|
|
||||||
ws.onclose = { onClose(ws, it) }
|
|
||||||
ws.onerror = { onError(ws, it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun close() {
|
|
||||||
websocket?.close(-1, "Application closed socket.")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onOpen(
|
|
||||||
ws: WebSocket,
|
|
||||||
event: Event
|
|
||||||
) {
|
|
||||||
interval = window.setInterval({
|
|
||||||
val actualWs = websocket
|
|
||||||
|
|
||||||
if (actualWs == null) {
|
|
||||||
window.clearInterval(interval)
|
|
||||||
|
|
||||||
console.log("Connection to the server was lost!\\nPlease try again later.")
|
|
||||||
reconnect()
|
|
||||||
}
|
|
||||||
}, 10000)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reconnect() {
|
|
||||||
val actualWs = websocket
|
|
||||||
|
|
||||||
if (actualWs != null) {
|
|
||||||
if (actualWs.readyState == WebSocket.OPEN) {
|
|
||||||
console.log("Connection to the server was lost!\\nPlease try again later.")
|
|
||||||
} else {
|
|
||||||
window.setTimeout({
|
|
||||||
reconnect()
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
connect {}
|
|
||||||
|
|
||||||
window.setTimeout({
|
|
||||||
reconnect()
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onMessage(
|
|
||||||
ws: WebSocket,
|
|
||||||
event: Event
|
|
||||||
) {
|
|
||||||
if (event is MessageEvent) {
|
|
||||||
val data = event.data
|
|
||||||
|
|
||||||
if (data is String) {
|
|
||||||
console.log("Received message: $data")
|
|
||||||
if (data.startsWith("LOAD")) {
|
|
||||||
val patchJson = data.substring(5)
|
|
||||||
val patch = JSON.parse<PatchDTO>(patchJson)
|
|
||||||
|
|
||||||
Midi.setInput(patch.midiId, patch.midiName)
|
|
||||||
VstChipWorklet.load(patch)
|
|
||||||
Views.mainView.requestUpdate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onClose(
|
|
||||||
ws: WebSocket,
|
|
||||||
event: Event
|
|
||||||
): dynamic {
|
|
||||||
websocket = null
|
|
||||||
|
|
||||||
return "dynamic"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onError(
|
|
||||||
ws: WebSocket,
|
|
||||||
event: Event
|
|
||||||
): dynamic {
|
|
||||||
console.log("Error websocket!", ws, event)
|
|
||||||
|
|
||||||
websocket = null
|
|
||||||
|
|
||||||
return "dynamic"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun send(message: String) {
|
|
||||||
websocket?.send(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
2
src/jvmMain/kotlin/nl/astraeus/vst/chip/Index.kt
Normal file
2
src/jvmMain/kotlin/nl/astraeus/vst/chip/Index.kt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
package nl.astraeus.vst.chip
|
||||||
|
|
||||||
@@ -1,25 +1,20 @@
|
|||||||
package nl.astraeus.vst.chip
|
package nl.astraeus.vst.chip
|
||||||
|
|
||||||
import nl.astraeus.vst.base.Settings
|
import io.undertow.Undertow
|
||||||
import nl.astraeus.vst.base.db.Database
|
import io.undertow.UndertowOptions
|
||||||
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
Settings.port = 9005
|
val server = Undertow.builder()
|
||||||
Settings.jdbcStatsPort = 6005
|
.addHttpListener(Settings.port, "localhost")
|
||||||
|
.setIoThreads(4)
|
||||||
|
.setHandler(RequestHandler)
|
||||||
|
.setServerOption(UndertowOptions.SHUTDOWN_TIMEOUT, 1000)
|
||||||
|
.build()
|
||||||
|
|
||||||
Database.start()
|
println("Starting server at port ${Settings.port}...")
|
||||||
|
server?.start()
|
||||||
UndertowServer.start(
|
|
||||||
"Vst Chip",
|
|
||||||
"/vst-chip-worklet-ui.js"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
15
src/jvmMain/kotlin/nl/astraeus/vst/chip/RequestHandler.kt
Normal file
15
src/jvmMain/kotlin/nl/astraeus/vst/chip/RequestHandler.kt
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package nl.astraeus.vst.chip
|
||||||
|
|
||||||
|
import io.undertow.server.HttpHandler
|
||||||
|
import io.undertow.server.HttpServerExchange
|
||||||
|
import io.undertow.server.handlers.resource.PathResourceManager
|
||||||
|
import io.undertow.server.handlers.resource.ResourceHandler
|
||||||
|
import java.nio.file.Paths
|
||||||
|
|
||||||
|
object RequestHandler : HttpHandler {
|
||||||
|
val resourceHandler = ResourceHandler(PathResourceManager(Paths.get("web")))
|
||||||
|
|
||||||
|
override fun handleRequest(exchange: HttpServerExchange) {
|
||||||
|
resourceHandler.handleRequest(exchange)
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/jvmMain/kotlin/nl/astraeus/vst/chip/Settings.kt
Normal file
50
src/jvmMain/kotlin/nl/astraeus/vst/chip/Settings.kt
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package nl.astraeus.vst.chip
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
object Settings {
|
||||||
|
var runningAsRoot: Boolean = false
|
||||||
|
var port = 9000
|
||||||
|
var sslPort = 8443
|
||||||
|
var connectionTimeout = 30000
|
||||||
|
|
||||||
|
var jdbcDriver = "nl.astraeus.jdbc.Driver"
|
||||||
|
var jdbcConnectionUrl = "jdbc:stat:webServerPort=6001:jdbc:sqlite:data/srp.db"
|
||||||
|
var jdbcUser = "sa"
|
||||||
|
var jdbcPassword = ""
|
||||||
|
|
||||||
|
var adminUser = "rnentjes"
|
||||||
|
var adminPassword = "9/SG_Bd}9gWz~?j\\A.U]n9]OO"
|
||||||
|
|
||||||
|
fun getPropertiesFromFile(filename: String): Properties? {
|
||||||
|
val propertiesFile = File(filename)
|
||||||
|
return if (propertiesFile.exists()) {
|
||||||
|
val properties = Properties()
|
||||||
|
FileInputStream(propertiesFile).use {
|
||||||
|
properties.load(it)
|
||||||
|
}
|
||||||
|
properties
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readProperties(args: Array<String>) {
|
||||||
|
val filename = if (args.isNotEmpty()) args[0] else "srp.properties"
|
||||||
|
val properties = getPropertiesFromFile(filename) ?: return // return if properties couldn't be loaded
|
||||||
|
|
||||||
|
runningAsRoot = properties.getProperty("runningAsRoot", runningAsRoot.toString()).toBoolean()
|
||||||
|
port = properties.getProperty("port", port.toString()).toInt()
|
||||||
|
sslPort = properties.getProperty("sslPort", sslPort.toString()).toInt()
|
||||||
|
connectionTimeout = properties.getProperty("connectionTimeout", connectionTimeout.toString()).toInt()
|
||||||
|
jdbcDriver = properties.getProperty("jdbcDriver", jdbcDriver)
|
||||||
|
jdbcConnectionUrl = properties.getProperty("jdbcConnectionUrl", jdbcConnectionUrl)
|
||||||
|
jdbcUser = properties.getProperty("jdbcUser", jdbcUser)
|
||||||
|
jdbcPassword = properties.getProperty("jdbcPassword", jdbcPassword)
|
||||||
|
|
||||||
|
adminUser = properties.getProperty("adminUser", adminUser)
|
||||||
|
adminPassword = properties.getProperty("adminPassword", adminPassword)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import java.util.*
|
|
||||||
|
|
||||||
tasks.register("generateVersionProperties") {
|
|
||||||
doLast {
|
|
||||||
val versionDir = layout.buildDirectory.dir("processedResources/jvm/main")
|
|
||||||
val versionFile = versionDir.get().file("version.properties").asFile
|
|
||||||
versionDir.get().asFile.mkdirs()
|
|
||||||
|
|
||||||
val properties = Properties().apply {
|
|
||||||
setProperty("group", project.group.toString())
|
|
||||||
setProperty("name", project.name.toString())
|
|
||||||
setProperty("version", project.version.toString())
|
|
||||||
setProperty("buildTime", Date().toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
versionFile.writer().use { writer ->
|
|
||||||
properties.store(writer, "Version information")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user