Compare commits
16 Commits
0281d2751f
...
import-pro
| Author | SHA1 | Date | |
|---|---|---|---|
| 8accd60b46 | |||
| e977b2c88a | |||
| 573fc921bb | |||
| 37a42dd88c | |||
| 76866eb392 | |||
| 1eed613b2a | |||
| 8df6a4fff6 | |||
| f2269c8865 | |||
| 6554fd746a | |||
| 976328ed69 | |||
| 194857d687 | |||
| f22a800c93 | |||
| ccc7e9a4e9 | |||
| b02c7733b0 | |||
| 0cfd6f31d5 | |||
| 05764ec588 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -41,7 +41,7 @@ bin/
|
|||||||
### Mac OS ###
|
### Mac OS ###
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
web
|
/web
|
||||||
|
|
||||||
.kotlin
|
.kotlin
|
||||||
.idea
|
.idea
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
<component name="ArtifactManager">
|
<component name="ArtifactManager">
|
||||||
<artifact type="jar" name="audio-worklet-js-1.0.0-SNAPSHOT">
|
<artifact type="jar" name="audio-worklet-js-1.0.0-SNAPSHOT">
|
||||||
<output-path>$PROJECT_DIR$/audio-worklet/build/libs</output-path>
|
<output-path>$PROJECT_DIR$/audio-worklet/build/libs</output-path>
|
||||||
<root id="archive" name="audio-worklet-js-1.0.0-SNAPSHOT.jar">
|
<root id="archive" name="audio-worklet-js-1.0.0-SNAPSHOT.jar" />
|
||||||
<element id="module-output" name="vst-chip.audio-worklet.jsMain" />
|
|
||||||
</root>
|
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</component>
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
<component name="ArtifactManager">
|
<component name="ArtifactManager">
|
||||||
<artifact type="jar" name="audio-worklet-jvm-1.0.0-SNAPSHOT">
|
<artifact type="jar" name="audio-worklet-jvm-1.0.0-SNAPSHOT">
|
||||||
<output-path>$PROJECT_DIR$/audio-worklet/build/libs</output-path>
|
<output-path>$PROJECT_DIR$/audio-worklet/build/libs</output-path>
|
||||||
<root id="archive" name="audio-worklet-jvm-1.0.0-SNAPSHOT.jar">
|
<root id="archive" name="audio-worklet-jvm-1.0.0-SNAPSHOT.jar" />
|
||||||
<element id="module-output" name="vst-chip.audio-worklet.jvmMain" />
|
|
||||||
</root>
|
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</component>
|
||||||
4
.idea/artifacts/common_js_1_0_0_SNAPSHOT.xml
generated
4
.idea/artifacts/common_js_1_0_0_SNAPSHOT.xml
generated
@@ -1,8 +1,6 @@
|
|||||||
<component name="ArtifactManager">
|
<component name="ArtifactManager">
|
||||||
<artifact type="jar" name="common-js-1.0.0-SNAPSHOT">
|
<artifact type="jar" name="common-js-1.0.0-SNAPSHOT">
|
||||||
<output-path>$PROJECT_DIR$/common/build/libs</output-path>
|
<output-path>$PROJECT_DIR$/common/build/libs</output-path>
|
||||||
<root id="archive" name="common-js-1.0.0-SNAPSHOT.jar">
|
<root id="archive" name="common-js-1.0.0-SNAPSHOT.jar" />
|
||||||
<element id="module-output" name="vst-chip.common.jsMain" />
|
|
||||||
</root>
|
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</component>
|
||||||
4
.idea/artifacts/common_jvm_1_0_0_SNAPSHOT.xml
generated
4
.idea/artifacts/common_jvm_1_0_0_SNAPSHOT.xml
generated
@@ -1,8 +1,6 @@
|
|||||||
<component name="ArtifactManager">
|
<component name="ArtifactManager">
|
||||||
<artifact type="jar" name="common-jvm-1.0.0-SNAPSHOT">
|
<artifact type="jar" name="common-jvm-1.0.0-SNAPSHOT">
|
||||||
<output-path>$PROJECT_DIR$/common/build/libs</output-path>
|
<output-path>$PROJECT_DIR$/common/build/libs</output-path>
|
||||||
<root id="archive" name="common-jvm-1.0.0-SNAPSHOT.jar">
|
<root id="archive" name="common-jvm-1.0.0-SNAPSHOT.jar" />
|
||||||
<element id="module-output" name="vst-chip.common.jvmMain" />
|
|
||||||
</root>
|
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</component>
|
||||||
7
.idea/jsLibraryMappings.xml
generated
7
.idea/jsLibraryMappings.xml
generated
@@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="JavaScriptLibraryMappings">
|
|
||||||
<excludedPredefinedLibrary name="vst-chip/build/js/node_modules" />
|
|
||||||
<excludedPredefinedLibrary name="vst-chip/build/js/packages/vst-base-test/node_modules" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<option name="executionName"/>
|
<option name="executionName"/>
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$"/>
|
<option name="externalProjectPath" value="$PROJECT_DIR$"/>
|
||||||
<option name="externalSystemIdString" value="GRADLE"/>
|
<option name="externalSystemIdString" value="GRADLE"/>
|
||||||
<option name="scriptParameters" value="-DmainClass=nl.astraeus.vst.chip.MainKt --quiet"/>
|
<option name="scriptParameters" value="-DmainClass=nl.astraeus.vst.string.MainKt --quiet"/>
|
||||||
<option name="taskDescriptions">
|
<option name="taskDescriptions">
|
||||||
<list/>
|
<list/>
|
||||||
</option>
|
</option>
|
||||||
|
|||||||
11
LICENSE.txt
Normal file
11
LICENSE.txt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
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,3 +1,6 @@
|
|||||||
|
@file:OptIn(ExperimentalDistributionDsl::class)
|
||||||
|
|
||||||
|
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 {
|
||||||
@@ -17,13 +20,13 @@ kotlin {
|
|||||||
|
|
||||||
browser {
|
browser {
|
||||||
commonWebpackConfig {
|
commonWebpackConfig {
|
||||||
outputFileName = "vst-chip-worklet.js"
|
outputFileName = "vst-string-worklet.js"
|
||||||
sourceMaps = true
|
sourceMaps = true
|
||||||
}
|
}
|
||||||
|
|
||||||
webpackTask {
|
webpackTask {
|
||||||
output.libraryTarget = KotlinWebpackOutput.Target.VAR
|
output.libraryTarget = KotlinWebpackOutput.Target.VAR
|
||||||
output.library = "vstChipWorklet"
|
output.library = "vstStringWorklet"
|
||||||
}
|
}
|
||||||
|
|
||||||
distribution {
|
distribution {
|
||||||
@@ -38,7 +41,7 @@ kotlin {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":common"))
|
implementation(project(":common"))
|
||||||
|
|
||||||
implementation("nl.astraeus:vst-worklet-base:1.0.0-SNAPSHOT")
|
implementation("nl.astraeus:vst-worklet-base:1.0.1")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val jsMain by getting {
|
val jsMain by getting {
|
||||||
|
|||||||
@@ -1,311 +0,0 @@
|
|||||||
@file:OptIn(ExperimentalJsExport::class)
|
|
||||||
|
|
||||||
package nl.astraeus.vst.chip
|
|
||||||
|
|
||||||
import nl.astraeus.vst.AudioWorkletProcessor
|
|
||||||
import nl.astraeus.vst.Note
|
|
||||||
import nl.astraeus.vst.registerProcessor
|
|
||||||
import nl.astraeus.vst.sampleRate
|
|
||||||
import org.khronos.webgl.ArrayBuffer
|
|
||||||
import org.khronos.webgl.Float32Array
|
|
||||||
import org.khronos.webgl.Int32Array
|
|
||||||
import org.khronos.webgl.Uint8Array
|
|
||||||
import org.khronos.webgl.get
|
|
||||||
import org.khronos.webgl.set
|
|
||||||
import org.w3c.dom.MessageEvent
|
|
||||||
import kotlin.math.PI
|
|
||||||
import kotlin.math.sin
|
|
||||||
|
|
||||||
val POLYPHONICS = 10
|
|
||||||
val PI2 = PI * 2
|
|
||||||
|
|
||||||
@ExperimentalJsExport
|
|
||||||
@JsExport
|
|
||||||
enum class NoteState {
|
|
||||||
ON,
|
|
||||||
RELEASED,
|
|
||||||
OFF
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExperimentalJsExport
|
|
||||||
@JsExport
|
|
||||||
class PlayingNote(
|
|
||||||
val note: Int,
|
|
||||||
var velocity: Int = 0
|
|
||||||
) {
|
|
||||||
fun retrigger(velocity: Int) {
|
|
||||||
this.velocity = velocity
|
|
||||||
state = NoteState.ON
|
|
||||||
sample = 0
|
|
||||||
attackSamples = 2500
|
|
||||||
releaseSamples = 10000
|
|
||||||
}
|
|
||||||
|
|
||||||
var state = NoteState.OFF
|
|
||||||
var cycleOffset = 0.0
|
|
||||||
var sample = 0
|
|
||||||
var attackSamples = 2500
|
|
||||||
var releaseSamples = 10000
|
|
||||||
var actualVolume = 0f
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class Waveform {
|
|
||||||
SINE,
|
|
||||||
SQUARE,
|
|
||||||
TRIANGLE,
|
|
||||||
SAWTOOTH
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExperimentalJsExport
|
|
||||||
@JsExport
|
|
||||||
class VstChipProcessor : AudioWorkletProcessor() {
|
|
||||||
var midiChannel = 0
|
|
||||||
val notes = Array(POLYPHONICS) {
|
|
||||||
PlayingNote(
|
|
||||||
0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
var waveform = Waveform.SINE.ordinal
|
|
||||||
var dutyCycle = 0.5
|
|
||||||
var fmFreq = 0.0
|
|
||||||
var fmAmp = 0.0
|
|
||||||
var amFreq = 0.0
|
|
||||||
var amAmp = 0.0
|
|
||||||
val sampleLength = 1 / sampleRate.toDouble()
|
|
||||||
|
|
||||||
init {
|
|
||||||
this.port.onmessage = ::handleMessage
|
|
||||||
Note.updateSampleRate(sampleRate)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleMessage(message: MessageEvent) {
|
|
||||||
console.log("VstChipProcessor: Received message:", message.data)
|
|
||||||
|
|
||||||
val data = message.data
|
|
||||||
|
|
||||||
try {
|
|
||||||
when (data) {
|
|
||||||
is String -> {
|
|
||||||
if (data.startsWith("set_channel")) {
|
|
||||||
val parts = data.split('\n')
|
|
||||||
if (parts.size == 2) {
|
|
||||||
midiChannel = parts[1].toInt()
|
|
||||||
println("Setting channel: $midiChannel")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 playMidi(bytes: Int32Array) {
|
|
||||||
console.log("playMidi", bytes)
|
|
||||||
if (bytes.length > 0) {
|
|
||||||
var cmdByte = bytes[0]
|
|
||||||
val channelCmd = ((cmdByte shr 4) and 0xf) in 0x8 .. 0xe
|
|
||||||
val channel = cmdByte and 0xf
|
|
||||||
println("Channel cmd: $channelCmd")
|
|
||||||
if (channelCmd && channel != midiChannel) {
|
|
||||||
console.log("Wrong channel", midiChannel, bytes)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cmdByte = cmdByte and 0xf0
|
|
||||||
|
|
||||||
//console.log("Received", bytes)
|
|
||||||
when(cmdByte) {
|
|
||||||
0x90 -> {
|
|
||||||
if (bytes.length == 3) {
|
|
||||||
val note = bytes[1]
|
|
||||||
val velocity = bytes[2]
|
|
||||||
|
|
||||||
if (velocity > 0) {
|
|
||||||
noteOn(note, velocity)
|
|
||||||
} else {
|
|
||||||
noteOff(note)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
0x80 -> {
|
|
||||||
if (bytes.length >= 2) {
|
|
||||||
val note = bytes[1]
|
|
||||||
|
|
||||||
noteOff(note)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
0xc9 -> {
|
|
||||||
if (bytes.length >= 1) {
|
|
||||||
val waveform = bytes[1]
|
|
||||||
|
|
||||||
if (waveform < 4) {
|
|
||||||
this.waveform = waveform
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
0xb0 -> {
|
|
||||||
if (bytes.length == 3) {
|
|
||||||
val knob = bytes[1]
|
|
||||||
val value = bytes[2]
|
|
||||||
|
|
||||||
when (knob) {
|
|
||||||
0x4a -> {
|
|
||||||
dutyCycle = value / 127.0
|
|
||||||
}
|
|
||||||
|
|
||||||
0x4b -> {
|
|
||||||
fmFreq = value / 127.0
|
|
||||||
}
|
|
||||||
|
|
||||||
0x4c -> {
|
|
||||||
fmAmp = value / 127.0
|
|
||||||
}
|
|
||||||
|
|
||||||
0x47 -> {
|
|
||||||
amFreq = value / 127.0
|
|
||||||
}
|
|
||||||
|
|
||||||
0x48 -> {
|
|
||||||
amAmp = value / 127.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
0xe0 -> {
|
|
||||||
if (bytes.length == 3) {
|
|
||||||
val lsb = bytes[1]
|
|
||||||
val msb = bytes[2]
|
|
||||||
|
|
||||||
amFreq = (((msb - 0x40) + (lsb / 127.0)) / 0x40) * 10.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun noteOn(note: Int, velocity: Int) {
|
|
||||||
for (i in 0 until POLYPHONICS) {
|
|
||||||
if (notes[i].note == note) {
|
|
||||||
notes[i].retrigger(velocity)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (i in 0 until POLYPHONICS) {
|
|
||||||
if (notes[i].state == NoteState.OFF) {
|
|
||||||
notes[i] = PlayingNote(
|
|
||||||
note,
|
|
||||||
velocity
|
|
||||||
)
|
|
||||||
notes[i].state = NoteState.ON
|
|
||||||
|
|
||||||
val n = Note.fromMidi(note)
|
|
||||||
//console.log("Playing note: ${n.sharp} (${n.freq})")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun noteOff(note: Int) {
|
|
||||||
for (i in 0 until POLYPHONICS) {
|
|
||||||
if (notes[i].note == note && notes[i].state == NoteState.ON) {
|
|
||||||
notes[i].state = NoteState.RELEASED
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun process (
|
|
||||||
inputs: Array<Array<Float32Array>>,
|
|
||||||
outputs: Array<Array<Float32Array>>,
|
|
||||||
parameters: dynamic
|
|
||||||
) : Boolean {
|
|
||||||
val samples = outputs[0][0].length
|
|
||||||
|
|
||||||
val left = outputs[0][0]
|
|
||||||
val right = outputs[0][1]
|
|
||||||
|
|
||||||
for (note in notes) {
|
|
||||||
if (note.state != NoteState.OFF) {
|
|
||||||
val sampleDelta = Note.fromMidi(note.note).sampleDelta
|
|
||||||
|
|
||||||
for (i in 0 until samples) {
|
|
||||||
var targetVolume = note.velocity / 127f
|
|
||||||
if (note.state == NoteState.ON && note.sample < note.attackSamples) {
|
|
||||||
note.attackSamples--
|
|
||||||
targetVolume *= ( 1f - (note.attackSamples / 2500f))
|
|
||||||
} else if (note.state == NoteState.RELEASED) {
|
|
||||||
note.releaseSamples--
|
|
||||||
targetVolume *= (note.releaseSamples / 10000f)
|
|
||||||
}
|
|
||||||
note.actualVolume += (targetVolume - note.actualVolume) * 0.0001f
|
|
||||||
|
|
||||||
if (note.state == NoteState.RELEASED && note.actualVolume <= 0) {
|
|
||||||
note.state = NoteState.OFF
|
|
||||||
}
|
|
||||||
|
|
||||||
var cycleOffset = note.cycleOffset
|
|
||||||
val fmModulation = sin(sampleLength * fmFreq * 10f * PI2 * note.sample).toFloat() * fmAmp * 5f
|
|
||||||
val amModulation = 1f + (sin(sampleLength * amFreq * 10f * PI2 * note.sample) * amAmp).toFloat()
|
|
||||||
cycleOffset += fmModulation
|
|
||||||
|
|
||||||
val waveValue: Float = when (waveform) {
|
|
||||||
0 -> {
|
|
||||||
sin(cycleOffset * PI2).toFloat()
|
|
||||||
}
|
|
||||||
1 -> {
|
|
||||||
if (cycleOffset < dutyCycle) { 1f } else { -1f }
|
|
||||||
}
|
|
||||||
2 -> when {
|
|
||||||
cycleOffset < 0.25 -> 4 * cycleOffset
|
|
||||||
cycleOffset < 0.75 -> 2 - 4 * cycleOffset
|
|
||||||
else -> 4 * cycleOffset - 4
|
|
||||||
}.toFloat()
|
|
||||||
3 -> {
|
|
||||||
((cycleOffset * 2f) - 1f).toFloat()
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
if (cycleOffset < 0.5) { 1f } else { -1f }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
left[i] = left[i] + waveValue * note.actualVolume * 0.3f * amModulation
|
|
||||||
right[i] = right[i] + waveValue * note.actualVolume * 0.3f * amModulation
|
|
||||||
|
|
||||||
note.cycleOffset += sampleDelta
|
|
||||||
if (cycleOffset > 1f) {
|
|
||||||
note.cycleOffset -= 1f
|
|
||||||
}
|
|
||||||
note.sample++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun main() {
|
|
||||||
registerProcessor("vst-chip-processor", VstChipProcessor::class.js)
|
|
||||||
|
|
||||||
println("VstChipProcessor registered!")
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
@file:OptIn(ExperimentalJsExport::class)
|
||||||
|
|
||||||
|
package nl.astraeus.vst.string
|
||||||
|
|
||||||
|
import nl.astraeus.vst.AudioWorkletProcessor
|
||||||
|
import nl.astraeus.vst.Note
|
||||||
|
import nl.astraeus.vst.currentTime
|
||||||
|
import nl.astraeus.vst.registerProcessor
|
||||||
|
import nl.astraeus.vst.sampleRate
|
||||||
|
import org.khronos.webgl.Float32Array
|
||||||
|
import org.khronos.webgl.Int32Array
|
||||||
|
import org.khronos.webgl.Uint8Array
|
||||||
|
import org.khronos.webgl.get
|
||||||
|
import org.khronos.webgl.set
|
||||||
|
import org.w3c.dom.MessageEvent
|
||||||
|
import kotlin.math.PI
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
val POLYPHONICS = 10
|
||||||
|
val PI2 = PI * 2
|
||||||
|
|
||||||
|
@ExperimentalJsExport
|
||||||
|
@JsExport
|
||||||
|
class PlayingNote(
|
||||||
|
val note: Int,
|
||||||
|
var velocity: Int = 0
|
||||||
|
) {
|
||||||
|
fun retrigger(velocity: Int) {
|
||||||
|
this.velocity = velocity
|
||||||
|
sample = 0
|
||||||
|
noteStart = currentTime
|
||||||
|
noteRelease = null
|
||||||
|
}
|
||||||
|
|
||||||
|
var noteStart = currentTime
|
||||||
|
var noteRelease: Double? = null
|
||||||
|
var cycleOffset = 0.0
|
||||||
|
var sample = 0
|
||||||
|
var actualVolume = 0f
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalJsExport
|
||||||
|
@JsExport
|
||||||
|
enum class RecordingState {
|
||||||
|
STOPPED,
|
||||||
|
WAITING_TO_START,
|
||||||
|
RECORDING
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalJsExport
|
||||||
|
@JsExport
|
||||||
|
class VstStringProcessor : AudioWorkletProcessor() {
|
||||||
|
var midiChannel = 0
|
||||||
|
|
||||||
|
var volume = 0.75f
|
||||||
|
var damping = 0.996
|
||||||
|
|
||||||
|
val recordingBuffer = Float32Array(sampleRate / 60)
|
||||||
|
var recordingState = RecordingState.STOPPED
|
||||||
|
var recordingSample = 0
|
||||||
|
var recordingStart = 0
|
||||||
|
|
||||||
|
val strings = Array(POLYPHONICS) {
|
||||||
|
PhysicalString(sampleRate, damping)
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
this.port.onmessage = ::handleMessage
|
||||||
|
Note.updateSampleRate(sampleRate)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleMessage(message: MessageEvent) {
|
||||||
|
//console.log("VstChipProcessor: Received message:", message.data)
|
||||||
|
|
||||||
|
val data = message.data
|
||||||
|
|
||||||
|
try {
|
||||||
|
when (data) {
|
||||||
|
is String -> {
|
||||||
|
when (data) {
|
||||||
|
"start_recording" -> {
|
||||||
|
port.postMessage(recordingBuffer)
|
||||||
|
if (recordingState == RecordingState.STOPPED) {
|
||||||
|
recordingState = RecordingState.WAITING_TO_START
|
||||||
|
recordingSample = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else ->
|
||||||
|
if (data.startsWith("set_channel")) {
|
||||||
|
val parts = data.split('\n')
|
||||||
|
if (parts.size == 2) {
|
||||||
|
midiChannel = parts[1].toInt()
|
||||||
|
println("Setting channel: $midiChannel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 playMidi(bytes: Int32Array) {
|
||||||
|
console.log("playMidi", bytes)
|
||||||
|
if (bytes.length > 0) {
|
||||||
|
var cmdByte = bytes[0]
|
||||||
|
val channelCmd = ((cmdByte shr 4) and 0xf) != 0xf0
|
||||||
|
val channel = cmdByte and 0xf
|
||||||
|
println("Channel cmd: $channelCmd")
|
||||||
|
if (channelCmd && channel != midiChannel) {
|
||||||
|
console.log("Wrong channel", midiChannel, bytes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdByte = cmdByte and 0xf0
|
||||||
|
|
||||||
|
//console.log("Received", bytes)
|
||||||
|
when (cmdByte) {
|
||||||
|
0x90 -> {
|
||||||
|
if (bytes.length == 3) {
|
||||||
|
val note = bytes[1]
|
||||||
|
val velocity = bytes[2]
|
||||||
|
|
||||||
|
if (velocity > 0) {
|
||||||
|
noteOn(note, velocity)
|
||||||
|
} else {
|
||||||
|
noteOff(note)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
0x80 -> {
|
||||||
|
if (bytes.length >= 2) {
|
||||||
|
val note = bytes[1]
|
||||||
|
|
||||||
|
noteOff(note)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
0xb0 -> {
|
||||||
|
if (bytes.length == 3) {
|
||||||
|
val knob = bytes[1]
|
||||||
|
val value = bytes[2]
|
||||||
|
|
||||||
|
when (knob) {
|
||||||
|
7 -> {
|
||||||
|
volume = value / 127f
|
||||||
|
}
|
||||||
|
|
||||||
|
0x47 -> {
|
||||||
|
damping = 0.8 + value / 127.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun noteOn(midiNote: Int, velocity: Int) {
|
||||||
|
val note = Note.fromMidi(midiNote)
|
||||||
|
for (i in 0 until POLYPHONICS) {
|
||||||
|
if (strings[i].currentNote == note) {
|
||||||
|
strings[i].pluck(note, velocity / 127.0)
|
||||||
|
strings[i].damping = damping
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (i in 0 until POLYPHONICS) {
|
||||||
|
if (strings[i].available) {
|
||||||
|
strings[i].pluck(note, velocity / 127.0)
|
||||||
|
strings[i].damping = damping
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun noteOff(midiNote: Int) {
|
||||||
|
val note = Note.fromMidi(midiNote)
|
||||||
|
for (i in 0 until POLYPHONICS) {
|
||||||
|
if (strings[i].currentNote.ordinal == note.ordinal) {
|
||||||
|
strings[i].available = true
|
||||||
|
strings[i].damping = damping * 0.9
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun process(
|
||||||
|
inputs: Array<Array<Float32Array>>,
|
||||||
|
outputs: Array<Array<Float32Array>>,
|
||||||
|
parameters: dynamic
|
||||||
|
): Boolean {
|
||||||
|
val samples = outputs[0][0].length
|
||||||
|
|
||||||
|
val left = outputs[0][0]
|
||||||
|
val right = outputs[0][1]
|
||||||
|
|
||||||
|
var lowestNote = 200
|
||||||
|
|
||||||
|
for (string in strings) {
|
||||||
|
if (string.available) {
|
||||||
|
lowestNote = min(lowestNote, string.currentNote.ordinal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lowestNote == 200 && recordingState == RecordingState.WAITING_TO_START) {
|
||||||
|
recordingState = RecordingState.RECORDING
|
||||||
|
recordingSample = 0
|
||||||
|
recordingStart = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
for (string in strings) {
|
||||||
|
for (i in 0 until samples) {
|
||||||
|
val waveValue: Float = string.tick().toFloat()
|
||||||
|
|
||||||
|
left[i] = left[i] + waveValue * volume
|
||||||
|
right[i] = right[i] + waveValue * volume
|
||||||
|
|
||||||
|
if (lowestNote == string.currentNote.ordinal && recordingState == RecordingState.WAITING_TO_START && string.index == 0) {
|
||||||
|
recordingState = RecordingState.RECORDING
|
||||||
|
recordingSample = 0
|
||||||
|
recordingStart = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
registerProcessor("vst-string-processor", VstStringProcessor::class.js)
|
||||||
|
|
||||||
|
println("VstStringProcessor registered!")
|
||||||
|
}
|
||||||
106
build.gradle.kts
106
build.gradle.kts
@@ -1,7 +1,9 @@
|
|||||||
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 {
|
||||||
@@ -21,7 +23,7 @@ kotlin {
|
|||||||
binaries.executable()
|
binaries.executable()
|
||||||
browser {
|
browser {
|
||||||
commonWebpackConfig {
|
commonWebpackConfig {
|
||||||
outputFileName = "vst-chip-worklet-ui.js"
|
outputFileName = "vst-string-worklet-ui.js"
|
||||||
sourceMaps = true
|
sourceMaps = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,13 +41,15 @@ kotlin {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":common"))
|
implementation(project(":common"))
|
||||||
//base
|
//base
|
||||||
api("nl.astraeus:kotlin-css-generator:1.0.7")
|
implementation("nl.astraeus:kotlin-css-generator:1.0.10")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
|
||||||
|
implementation("nl.astraeus:vst-ui-base:1.1.2")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val jsMain by getting {
|
val jsMain by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
//base
|
//base
|
||||||
implementation("nl.astraeus:kotlin-komponent-js:1.2.2")
|
implementation("nl.astraeus:kotlin-komponent-js:1.2.4")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val jsTest by getting {
|
val jsTest by getting {
|
||||||
@@ -57,9 +61,101 @@ kotlin {
|
|||||||
dependencies {
|
dependencies {
|
||||||
//base
|
//base
|
||||||
|
|
||||||
implementation("io.undertow:undertow-core:2.3.13.Final")
|
implementation("io.undertow:undertow-core:2.3.14.Final")
|
||||||
|
implementation("io.undertow:undertow-websockets-jsr:2.3.14.Final")
|
||||||
|
implementation("org.jboss.xnio:xnio-nio:3.8.16.Final")
|
||||||
|
|
||||||
|
implementation("org.xerial:sqlite-jdbc:3.46.0.0")
|
||||||
|
implementation("com.zaxxer:HikariCP:4.0.3")
|
||||||
|
implementation("nl.astraeus:simple-jdbc-stats:1.6.1")
|
||||||
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.11.0")
|
implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.11.0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
application {
|
||||||
|
mainClass.set("nl.astraeus.vst.string.MainKt")
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hardcoded deploy configuration */
|
||||||
|
|
||||||
|
val deployDirectory = "vst-string.midi-vst.com"
|
||||||
|
|
||||||
|
tasks.register<Copy>("unzipDistribution") {
|
||||||
|
mustRunAfter("removeSymbolicLink")
|
||||||
|
val zipDir = layout.projectDirectory.dir("build/distributions")
|
||||||
|
val zipFile = zipDir.file("${project.name}-${project.version}.zip")
|
||||||
|
|
||||||
|
val outputDir = file("/home/rnentjes/www/${deployDirectory}")
|
||||||
|
|
||||||
|
from(zipTree(zipFile))
|
||||||
|
into(outputDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("createSymbolicLink") {
|
||||||
|
mustRunAfter("unzipDistribution")
|
||||||
|
doLast {
|
||||||
|
val targetDir =
|
||||||
|
Paths.get("/home/rnentjes/www/${deployDirectory}/${project.name}-${project.version}") // Directory to link to
|
||||||
|
val symlink =
|
||||||
|
Paths.get("/home/rnentjes/www/${deployDirectory}/${project.name}") // Path for the symbolic link
|
||||||
|
|
||||||
|
if (!Files.exists(targetDir)) {
|
||||||
|
throw IllegalArgumentException("Target directory does not exist: $targetDir")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Files.exists(symlink)) {
|
||||||
|
println("Symbolic link already exists: $symlink")
|
||||||
|
} else {
|
||||||
|
Files.createSymbolicLink(symlink, targetDir)
|
||||||
|
println("Symbolic link created: $symlink -> $targetDir")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<Copy>("copyWeb") {
|
||||||
|
val webDir = layout.projectDirectory.dir("web")
|
||||||
|
val outputDir = file("/home/rnentjes/www/${deployDirectory}/web")
|
||||||
|
|
||||||
|
from(webDir)
|
||||||
|
into(outputDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named<Task>("build") {
|
||||||
|
dependsOn("generateVersionProperties")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named("kotlinUpgradeYarnLock") {
|
||||||
|
mustRunAfter("clean")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named("build") {
|
||||||
|
mustRunAfter("kotlinUpgradeYarnLock")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named("build") {
|
||||||
|
mustRunAfter("kotlinUpgradeYarnLock")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named("copyWeb") {
|
||||||
|
mustRunAfter("build")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("removeSymbolicLink") {
|
||||||
|
mustRunAfter("build")
|
||||||
|
doLast {
|
||||||
|
delete(layout.projectDirectory.file("/home/rnentjes/www/${deployDirectory}/${project.name}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("deploy") {
|
||||||
|
dependsOn("clean")
|
||||||
|
dependsOn("kotlinUpgradeYarnLock")
|
||||||
|
dependsOn("build")
|
||||||
|
dependsOn("copyWeb")
|
||||||
|
dependsOn("removeSymbolicLink")
|
||||||
|
dependsOn("unzipDistribution")
|
||||||
|
dependsOn("createSymbolicLink")
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
group = "nl.astraeus"
|
group = "nl.astraeus"
|
||||||
version = "1.0.0-SNAPSHOT"
|
version = "0.1.0"
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
mavenLocal()
|
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven("https://reposilite.astraeus.nl/releases")
|
|
||||||
maven {
|
maven {
|
||||||
url = uri("https://nexus.astraeus.nl/nexus/content/groups/public")
|
url = uri("https://gitea.astraeus.nl:8443/api/packages/rnentjes/maven")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
@file:OptIn(ExperimentalKotlinGradlePluginApi::class)
|
|
||||||
|
|
||||||
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
|
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
apply(from = "../common.gradle.kts")
|
apply(from = "../common.gradle.kts")
|
||||||
}
|
}
|
||||||
@@ -20,13 +16,8 @@ kotlin {
|
|||||||
jvm()
|
jvm()
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
val commonMain by getting {
|
val commonMain by getting
|
||||||
dependencies {
|
val jsMain by getting
|
||||||
}
|
val jvmMain by getting
|
||||||
}
|
|
||||||
val jsMain by getting {
|
|
||||||
dependencies {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
|
@file:OptIn(ExperimentalJsExport::class)
|
||||||
|
|
||||||
package nl.astraeus.vst
|
package nl.astraeus.vst
|
||||||
|
|
||||||
|
import kotlin.js.ExperimentalJsExport
|
||||||
|
import kotlin.js.JsExport
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
@@ -11,6 +15,8 @@ import kotlin.math.round
|
|||||||
* Time: 11:50
|
* Time: 11:50
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ExperimentalJsExport
|
||||||
|
@JsExport
|
||||||
enum class Note(
|
enum class Note(
|
||||||
val sharp: String,
|
val sharp: String,
|
||||||
val flat: String
|
val flat: String
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
@file:OptIn(ExperimentalJsExport::class)
|
||||||
|
|
||||||
|
package nl.astraeus.vst.string
|
||||||
|
|
||||||
|
import nl.astraeus.vst.Note
|
||||||
|
import kotlin.js.ExperimentalJsExport
|
||||||
|
import kotlin.js.JsExport
|
||||||
|
import kotlin.math.round
|
||||||
|
|
||||||
|
expect fun randomDouble(): Double
|
||||||
|
|
||||||
|
const val BUFFER_MULTIPLY = 4
|
||||||
|
|
||||||
|
@ExperimentalJsExport
|
||||||
|
@JsExport
|
||||||
|
class PhysicalString(
|
||||||
|
val sampleRate: Int,
|
||||||
|
var damping: Double,
|
||||||
|
) {
|
||||||
|
val sampleLength = 1.0 / sampleRate.toDouble()
|
||||||
|
val maxLength = sampleRate / Note.NO01.freq
|
||||||
|
var length = 1
|
||||||
|
val buffer = Array((maxLength * BUFFER_MULTIPLY).toInt() + 1) { 0.0 }
|
||||||
|
var sample = 0
|
||||||
|
var index = 0
|
||||||
|
var remaining = 0.0
|
||||||
|
var currentNote = Note.C4
|
||||||
|
var available = true
|
||||||
|
|
||||||
|
fun pluck(note: Note, velocity: Double, smoothing: Int = 0) {
|
||||||
|
available = false
|
||||||
|
currentNote = note
|
||||||
|
length = round(BUFFER_MULTIPLY * sampleRate / note.freq).toInt()
|
||||||
|
sample = 0
|
||||||
|
index = 0
|
||||||
|
|
||||||
|
for (i in 0..<length) {
|
||||||
|
if (i < length / 2) {
|
||||||
|
buffer[i] = randomDouble() * velocity
|
||||||
|
} else {
|
||||||
|
buffer[i] = -randomDouble() * velocity
|
||||||
|
}
|
||||||
|
//buffer[i] = (randomDouble() - 0.5) * 2.0 * velocity
|
||||||
|
//buffer[i] = sin(PI * 2 * i/length) * randomDouble() * velocity
|
||||||
|
//buffer[i] = (i/length.toDouble() * 2.0) - 1.0 //if (i < length / 2) { 1.0 } else { -1.0 }
|
||||||
|
}
|
||||||
|
repeat(smoothing) {
|
||||||
|
for (i in 0..<length) {
|
||||||
|
tick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update(time: Double) {
|
||||||
|
remaining += (time / 1000.0)
|
||||||
|
while (remaining > sampleLength) {
|
||||||
|
remaining -= sampleLength
|
||||||
|
tick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun tick(): Double {
|
||||||
|
val result = buffer[index]
|
||||||
|
|
||||||
|
repeat(BUFFER_MULTIPLY) {
|
||||||
|
var newValue = 0.0
|
||||||
|
newValue += getValueFromBuffer(index + 1) * 0.1
|
||||||
|
newValue += getValueFromBuffer(index + 2) * 0.15
|
||||||
|
newValue += getValueFromBuffer(index + 3) * 0.25
|
||||||
|
newValue += getValueFromBuffer(index + 4) * 0.25
|
||||||
|
newValue += getValueFromBuffer(index + 5) * 0.15
|
||||||
|
newValue += getValueFromBuffer(index + 6) * 0.1
|
||||||
|
newValue *= damping
|
||||||
|
|
||||||
|
buffer[index] = newValue
|
||||||
|
|
||||||
|
index = (index + 1) % length
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getValueFromBuffer(index: Int): Double {
|
||||||
|
return buffer[(index + length) % length]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package nl.astraeus.vst.string
|
||||||
|
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
actual fun randomDouble(): Double = Random.nextDouble()
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package nl.astraeus.vst.string
|
||||||
|
|
||||||
|
actual fun randomDouble(): Double {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
pluginManagement {
|
pluginManagement {
|
||||||
plugins {
|
plugins {
|
||||||
kotlin("multiplatform") version "2.0.0"
|
kotlin("multiplatform") version "2.1.0"
|
||||||
id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0"
|
|
||||||
}
|
}
|
||||||
repositories {
|
repositories {
|
||||||
gradlePluginPortal()
|
gradlePluginPortal()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
apply(from = "settings.common.gradle.kts")
|
apply(from = "settings.common.gradle.kts")
|
||||||
|
|
||||||
rootProject.name = "vst-chip"
|
rootProject.name = "vst-string"
|
||||||
|
|
||||||
include(":common")
|
include(":common")
|
||||||
include(":audio-worklet")
|
include(":audio-worklet")
|
||||||
|
|||||||
24
src/commonMain/kotlin/nl/astraeus/vst/string/PatchDTO.kt
Normal file
24
src/commonMain/kotlin/nl/astraeus/vst/string/PatchDTO.kt
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package nl.astraeus.vst.string
|
||||||
|
|
||||||
|
import kotlin.js.ExperimentalJsExport
|
||||||
|
import kotlin.js.JsExport
|
||||||
|
import kotlin.js.JsName
|
||||||
|
|
||||||
|
@ExperimentalJsExport
|
||||||
|
@JsExport
|
||||||
|
data class PatchDTO(
|
||||||
|
@JsName("midiId")
|
||||||
|
val midiId: String = "",
|
||||||
|
@JsName("midiName")
|
||||||
|
val midiName: String = "",
|
||||||
|
@JsName("midiChannel")
|
||||||
|
var midiChannel: Int = 0,
|
||||||
|
@JsName("volume")
|
||||||
|
var volume: Double = 0.75,
|
||||||
|
@JsName("damping")
|
||||||
|
var damping: Double = 0.5,
|
||||||
|
@JsName("delay")
|
||||||
|
var delay: Double = 0.0,
|
||||||
|
@JsName("delayDepth")
|
||||||
|
var delayDepth: Double = 0.0,
|
||||||
|
)
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package nl.astraeus.vst.chip
|
|
||||||
|
|
||||||
import kotlinx.browser.document
|
|
||||||
import kotlinx.browser.window
|
|
||||||
import nl.astraeus.komp.Komponent
|
|
||||||
import nl.astraeus.vst.chip.midi.Broadcaster
|
|
||||||
import nl.astraeus.vst.chip.midi.MidiMessage
|
|
||||||
import nl.astraeus.vst.chip.midi.Midi
|
|
||||||
import nl.astraeus.vst.chip.view.MainView
|
|
||||||
import org.khronos.webgl.Uint8Array
|
|
||||||
|
|
||||||
fun main() {
|
|
||||||
Komponent.create(document.body!!, MainView)
|
|
||||||
|
|
||||||
Midi.start()
|
|
||||||
|
|
||||||
console.log("Performance", window.performance)
|
|
||||||
Broadcaster.getChannel(0).postMessage(
|
|
||||||
MidiMessage(
|
|
||||||
Uint8Array(arrayOf(0x80.toByte(), 60, 60)),
|
|
||||||
window.performance.now()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
window.setInterval({
|
|
||||||
Broadcaster.sync()
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package nl.astraeus.vst.chip.audio
|
|
||||||
|
|
||||||
import nl.astraeus.vst.chip.AudioContext
|
|
||||||
|
|
||||||
object AudioContextHandler {
|
|
||||||
val audioContext: dynamic = AudioContext()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package nl.astraeus.vst.chip.audio
|
|
||||||
|
|
||||||
import org.w3c.dom.MessageEvent
|
|
||||||
|
|
||||||
object VstChipWorklet : AudioNode(
|
|
||||||
"vst-chip-worklet.js",
|
|
||||||
"vst-chip-processor"
|
|
||||||
) {
|
|
||||||
|
|
||||||
override fun onMessage(message: MessageEvent) {
|
|
||||||
console.log("Message from worklet: ", message)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,273 +0,0 @@
|
|||||||
package nl.astraeus.vst.chip.view
|
|
||||||
|
|
||||||
import daw.style.Css
|
|
||||||
import daw.style.Css.defineCss
|
|
||||||
import daw.style.Css.noTextSelect
|
|
||||||
import daw.style.CssId
|
|
||||||
import daw.style.CssName
|
|
||||||
import daw.style.hover
|
|
||||||
import kotlinx.browser.window
|
|
||||||
import kotlinx.html.*
|
|
||||||
import kotlinx.html.js.onChangeFunction
|
|
||||||
import kotlinx.html.js.onClickFunction
|
|
||||||
import kotlinx.html.js.onInputFunction
|
|
||||||
import nl.astraeus.css.properties.*
|
|
||||||
import nl.astraeus.css.style.cls
|
|
||||||
import nl.astraeus.komp.HtmlBuilder
|
|
||||||
import nl.astraeus.komp.Komponent
|
|
||||||
import nl.astraeus.vst.chip.audio.VstChipWorklet
|
|
||||||
import nl.astraeus.vst.chip.midi.Midi
|
|
||||||
import org.khronos.webgl.Uint8Array
|
|
||||||
import org.w3c.dom.HTMLInputElement
|
|
||||||
import org.w3c.dom.HTMLSelectElement
|
|
||||||
|
|
||||||
object MainView : Komponent() {
|
|
||||||
private var messages: MutableList<String> = ArrayList()
|
|
||||||
private var started = false
|
|
||||||
|
|
||||||
init {
|
|
||||||
MainViewCss
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addMessage(message: String) {
|
|
||||||
messages.add(message)
|
|
||||||
while (messages.size > 10) {
|
|
||||||
messages.removeAt(0)
|
|
||||||
}
|
|
||||||
requestUpdate()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun HtmlBuilder.render() {
|
|
||||||
div(MainViewCss.MainDivCss.name) {
|
|
||||||
if (!started) {
|
|
||||||
div(MainViewCss.StartSplashCss.name) {
|
|
||||||
div(MainViewCss.StartBoxCss.name) {
|
|
||||||
div(MainViewCss.StartButtonCss.name) {
|
|
||||||
+"START"
|
|
||||||
onClickFunction = {
|
|
||||||
started = true
|
|
||||||
VstChipWorklet.create {
|
|
||||||
requestUpdate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
+"VST Chip"
|
|
||||||
}
|
|
||||||
div {
|
|
||||||
span {
|
|
||||||
+"Midi input: "
|
|
||||||
select {
|
|
||||||
option {
|
|
||||||
+"None"
|
|
||||||
value = "none"
|
|
||||||
}
|
|
||||||
for (mi in Midi.inputs) {
|
|
||||||
option {
|
|
||||||
+mi.name
|
|
||||||
value = mi.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeFunction = { event ->
|
|
||||||
val target = event.target as HTMLSelectElement
|
|
||||||
if (target.value == "none") {
|
|
||||||
Midi.setInput(null)
|
|
||||||
} else {
|
|
||||||
val selected = Midi.inputs.find { it.id == target.value }
|
|
||||||
if (selected != null) {
|
|
||||||
Midi.setInput(selected)
|
|
||||||
} else if (target.value == "midi-broadcast") {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
span {
|
|
||||||
+"channel:"
|
|
||||||
input {
|
|
||||||
type = InputType.number
|
|
||||||
value = Midi.inputChannel.toString()
|
|
||||||
onInputFunction = { event ->
|
|
||||||
val target = event.target as HTMLInputElement
|
|
||||||
Midi.inputChannel = target.value.toInt()
|
|
||||||
println("onInput channel: ${Midi.inputChannel}")
|
|
||||||
VstChipWorklet.postMessage("set_channel\n${Midi.inputChannel}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div {
|
|
||||||
span {
|
|
||||||
+"Midi output: "
|
|
||||||
select {
|
|
||||||
option {
|
|
||||||
+"None"
|
|
||||||
value = "none"
|
|
||||||
}
|
|
||||||
for (mi in Midi.outputs) {
|
|
||||||
option {
|
|
||||||
+mi.name
|
|
||||||
value = mi.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeFunction = { event ->
|
|
||||||
val target = event.target as HTMLSelectElement
|
|
||||||
if (target.value == "none") {
|
|
||||||
Midi.setOutput(null)
|
|
||||||
} else {
|
|
||||||
val selected = Midi.outputs.find { it.id == target.value }
|
|
||||||
if (selected != null) {
|
|
||||||
Midi.setOutput(selected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
span {
|
|
||||||
+"channel:"
|
|
||||||
input {
|
|
||||||
type = InputType.number
|
|
||||||
value = Midi.outputChannel.toString()
|
|
||||||
onInputFunction = { event ->
|
|
||||||
val target = event.target as HTMLInputElement
|
|
||||||
Midi.outputChannel = target.value.toInt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div(MainViewCss.ButtonCss.name) {
|
|
||||||
+"Send note on to output"
|
|
||||||
onClickFunction = {
|
|
||||||
val data = Uint8Array(
|
|
||||||
arrayOf(
|
|
||||||
0x90.toByte(),
|
|
||||||
0x3c.toByte(),
|
|
||||||
0x70.toByte()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Midi.send(data, window.performance.now() + 1000)
|
|
||||||
Midi.send(data, window.performance.now() + 2000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div(MainViewCss.ButtonCss.name) {
|
|
||||||
+"Send note off to output"
|
|
||||||
onClickFunction = {
|
|
||||||
val data = Uint8Array(
|
|
||||||
arrayOf(
|
|
||||||
0x90.toByte(),
|
|
||||||
0x3c.toByte(),
|
|
||||||
0x0.toByte(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Midi.send(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object MainViewCss : CssId("main") {
|
|
||||||
object MainDivCss : CssName()
|
|
||||||
object ActiveCss : CssName()
|
|
||||||
object ButtonCss : CssName()
|
|
||||||
object NoteBarCss : CssName()
|
|
||||||
object StartSplashCss : CssName()
|
|
||||||
object StartBoxCss : CssName()
|
|
||||||
object StartButtonCss : CssName()
|
|
||||||
|
|
||||||
init {
|
|
||||||
defineCss {
|
|
||||||
select("*") {
|
|
||||||
select("*:before") {
|
|
||||||
select("*:after") {
|
|
||||||
boxSizing(BoxSizing.borderBox)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
select("html", "body") {
|
|
||||||
margin(0.px)
|
|
||||||
padding(0.px)
|
|
||||||
height(100.prc)
|
|
||||||
}
|
|
||||||
select("html", "body") {
|
|
||||||
backgroundColor(Css.currentStyle.mainBackgroundColor)
|
|
||||||
color(Css.currentStyle.mainFontColor)
|
|
||||||
|
|
||||||
fontFamily("JetbrainsMono, monospace")
|
|
||||||
fontSize(14.px)
|
|
||||||
fontWeight(FontWeight.bold)
|
|
||||||
|
|
||||||
//transition()
|
|
||||||
noTextSelect()
|
|
||||||
}
|
|
||||||
select(cls(ButtonCss)) {
|
|
||||||
margin(1.rem)
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
select(cls(ActiveCss)) {
|
|
||||||
//backgroundColor(Css.currentStyle.selectedBackgroundColor)
|
|
||||||
}
|
|
||||||
select(cls(NoteBarCss)) {
|
|
||||||
minHeight(4.rem)
|
|
||||||
}
|
|
||||||
select(cls(MainDivCss)) {
|
|
||||||
margin(1.rem)
|
|
||||||
}
|
|
||||||
select("select") {
|
|
||||||
plain("appearance", "none")
|
|
||||||
border("0")
|
|
||||||
outline("0")
|
|
||||||
width(20.rem)
|
|
||||||
padding(0.5.rem, 2.rem, 0.5.rem, 0.5.rem)
|
|
||||||
backgroundImage("url('https://upload.wikimedia.org/wikipedia/commons/9/9d/Caret_down_font_awesome_whitevariation.svg')")
|
|
||||||
background("right 0.8em center/1.4em")
|
|
||||||
backgroundColor(Css.currentStyle.inputBackgroundColor)
|
|
||||||
//color(Css.currentStyle.entryFontColor)
|
|
||||||
borderRadius(0.25.em)
|
|
||||||
}
|
|
||||||
select(cls(StartSplashCss)) {
|
|
||||||
position(Position.fixed)
|
|
||||||
left(0.px)
|
|
||||||
top(0.px)
|
|
||||||
width(100.vw)
|
|
||||||
height(100.vh)
|
|
||||||
zIndex(100)
|
|
||||||
backgroundColor(hsla(32, 0, 50, 0.6))
|
|
||||||
|
|
||||||
select(cls(StartBoxCss)) {
|
|
||||||
position(Position.relative)
|
|
||||||
left(25.vw)
|
|
||||||
top(25.vh)
|
|
||||||
width(50.vw)
|
|
||||||
height(50.vh)
|
|
||||||
backgroundColor(hsla(0, 0, 50, 0.25))
|
|
||||||
|
|
||||||
select(cls(StartButtonCss)) {
|
|
||||||
position(Position.absolute)
|
|
||||||
left(50.prc)
|
|
||||||
top(50.prc)
|
|
||||||
transform(Transform("translate(-50%, -50%)"))
|
|
||||||
padding(1.rem)
|
|
||||||
backgroundColor(Css.currentStyle.buttonBackgroundColor)
|
|
||||||
cursor("pointer")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
package daw.style
|
|
||||||
|
|
||||||
import kotlinx.browser.document
|
|
||||||
import nl.astraeus.css.properties.*
|
|
||||||
import nl.astraeus.css.style
|
|
||||||
import nl.astraeus.css.style.ConditionalStyle
|
|
||||||
import nl.astraeus.css.style.Style
|
|
||||||
|
|
||||||
class StyleDefinition(
|
|
||||||
val mainFontColor: Color = hsla(178, 70, 55, 1.0),
|
|
||||||
val mainBackgroundColor: Color = hsl(239, 50, 10),
|
|
||||||
//val entryFontColor: Color = hsl(Css.mainFontColorNumber, 70, 55),
|
|
||||||
val inputBackgroundColor : Color = mainBackgroundColor.lighten(15),
|
|
||||||
val buttonBackgroundColor : Color = mainBackgroundColor.lighten(15),
|
|
||||||
val buttonBorderColor : Color = mainFontColor.changeAlpha(0.25),
|
|
||||||
val buttonBorderWidth : Measurement = 1.px,
|
|
||||||
)
|
|
||||||
|
|
||||||
object NoTextSelectCls : CssName("no-text-select")
|
|
||||||
object SelectedCls : CssName("selected")
|
|
||||||
object ActiveCls : CssName("active")
|
|
||||||
|
|
||||||
fun Color.hover(): Color = if (Css.currentStyle == Css.darkStyle) {
|
|
||||||
this.lighten(15)
|
|
||||||
} else {
|
|
||||||
this.darken(15)
|
|
||||||
}
|
|
||||||
|
|
||||||
object Css {
|
|
||||||
var minified = false
|
|
||||||
var dynamicStyles = mutableMapOf<CssId, ConditionalStyle.() -> Unit>()
|
|
||||||
|
|
||||||
fun CssId.defineCss(conditionalStyle: ConditionalStyle.() -> Unit) {
|
|
||||||
check(!dynamicStyles.containsKey(this)) {
|
|
||||||
"CssId with name ${this.name} already defined!"
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCss(conditionalStyle)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun CssId.updateCss(conditionalStyle: ConditionalStyle.() -> Unit) {
|
|
||||||
val elementId = this.description()
|
|
||||||
var dynamicStyleElement = document.getElementById(elementId)
|
|
||||||
|
|
||||||
dynamicStyles[this] = conditionalStyle
|
|
||||||
|
|
||||||
if (dynamicStyleElement == null) {
|
|
||||||
dynamicStyleElement = document.createElement("style")
|
|
||||||
dynamicStyleElement.id = elementId
|
|
||||||
|
|
||||||
document.head?.append(dynamicStyleElement)
|
|
||||||
}
|
|
||||||
|
|
||||||
val css = style(conditionalStyle)
|
|
||||||
|
|
||||||
dynamicStyleElement.innerHTML = css.generateCss(minified = false)
|
|
||||||
}
|
|
||||||
|
|
||||||
val darkStyle = StyleDefinition(
|
|
||||||
)
|
|
||||||
|
|
||||||
val lightStyle = StyleDefinition(
|
|
||||||
mainBackgroundColor = hsl(239+180, 50, 15),
|
|
||||||
)
|
|
||||||
|
|
||||||
var currentStyle: StyleDefinition = darkStyle
|
|
||||||
|
|
||||||
fun updateStyle() {
|
|
||||||
for ((cssId, dynStyle) in dynamicStyles) {
|
|
||||||
cssId.apply {
|
|
||||||
updateCss(dynStyle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun switchLayout() {
|
|
||||||
currentStyle = if (currentStyle == darkStyle) {
|
|
||||||
lightStyle
|
|
||||||
} else {
|
|
||||||
darkStyle
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStyle()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Style.transition() {
|
|
||||||
transition("all 0.5s ease")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Style.noTextSelect() {
|
|
||||||
plain("-webkit-touch-callout", "none")
|
|
||||||
plain("-webkit-user-select", "none")
|
|
||||||
plain("-moz-user-select", "none")
|
|
||||||
plain("-ms-user-select", "none")
|
|
||||||
|
|
||||||
userSelect(UserSelect.none)
|
|
||||||
|
|
||||||
select("::selection") {
|
|
||||||
background("none")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object GenericCss : CssId("generic") {
|
|
||||||
init {
|
|
||||||
fun generateStyle(): String {
|
|
||||||
val css = style {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return css.generateCss(minified = minified)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
package daw.style
|
|
||||||
|
|
||||||
import nl.astraeus.css.style.DescriptionProvider
|
|
||||||
import nl.astraeus.css.style.cls
|
|
||||||
|
|
||||||
private val CAPITAL_LETTER = Regex("[A-Z]")
|
|
||||||
|
|
||||||
fun String.hyphenize(): String =
|
|
||||||
replace(CAPITAL_LETTER) {
|
|
||||||
"-${it.value.lowercase()}"
|
|
||||||
}
|
|
||||||
|
|
||||||
private val shortId = false
|
|
||||||
private var nextCssId = 1
|
|
||||||
|
|
||||||
private fun nextShortId(): String {
|
|
||||||
var id = nextCssId++
|
|
||||||
val result = StringBuilder()
|
|
||||||
|
|
||||||
while(id > 0) {
|
|
||||||
val ch = ((id % 26) + 'a'.code).toChar()
|
|
||||||
result.append(ch)
|
|
||||||
|
|
||||||
id /= 26
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
open class CssName(name: String? = null) : DescriptionProvider {
|
|
||||||
val name: String = if (shortId) {
|
|
||||||
nextShortId()
|
|
||||||
} else if (name != null) {
|
|
||||||
"daw-$name"
|
|
||||||
} else {
|
|
||||||
"daw${this::class.simpleName?.hyphenize() ?: this::class}"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun description() = name
|
|
||||||
}
|
|
||||||
|
|
||||||
fun CssName.cls() : DescriptionProvider = cls(this)
|
|
||||||
|
|
||||||
open class CssId(name: String) : DescriptionProvider {
|
|
||||||
val name: String = if (shortId) {
|
|
||||||
nextShortId()
|
|
||||||
} else {
|
|
||||||
"daw-$name-css"
|
|
||||||
}
|
|
||||||
override fun description() = name
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (other !is CssId) return false
|
|
||||||
|
|
||||||
if (name != other.name) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
return name.hashCode()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package nl.astraeus.vst.chip
|
package nl.astraeus.vst.string
|
||||||
|
|
||||||
external class AudioContext {
|
external class AudioContext {
|
||||||
var sampleRate: Int
|
var sampleRate: Int
|
||||||
30
src/jsMain/kotlin/nl/astraeus/vst/string/Main.kt
Normal file
30
src/jsMain/kotlin/nl/astraeus/vst/string/Main.kt
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package nl.astraeus.vst.string
|
||||||
|
|
||||||
|
import kotlinx.browser.document
|
||||||
|
import nl.astraeus.komp.Komponent
|
||||||
|
import nl.astraeus.komp.UnsafeMode
|
||||||
|
import nl.astraeus.vst.string.audio.VstStringWorklet
|
||||||
|
import nl.astraeus.vst.string.logger.log
|
||||||
|
import nl.astraeus.vst.string.midi.Midi
|
||||||
|
import nl.astraeus.vst.string.view.MainView
|
||||||
|
import nl.astraeus.vst.string.ws.WebsocketClient
|
||||||
|
import nl.astraeus.vst.ui.css.CssSettings
|
||||||
|
import nl.astraeus.vst.ui.view.BaseVstView
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
CssSettings.shortId = false
|
||||||
|
CssSettings.preFix = "vst-string"
|
||||||
|
|
||||||
|
Komponent.unsafeMode = UnsafeMode.UNSAFE_SVG_ONLY
|
||||||
|
Komponent.create(document.body!!, BaseVstView("VST Guiter", MainView) {
|
||||||
|
VstStringWorklet.create {
|
||||||
|
WebsocketClient.send("LOAD\n")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Midi.start()
|
||||||
|
|
||||||
|
WebsocketClient.connect {
|
||||||
|
log.debug { "Connected to server" }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package nl.astraeus.vst.string.audio
|
||||||
|
|
||||||
|
import nl.astraeus.vst.string.AudioContext
|
||||||
|
|
||||||
|
object AudioContextHandler {
|
||||||
|
val audioContext: dynamic = AudioContext()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package nl.astraeus.vst.chip.audio
|
package nl.astraeus.vst.string.audio
|
||||||
|
|
||||||
import nl.astraeus.vst.chip.AudioWorkletNode
|
import nl.astraeus.vst.string.AudioWorkletNode
|
||||||
import nl.astraeus.vst.chip.AudioWorkletNodeParameters
|
import nl.astraeus.vst.string.AudioWorkletNodeParameters
|
||||||
import nl.astraeus.vst.chip.audio.AudioContextHandler.audioContext
|
import nl.astraeus.vst.string.audio.AudioContextHandler.audioContext
|
||||||
import org.w3c.dom.MessageEvent
|
import org.w3c.dom.MessageEvent
|
||||||
import org.w3c.dom.MessagePort
|
import org.w3c.dom.MessagePort
|
||||||
|
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
@file:OptIn(ExperimentalJsExport::class)
|
||||||
|
|
||||||
|
package nl.astraeus.vst.string.audio
|
||||||
|
|
||||||
|
import nl.astraeus.vst.string.PatchDTO
|
||||||
|
import nl.astraeus.vst.string.view.MainView
|
||||||
|
import nl.astraeus.vst.string.view.WaveformView
|
||||||
|
import nl.astraeus.vst.ui.util.uInt8ArrayOf
|
||||||
|
import org.khronos.webgl.Float32Array
|
||||||
|
import org.khronos.webgl.Uint8Array
|
||||||
|
import org.khronos.webgl.get
|
||||||
|
import org.w3c.dom.MessageEvent
|
||||||
|
import kotlin.experimental.and
|
||||||
|
|
||||||
|
object VstStringWorklet : AudioNode(
|
||||||
|
"/vst-string-worklet.js",
|
||||||
|
"vst-string-processor"
|
||||||
|
) {
|
||||||
|
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(
|
||||||
|
uInt8ArrayOf(0xb0 + midiChannel, 7, (value * 127).toInt())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
var damping = 0.996
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
super.postMessage(
|
||||||
|
uInt8ArrayOf(0xb0 + midiChannel, 0x47, ((value - 0.8) * 127).toInt())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
var delay = 0.0
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
super.postMessage(
|
||||||
|
uInt8ArrayOf(0xb0 + midiChannel, 0x4e, (value * 127).toInt())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
var delayDepth = 0.0
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
super.postMessage(
|
||||||
|
uInt8ArrayOf(0xb0 + midiChannel, 0x4f, (value * 127).toInt())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var recording: Float32Array? = null
|
||||||
|
|
||||||
|
override fun onMessage(message: MessageEvent) {
|
||||||
|
//console.log("Message from worklet: ", message)
|
||||||
|
|
||||||
|
val data = message.data
|
||||||
|
if (data is Float32Array) {
|
||||||
|
this.recording = data
|
||||||
|
WaveformView.requestUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun postDirectlyToWorklet(msg: Any) {
|
||||||
|
super.postMessage(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun postMessage(msg: Any) {
|
||||||
|
if (msg is Uint8Array) {
|
||||||
|
if (
|
||||||
|
msg.length == 3
|
||||||
|
&& (msg[0] and 0xf == midiChannel.toByte())
|
||||||
|
&& (msg[0] and 0xf0.toByte() == 0xb0.toByte())
|
||||||
|
) {
|
||||||
|
val knob = msg[1]
|
||||||
|
val value = msg[2]
|
||||||
|
|
||||||
|
handleIncomingMidi(knob, value)
|
||||||
|
} else {
|
||||||
|
super.postMessage(msg)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
super.postMessage(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleIncomingMidi(knob: Byte, value: Byte) {
|
||||||
|
when (knob) {
|
||||||
|
0x46.toByte() -> {
|
||||||
|
volume = value / 127.0
|
||||||
|
MainView.requestUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
0x4a.toByte() -> {
|
||||||
|
damping = value / 127.0
|
||||||
|
MainView.requestUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun load(patch: PatchDTO) {
|
||||||
|
midiChannel = patch.midiChannel
|
||||||
|
volume = patch.volume
|
||||||
|
damping = patch.damping
|
||||||
|
delay = patch.delay
|
||||||
|
delayDepth = patch.delayDepth
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save(): PatchDTO {
|
||||||
|
return PatchDTO(
|
||||||
|
midiChannel = midiChannel,
|
||||||
|
volume = volume,
|
||||||
|
damping = damping,
|
||||||
|
delay = delay,
|
||||||
|
delayDepth = delayDepth,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
@file:OptIn(ExperimentalJsExport::class)
|
@file:OptIn(ExperimentalJsExport::class)
|
||||||
|
|
||||||
package nl.astraeus.vst.chip.midi
|
package nl.astraeus.vst.string.midi
|
||||||
|
|
||||||
import kotlinx.browser.window
|
import kotlinx.browser.window
|
||||||
import org.khronos.webgl.Uint8Array
|
import org.khronos.webgl.Uint8Array
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package nl.astraeus.vst.chip.midi
|
package nl.astraeus.vst.string.midi
|
||||||
|
|
||||||
import kotlinx.browser.window
|
import kotlinx.browser.window
|
||||||
import nl.astraeus.vst.chip.audio.VstChipWorklet
|
import nl.astraeus.vst.string.audio.VstStringWorklet
|
||||||
import nl.astraeus.vst.chip.view.MainView
|
import nl.astraeus.vst.string.view.MainView
|
||||||
import org.khronos.webgl.Uint8Array
|
import org.khronos.webgl.Uint8Array
|
||||||
import org.khronos.webgl.get
|
import org.khronos.webgl.get
|
||||||
|
|
||||||
@@ -37,7 +37,6 @@ external class MIDIOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
object Midi {
|
object Midi {
|
||||||
var inputChannel: Int = -1
|
|
||||||
var outputChannel: Int = -1
|
var outputChannel: Int = -1
|
||||||
|
|
||||||
var inputs = mutableListOf<MIDIInput>()
|
var inputs = mutableListOf<MIDIInput>()
|
||||||
@@ -75,6 +74,39 @@ object Midi {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setInput(id: String, name: String = "") {
|
||||||
|
var selected = inputs.find { it.id == id }
|
||||||
|
if (selected == null) {
|
||||||
|
var maxMatchChar = 0
|
||||||
|
inputs.forEach {
|
||||||
|
val matchChars = matchChars(it.name, name)
|
||||||
|
if (matchChars > maxMatchChar) {
|
||||||
|
selected = it
|
||||||
|
maxMatchChar = matchChars
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setInput(selected)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun matchChars(str1: String, str2: String): Int {
|
||||||
|
var result = 0
|
||||||
|
if (str1.length > str2.length) {
|
||||||
|
for (ch in str1.toCharArray()) {
|
||||||
|
if (str2.contains(ch)) {
|
||||||
|
result++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (ch in str2.toCharArray()) {
|
||||||
|
if (str1.contains(ch)) {
|
||||||
|
result++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
fun setInput(input: MIDIInput?) {
|
fun setInput(input: MIDIInput?) {
|
||||||
console.log("Setting input", input)
|
console.log("Setting input", input)
|
||||||
currentInput?.close()
|
currentInput?.close()
|
||||||
@@ -93,7 +125,7 @@ object Midi {
|
|||||||
hex.append(" ")
|
hex.append(" ")
|
||||||
}
|
}
|
||||||
console.log("Midi message:", hex)
|
console.log("Midi message:", hex)
|
||||||
VstChipWorklet.postMessage(
|
VstStringWorklet.postMessage(
|
||||||
message.data
|
message.data
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -110,7 +142,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
246
src/jsMain/kotlin/nl/astraeus/vst/string/view/MainView.kt
Normal file
246
src/jsMain/kotlin/nl/astraeus/vst/string/view/MainView.kt
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
@file:OptIn(ExperimentalJsExport::class)
|
||||||
|
|
||||||
|
package nl.astraeus.vst.string.view
|
||||||
|
|
||||||
|
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.onClickFunction
|
||||||
|
import kotlinx.html.js.onInputFunction
|
||||||
|
import kotlinx.html.option
|
||||||
|
import kotlinx.html.select
|
||||||
|
import kotlinx.html.span
|
||||||
|
import nl.astraeus.css.properties.AlignItems
|
||||||
|
import nl.astraeus.css.properties.BoxSizing
|
||||||
|
import nl.astraeus.css.properties.Display
|
||||||
|
import nl.astraeus.css.properties.FlexDirection
|
||||||
|
import nl.astraeus.css.properties.FontWeight
|
||||||
|
import nl.astraeus.css.properties.JustifyContent
|
||||||
|
import nl.astraeus.css.properties.em
|
||||||
|
import nl.astraeus.css.properties.prc
|
||||||
|
import nl.astraeus.css.properties.px
|
||||||
|
import nl.astraeus.css.properties.rem
|
||||||
|
import nl.astraeus.css.style.Style
|
||||||
|
import nl.astraeus.css.style.cls
|
||||||
|
import nl.astraeus.komp.HtmlBuilder
|
||||||
|
import nl.astraeus.komp.Komponent
|
||||||
|
import nl.astraeus.vst.string.PhysicalString
|
||||||
|
import nl.astraeus.vst.string.audio.VstStringWorklet
|
||||||
|
import nl.astraeus.vst.string.audio.VstStringWorklet.midiChannel
|
||||||
|
import nl.astraeus.vst.string.midi.Midi
|
||||||
|
import nl.astraeus.vst.string.ws.WebsocketClient
|
||||||
|
import nl.astraeus.vst.ui.components.ExpKnobComponent
|
||||||
|
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 nl.astraeus.vst.ui.util.uInt8ArrayOf
|
||||||
|
import org.w3c.dom.HTMLInputElement
|
||||||
|
import org.w3c.dom.HTMLSelectElement
|
||||||
|
|
||||||
|
object MainView : Komponent(), CssName {
|
||||||
|
private var messages: MutableList<String> = ArrayList()
|
||||||
|
val playString = PhysicalStringView(
|
||||||
|
PhysicalString(
|
||||||
|
sampleRate = 48000,
|
||||||
|
damping = 0.996,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
init {
|
||||||
|
css()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addMessage(message: String) {
|
||||||
|
messages.add(message)
|
||||||
|
while (messages.size > 10) {
|
||||||
|
messages.removeAt(0)
|
||||||
|
}
|
||||||
|
requestUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun HtmlBuilder.render() {
|
||||||
|
div(MainDivCss.name) {
|
||||||
|
h1 {
|
||||||
|
+"VST Guitar"
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
span {
|
||||||
|
+"Midi input: "
|
||||||
|
select {
|
||||||
|
option {
|
||||||
|
+"None"
|
||||||
|
value = "none"
|
||||||
|
}
|
||||||
|
for (mi in Midi.inputs) {
|
||||||
|
option {
|
||||||
|
+mi.name
|
||||||
|
value = mi.id
|
||||||
|
selected = mi.id == Midi.currentInput?.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeFunction = { event ->
|
||||||
|
val target = event.target as HTMLSelectElement
|
||||||
|
if (target.value == "none") {
|
||||||
|
Midi.setInput(null)
|
||||||
|
} else {
|
||||||
|
Midi.setInput(target.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
+"channel:"
|
||||||
|
input {
|
||||||
|
type = InputType.number
|
||||||
|
value = midiChannel.toString()
|
||||||
|
onInputFunction = { event ->
|
||||||
|
val target = event.target as HTMLInputElement
|
||||||
|
println("onInput channel: $target")
|
||||||
|
VstStringWorklet.midiChannel = target.value.toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
span(ButtonBarCss.name) {
|
||||||
|
+"SAVE"
|
||||||
|
onClickFunction = {
|
||||||
|
val patch = VstStringWorklet.save().copy(
|
||||||
|
midiId = Midi.currentInput?.id ?: "",
|
||||||
|
midiName = Midi.currentInput?.name ?: ""
|
||||||
|
)
|
||||||
|
|
||||||
|
WebsocketClient.send("SAVE\n${JSON.stringify(patch)}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
span(ButtonBarCss.name) {
|
||||||
|
+"STOP"
|
||||||
|
onClickFunction = {
|
||||||
|
VstStringWorklet.postDirectlyToWorklet(
|
||||||
|
uInt8ArrayOf(0xb0 + midiChannel, 123, 0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div(ControlsCss.name) {
|
||||||
|
include(
|
||||||
|
ExpKnobComponent(
|
||||||
|
value = VstStringWorklet.volume,
|
||||||
|
label = "Volume",
|
||||||
|
minValue = 0.005,
|
||||||
|
maxValue = 1.0,
|
||||||
|
step = 5.0 / 127.0,
|
||||||
|
width = 100,
|
||||||
|
height = 120,
|
||||||
|
) { value ->
|
||||||
|
VstStringWorklet.volume = value
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
include(WaveformView)
|
||||||
|
include(playString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object MainDivCss : CssName
|
||||||
|
object ActiveCss : CssName
|
||||||
|
object ButtonCss : CssName
|
||||||
|
object ButtonBarCss : CssName
|
||||||
|
object SelectedCss : CssName
|
||||||
|
object NoteBarCss : CssName
|
||||||
|
object ControlsCss : CssName
|
||||||
|
|
||||||
|
private fun css() {
|
||||||
|
defineCss {
|
||||||
|
select("*") {
|
||||||
|
select("*:before") {
|
||||||
|
select("*:after") {
|
||||||
|
boxSizing(BoxSizing.borderBox)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
select("html", "body") {
|
||||||
|
margin(0.px)
|
||||||
|
padding(0.px)
|
||||||
|
height(100.prc)
|
||||||
|
}
|
||||||
|
select("html", "body") {
|
||||||
|
backgroundColor(Css.currentStyle.mainBackgroundColor)
|
||||||
|
color(Css.currentStyle.mainFontColor)
|
||||||
|
|
||||||
|
fontFamily("JetbrainsMono, monospace")
|
||||||
|
fontSize(14.px)
|
||||||
|
fontWeight(FontWeight.bold)
|
||||||
|
|
||||||
|
//transition()
|
||||||
|
noTextSelect()
|
||||||
|
}
|
||||||
|
select("input", "textarea") {
|
||||||
|
backgroundColor(Css.currentStyle.inputBackgroundColor)
|
||||||
|
color(Css.currentStyle.mainFontColor)
|
||||||
|
border("none")
|
||||||
|
}
|
||||||
|
select(cls(ButtonCss)) {
|
||||||
|
margin(1.rem)
|
||||||
|
commonButton()
|
||||||
|
}
|
||||||
|
select(cls(ButtonBarCss)) {
|
||||||
|
margin(1.rem, 0.px)
|
||||||
|
commonButton()
|
||||||
|
}
|
||||||
|
select(cls(ActiveCss)) {
|
||||||
|
//backgroundColor(Css.currentStyle.selectedBackgroundColor)
|
||||||
|
}
|
||||||
|
select(cls(NoteBarCss)) {
|
||||||
|
minHeight(4.rem)
|
||||||
|
}
|
||||||
|
select(cls(MainDivCss)) {
|
||||||
|
margin(1.rem)
|
||||||
|
}
|
||||||
|
select("select") {
|
||||||
|
plain("appearance", "none")
|
||||||
|
border("0")
|
||||||
|
outline("0")
|
||||||
|
width(20.rem)
|
||||||
|
padding(0.5.rem, 2.rem, 0.5.rem, 0.5.rem)
|
||||||
|
backgroundImage("url('https://upload.wikimedia.org/wikipedia/commons/9/9d/Caret_down_font_awesome_whitevariation.svg')")
|
||||||
|
background("right 0.8em center/1.4em")
|
||||||
|
backgroundColor(Css.currentStyle.inputBackgroundColor)
|
||||||
|
color(Css.currentStyle.mainFontColor)
|
||||||
|
borderRadius(0.25.em)
|
||||||
|
}
|
||||||
|
select(ControlsCss.cls()) {
|
||||||
|
display(Display.flex)
|
||||||
|
flexDirection(FlexDirection.row)
|
||||||
|
justifyContent(JustifyContent.flexStart)
|
||||||
|
alignItems(AlignItems.center)
|
||||||
|
margin(1.rem)
|
||||||
|
padding(1.rem)
|
||||||
|
backgroundColor(Css.currentStyle.mainBackgroundColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Style.commonButton() {
|
||||||
|
display(Display.inlineBlock)
|
||||||
|
padding(1.rem)
|
||||||
|
backgroundColor(Css.currentStyle.buttonBackgroundColor)
|
||||||
|
borderColor(Css.currentStyle.buttonBorderColor)
|
||||||
|
borderWidth(Css.currentStyle.buttonBorderWidth)
|
||||||
|
color(Css.currentStyle.mainFontColor)
|
||||||
|
|
||||||
|
hover {
|
||||||
|
backgroundColor(Css.currentStyle.buttonBackgroundColor.hover())
|
||||||
|
}
|
||||||
|
and(SelectedCss.cls()) {
|
||||||
|
backgroundColor(Css.currentStyle.buttonBackgroundColor.hover().hover().hover())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package nl.astraeus.vst.string.view
|
||||||
|
|
||||||
|
import kotlinx.browser.window
|
||||||
|
import kotlinx.html.canvas
|
||||||
|
import kotlinx.html.div
|
||||||
|
import kotlinx.html.js.onClickFunction
|
||||||
|
import kotlinx.html.span
|
||||||
|
import nl.astraeus.komp.HtmlBuilder
|
||||||
|
import nl.astraeus.komp.Komponent
|
||||||
|
import nl.astraeus.komp.currentElement
|
||||||
|
import nl.astraeus.vst.Note
|
||||||
|
import nl.astraeus.vst.string.PhysicalString
|
||||||
|
import nl.astraeus.vst.string.audio.VstStringWorklet
|
||||||
|
import nl.astraeus.vst.string.view.MainView.ControlsCss
|
||||||
|
import nl.astraeus.vst.ui.components.KnobComponent
|
||||||
|
import nl.astraeus.vst.util.formatDouble
|
||||||
|
import org.w3c.dom.CanvasRenderingContext2D
|
||||||
|
import org.w3c.dom.HTMLCanvasElement
|
||||||
|
|
||||||
|
class PhysicalStringView(
|
||||||
|
val string: PhysicalString
|
||||||
|
) : Komponent() {
|
||||||
|
var context: CanvasRenderingContext2D? = null
|
||||||
|
var interval: Int = -1
|
||||||
|
var lastUpdateTime: Double = window.performance.now()
|
||||||
|
|
||||||
|
init {
|
||||||
|
window.requestAnimationFrame(::onAnimationFrame)
|
||||||
|
interval = window.setInterval({
|
||||||
|
if (context?.canvas?.isConnected == true) {
|
||||||
|
val now: Double = window.performance.now()
|
||||||
|
val time = now - lastUpdateTime
|
||||||
|
lastUpdateTime = now
|
||||||
|
|
||||||
|
string.update(time)
|
||||||
|
} else {
|
||||||
|
window.clearInterval(interval)
|
||||||
|
}
|
||||||
|
}, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onAnimationFrame(time: Double) {
|
||||||
|
draw()
|
||||||
|
|
||||||
|
window.requestAnimationFrame(::onAnimationFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun HtmlBuilder.render() {
|
||||||
|
div {
|
||||||
|
div(ControlsCss.name) {
|
||||||
|
include(
|
||||||
|
KnobComponent(
|
||||||
|
value = VstStringWorklet.damping,
|
||||||
|
label = "Damping",
|
||||||
|
minValue = 0.8,
|
||||||
|
maxValue = 1.0,
|
||||||
|
step = 0.2 / 127.0,
|
||||||
|
width = 100,
|
||||||
|
height = 120,
|
||||||
|
renderer = { formatDouble(it, 3) }
|
||||||
|
) { value ->
|
||||||
|
VstStringWorklet.damping = value
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
span {
|
||||||
|
+"Play C3"
|
||||||
|
onClickFunction = {
|
||||||
|
string.pluck(Note.C3, 1.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
+"Play C4"
|
||||||
|
onClickFunction = {
|
||||||
|
string.pluck(Note.C4, 1.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
canvas {
|
||||||
|
width = "1000"
|
||||||
|
height = "400"
|
||||||
|
context = (currentElement() as? HTMLCanvasElement)?.getContext("2d") as? CanvasRenderingContext2D
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun draw() {
|
||||||
|
val ctx = context
|
||||||
|
if (ctx != null && ctx.canvas.isConnected) {
|
||||||
|
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 = width / string.length
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.strokeStyle = "rgba(0, 255, 255, 0.5)"
|
||||||
|
for (i in 0 until string.length) {
|
||||||
|
ctx.moveTo(i * step, halfHeight)
|
||||||
|
ctx.lineTo(i * step, halfHeight + string.buffer[i] * halfHeight)
|
||||||
|
}
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package nl.astraeus.vst.string.view
|
||||||
|
|
||||||
|
import kotlinx.browser.window
|
||||||
|
import kotlinx.html.canvas
|
||||||
|
import kotlinx.html.div
|
||||||
|
import nl.astraeus.komp.HtmlBuilder
|
||||||
|
import nl.astraeus.komp.Komponent
|
||||||
|
import nl.astraeus.komp.currentElement
|
||||||
|
import nl.astraeus.vst.string.audio.VstStringWorklet
|
||||||
|
import org.khronos.webgl.get
|
||||||
|
import org.w3c.dom.CanvasRenderingContext2D
|
||||||
|
import org.w3c.dom.HTMLCanvasElement
|
||||||
|
|
||||||
|
object WaveformView : Komponent() {
|
||||||
|
|
||||||
|
init {
|
||||||
|
window.requestAnimationFrame(::onAnimationFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAnimationFrame(time: Double) {
|
||||||
|
if (VstStringWorklet.recording == null) {
|
||||||
|
VstStringWorklet.postMessage("start_recording")
|
||||||
|
}
|
||||||
|
|
||||||
|
window.requestAnimationFrame(::onAnimationFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun HtmlBuilder.render() {
|
||||||
|
div {
|
||||||
|
if (VstStringWorklet.recording != null) {
|
||||||
|
canvas {
|
||||||
|
width = "1000"
|
||||||
|
height = "400"
|
||||||
|
val ctx = (currentElement() as? HTMLCanvasElement)?.getContext("2d") as? CanvasRenderingContext2D
|
||||||
|
val data = VstStringWorklet.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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VstStringWorklet.recording = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
120
src/jsMain/kotlin/nl/astraeus/vst/string/ws/WebsocketClient.kt
Normal file
120
src/jsMain/kotlin/nl/astraeus/vst/string/ws/WebsocketClient.kt
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
package nl.astraeus.vst.string.ws
|
||||||
|
|
||||||
|
import kotlinx.browser.window
|
||||||
|
import nl.astraeus.vst.string.PatchDTO
|
||||||
|
import nl.astraeus.vst.string.audio.VstStringWorklet
|
||||||
|
import nl.astraeus.vst.string.midi.Midi
|
||||||
|
import nl.astraeus.vst.string.view.MainView
|
||||||
|
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)
|
||||||
|
VstStringWorklet.load(patch)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
1622
src/jvmMain/java/BarWavesCanvas.java
Normal file
1622
src/jvmMain/java/BarWavesCanvas.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,2 +0,0 @@
|
|||||||
package nl.astraeus.vst.chip
|
|
||||||
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package nl.astraeus.vst.chip
|
|
||||||
|
|
||||||
import io.undertow.Undertow
|
|
||||||
import io.undertow.UndertowOptions
|
|
||||||
|
|
||||||
fun main() {
|
|
||||||
Thread.setDefaultUncaughtExceptionHandler { _, e ->
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
|
|
||||||
val server = Undertow.builder()
|
|
||||||
.addHttpListener(Settings.port, "localhost")
|
|
||||||
.setIoThreads(4)
|
|
||||||
.setHandler(RequestHandler)
|
|
||||||
.setServerOption(UndertowOptions.SHUTDOWN_TIMEOUT, 1000)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
println("Starting server at port ${Settings.port}...")
|
|
||||||
server?.start()
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package nl.astraeus.vst.chip
|
|
||||||
|
|
||||||
import io.undertow.server.HttpHandler
|
|
||||||
import io.undertow.server.HttpServerExchange
|
|
||||||
import io.undertow.server.handlers.resource.PathResourceManager
|
|
||||||
import io.undertow.server.handlers.resource.ResourceHandler
|
|
||||||
import java.nio.file.Paths
|
|
||||||
|
|
||||||
object RequestHandler : HttpHandler {
|
|
||||||
val resourceHandler = ResourceHandler(PathResourceManager(Paths.get("web")))
|
|
||||||
|
|
||||||
override fun handleRequest(exchange: HttpServerExchange) {
|
|
||||||
resourceHandler.handleRequest(exchange)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
package nl.astraeus.vst.chip
|
|
||||||
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileInputStream
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
object Settings {
|
|
||||||
var runningAsRoot: Boolean = false
|
|
||||||
var port = 9000
|
|
||||||
var sslPort = 8443
|
|
||||||
var connectionTimeout = 30000
|
|
||||||
|
|
||||||
var jdbcDriver = "nl.astraeus.jdbc.Driver"
|
|
||||||
var jdbcConnectionUrl = "jdbc:stat:webServerPort=6001:jdbc:sqlite:data/srp.db"
|
|
||||||
var jdbcUser = "sa"
|
|
||||||
var jdbcPassword = ""
|
|
||||||
|
|
||||||
var adminUser = "rnentjes"
|
|
||||||
var adminPassword = "9/SG_Bd}9gWz~?j\\A.U]n9]OO"
|
|
||||||
|
|
||||||
fun getPropertiesFromFile(filename: String): Properties? {
|
|
||||||
val propertiesFile = File(filename)
|
|
||||||
return if (propertiesFile.exists()) {
|
|
||||||
val properties = Properties()
|
|
||||||
FileInputStream(propertiesFile).use {
|
|
||||||
properties.load(it)
|
|
||||||
}
|
|
||||||
properties
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun readProperties(args: Array<String>) {
|
|
||||||
val filename = if (args.isNotEmpty()) args[0] else "srp.properties"
|
|
||||||
val properties = getPropertiesFromFile(filename) ?: return // return if properties couldn't be loaded
|
|
||||||
|
|
||||||
runningAsRoot = properties.getProperty("runningAsRoot", runningAsRoot.toString()).toBoolean()
|
|
||||||
port = properties.getProperty("port", port.toString()).toInt()
|
|
||||||
sslPort = properties.getProperty("sslPort", sslPort.toString()).toInt()
|
|
||||||
connectionTimeout = properties.getProperty("connectionTimeout", connectionTimeout.toString()).toInt()
|
|
||||||
jdbcDriver = properties.getProperty("jdbcDriver", jdbcDriver)
|
|
||||||
jdbcConnectionUrl = properties.getProperty("jdbcConnectionUrl", jdbcConnectionUrl)
|
|
||||||
jdbcUser = properties.getProperty("jdbcUser", jdbcUser)
|
|
||||||
jdbcPassword = properties.getProperty("jdbcPassword", jdbcPassword)
|
|
||||||
|
|
||||||
adminUser = properties.getProperty("adminUser", adminUser)
|
|
||||||
adminPassword = properties.getProperty("adminPassword", adminPassword)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
25
src/jvmMain/kotlin/nl/astraeus/vst/string/Main.kt
Normal file
25
src/jvmMain/kotlin/nl/astraeus/vst/string/Main.kt
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package nl.astraeus.vst.string
|
||||||
|
|
||||||
|
import nl.astraeus.vst.base.Settings
|
||||||
|
import nl.astraeus.vst.base.db.Database
|
||||||
|
import nl.astraeus.vst.base.web.UndertowServer
|
||||||
|
import nl.astraeus.vst.string.logger.LogLevel
|
||||||
|
import nl.astraeus.vst.string.logger.Logger
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
Logger.level = LogLevel.DEBUG
|
||||||
|
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler { _, e ->
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
Settings.port = 9004
|
||||||
|
Settings.jdbcStatsPort = 6004
|
||||||
|
|
||||||
|
Database.start()
|
||||||
|
|
||||||
|
UndertowServer.start(
|
||||||
|
"Vst String",
|
||||||
|
"/vst-string-worklet-ui.js"
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<body>
|
|
||||||
<script type="application/javascript" src="vst-chip-worklet-ui.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
21
version.gradle.kts
Normal file
21
version.gradle.kts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import java.util.Date
|
||||||
|
import java.util.Properties
|
||||||
|
|
||||||
|
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