1 Commits

Author SHA1 Message Date
b3cd36b8f8 Remove dependency 2024-06-27 10:07:45 +02:00
38 changed files with 843 additions and 1563 deletions

2
.gitignore vendored
View File

@@ -41,7 +41,7 @@ bin/
### Mac OS ### ### Mac OS ###
.DS_Store .DS_Store
/web web
.kotlin .kotlin
.idea .idea

View File

@@ -1,6 +1,8 @@
<component name="ArtifactManager"> <component name="ArtifactManager">
<artifact type="jar" name="audio-worklet-js-1.0.0-SNAPSHOT"> <artifact type="jar" name="audio-worklet-js-1.0.0-SNAPSHOT">
<output-path>$PROJECT_DIR$/audio-worklet/build/libs</output-path> <output-path>$PROJECT_DIR$/audio-worklet/build/libs</output-path>
<root id="archive" name="audio-worklet-js-1.0.0-SNAPSHOT.jar" /> <root id="archive" name="audio-worklet-js-1.0.0-SNAPSHOT.jar">
<element id="module-output" name="vst-chip.audio-worklet.jsMain" />
</root>
</artifact> </artifact>
</component> </component>

View File

@@ -1,6 +1,8 @@
<component name="ArtifactManager"> <component name="ArtifactManager">
<artifact type="jar" name="audio-worklet-jvm-1.0.0-SNAPSHOT"> <artifact type="jar" name="audio-worklet-jvm-1.0.0-SNAPSHOT">
<output-path>$PROJECT_DIR$/audio-worklet/build/libs</output-path> <output-path>$PROJECT_DIR$/audio-worklet/build/libs</output-path>
<root id="archive" name="audio-worklet-jvm-1.0.0-SNAPSHOT.jar" /> <root id="archive" name="audio-worklet-jvm-1.0.0-SNAPSHOT.jar">
<element id="module-output" name="vst-chip.audio-worklet.jvmMain" />
</root>
</artifact> </artifact>
</component> </component>

View File

@@ -1,6 +1,8 @@
<component name="ArtifactManager"> <component name="ArtifactManager">
<artifact type="jar" name="common-js-1.0.0-SNAPSHOT"> <artifact type="jar" name="common-js-1.0.0-SNAPSHOT">
<output-path>$PROJECT_DIR$/common/build/libs</output-path> <output-path>$PROJECT_DIR$/common/build/libs</output-path>
<root id="archive" name="common-js-1.0.0-SNAPSHOT.jar" /> <root id="archive" name="common-js-1.0.0-SNAPSHOT.jar">
<element id="module-output" name="vst-chip.common.jsMain" />
</root>
</artifact> </artifact>
</component> </component>

View File

@@ -1,6 +1,8 @@
<component name="ArtifactManager"> <component name="ArtifactManager">
<artifact type="jar" name="common-jvm-1.0.0-SNAPSHOT"> <artifact type="jar" name="common-jvm-1.0.0-SNAPSHOT">
<output-path>$PROJECT_DIR$/common/build/libs</output-path> <output-path>$PROJECT_DIR$/common/build/libs</output-path>
<root id="archive" name="common-jvm-1.0.0-SNAPSHOT.jar" /> <root id="archive" name="common-jvm-1.0.0-SNAPSHOT.jar">
<element id="module-output" name="vst-chip.common.jvmMain" />
</root>
</artifact> </artifact>
</component> </component>

View File

@@ -1,6 +1,8 @@
<component name="ArtifactManager"> <component name="ArtifactManager">
<artifact type="jar" name="vst-chip-js-1.0.0-SNAPSHOT"> <artifact type="jar" name="vst-chip-js-1.0.0-SNAPSHOT">
<output-path>$PROJECT_DIR$/build/libs</output-path> <output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="vst-chip-js-1.0.0-SNAPSHOT.jar" /> <root id="archive" name="vst-chip-js-1.0.0-SNAPSHOT.jar">
<element id="module-output" name="vst-chip.jsMain" />
</root>
</artifact> </artifact>
</component> </component>

View File

@@ -1,6 +1,8 @@
<component name="ArtifactManager"> <component name="ArtifactManager">
<artifact type="jar" name="vst-chip-jvm-1.0.0-SNAPSHOT"> <artifact type="jar" name="vst-chip-jvm-1.0.0-SNAPSHOT">
<output-path>$PROJECT_DIR$/build/libs</output-path> <output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="vst-chip-jvm-1.0.0-SNAPSHOT.jar" /> <root id="archive" name="vst-chip-jvm-1.0.0-SNAPSHOT.jar">
<element id="module-output" name="vst-chip.jvmMain" />
</root>
</artifact> </artifact>
</component> </component>

1
.idea/gradle.xml generated
View File

@@ -10,6 +10,7 @@
<set> <set>
<option value="$PROJECT_DIR$" /> <option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/audio-worklet" /> <option value="$PROJECT_DIR$/audio-worklet" />
<option value="$PROJECT_DIR$/common" />
</set> </set>
</option> </option>
</GradleProjectSettings> </GradleProjectSettings>

7
.idea/jsLibraryMappings.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<excludedPredefinedLibrary name="vst-chip/build/js/node_modules" />
<excludedPredefinedLibrary name="vst-chip/build/js/packages/vst-base-test/node_modules" />
</component>
</project>

2
.idea/misc.xml generated
View File

@@ -4,7 +4,7 @@
<component name="FrameworkDetectionExcludesConfiguration"> <component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" /> <file type="web" url="file://$PROJECT_DIR$" />
</component> </component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="temurin-21" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" /> <output url="file://$PROJECT_DIR$/out" />
</component> </component>
<component name="accountSettings"> <component name="accountSettings">

View File

@@ -1,27 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Main [jvm]" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName"/>
<option name="externalProjectPath" value="$PROJECT_DIR$"/>
<option name="externalSystemIdString" value="GRADLE"/>
<option name="scriptParameters" value="-DmainClass=nl.astraeus.vst.chip.MainKt --quiet"/>
<option name="taskDescriptions">
<list/>
</option>
<option name="taskNames">
<list>
<option value="jvmRun"/>
</list>
</option>
<option name="vmOptions"/>
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<method v="2">
<option name="Gradle.BeforeRunTask" enabled="true" tasks="clean build" externalProjectPath="$PROJECT_DIR$"
vmOptions="" scriptParameters=""/>
</method>
</configuration>
</component>

View File

@@ -1,7 +1,3 @@
@file:OptIn(ExperimentalKotlinGradlePluginApi::class, ExperimentalDistributionDsl::class)
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalDistributionDsl
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackOutput import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackOutput
buildscript { buildscript {
@@ -40,9 +36,13 @@ kotlin {
sourceSets { sourceSets {
val commonMain by getting { val commonMain by getting {
dependencies { dependencies {
implementation("nl.astraeus:vst-worklet-base:1.0.0-SNAPSHOT") implementation(project(":common"))
}
}
val jsMain by getting {
dependencies {
implementation(project(":common"))
} }
} }
val jsMain by getting
} }
} }

View File

@@ -0,0 +1,45 @@
package nl.astraeus.vst
import org.khronos.webgl.Float32Array
import org.w3c.dom.MessagePort
enum class AutomationRate(
val rate: String
) {
A_RATE("a-rate"),
K_RATE("k-rate")
}
interface AudioParam {
var value: Double
var automationRate: AutomationRate
val defaultValue: Double
val minValue: Double
val maxValue: Double
}
interface AudioParamMap {
operator fun get(name: String): AudioParam
}
abstract external class AudioWorkletProcessor {
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/AudioWorkletNode/parameters) */
//val parameters: AudioParamMap;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/AudioWorkletNode/port) */
@JsName("port")
val port: MessagePort
@JsName("process")
open fun process(
inputs: Array<Array<Float32Array>>,
outputs: Array<Array<Float32Array>>,
parameters: dynamic
): Boolean {
definedExternally
}
}
external fun registerProcessor(name: String, processorCtor: JsClass<*>)
external val sampleRate: Int
external val currentTime: Double

View File

