Compare commits
14 Commits
0281d2751f
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 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>
|
||||||
|
|||||||
@@ -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!")
|
||||||
|
}
|
||||||
108
build.gradle.kts
108
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +32,7 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jvm{
|
jvm {
|
||||||
withJava()
|
withJava()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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,12 @@
|
|||||||
group = "nl.astraeus"
|
group = "nl.astraeus"
|
||||||
version = "1.0.0-SNAPSHOT"
|
version = "0.1.0"
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
mavenLocal()
|
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.0.21"
|
||||||
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