@@ -2,11 +2,11 @@
package nl.astraeus.vst.chip package nl.astraeus.vst.chip
import nl.astraeus.vst.ADSR
import nl.astraeus.vst.AudioWorkletProcessor import nl.astraeus.vst.AudioWorkletProcessor
import nl.astraeus.vst.currentTime import nl.astraeus.vst.Note
import nl.astraeus.vst.registerProcessor import nl.astraeus.vst.registerProcessor
import nl.astraeus.vst.sampleRate import nl.astraeus.vst.sampleRate
import org.khronos.webgl.ArrayBuffer
import org.khronos.webgl.Float32Array import org.khronos.webgl.Float32Array
import org.khronos.webgl.Int32Array import org.khronos.webgl.Int32Array
import org.khronos.webgl.Uint8Array import org.khronos.webgl.Uint8Array
@@ -14,36 +14,39 @@ import org.khronos.webgl.get
import org.khronos.webgl.set import org.khronos.webgl.set
import org.w3c.dom.MessageEvent import org.w3c.dom.MessageEvent
import kotlin.math.PI import kotlin.math.PI
import kotlin.math.min
import kotlin.math.sin import kotlin.math.sin
val POLYPHONICS = 10 val POLYPHONICS = 10
val PI2 = PI * 2 val PI2 = PI * 2
@ExperimentalJsExport
@JsExport
enum class NoteState {
ON,
RELEASED,
OFF
}
@ExperimentalJsExport @ExperimentalJsExport
@JsExport @JsExport
class PlayingNote( class PlayingNote(
val note: Int, val note: Int,
var velocity: Int = 0 var velocity: Int = 0
) { ) {
val noteObj = Note.fromMidi(note)
fun retrigger(velocity: Int) { fun retrigger(velocity: Int) {
this.velocity = velocity this.velocity = velocity
state = NoteState.ON
sample = 0 sample = 0
noteStart = currentTime attackSamples = 2500
noteRelease = null releaseSamples = 10000
for (i in 0 until combDelayBuffer.length) {
combDelayBuffer[i] = 0f
}
} }
var noteStart = currentTime var state = NoteState.OFF
var noteRelease: Double? = null
var cycleOffset = 0.0 var cycleOffset = 0.0
var sample = 0 var sample = 0
var attackSamples = 2500
var releaseSamples = 10000
var actualVolume = 0f var actualVolume = 0f
val combDelayBuffer = Float32Array((sampleRate / noteObj.freq).toInt())
} }
enum class Waveform { enum class Waveform {
@@ -53,123 +56,63 @@ enum class Waveform {
SAWTOOTH SAWTOOTH
} }
@ExperimentalJsExport
@JsExport
enum class RecordingState {
STOPPED,
WAITING_TO_START,
RECORDING
}
@ExperimentalJsExport @ExperimentalJsExport
@JsExport @JsExport
class VstChipProcessor : AudioWorkletProcessor() { class VstChipProcessor : AudioWorkletProcessor() {
var midiChannel = 0 val notes = Array(POLYPHONICS) {
val notes = Array<PlayingNote?>(POLYPHONICS) { null } PlayingNote(
0
)
}
var waveform = Waveform.SINE.ordinal var waveform = Waveform.SINE.ordinal
var volume = 0.75f
var dutyCycle = 0.5 var dutyCycle = 0.5
var fmFreq = 0.0 var fmFreq = 0.0
var fmAmp = 0.0 var fmAmp = 0.0
var amFreq = 0.0 var amFreq = 0.0
var amAmp = 0.0 var amAmp = 0.0
var attack = 0.1
var decay = 0.2
var sustain = 0.5
var release = 0.2
val recordingBuffer = Float32Array(sampleRate / 60)
var recordingState = RecordingState.STOPPED
var recordingSample = 0
var recordingStart = 0
val sampleLength = 1 / sampleRate.toDouble() val sampleLength = 1 / sampleRate.toDouble()
val rightDelayBuffer = Float32Array(sampleRate)
val leftDelayBuffer = Float32Array(sampleRate)
var delayIndex = 0
var delay = 0.0
var delayDepth = 0.0
var feedback = 0.0
init { init {
this.port.onmessage = ::handleMessage this.port.onmessage = ::handleMessage
Note.updateSampleRate(sampleRate) Note.updateSampleRate(sampleRate)
} }
private fun handleMessage(message: MessageEvent) { private fun handleMessage(message: MessageEvent) {
//console.log("VstChipProcessor: Received message:", message.data) //console.log("VstChipProcessor: Received message", message)
val data = message.data val data = message.data
try { when (data) {
when (data) { "test_on" -> {
is String -> { playMidi(Int32Array(arrayOf(0x90, 60, 64)))
when (data) {
"start_recording" -> {
port.postMessage(recordingBuffer)
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")
}
} else if (data.startsWith("waveform")) {
val parts = data.split('\n')
if (parts.size == 2) {
waveform = parts[1].toInt()
println("Setting waveform: $waveform")
}
}
}
}
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) { "test_off" -> {
console.log(e.message, e) playMidi(Int32Array(arrayOf(0x90, 60, 0)))
}
is String -> {
}
is ArrayBuffer -> {
}
is Uint8Array -> {
val data32 = Int32Array(data.length)
for (i in 0 until data.length) {
data32[i] = (data[i].toInt() and 0xff)
}
playMidi(data32)
}
is Int32Array -> {
playMidi(data)
}
else ->
console.error("Don't kow how to handle message", message)
} }
} }
private fun playMidi(bytes: Int32Array) { private fun playMidi(bytes: Int32Array) {
console.log("playMidi", bytes)
if (bytes.length > 0) { 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) //console.log("Received", bytes)
when (cmdByte) { when(bytes[0]) {
0x90 -> { 0x90 -> {
if (bytes.length == 3) { if (bytes.length == 3) {
val note = bytes[1] val note = bytes[1]
@@ -207,65 +150,24 @@ class VstChipProcessor : AudioWorkletProcessor() {
val value = bytes[2] val value = bytes[2]
when (knob) { when (knob) {
7 -> { 0x4a -> {
volume = value / 127f
}
0x47 -> {
dutyCycle = value / 127.0 dutyCycle = value / 127.0
} }
0x40 -> { 0x4b -> {
fmFreq = value / 127.0 fmFreq = value / 127.0
} }
0x41 -> { 0x4c -> {
fmAmp = value / 127.0 fmAmp = value / 127.0
} }
0x42 -> { 0x47 -> {
amFreq = value / 127.0 amFreq = value / 127.0
} }
0x43 -> {
amAmp = value / 127.0
}
0x49 -> {
attack = value / 127.0
}
0x4b -> {
decay = value / 127.0
}
0x46 -> {
sustain = value / 127.0
}
0x48 -> { 0x48 -> {
release = value / 127.0 amAmp = value / 127.0
}
0x4e -> {
delay = value / 127.0
println("Setting delay $delay")
}
0x4f -> {
delayDepth = value / 127.0
println("Setting delayDepth $delayDepth")
}
0x50 -> {
feedback = value / 127.0
println("Setting feedback $delayDepth")
}
123 -> {
for (note in notes) {
note?.noteRelease = currentTime
}
} }
} }
} }
@@ -285,17 +187,21 @@ class VstChipProcessor : AudioWorkletProcessor() {
private fun noteOn(note: Int, velocity: Int) { private fun noteOn(note: Int, velocity: Int) {
for (i in 0 until POLYPHONICS) { for (i in 0 until POLYPHONICS) {
if (notes[i]?.note == note) { if (notes[i].note == note) {
notes[i]?.retrigger(velocity) notes[i].retrigger(velocity)
return return
} }
} }
for (i in 0 until POLYPHONICS) { for (i in 0 until POLYPHONICS) {
if (notes[i] == null) { if (notes[i].state == NoteState.OFF) {
notes[i] = PlayingNote( notes[i] = PlayingNote(
note, note,
velocity velocity
) )
notes[i].state = NoteState.ON
val n = Note.fromMidi(note)
//console.log("Playing note: ${n.sharp} (${n.freq})")
break break
} }
} }
@@ -303,162 +209,79 @@ class VstChipProcessor : AudioWorkletProcessor() {
private fun noteOff(note: Int) { private fun noteOff(note: Int) {
for (i in 0 until POLYPHONICS) { for (i in 0 until POLYPHONICS) {
if (notes[i]?.note == note) { if (notes[i].note == note && notes[i].state == NoteState.ON) {
notes[i]?.noteRelease = currentTime notes[i].state = NoteState.RELEASED
break break
} }
} }
} }
override fun process( override fun process (
inputs: Array<Array<Float32Array>>, inputs: Array<Array<Float32Array>>,
outputs: Array<Array<Float32Array>>, outputs: Array<Array<Float32Array>>,
parameters: dynamic parameters: dynamic
): Boolean { ) : Boolean {
val samples = outputs[0][0].length val samples = outputs[0][0].length
val left = outputs[0][0] val left = outputs[0][0]
val right = outputs[0][1] val right = outputs[0][1]
var lowestNote = 200
for (note in notes) { for (note in notes) {
if (note != null) { if (note.state != NoteState.OFF) {
lowestNote = min(lowestNote, note.note) val sampleDelta = Note.fromMidi(note.note).sampleDelta
}
}
if (lowestNote == 200 && recordingState == RecordingState.WAITING_TO_START) {
recordingState = RecordingState.RECORDING
recordingSample = 0
recordingStart = 0
}
for ((index, note) in notes.withIndex()) {
if (note != null) {
val midiNote = Note.fromMidi(note.note)
val sampleDelta = midiNote.sampleDelta
for (i in 0 until samples) { for (i in 0 until samples) {
var targetVolume = note.velocity / 127f * 10f var targetVolume = note.velocity / 127f
targetVolume *= ADSR.calculate( if (note.state == NoteState.ON && note.sample < note.attackSamples) {
attack, note.attackSamples--
decay, targetVolume *= ( 1f - (note.attackSamples / 2500f))
sustain, } else if (note.state == NoteState.RELEASED) {
release, note.releaseSamples--
note.noteStart, targetVolume *= (note.releaseSamples / 10000f)
currentTime, }
note.noteRelease note.actualVolume += (targetVolume - note.actualVolume) * 0.0001f
).toFloat()
note.actualVolume += (targetVolume - note.actualVolume) * 0.01f
if (note.noteRelease != null && note.actualVolume <= 0.01) { if (note.state == NoteState.RELEASED && note.actualVolume <= 0) {
notes[index] = null note.state = NoteState.OFF
} }
var cycleOffset = note.cycleOffset var cycleOffset = note.cycleOffset
val fmModulation = val fmModulation = sin(sampleLength * fmFreq * 10f * PI2 * note.sample).toFloat() * fmAmp * 5f
sampleDelta + (sin(fmFreq * 1000f * PI2 * (note.sample / sampleRate.toDouble())).toFloat() * (100f * fmAmp * sampleDelta)) val amModulation = 1f + (sin(sampleLength * amFreq * 10f * PI2 * note.sample) * amAmp).toFloat()
val amModulation = cycleOffset += fmModulation
1f + (sin(sampleLength * amFreq * 1000f * PI2 * note.sample) * amAmp).toFloat()
cycleOffset = if (cycleOffset < dutyCycle) {
cycleOffset / dutyCycle / 2.0
} else {
0.5 + ((cycleOffset - dutyCycle) / (1.0 - dutyCycle) / 2.0)
}
val waveValue: Float = when (waveform) { val waveValue: Float = when (waveform) {
0 -> { 0 -> {
sin(cycleOffset * PI2).toFloat() sin(cycleOffset * PI2).toFloat()
} }
1 -> { 1 -> {
if (cycleOffset < 0.5) { if (cycleOffset < dutyCycle) { 1f } else { -1f }
1f
} else {
-1f
}
} }
2 -> when { 2 -> when {
cycleOffset < 0.25 -> 4 * cycleOffset cycleOffset < 0.25 -> 4 * cycleOffset
cycleOffset < 0.75 -> 2 - 4 * cycleOffset cycleOffset < 0.75 -> 2 - 4 * cycleOffset
else -> 4 * cycleOffset - 4 else -> 4 * cycleOffset - 4
}.toFloat() }.toFloat()
3 -> { 3 -> {
((cycleOffset * 2f) - 1f).toFloat() ((cycleOffset * 2f) - 1f).toFloat()
} }
else -> { else -> {
if (cycleOffset < 0.5) { if (cycleOffset < 0.5) { 1f } else { -1f }
1f
} else {
-1f
}
} }
} }
left[i] = left[i] + waveValue * note.actualVolume * volume * amModulation left[i] = left[i] + waveValue * note.actualVolume * 0.3f * amModulation
right[i] = right[i] + waveValue * note.actualVolume * volume * amModulation right[i] = right[i] + waveValue * note.actualVolume * 0.3f * amModulation
note.cycleOffset += sampleDelta
// comb filter delay if (cycleOffset > 1f) {
val delaySampleIndex =
(note.sample + note.combDelayBuffer.length) % note.combDelayBuffer.length
left[i] = left[i] + (note.combDelayBuffer[delaySampleIndex] * feedback.toFloat())
right[i] = right[i] + (note.combDelayBuffer[delaySampleIndex] * feedback.toFloat())
note.combDelayBuffer[delaySampleIndex] = (left[i] + right[i]) / 2f
// end - comb filter delay
note.cycleOffset += sampleDelta + fmModulation
if (note.cycleOffset > 1f) {
note.cycleOffset -= 1f note.cycleOffset -= 1f
if (note.note == lowestNote && recordingState == RecordingState.WAITING_TO_START) {
recordingState = RecordingState.RECORDING
recordingSample = 0
recordingStart = i
}
} }
note.sample++ note.sample++
} }
} }
} }
// if sin enable
for (i in 0 until samples) {
left[i] = sin(left[i] * PI2).toFloat()
right[i] = sin(right[i] * PI2).toFloat()
}
val delaySamples = (delay * leftDelayBuffer.length).toInt()
for (i in 0 until samples) {
if (delaySamples > 0) {
val delaySampleIndex = (delayIndex + sampleRate - delaySamples) % sampleRate
left[i] = left[i] + (leftDelayBuffer[delaySampleIndex] * delayDepth.toFloat())
right[i] = right[i] + (rightDelayBuffer[delaySampleIndex] * delayDepth.toFloat())
}
leftDelayBuffer[delayIndex] = left[i]
rightDelayBuffer[delayIndex++] = right[i]
delayIndex %= sampleRate
}
if (recordingState == RecordingState.RECORDING) {
for (i in recordingStart until samples) {
recordingBuffer[recordingSample] = (left[i] + right[i]) / 2f
if (recordingSample < recordingBuffer.length - 1) {
recordingSample++
} else {
recordingState = RecordingState.STOPPED
}
}
recordingStart = 0
}
return true return true
} }
} }

View File

@@ -1,196 +0,0 @@
package nl.astraeus.vst.chip
import nl.astraeus.vst.chip.Note.entries
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
import kotlin.math.round
/**
* User: rnentjes
* Date: 14-11-15
* Time: 11:50
*/
@ExperimentalJsExport
@JsExport
enum class Note(
val sharp: String,
val flat: String
) {
NO01("C--", "C--"),
NO02("C#-", "Db-"),
NO03("D--", "D--"),
NO04("D#-", "Eb-"),
NO05("E--", "E--"),
NO06("F--", "F--"),
NO07("F#-", "Gb-"),
NO08("G--", "G--"),
NO09("G#-", "Ab-"),
NO10("A--", "A--"),
NO11("A#-", "Bb-"),
NO12("B--", "B--"),
C0("C-0", "C-0"),
C0s("C#0", "Db0"),
D0("D-0", "D-0"),
D0s("D#0", "Eb0"),
E0("E-0", "E-0"),
F0("F-0", "F-0"),
F0s("F#0", "Gb0"),
G0("G-0", "G-0"),
G0s("G#0", "Ab0"),
A0("A-0", "A-0"),
A0s("A#0", "Bb0"),
B0("B-0", "B-0"),
C1("C-1", "C-1"),
C1s("C#1", "Db1"),
D1("D-1", "D-1"),
D1s("D#1", "Eb1"),
E1("E-1", "E-1"),
F1("F-1", "F-1"),
F1s("F#1", "Gb1"),
G1("G-1", "G-1"),
G1s("G#1", "Ab1"),
A1("A-1", "A-1"),
A1s("A#1", "Bb1"),
B1("B-1", "B-1"),
C2("C-2", "C-2"),
C2s("C#2", "Db2"),
D2("D-2", "D-2"),
D2s("D#2", "Eb2"),
E2("E-2", "E-2"),
F2("F-2", "F-2"),
F2s("F#2", "Gb2"),
G2("G-2", "G-2"),
G2s("G#2", "Ab2"),
A2("A-2", "A-2"),
A2s("A#2", "Bb2"),
B2("B-2", "B-2"),
C3("C-3", "C-3"),
C3s("C#3", "Db3"),
D3("D-3", "D-3"),
D3s("D#3", "Eb3"),
E3("E-3", "E-3"),
F3("F-3", "F-3"),
F3s("F#3", "Gb3"),
G3("G-3", "G-3"),
G3s("G#3", "Ab3"),
A3("A-3", "A-3"),
A3s("A#3", "Bb3"),
B3("B-3", "B-3"),
C4("C-4", "C-4"),
C4s("C#4", "Db4"),
D4("D-4", "D-4"),
D4s("D#4", "Eb4"),
E4("E-4", "E-4"),
F4("F-4", "F-4"),
F4s("F#4", "Gb4"),
G4("G-4", "G-4"),
G4s("G#4", "Ab4"),
A4("A-4", "A-4"),
A4s("A#4", "Bb4"),
B4("B-4", "B-4"),
C5("C-5", "C-5"),
C5s("C#5", "Db5"),
D5("D-5", "D-5"),
D5s("D#5", "Eb5"),
E5("E-5", "E-5"),
F5("F-5", "F-5"),
F5s("F#5", "Gb5"),
G5("G-5", "G-5"),
G5s("G#5", "Ab5"),
A5("A-5", "A-5"),
A5s("A#5", "Bb5"),
B5("B-5", "B-5"),
C6("C-6", "C-6"),
C6s("C#6", "Db6"),
D6("D-6", "D-6"),
D6s("D#6", "Eb6"),
E6("E-6", "E-6"),
F6("F-6", "F-6"),
F6s("F#6", "Gb6"),
G6("G-6", "G-6"),
G6s("G#6", "Ab6"),
A6("A-6", "A-6"),
A6s("A#6", "Bb6"),
B6("B-6", "B-6"),
C7("C-7", "C-7"),
C7s("C#7", "Db7"),
D7("D-7", "D-7"),
D7s("D#7", "Eb7"),
E7("E-7", "E-7"),
F7("F-7", "F-7"),
F7s("F#7", "Gb7"),
G7("G-7", "G-7"),
G7s("G#7", "Ab7"),
A7("A-7", "A-7"),
A7s("A#7", "Bb7"),
B7("B-7", "B-7"),
C8("C-8", "C-8"),
C8s("C#8", "Db8"),
D8("D-8", "D-8"),
D8s("D#8", "Eb8"),
E8("E-8", "E-8"),
F8("F-8", "F-8"),
F8s("F#8", "Gb8"),
G8("G-8", "G-8"),
G8s("G#8", "Ab8"),
A8("A-8", "A-8"),
A8s("A#8", "Bb8"),
B8("B-8", "B-8"),
C9("C-9", "C-9"),
C9s("C#9", "Db9"),
D9("D-9", "D-9"),
D9s("D#9", "Eb9"),
E9("E-9", "E-9"),
F9("F-9", "F-9"),
F9s("F#9", "Gb9"),
G9("G-9", "G-9"),
// out of midi range
//G9s("G#9","Ab9"),
//A9("A-9","A-9"),
//A9s("A#9","Bb9"),
//B9("B-9","B-9"),
NONE("---", "---"),
UP("^^^", "^^^"),
END("XXX", "XXX"),
;
// 69 = A4.ordinal
val freq: Double = round(440.0 * 2.0.pow((ordinal - 69) / 12.0)) // * 10000.0) / 10000.0
val cycleLength: Double = 1.0 / freq
var sampleDelta: Double = 0.0
fun transpose(semiNotes: Int): Note = if (ordinal >= C0.ordinal && ordinal <= G9.ordinal) {
var result = this.ordinal + semiNotes
result = min(result, G9.ordinal)
result = max(result, C0.ordinal)
fromMidi(result)
} else {
this
}
companion object {
fun fromMidi(midi: Int): Note {
// todo: add check
return entries[midi]
}
fun updateSampleRate(rate: Int) {
println("Setting sample rate to $rate")
for (note in Note.entries) {
note.sampleDelta = (1.0 / rate.toDouble()) / note.cycleLength
}
}
}
}
// freq = 10Hz
// cycleLength = 0.1
// sampleRate = 48000
// sampleDelta = 4800
// (1.0 / freq) * sampleRate

View File

@@ -1,181 +1,66 @@
import java.nio.file.Files import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackOutput
import java.nio.file.Paths
buildscript { buildscript {
apply(from = "common.gradle.kts") apply(from = "common.gradle.kts")
apply(from = "version.gradle.kts")
} }
plugins { plugins {
kotlin("multiplatform") kotlin("multiplatform")
id("maven-publish") kotlin("plugin.serialization")
application id("maven-publish")
application
} }
kotlin { kotlin {
js { js {
compilerOptions { compilerOptions {
target.set("es2015") target.set("es2015")
}
//useEsModules()
//useCommonJs()
binaries.executable()
browser {
commonWebpackConfig {
outputFileName = "vst-chip-worklet-ui.js"
sourceMaps = true
}
distribution {
outputDirectory.set(File("$projectDir/web/"))
}
}
} }
//useEsModules() jvm{
//useCommonJs() withJava()
binaries.executable()
browser {
commonWebpackConfig {
outputFileName = "vst-chip-worklet-ui.js"
sourceMaps = true
}
distribution {
outputDirectory.set(File("$projectDir/web/"))
}
}
}
/*
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
binaries.executable()
browser{
distribution {
outputDirectory.set(File("$projectDir/web/"))
}
}
mavenPublication {
groupId = group as String
pom { name = "${project.name}-wasm-js" }
}
}
*/
jvm {
withJava()
}
sourceSets {
val commonMain by getting {
dependencies {
//base
implementation("nl.astraeus:kotlin-css-generator:1.0.10")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.0")
implementation("nl.astraeus:vst-ui-base:1.1.2")
}
}
val jsMain by getting {
dependencies {
implementation("nl.astraeus:kotlin-komponent:1.2.4")
}
}
val jsTest by getting {
dependencies {
implementation(kotlin("test-js"))
}
}
/* val wasmJsMain by getting {
dependencies {
implementation("nl.astraeus:kotlin-komponent:1.2.4-SNAPSHOT")
implementation("nl.astraeus:vst-ui-base:1.0.1-SNAPSHOT")
}
}*/
val jvmMain by getting {
dependencies {
//base
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")
}
}
}
}
application {
mainClass.set("nl.astraeus.vst.chip.MainKt")
}
/* Hardcoded deploy configuration */
val deployDirectory = "vst-chip.midi-vst.com"
tasks.register<Copy>("unzipDistribution") {
mustRunAfter("removeSymbolicLink")
val zipDir = layout.projectDirectory.dir("build/distributions")
val zipFile = zipDir.file("${project.name}-${project.version}.zip")
val outputDir = file("/home/rnentjes/www/${deployDirectory}")
from(zipTree(zipFile))
into(outputDir)
}
tasks.register("createSymbolicLink") {
mustRunAfter("unzipDistribution")
doLast {
val targetDir =
Paths.get("/home/rnentjes/www/${deployDirectory}/${project.name}-${project.version}") // Directory to link to
val symlink =
Paths.get("/home/rnentjes/www/${deployDirectory}/${project.name}") // Path for the symbolic link
if (!Files.exists(targetDir)) {
throw IllegalArgumentException("Target directory does not exist: $targetDir")
} }
if (Files.exists(symlink)) { sourceSets {
println("Symbolic link already exists: $symlink") val commonMain by getting {
} else { dependencies {
Files.createSymbolicLink(symlink, targetDir) implementation(project(":common"))
println("Symbolic link created: $symlink -> $targetDir") //base
api("nl.astraeus:kotlin-css-generator:1.0.7")
}
}
val jsMain by getting {
dependencies {
//base
implementation("nl.astraeus:kotlin-komponent-js:1.2.2")
}
}
val jsTest by getting {
dependencies {
implementation(kotlin("test-js"))
}
}
val jvmMain by getting {
dependencies {
//base
implementation("io.undertow:undertow-core:2.3.13.Final")
implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.11.0")
}
}
} }
}
}
tasks.register<Copy>("copyWeb") {
val webDir = layout.projectDirectory.dir("web")
val outputDir = file("/home/rnentjes/www/${deployDirectory}/web")
from(webDir)
into(outputDir)
}
tasks.named<Task>("build") {
dependsOn("generateVersionProperties")
}
tasks.named("kotlinUpgradeYarnLock") {
mustRunAfter("clean")
}
tasks.named("build") {
mustRunAfter("kotlinUpgradeYarnLock")
}
tasks.named("build") {
mustRunAfter("kotlinUpgradeYarnLock")
}
tasks.named("copyWeb") {
mustRunAfter("build")
}
tasks.register("removeSymbolicLink") {
mustRunAfter("build")
doLast {
delete(layout.projectDirectory.file("/home/rnentjes/www/${deployDirectory}/${project.name}"))
}
}
tasks.register("deploy") {
dependsOn("clean")
dependsOn("kotlinUpgradeYarnLock")
dependsOn("build")
dependsOn("copyWeb")
dependsOn("removeSymbolicLink")
dependsOn("unzipDistribution")
dependsOn("createSymbolicLink")
} }

View File

@@ -1,12 +1,13 @@
group = "nl.astraeus" group = "nl.astraeus"
version = "0.1.0" version = "1.0.0-SNAPSHOT"
allprojects { allprojects {
repositories { repositories {
mavenLocal() mavenLocal()
mavenCentral() mavenCentral()
maven("https://reposilite.astraeus.nl/releases")
maven { maven {
url = uri("https://gitea.astraeus.nl:8443/api/packages/rnentjes/maven") url = uri("https://nexus.astraeus.nl/nexus/content/groups/public")
} }
} }
} }

32
common/build.gradle.kts Normal file
View File

@@ -0,0 +1,32 @@
@file:OptIn(ExperimentalKotlinGradlePluginApi::class)
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
buildscript {
apply(from = "../common.gradle.kts")
}
plugins {
kotlin("multiplatform")
}
kotlin {
js {
compilerOptions {
target.set("es2015")
}
browser()
}
jvm()
sourceSets {
val commonMain by getting {
dependencies {
}
}
val jsMain by getting {
dependencies {
}
}
}
}

View File

@@ -0,0 +1 @@
apply(from = "../settings.common.gradle.kts")

View File

@@ -0,0 +1,192 @@
package nl.astraeus.vst
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
import kotlin.math.round
/**
* User: rnentjes
* Date: 14-11-15
* Time: 11:50
*/
enum class Note(
val sharp: String,
val flat: String
) {
NO01("C--","C--"),
NO02("C#-","Db-"),
NO03("D--","D--"),
NO04("D#-","Eb-"),
NO05("E--","E--"),
NO06("F--","F--"),
NO07("F#-","Gb-"),
NO08("G--","G--"),
NO09("G#-","Ab-"),
NO10("A--","A--"),
NO11("A#-","Bb-"),
NO12("B--","B--"),
C0("C-0","C-0"),
C0s("C#0","Db0"),
D0("D-0","D-0"),
D0s("D#0","Eb0"),
E0("E-0","E-0"),
F0("F-0","F-0"),
F0s("F#0","Gb0"),
G0("G-0","G-0"),
G0s("G#0","Ab0"),
A0("A-0","A-0"),
A0s("A#0","Bb0"),
B0("B-0","B-0"),
C1("C-1","C-1"),
C1s("C#1","Db1"),
D1("D-1","D-1"),
D1s("D#1","Eb1"),
E1("E-1","E-1"),
F1("F-1","F-1"),
F1s("F#1","Gb1"),
G1("G-1","G-1"),
G1s("G#1","Ab1"),
A1("A-1","A-1"),
A1s("A#1","Bb1"),
B1("B-1","B-1"),
C2("C-2","C-2"),
C2s("C#2","Db2"),
D2("D-2","D-2"),
D2s("D#2","Eb2"),
E2("E-2","E-2"),
F2("F-2","F-2"),
F2s("F#2","Gb2"),
G2("G-2","G-2"),
G2s("G#2","Ab2"),
A2("A-2","A-2"),
A2s("A#2","Bb2"),
B2("B-2","B-2"),
C3("C-3","C-3"),
C3s("C#3","Db3"),
D3("D-3","D-3"),
D3s("D#3","Eb3"),
E3("E-3","E-3"),
F3("F-3","F-3"),
F3s("F#3","Gb3"),
G3("G-3","G-3"),
G3s("G#3","Ab3"),
A3("A-3","A-3"),
A3s("A#3","Bb3"),
B3("B-3","B-3"),
C4("C-4","C-4"),
C4s("C#4","Db4"),
D4("D-4","D-4"),
D4s("D#4","Eb4"),
E4("E-4","E-4"),
F4("F-4","F-4"),
F4s("F#4","Gb4"),
G4("G-4","G-4"),
G4s("G#4","Ab4"),
A4("A-4","A-4"),
A4s("A#4","Bb4"),
B4("B-4","B-4"),
C5("C-5","C-5"),
C5s("C#5","Db5"),
D5("D-5","D-5"),
D5s("D#5","Eb5"),
E5("E-5","E-5"),
F5("F-5","F-5"),
F5s("F#5","Gb5"),
G5("G-5","G-5"),
G5s("G#5","Ab5"),
A5("A-5","A-5"),
A5s("A#5","Bb5"),
B5("B-5","B-5"),
C6("C-6","C-6"),
C6s("C#6","Db6"),
D6("D-6","D-6"),
D6s("D#6","Eb6"),
E6("E-6","E-6"),
F6("F-6","F-6"),
F6s("F#6","Gb6"),
G6("G-6","G-6"),
G6s("G#6","Ab6"),
A6("A-6","A-6"),
A6s("A#6","Bb6"),
B6("B-6","B-6"),
C7("C-7","C-7"),
C7s("C#7","Db7"),
D7("D-7","D-7"),
D7s("D#7","Eb7"),
E7("E-7","E-7"),
F7("F-7","F-7"),
F7s("F#7","Gb7"),
G7("G-7","G-7"),
G7s("G#7","Ab7"),
A7("A-7","A-7"),
A7s("A#7","Bb7"),
B7("B-7","B-7"),
C8("C-8","C-8"),
C8s("C#8","Db8"),
D8("D-8","D-8"),
D8s("D#8","Eb8"),
E8("E-8","E-8"),
F8("F-8","F-8"),
F8s("F#8","Gb8"),
G8("G-8","G-8"),
G8s("G#8","Ab8"),
A8("A-8","A-8"),
A8s("A#8","Bb8"),
B8("B-8","B-8"),
C9("C-9","C-9"),
C9s("C#9","Db9"),
D9("D-9","D-9"),
D9s("D#9","Eb9"),
E9("E-9","E-9"),
F9("F-9","F-9"),
F9s("F#9","Gb9"),
G9("G-9","G-9"),
// out of midi range
//G9s("G#9","Ab9"),
//A9("A-9","A-9"),
//A9s("A#9","Bb9"),
//B9("B-9","B-9"),
NONE("---", "---"),
UP("^^^","^^^"),
END("XXX","XXX"),
;
// 69 = A4.ordinal
val freq: Double = round(440.0 * 2.0.pow((ordinal - 69)/12.0)) // * 10000.0) / 10000.0
val cycleLength: Double = 1.0 / freq
var sampleDelta: Double = 0.0
fun transpose(semiNotes: Int): Note = if (ordinal >= C0.ordinal && ordinal <= G9.ordinal) {
var result = this.ordinal + semiNotes
result = min(result, G9.ordinal)
result = max(result, C0.ordinal)
fromMidi(result)
} else {
this
}
companion object {
fun fromMidi(midi: Int): Note {
// todo: add check
return entries[midi]
}
fun updateSampleRate(rate: Int) {
println("Setting sample rate to $rate")
for (note in Note.entries) {
note.sampleDelta = (1.0 / rate.toDouble()) / note.cycleLength
}
}
}
}
// freq = 10Hz
// cycleLength = 0.1
// sampleRate = 48000
// sampleDelta = 4800
// (1.0 / freq) * sampleRate

View File

@@ -1,6 +1,8 @@
pluginManagement { pluginManagement {
plugins { plugins {
kotlin("multiplatform") version "2.0.21" kotlin("multiplatform") version "2.0.0"
kotlin("plugin.serialization") version "2.0.0"
id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0"
} }
repositories { repositories {
gradlePluginPortal() gradlePluginPortal()

View File

@@ -2,4 +2,5 @@ apply(from = "settings.common.gradle.kts")
rootProject.name = "vst-chip" rootProject.name = "vst-chip"
include(":common")
include(":audio-worklet") include(":audio-worklet")

View File

@@ -1,44 +0,0 @@
package nl.astraeus.vst.chip
import kotlin.js.ExperimentalJsExport
import kotlin.js.JsExport
import kotlin.js.JsName
@ExperimentalJsExport
@JsExport
data class PatchDTO(
@JsName("waveform")
val waveform: Int = 0,
@JsName("midiId")
val midiId: String = "",
@JsName("midiName")
val midiName: String = "",
@JsName("midiChannel")
var midiChannel: Int = 0,
@JsName("volume")
var volume: Double = 0.75,
@JsName("dutyCycle")
var dutyCycle: Double = 0.5,
@JsName("fmModFreq")
var fmModFreq: Double = 0.0,
@JsName("fmModAmp")
var fmModAmp: Double = 0.0,
@JsName("amModFreq")
var amModFreq: Double = 0.0,
@JsName("amModAmp")
var amModAmp: Double = 0.0,
@JsName("attack")
var attack: Double = 0.1,
@JsName("decay")
var decay: Double = 0.2,
@JsName("sustain")
var sustain: Double = 0.5,
@JsName("release")
var release: Double = 0.2,
@JsName("delay")
var delay: Double = 0.0,
@JsName("delayDepth")
var delayDepth: Double = 0.0,
@JsName("feedback")
var feedback: Double = 0.0,
)

View File

@@ -1,53 +0,0 @@
package nl.astraeus.vst.chip.logger
val log = Logger
enum class LogLevel {
TRACE,
DEBUG,
INFO,
WARN,
ERROR,
FATAL
}
object Logger {
var level: LogLevel = LogLevel.INFO
fun trace(message: () -> String?) {
if (level.ordinal <= LogLevel.TRACE.ordinal) {
println("TRACE: ${message()}")
}
}
fun debug(message: () -> String?) {
if (level.ordinal <= LogLevel.DEBUG.ordinal) {
println("DEBUG: ${message()}")
}
}
fun info(message: () -> String?) {
if (level.ordinal <= LogLevel.INFO.ordinal) {
println("INFO: ${message()}")
}
}
fun warn(e: Throwable? = null, message: () -> String?) {
if (level.ordinal <= LogLevel.WARN.ordinal) {
println("WARN: ${message()}")
e?.printStackTrace()
}
}
fun error(e: Throwable? = null, message: () -> String?) {
if (level.ordinal <= LogLevel.ERROR.ordinal) {
println("ERROR: ${message()}")
e?.printStackTrace()
}
}
fun fatal(e: Throwable, message: () -> String?) {
println("FATAL: ${message()}")
e.printStackTrace()
}
}

View File

@@ -1,24 +1,29 @@
package nl.astraeus.vst.chip package nl.astraeus.vst.chip
import kotlinx.browser.document import kotlinx.browser.document
import kotlinx.browser.window
import nl.astraeus.komp.Komponent import nl.astraeus.komp.Komponent
import nl.astraeus.komp.UnsafeMode import nl.astraeus.vst.chip.midi.Broadcaster
import nl.astraeus.vst.chip.logger.log import nl.astraeus.vst.chip.midi.MidiMessage
import nl.astraeus.vst.chip.midi.Midi import nl.astraeus.vst.chip.midi.Midi
import nl.astraeus.vst.chip.view.MainView import nl.astraeus.vst.chip.view.MainView
import nl.astraeus.vst.chip.ws.WebsocketClient import org.khronos.webgl.Uint8Array
import nl.astraeus.vst.ui.css.CssSettings
fun main() { fun main() {
CssSettings.shortId = false
CssSettings.preFix = "vst-chip"
Komponent.unsafeMode = UnsafeMode.UNSAFE_SVG_ONLY
Komponent.create(document.body!!, MainView) Komponent.create(document.body!!, MainView)
Midi.start() Midi.start()
WebsocketClient.connect { console.log("Performance", window.performance)
log.debug { "Connected to server" } Broadcaster.getChannel(0).postMessage(
} MidiMessage(
Uint8Array(arrayOf(0x80.toByte(), 60, 60)),
window.performance.now()
)
)
window.setInterval({
Broadcaster.sync()
}, 1000)
} }

View File

@@ -54,9 +54,6 @@ abstract class AudioNode(
abstract fun onMessage(message: MessageEvent) abstract fun onMessage(message: MessageEvent)
open fun postMessage(msg: Any) { open fun postMessage(msg: Any) {
if (port == null) {
console.log("postMessage port is NULL!")
}
port?.postMessage(msg) port?.postMessage(msg)
} }

View File

@@ -1,232 +1,14 @@
@file:OptIn(ExperimentalJsExport::class)
package nl.astraeus.vst.chip.audio package nl.astraeus.vst.chip.audio
import nl.astraeus.vst.chip.PatchDTO
import nl.astraeus.vst.chip.view.MainView
import nl.astraeus.vst.chip.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 org.w3c.dom.MessageEvent
import kotlin.experimental.and
object VstChipWorklet : AudioNode( object VstChipWorklet : AudioNode(
"/vst-chip-worklet.js", "vst-chip-worklet.js",
"vst-chip-processor" "vst-chip-processor"
) { ) {
var waveform: Int = 0
set(value) {
field = value
postMessage("waveform\n$value")
}
var midiChannel = 0
set(value) {
check(value in 0..15) {
"Midi channel must be between 0 and 15."
}
field = value
postMessage("set_channel\n${midiChannel}")
}
var volume = 0.75
set(value) {
field = value
super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 7, (value * 127).toInt())
)
}
var dutyCycle = 0.5
set(value) {
field = value
super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x47, (value * 127).toInt())
)
}
var fmModFreq = 0.0
set(value) {
field = value
super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x40, (value * 127).toInt())
)
}
var fmModAmp = 0.0
set(value) {
field = value
super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x41, (value * 127).toInt())
)
}
var amModFreq = 0.0
set(value) {
field = value
super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x42, (value * 127).toInt())
)
}
var amModAmp = 0.0
set(value) {
field = value
super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x43, (value * 127).toInt())
)
}
var feedback = 0.0
set(value) {
field = value
super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x50, (value * 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 attack = 0.1
set(value) {
field = value
super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x49, (value * 127).toInt())
)
}
var decay = 0.2
set(value) {
field = value
super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x4b, (value * 127).toInt())
)
}
var sustain = 0.5
set(value) {
field = value
super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x46, (value * 127).toInt())
)
}
var release = 0.2
set(value) {
field = value
super.postMessage(
uInt8ArrayOf(0xb0 + midiChannel, 0x48, (value * 127).toInt())
)
}
var recording: Float32Array? = null
override fun onMessage(message: MessageEvent) { override fun onMessage(message: MessageEvent) {
//console.log("Message from worklet: ", message) console.log("Message from worklet: ", message)
val data = message.data
if (data is Float32Array) {
this.recording = data
WaveformView.requestUpdate()
}
}
fun postDirectlyToWorklet(msg: Any) {
super.postMessage(msg)
}
override fun postMessage(msg: Any) {
if (msg is 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() -> {
dutyCycle = value / 127.0
MainView.requestUpdate()
}
0x40.toByte() -> {
fmModFreq = value / 127.0
MainView.requestUpdate()
}
0x41.toByte() -> {
fmModAmp = value / 127.0
MainView.requestUpdate()
}
0x42.toByte() -> {
amModFreq = value / 127.0
MainView.requestUpdate()
}
0x43.toByte() -> {
amModAmp = value / 127.0
MainView.requestUpdate()
}
}
}
fun load(patch: PatchDTO) {
waveform = patch.waveform
midiChannel = patch.midiChannel
volume = patch.volume
dutyCycle = patch.dutyCycle
fmModFreq = patch.fmModFreq
fmModAmp = patch.fmModAmp
amModFreq = patch.amModFreq
amModAmp = patch.amModAmp
attack = patch.attack
decay = patch.decay
sustain = patch.sustain
release = patch.release
delay = patch.delay
delayDepth = patch.delayDepth
feedback = patch.feedback
}
fun save(): PatchDTO {
return PatchDTO(
waveform = waveform,
midiChannel = midiChannel,
volume = volume,
dutyCycle = dutyCycle,
fmModFreq = fmModFreq,
fmModAmp = fmModAmp,
amModFreq = amModFreq,
amModAmp = amModAmp,
attack = attack,
decay = decay,
sustain = sustain,
release = release,
delay = delay,
delayDepth = delayDepth,
feedback = feedback
)
} }
} }

View File

@@ -37,6 +37,7 @@ external class MIDIOutput {
} }
object Midi { object Midi {
var inputChannel: Int = -1
var outputChannel: Int = -1 var outputChannel: Int = -1
var inputs = mutableListOf<MIDIInput>() var inputs = mutableListOf<MIDIInput>()
@@ -74,39 +75,6 @@ object Midi {
) )
} }
fun setInput(id: String, name: String = "") {
var selected = inputs.find { it.id == id }
if (selected == null) {
var maxMatchChar = 0
inputs.forEach {
val matchChars = matchChars(it.name, name)
if (matchChars > maxMatchChar) {
selected = it
maxMatchChar = matchChars
}
}
}
setInput(selected)
}
private fun matchChars(str1: String, str2: String): Int {
var result = 0
if (str1.length > str2.length) {
for (ch in str1.toCharArray()) {
if (str2.contains(ch)) {
result++
}
}
} else {
for (ch in str2.toCharArray()) {
if (str1.contains(ch)) {
result++
}
}
}
return result
}
fun setInput(input: MIDIInput?) { fun setInput(input: MIDIInput?) {
console.log("Setting input", input) console.log("Setting input", input)
currentInput?.close() currentInput?.close()
@@ -142,7 +110,7 @@ object Midi {
currentOutput?.open() currentOutput?.open()
} }
fun send(data: Uint8Array, timestamp: dynamic = null) { fun send(data: Uint8Array, timestamp: dynamic? = null) {
currentOutput?.send(data, timestamp) currentOutput?.send(data, timestamp)
} }

View File

@@ -1,26 +1,23 @@
@file:OptIn(ExperimentalJsExport::class)
package nl.astraeus.vst.chip.view package nl.astraeus.vst.chip.view
import daw.style.Css
import daw.style.Css.defineCss
import daw.style.Css.noTextSelect
import daw.style.CssId
import daw.style.CssName
import daw.style.hover
import kotlinx.browser.window import kotlinx.browser.window
import kotlinx.html.InputType import kotlinx.html.InputType
import kotlinx.html.canvas
import kotlinx.html.classes
import kotlinx.html.div import kotlinx.html.div
import kotlinx.html.h1 import kotlinx.html.h1
import kotlinx.html.input import kotlinx.html.input
import kotlinx.html.js.onChangeFunction import kotlinx.html.js.onChangeFunction
import kotlinx.html.js.onClickFunction import kotlinx.html.js.onClickFunction
import kotlinx.html.js.onInputFunction
import kotlinx.html.option import kotlinx.html.option
import kotlinx.html.select import kotlinx.html.select
import kotlinx.html.span import kotlinx.html.span
import nl.astraeus.css.properties.AlignItems
import nl.astraeus.css.properties.BoxSizing import nl.astraeus.css.properties.BoxSizing
import nl.astraeus.css.properties.Display
import nl.astraeus.css.properties.FlexDirection
import nl.astraeus.css.properties.FontWeight import nl.astraeus.css.properties.FontWeight
import nl.astraeus.css.properties.JustifyContent
import nl.astraeus.css.properties.Position import nl.astraeus.css.properties.Position
import nl.astraeus.css.properties.Transform import nl.astraeus.css.properties.Transform
import nl.astraeus.css.properties.em import nl.astraeus.css.properties.em
@@ -30,80 +27,21 @@ import nl.astraeus.css.properties.px
import nl.astraeus.css.properties.rem import nl.astraeus.css.properties.rem
import nl.astraeus.css.properties.vh import nl.astraeus.css.properties.vh
import nl.astraeus.css.properties.vw import nl.astraeus.css.properties.vw
import nl.astraeus.css.style.Style
import nl.astraeus.css.style.cls import nl.astraeus.css.style.cls
import nl.astraeus.komp.HtmlBuilder import nl.astraeus.komp.HtmlBuilder
import nl.astraeus.komp.Komponent import nl.astraeus.komp.Komponent
import nl.astraeus.komp.currentElement
import nl.astraeus.vst.chip.audio.VstChipWorklet import nl.astraeus.vst.chip.audio.VstChipWorklet
import nl.astraeus.vst.chip.audio.VstChipWorklet.midiChannel
import nl.astraeus.vst.chip.midi.Midi import nl.astraeus.vst.chip.midi.Midi
import nl.astraeus.vst.chip.ws.WebsocketClient import org.khronos.webgl.Uint8Array
import nl.astraeus.vst.ui.components.ExpKnobComponent
import nl.astraeus.vst.ui.components.KnobComponent
import nl.astraeus.vst.ui.css.Css
import nl.astraeus.vst.ui.css.Css.defineCss
import nl.astraeus.vst.ui.css.Css.noTextSelect
import nl.astraeus.vst.ui.css.CssName
import nl.astraeus.vst.ui.css.hover
import nl.astraeus.vst.ui.util.uInt8ArrayOf
import org.khronos.webgl.get
import org.w3c.dom.CanvasRenderingContext2D
import org.w3c.dom.HTMLCanvasElement
import org.w3c.dom.HTMLInputElement import org.w3c.dom.HTMLInputElement
import org.w3c.dom.HTMLSelectElement import org.w3c.dom.HTMLSelectElement
object WaveformView: Komponent() { object MainView : Komponent() {
init {
window.requestAnimationFrame(::onAnimationFrame)
}
fun onAnimationFrame(time: Double) {
if (MainView.started) {
VstChipWorklet.postMessage("start_recording")
}
window.requestAnimationFrame(::onAnimationFrame)
}
override fun HtmlBuilder.render() {
div {
if (VstChipWorklet.recording != null) {
canvas {
width = "1000"
height = "400"
val ctx = (currentElement() as? HTMLCanvasElement)?.getContext("2d") as? CanvasRenderingContext2D
val data = VstChipWorklet.recording
if (ctx != null && data != null) {
val width = ctx.canvas.width.toDouble()
val height = ctx.canvas.height.toDouble()
val halfHeight = height / 2.0
ctx.lineWidth = 2.0
ctx.clearRect(0.0, 0.0, width, height)
val step = 1000.0 / data.length
ctx.beginPath()
ctx.strokeStyle = "rgba(0, 255, 255, 0.5)"
ctx.moveTo(0.0, halfHeight)
for (i in 0 until data.length) {
ctx.lineTo(i * step, halfHeight - data[i] * halfHeight)
}
ctx.stroke()
}
}
}
}
}
}
object MainView : Komponent(), CssName {
private var messages: MutableList<String> = ArrayList() private var messages: MutableList<String> = ArrayList()
var started = false private var started = false
init { init {
css() MainViewCss
} }
fun addMessage(message: String) { fun addMessage(message: String) {
@@ -115,19 +53,18 @@ object MainView : Komponent(), CssName {
} }
override fun HtmlBuilder.render() { override fun HtmlBuilder.render() {
div(MainDivCss.name) { div(MainViewCss.MainDivCss.name) {
if (!started) { if (!started) {
div(StartSplashCss.name) { div(MainViewCss.StartSplashCss.name) {
div(StartBoxCss.name) { div(MainViewCss.StartBoxCss.name) {
div(StartButtonCss.name) { div(MainViewCss.StartButtonCss.name) {
+"START" +"START"
} onClickFunction = {
} started = true
onClickFunction = { VstChipWorklet.create {
VstChipWorklet.create { requestUpdate()
started = true }
requestUpdate() }
WebsocketClient.send("LOAD\n")
} }
} }
} }
@@ -141,22 +78,30 @@ object MainView : Komponent(), CssName {
select { select {
option { option {
+"None" +"None"
value = "none" value = ""
}
option {
+"Midi over Broadcast"
value = "midi-broadcast"
} }
for (mi in Midi.inputs) { for (mi in Midi.inputs) {
option { option {
+mi.name +mi.name
value = mi.id value = mi.id
selected = mi.id == Midi.currentInput?.id
} }
} }
onChangeFunction = { event -> onChangeFunction = { event ->
val target = event.target as HTMLSelectElement val target = event.target as HTMLSelectElement
if (target.value == "none") { if (target.value == "") {
Midi.setInput(null) Midi.setInput(null)
} else { } else {
Midi.setInput(target.value) val selected = Midi.inputs.find { it.id == target.value }
if (selected != null) {
Midi.setInput(selected)
} else if (target.value == "midi-broadcast") {
//
}
} }
} }
} }
@@ -165,267 +110,98 @@ object MainView : Komponent(), CssName {
+"channel:" +"channel:"
input { input {
type = InputType.number type = InputType.number
value = VstChipWorklet.midiChannel.toString() value = Midi.inputChannel.toString()
onInputFunction = { event -> onChangeFunction = { event ->
val target = event.target as HTMLInputElement val target = event.target as HTMLInputElement
println("onInput channel: $target") Midi.inputChannel = target.value.toInt()
VstChipWorklet.midiChannel = target.value.toInt()
} }
} }
} }
} }
div { div {
span(ButtonBarCss.name) { span {
+"SAVE" +"Midi output: "
onClickFunction = { select {
val patch = VstChipWorklet.save().copy( option {
midiId = Midi.currentInput?.id ?: "", +"None"
midiName = Midi.currentInput?.name ?: "" value = ""
) }
option {
+"Midi over Broadcast"
value = "midi-broadcast"
}
for (mi in Midi.outputs) {
option {
+mi.name
value = mi.id
}
}
WebsocketClient.send("SAVE\n${JSON.stringify(patch)}") onChangeFunction = { event ->
val target = event.target as HTMLSelectElement
if (target.value == "") {
Midi.setOutput(null)
} else {
val selected = Midi.outputs.find { it.id == target.value }
if (selected != null) {
Midi.setOutput(selected)
}
}
}
} }
} }
span(ButtonBarCss.name) { span {
+"STOP" +"channel:"
onClickFunction = { input {
VstChipWorklet.postDirectlyToWorklet( type = InputType.number
uInt8ArrayOf(0xb0 + midiChannel, 123, 0) value = Midi.outputChannel.toString()
onChangeFunction = { 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 { div(MainViewCss.ButtonCss.name) {
span(ButtonBarCss.name) { +"Send note off to output"
+"Sine" onClickFunction = {
if (VstChipWorklet.waveform == 0) { val data = Uint8Array(
classes += SelectedCss.name arrayOf(
} 0x90.toByte(),
onClickFunction = { 0x3c.toByte(),
VstChipWorklet.waveform = 0 0x0.toByte(),
requestUpdate() )
} )
} Midi.send(data)
span(ButtonBarCss.name) {
+"Square"
if (VstChipWorklet.waveform == 1) {
classes += SelectedCss.name
}
onClickFunction = {
VstChipWorklet.waveform = 1
requestUpdate()
}
}
span(ButtonBarCss.name) {
+"Triangle"
if (VstChipWorklet.waveform == 2) {
classes += SelectedCss.name
}
onClickFunction = {
VstChipWorklet.waveform = 2
requestUpdate()
}
}
span(ButtonBarCss.name) {
+"Sawtooth"
if (VstChipWorklet.waveform == 3) {
classes += SelectedCss.name
}
onClickFunction = {
VstChipWorklet.waveform = 3
requestUpdate()
}
} }
} }
div(ControlsCss.name) {
include(
ExpKnobComponent(
value = VstChipWorklet.volume,
label = "Volume",
minValue = 0.005,
maxValue = 1.0,
step = 5.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.volume = value
}
)
include(
KnobComponent(
value = VstChipWorklet.dutyCycle,
label = "Duty cycle",
minValue = 0.0,
maxValue = 1.0,
step = 2.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.dutyCycle = value
}
)
include(
ExpKnobComponent(
value = VstChipWorklet.fmModFreq,
label = "FM Freq",
minValue = 0.005,
maxValue = 1.0,
step = 5.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.fmModFreq = value
}
)
include(
ExpKnobComponent(
value = VstChipWorklet.fmModAmp,
label = "FM Ampl",
minValue = 0.005,
maxValue = 1.0,
step = 5.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.fmModAmp = value
}
)
include(
ExpKnobComponent(
value = VstChipWorklet.amModFreq,
label = "AM Freq",
minValue = 0.005,
maxValue = 1.0,
step = 5.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.amModFreq = value
}
)
include(
ExpKnobComponent(
value = VstChipWorklet.amModAmp,
label = "AM Ampl",
minValue = 0.005,
maxValue = 1.0,
step = 5.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.amModAmp = value
}
)
include(
ExpKnobComponent(
value = VstChipWorklet.feedback,
label = "Feedback",
minValue = 0.005,
maxValue = 1.0,
step = 5.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.feedback = value
}
)
include(
ExpKnobComponent(
value = VstChipWorklet.delay,
label = "Delay",
minValue = 0.005,
maxValue = 1.0,
step = 5.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.delay = value
}
)
include(
ExpKnobComponent(
value = VstChipWorklet.delayDepth,
label = "Delay depth",
minValue = 0.005,
maxValue = 1.0,
step = 5.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.delayDepth = value
}
)
}
div(ControlsCss.name) {
include(
ExpKnobComponent(
value = VstChipWorklet.attack,
label = "Attack",
minValue = 0.005,
maxValue = 1.0,
step = 5.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.attack = value
}
)
include(
ExpKnobComponent(
value = VstChipWorklet.decay,
label = "Decay",
minValue = 0.005,
maxValue = 1.0,
step = 5.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.decay = value
}
)
include(
KnobComponent(
value = VstChipWorklet.sustain,
label = "Sustain",
minValue = 0.0,
maxValue = 1.0,
step = 2.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.sustain = value
}
)
include(
ExpKnobComponent(
value = VstChipWorklet.release,
label = "Release",
minValue = 0.005,
maxValue = 1.0,
step = 5.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.release = value
}
)
}
include(WaveformView)
} }
} }
object MainDivCss : CssName object MainViewCss : CssId("main") {
object ActiveCss : CssName object MainDivCss : CssName()
object ButtonCss : CssName object ActiveCss : CssName()
object ButtonBarCss : CssName object ButtonCss : CssName()
object SelectedCss : CssName object NoteBarCss : CssName()
object NoteBarCss : CssName object StartSplashCss : CssName()
object StartSplashCss : CssName object StartBoxCss : CssName()
object StartBoxCss : CssName object StartButtonCss : CssName()
object StartButtonCss : CssName
object ControlsCss : CssName
private fun css() { init {
defineCss { defineCss {
select("*") { select("*") {
select("*:before") { select("*:before") {
@@ -450,18 +226,15 @@ object MainView : Komponent(), CssName {
//transition() //transition()
noTextSelect() noTextSelect()
} }
select("input", "textarea") {
backgroundColor(Css.currentStyle.inputBackgroundColor)
color(Css.currentStyle.mainFontColor)
border("none")
}
select(cls(ButtonCss)) { select(cls(ButtonCss)) {
margin(1.rem) margin(1.rem)
commonButton() padding(1.rem)
} backgroundColor(Css.currentStyle.buttonBackgroundColor)
select(cls(ButtonBarCss)) { color(Css.currentStyle.mainFontColor)
margin(1.rem, 0.px)
commonButton() hover {
backgroundColor(Css.currentStyle.buttonBackgroundColor.hover())
}
} }
select(cls(ActiveCss)) { select(cls(ActiveCss)) {
//backgroundColor(Css.currentStyle.selectedBackgroundColor) //backgroundColor(Css.currentStyle.selectedBackgroundColor)
@@ -481,7 +254,7 @@ object MainView : Komponent(), CssName {
backgroundImage("url('https://upload.wikimedia.org/wikipedia/commons/9/9d/Caret_down_font_awesome_whitevariation.svg')") backgroundImage("url('https://upload.wikimedia.org/wikipedia/commons/9/9d/Caret_down_font_awesome_whitevariation.svg')")
background("right 0.8em center/1.4em") background("right 0.8em center/1.4em")
backgroundColor(Css.currentStyle.inputBackgroundColor) backgroundColor(Css.currentStyle.inputBackgroundColor)
color(Css.currentStyle.mainFontColor) //color(Css.currentStyle.entryFontColor)
borderRadius(0.25.em) borderRadius(0.25.em)
} }
select(cls(StartSplashCss)) { select(cls(StartSplashCss)) {
@@ -491,7 +264,7 @@ object MainView : Komponent(), CssName {
width(100.vw) width(100.vw)
height(100.vh) height(100.vh)
zIndex(100) zIndex(100)
backgroundColor(hsla(32, 0, 5, 0.65)) backgroundColor(hsla(32, 0, 50, 0.6))
select(cls(StartBoxCss)) { select(cls(StartBoxCss)) {
position(Position.relative) position(Position.relative)
@@ -499,9 +272,7 @@ object MainView : Komponent(), CssName {
top(25.vh) top(25.vh)
width(50.vw) width(50.vw)
height(50.vh) height(50.vh)
backgroundColor(hsla(239, 50, 10, 1.0)) backgroundColor(hsla(0, 0, 50, 0.25))
borderColor(Css.currentStyle.mainFontColor)
borderWidth(2.px)
select(cls(StartButtonCss)) { select(cls(StartButtonCss)) {
position(Position.absolute) position(Position.absolute)
@@ -514,33 +285,8 @@ object MainView : Komponent(), CssName {
} }
} }
} }
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())
} }
} }
} }

View File

@@ -0,0 +1,115 @@
package daw.style
import kotlinx.browser.document
import nl.astraeus.css.properties.*
import nl.astraeus.css.style
import nl.astraeus.css.style.ConditionalStyle
import nl.astraeus.css.style.Style
class StyleDefinition(
val mainFontColor: Color = hsla(178, 70, 55, 1.0),
val mainBackgroundColor: Color = hsl(239, 50, 10),
//val entryFontColor: Color = hsl(Css.mainFontColorNumber, 70, 55),
val inputBackgroundColor : Color = mainBackgroundColor.lighten(15),
val buttonBackgroundColor : Color = mainBackgroundColor.lighten(15),
val buttonBorderColor : Color = mainFontColor.changeAlpha(0.25),
val buttonBorderWidth : Measurement = 2.px,
)
object NoTextSelectCls : CssName("no-text-select")
object SelectedCls : CssName("selected")
object ActiveCls : CssName("active")
fun Color.hover(): Color = if (Css.currentStyle == Css.darkStyle) {
this.lighten(15)
} else {
this.darken(15)
}
object Css {
var minified = false
var dynamicStyles = mutableMapOf<CssId, ConditionalStyle.() -> Unit>()
fun CssId.defineCss(conditionalStyle: ConditionalStyle.() -> Unit) {
check(!dynamicStyles.containsKey(this)) {
"CssId with name ${this.name} already defined!"
}
updateCss(conditionalStyle)
}
private fun CssId.updateCss(conditionalStyle: ConditionalStyle.() -> Unit) {
val elementId = this.description()
var dynamicStyleElement = document.getElementById(elementId)
dynamicStyles[this] = conditionalStyle
if (dynamicStyleElement == null) {
dynamicStyleElement = document.createElement("style")
dynamicStyleElement.id = elementId
document.head?.append(dynamicStyleElement)
}
val css = style(conditionalStyle)
dynamicStyleElement.innerHTML = css.generateCss(minified = false)
}
val darkStyle = StyleDefinition(
)
val lightStyle = StyleDefinition(
mainBackgroundColor = hsl(239+180, 50, 15),
)
var currentStyle: StyleDefinition = darkStyle
fun updateStyle() {
for ((cssId, dynStyle) in dynamicStyles) {
cssId.apply {
updateCss(dynStyle)
}
}
}
fun switchLayout() {
currentStyle = if (currentStyle == darkStyle) {
lightStyle
} else {
darkStyle
}
updateStyle()
}
fun Style.transition() {
transition("all 0.5s ease")
}
fun Style.noTextSelect() {
plain("-webkit-touch-callout", "none")
plain("-webkit-user-select", "none")
plain("-moz-user-select", "none")
plain("-ms-user-select", "none")
userSelect(UserSelect.none)
select("::selection") {
background("none")
}
}
object GenericCss : CssId("generic") {
init {
fun generateStyle(): String {
val css = style {
}
return css.generateCss(minified = minified)
}
}
}
}

View File

@@ -0,0 +1,65 @@
package daw.style
import nl.astraeus.css.style.DescriptionProvider
import nl.astraeus.css.style.cls
private val CAPITAL_LETTER = Regex("[A-Z]")
fun String.hyphenize(): String =
replace(CAPITAL_LETTER) {
"-${it.value.lowercase()}"
}
private val shortId = false
private var nextCssId = 1
private fun nextShortId(): String {
var id = nextCssId++
val result = StringBuilder()
while(id > 0) {
val ch = ((id % 26) + 'a'.code).toChar()
result.append(ch)
id /= 26
}
return result.toString()
}
open class CssName(name: String? = null) : DescriptionProvider {
val name: String = if (shortId) {
nextShortId()
} else if (name != null) {
"daw-$name"
} else {
"daw${this::class.simpleName?.hyphenize() ?: this::class}"
}
override fun description() = name
}
fun CssName.cls() : DescriptionProvider = cls(this)
open class CssId(name: String) : DescriptionProvider {
val name: String = if (shortId) {
nextShortId()
} else {
"daw-$name-css"
}
override fun description() = name
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is CssId) return false
if (name != other.name) return false
return true
}
override fun hashCode(): Int {
return name.hashCode()
}
}

View File

@@ -1,122 +0,0 @@
@file:OptIn(ExperimentalJsExport::class)
package nl.astraeus.vst.chip.ws
import kotlinx.browser.window
import nl.astraeus.vst.chip.PatchDTO
import nl.astraeus.vst.chip.audio.VstChipWorklet
import nl.astraeus.vst.chip.midi.Midi
import nl.astraeus.vst.chip.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)
VstChipWorklet.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)
}
}

View File

@@ -0,0 +1,2 @@
package nl.astraeus.vst.chip

View File

@@ -1,25 +1,20 @@
package nl.astraeus.vst.chip package nl.astraeus.vst.chip
import nl.astraeus.vst.base.Settings import io.undertow.Undertow
import nl.astraeus.vst.base.db.Database import io.undertow.UndertowOptions
import nl.astraeus.vst.base.web.UndertowServer
import nl.astraeus.vst.chip.logger.LogLevel
import nl.astraeus.vst.chip.logger.Logger
fun main() { fun main() {
Logger.level = LogLevel.DEBUG
Thread.setDefaultUncaughtExceptionHandler { _, e -> Thread.setDefaultUncaughtExceptionHandler { _, e ->
e.printStackTrace() e.printStackTrace()
} }
Settings.port = 9005 val server = Undertow.builder()
Settings.jdbcStatsPort = 6005 .addHttpListener(Settings.port, "localhost")
.setIoThreads(4)
.setHandler(RequestHandler)
.setServerOption(UndertowOptions.SHUTDOWN_TIMEOUT, 1000)
.build()
Database.start() println("Starting server at port ${Settings.port}...")
server?.start()
UndertowServer.start(
"Vst Chip",
"/vst-chip-worklet-ui.js"
)
} }

View File

@@ -0,0 +1,15 @@
package nl.astraeus.vst.chip
import io.undertow.server.HttpHandler
import io.undertow.server.HttpServerExchange
import io.undertow.server.handlers.resource.PathResourceManager
import io.undertow.server.handlers.resource.ResourceHandler
import java.nio.file.Paths
object RequestHandler : HttpHandler {
val resourceHandler = ResourceHandler(PathResourceManager(Paths.get("web")))
override fun handleRequest(exchange: HttpServerExchange) {
resourceHandler.handleRequest(exchange)
}
}

View File

@@ -0,0 +1,50 @@
package nl.astraeus.vst.chip
import java.io.File
import java.io.FileInputStream
import java.util.*
object Settings {
var runningAsRoot: Boolean = false
var port = 9000
var sslPort = 8443
var connectionTimeout = 30000
var jdbcDriver = "nl.astraeus.jdbc.Driver"
var jdbcConnectionUrl = "jdbc:stat:webServerPort=6001:jdbc:sqlite:data/srp.db"
var jdbcUser = "sa"
var jdbcPassword = ""
var adminUser = "rnentjes"
var adminPassword = "9/SG_Bd}9gWz~?j\\A.U]n9]OO"
fun getPropertiesFromFile(filename: String): Properties? {
val propertiesFile = File(filename)
return if (propertiesFile.exists()) {
val properties = Properties()
FileInputStream(propertiesFile).use {
properties.load(it)
}
properties
} else {
null
}
}
fun readProperties(args: Array<String>) {
val filename = if (args.isNotEmpty()) args[0] else "srp.properties"
val properties = getPropertiesFromFile(filename) ?: return // return if properties couldn't be loaded
runningAsRoot = properties.getProperty("runningAsRoot", runningAsRoot.toString()).toBoolean()
port = properties.getProperty("port", port.toString()).toInt()
sslPort = properties.getProperty("sslPort", sslPort.toString()).toInt()
connectionTimeout = properties.getProperty("connectionTimeout", connectionTimeout.toString()).toInt()
jdbcDriver = properties.getProperty("jdbcDriver", jdbcDriver)
jdbcConnectionUrl = properties.getProperty("jdbcConnectionUrl", jdbcConnectionUrl)
jdbcUser = properties.getProperty("jdbcUser", jdbcUser)
jdbcPassword = properties.getProperty("jdbcPassword", jdbcPassword)
adminUser = properties.getProperty("adminUser", adminUser)
adminPassword = properties.getProperty("adminPassword", adminPassword)
}
}

View File

@@ -1,20 +0,0 @@
import java.util.*
tasks.register("generateVersionProperties") {
doLast {
val versionDir = layout.buildDirectory.dir("processedResources/jvm/main")
val versionFile = versionDir.get().file("version.properties").asFile
versionDir.get().asFile.mkdirs()
val properties = Properties().apply {
setProperty("group", project.group.toString())
setProperty("name", project.name.toString())
setProperty("version", project.version.toString())
setProperty("buildTime", Date().toString())
}
versionFile.writer().use { writer ->
properties.store(writer, "Version information")
}
}
}