From b412dd9b4e4a93368003de4d6bc2de04ed28d2fe Mon Sep 17 00:00:00 2001 From: rnentjes Date: Mon, 12 Aug 2024 20:36:30 +0200 Subject: [PATCH] Playing with settings --- .../nl/astraeus/vst/chip/ChipProcessor.kt | 120 ++++++++++++++---- build.gradle.kts | 31 ++++- common.gradle.kts | 4 - common/build.gradle.kts | 20 +-- settings.common.gradle.kts | 2 +- .../kotlin/nl/astraeus/vst/chip/PatchDTO.kt | 10 ++ .../astraeus/vst/chip/audio/VstChipWorklet.kt | 45 +++++-- .../nl/astraeus/vst/chip/view/MainView.kt | 88 +++++++++---- .../astraeus/vst/chip/ws/WebsocketClient.kt | 2 +- 9 files changed, 248 insertions(+), 74 deletions(-) diff --git a/audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/chip/ChipProcessor.kt b/audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/chip/ChipProcessor.kt index 0c7ec2b..fa7e784 100644 --- a/audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/chip/ChipProcessor.kt +++ b/audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/chip/ChipProcessor.kt @@ -21,17 +21,20 @@ import kotlin.math.sin val POLYPHONICS = 10 val PI2 = PI * 2 -@ExperimentalJsExport -@JsExport class PlayingNote( val note: Int, var velocity: Int = 0 ) { + val noteObj = Note.fromMidi(note) + fun retrigger(velocity: Int) { this.velocity = velocity sample = 0 noteStart = currentTime noteRelease = null + for (i in 0 until combDelayBuffer.length) { + combDelayBuffer[i] = 0f + } } var noteStart = currentTime @@ -39,6 +42,7 @@ class PlayingNote( var cycleOffset = 0.0 var sample = 0 var actualVolume = 0f + val combDelayBuffer = Float32Array((sampleRate / noteObj.freq).toInt()) } enum class Waveform { @@ -82,6 +86,14 @@ class VstChipProcessor : AudioWorkletProcessor() { var recordingSample = 0 var recordingStart = 0 + val rightDelayBuffer = Float32Array(sampleRate) + val leftDelayBuffer = Float32Array(sampleRate) + var delayIndex = 0 + var delay = 0.0 + var delayDepth = 0.0 + + var feedback = 0.0 + init { this.port.onmessage = ::handleMessage Note.updateSampleRate(sampleRate) @@ -95,7 +107,7 @@ class VstChipProcessor : AudioWorkletProcessor() { try { when (data) { is String -> { - when(data) { + when (data) { "start_recording" -> { port.postMessage(recordingBuffer) if (recordingState == RecordingState.STOPPED) { @@ -103,6 +115,7 @@ class VstChipProcessor : AudioWorkletProcessor() { recordingSample = 0 } } + else -> if (data.startsWith("set_channel")) { val parts = data.split('\n') @@ -113,7 +126,7 @@ class VstChipProcessor : AudioWorkletProcessor() { } else if (data.startsWith("waveform")) { val parts = data.split('\n') if (parts.size == 2) { - waveform =parts[1].toInt() + waveform = parts[1].toInt() println("Setting waveform: $waveform") } } @@ -135,7 +148,7 @@ class VstChipProcessor : AudioWorkletProcessor() { else -> console.error("Don't kow how to handle message", message) } - } catch(e: Exception) { + } catch (e: Exception) { console.log(e.message, e) } } @@ -155,7 +168,7 @@ class VstChipProcessor : AudioWorkletProcessor() { cmdByte = cmdByte and 0xf0 //console.log("Received", bytes) - when(cmdByte) { + when (cmdByte) { 0x90 -> { if (bytes.length == 3) { val note = bytes[1] @@ -201,18 +214,19 @@ class VstChipProcessor : AudioWorkletProcessor() { dutyCycle = value / 127.0 } - 0x4a -> { + 0x40 -> { fmFreq = value / 127.0 } - 0x4b -> { + 0x41 -> { fmAmp = value / 127.0 } - 0x4c -> { + 0x42 -> { amFreq = value / 127.0 } - 0x4d -> { + + 0x43 -> { amAmp = value / 127.0 } @@ -232,6 +246,21 @@ class VstChipProcessor : AudioWorkletProcessor() { release = 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 @@ -280,11 +309,11 @@ class VstChipProcessor : AudioWorkletProcessor() { } } - override fun process ( - inputs: Array>, - outputs: Array>, - parameters: dynamic - ) : Boolean { + override fun process( + inputs: Array>, + outputs: Array>, + parameters: dynamic + ): Boolean { val samples = outputs[0][0].length val left = outputs[0][0] @@ -304,10 +333,11 @@ class VstChipProcessor : AudioWorkletProcessor() { for ((index, note) in notes.withIndex()) { if (note != null) { - val sampleDelta = Note.fromMidi(note.note).sampleDelta + val midiNote = Note.fromMidi(note.note) + val sampleDelta = midiNote.sampleDelta for (i in 0 until samples) { - var targetVolume = note.velocity / 127f + var targetVolume = note.velocity / 127f * 10f targetVolume *= ADSR.calculate( attack, decay, @@ -324,38 +354,64 @@ class VstChipProcessor : AudioWorkletProcessor() { } var cycleOffset = note.cycleOffset - val fmModulation = sampleDelta * sin( fmFreq * 20f * PI2 * (note.sample / sampleRate.toDouble())).toFloat() * fmAmp - val amModulation = 1f + (sin(sampleLength * amFreq * 10f * PI2 * note.sample) * amAmp).toFloat() + val fmModulation = + sampleDelta + (sin(fmFreq * 1000f * PI2 * (note.sample / sampleRate.toDouble())).toFloat() * (100f * fmAmp * sampleDelta)) + val amModulation = + 1f + (sin(sampleLength * amFreq * 1000f * PI2 * note.sample) * amAmp).toFloat() cycleOffset = if (cycleOffset < dutyCycle) { cycleOffset / dutyCycle / 2.0 } else { - 0.5 + ((cycleOffset -dutyCycle) / (1.0 - dutyCycle) / 2.0) + 0.5 + ((cycleOffset - dutyCycle) / (1.0 - dutyCycle) / 2.0) } val waveValue: Float = when (waveform) { 0 -> { sin(cycleOffset * PI2).toFloat() } + 1 -> { - if (cycleOffset < 0.5) { 1f } else { -1f } + if (cycleOffset < 0.5) { + 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 } + if (cycleOffset < 0.5) { + 1f + } else { + -1f + } } } left[i] = left[i] + waveValue * note.actualVolume * volume * amModulation right[i] = right[i] + waveValue * note.actualVolume * volume * amModulation + + // comb filter delay + 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 @@ -370,6 +426,26 @@ class VstChipProcessor : AudioWorkletProcessor() { } } + // 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 diff --git a/build.gradle.kts b/build.gradle.kts index f2d74f9..5cd03a1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -28,6 +28,22 @@ kotlin { } } } + /* + @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() } @@ -37,15 +53,14 @@ kotlin { dependencies { implementation(project(":common")) //base - api("nl.astraeus:kotlin-css-generator:1.0.7") - implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") + api("nl.astraeus:kotlin-css-generator:1.0.9-SNAPSHOT") + implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.0") } } val jsMain by getting { dependencies { - //base - implementation("nl.astraeus:kotlin-komponent-js:1.2.2") - implementation("nl.astraeus:vst-ui-base:1.0.0-SNAPSHOT") + implementation("nl.astraeus:kotlin-komponent:1.2.4-SNAPSHOT") + implementation("nl.astraeus:vst-ui-base:1.0.1-SNAPSHOT") } } val jsTest by getting { @@ -53,6 +68,12 @@ kotlin { 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 diff --git a/common.gradle.kts b/common.gradle.kts index 34228df..e923fa6 100644 --- a/common.gradle.kts +++ b/common.gradle.kts @@ -5,9 +5,5 @@ allprojects { repositories { mavenLocal() mavenCentral() - maven("https://reposilite.astraeus.nl/releases") - maven { - url = uri("https://nexus.astraeus.nl/nexus/content/groups/public") - } } } diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 4e2f549..bdc9e38 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -1,6 +1,7 @@ @file:OptIn(ExperimentalKotlinGradlePluginApi::class) import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl buildscript { apply(from = "../common.gradle.kts") @@ -17,16 +18,19 @@ kotlin { } browser() } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + browser() + mavenPublication { + groupId = group as String + pom { name = "${project.name}-wasm-js" } + } + } jvm() sourceSets { - val commonMain by getting { - dependencies { - } - } - val jsMain by getting { - dependencies { - } - } + val commonMain by getting + val jsMain by getting + val wasmJsMain by getting } } diff --git a/settings.common.gradle.kts b/settings.common.gradle.kts index d8820bf..a098f32 100644 --- a/settings.common.gradle.kts +++ b/settings.common.gradle.kts @@ -1,6 +1,6 @@ pluginManagement { plugins { - kotlin("multiplatform") version "2.0.20-Beta1" + kotlin("multiplatform") version "2.0.20-RC" id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0" } repositories { diff --git a/src/commonMain/kotlin/nl/astraeus/vst/chip/PatchDTO.kt b/src/commonMain/kotlin/nl/astraeus/vst/chip/PatchDTO.kt index 408fc1e..25aadcb 100644 --- a/src/commonMain/kotlin/nl/astraeus/vst/chip/PatchDTO.kt +++ b/src/commonMain/kotlin/nl/astraeus/vst/chip/PatchDTO.kt @@ -1,7 +1,11 @@ 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, @@ -31,4 +35,10 @@ data class PatchDTO( 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, ) diff --git a/src/jsMain/kotlin/nl/astraeus/vst/chip/audio/VstChipWorklet.kt b/src/jsMain/kotlin/nl/astraeus/vst/chip/audio/VstChipWorklet.kt index 807ccc8..9dcd9b5 100644 --- a/src/jsMain/kotlin/nl/astraeus/vst/chip/audio/VstChipWorklet.kt +++ b/src/jsMain/kotlin/nl/astraeus/vst/chip/audio/VstChipWorklet.kt @@ -45,28 +45,49 @@ object VstChipWorklet : AudioNode( set(value) { field = value super.postMessage( - uInt8ArrayOf(0xb0 + midiChannel, 0x4a, (value * 127).toInt()) + uInt8ArrayOf(0xb0 + midiChannel, 0x40, (value * 127).toInt()) ) } var fmModAmp = 0.0 set(value) { field = value super.postMessage( - uInt8ArrayOf(0xb0 + midiChannel, 0x4b, (value * 127).toInt()) + uInt8ArrayOf(0xb0 + midiChannel, 0x41, (value * 127).toInt()) ) } var amModFreq = 0.0 set(value) { field = value super.postMessage( - uInt8ArrayOf(0xb0 + midiChannel, 0x4c, (value * 127).toInt()) + uInt8ArrayOf(0xb0 + midiChannel, 0x42, (value * 127).toInt()) ) } var amModAmp = 0.0 set(value) { field = value super.postMessage( - uInt8ArrayOf(0xb0 + midiChannel, 0x4d, (value * 127).toInt()) + 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()) ) } @@ -146,22 +167,22 @@ object VstChipWorklet : AudioNode( MainView.requestUpdate() } - 0x4b.toByte() -> { + 0x40.toByte() -> { fmModFreq = value / 127.0 MainView.requestUpdate() } - 0x4c.toByte() -> { + 0x41.toByte() -> { fmModAmp = value / 127.0 MainView.requestUpdate() } - 0x47.toByte() -> { + 0x42.toByte() -> { amModFreq = value / 127.0 MainView.requestUpdate() } - 0x48.toByte() -> { + 0x43.toByte() -> { amModAmp = value / 127.0 MainView.requestUpdate() } @@ -181,6 +202,9 @@ object VstChipWorklet : AudioNode( decay = patch.decay sustain = patch.sustain release = patch.release + delay = patch.delay + delayDepth = patch.delayDepth + feedback = patch.feedback } fun save(): PatchDTO { @@ -196,7 +220,10 @@ object VstChipWorklet : AudioNode( attack = attack, decay = decay, sustain = sustain, - release = release + release = release, + delay = delay, + delayDepth = delayDepth, + feedback = feedback ) } diff --git a/src/jsMain/kotlin/nl/astraeus/vst/chip/view/MainView.kt b/src/jsMain/kotlin/nl/astraeus/vst/chip/view/MainView.kt index e358a8c..01f0eac 100644 --- a/src/jsMain/kotlin/nl/astraeus/vst/chip/view/MainView.kt +++ b/src/jsMain/kotlin/nl/astraeus/vst/chip/view/MainView.kt @@ -37,6 +37,7 @@ 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.ws.WebsocketClient +import nl.astraeus.vst.ui.components.ExpKnobComponent import nl.astraeus.vst.ui.components.KnobComponent import nl.astraeus.vst.ui.css.Css import nl.astraeus.vst.ui.css.Css.defineCss @@ -236,12 +237,12 @@ object MainView : Komponent(), CssName { } div(ControlsCss.name) { include( - KnobComponent( + ExpKnobComponent( value = VstChipWorklet.volume, label = "Volume", - minValue = 0.0, + minValue = 0.005, maxValue = 1.0, - step = 2.0 / 127.0, + step = 5.0 / 127.0, width = 100, height = 120, ) { value -> @@ -262,12 +263,12 @@ object MainView : Komponent(), CssName { } ) include( - KnobComponent( + ExpKnobComponent( value = VstChipWorklet.fmModFreq, label = "FM Freq", - minValue = 0.0, + minValue = 0.005, maxValue = 1.0, - step = 2.0 / 127.0, + step = 5.0 / 127.0, width = 100, height = 120, ) { value -> @@ -275,12 +276,12 @@ object MainView : Komponent(), CssName { } ) include( - KnobComponent( + ExpKnobComponent( value = VstChipWorklet.fmModAmp, label = "FM Ampl", - minValue = 0.0, + minValue = 0.005, maxValue = 1.0, - step = 2.0 / 127.0, + step = 5.0 / 127.0, width = 100, height = 120, ) { value -> @@ -288,12 +289,12 @@ object MainView : Komponent(), CssName { } ) include( - KnobComponent( + ExpKnobComponent( value = VstChipWorklet.amModFreq, label = "AM Freq", - minValue = 0.0, + minValue = 0.005, maxValue = 1.0, - step = 2.0 / 127.0, + step = 5.0 / 127.0, width = 100, height = 120, ) { value -> @@ -301,27 +302,66 @@ object MainView : Komponent(), CssName { } ) include( - KnobComponent( + ExpKnobComponent( value = VstChipWorklet.amModAmp, label = "AM Ampl", - minValue = 0.0, + minValue = 0.005, maxValue = 1.0, - step = 2.0 / 127.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( - KnobComponent( + ExpKnobComponent( value = VstChipWorklet.attack, label = "Attack", - minValue = 0.0, + minValue = 0.005, maxValue = 1.0, - step = 2.0 / 127.0, + step = 5.0 / 127.0, width = 100, height = 120, ) { value -> @@ -329,12 +369,12 @@ object MainView : Komponent(), CssName { } ) include( - KnobComponent( + ExpKnobComponent( value = VstChipWorklet.decay, label = "Decay", - minValue = 0.0, + minValue = 0.005, maxValue = 1.0, - step = 2.0 / 127.0, + step = 5.0 / 127.0, width = 100, height = 120, ) { value -> @@ -355,12 +395,12 @@ object MainView : Komponent(), CssName { } ) include( - KnobComponent( + ExpKnobComponent( value = VstChipWorklet.release, label = "Release", - minValue = 0.0, + minValue = 0.005, maxValue = 1.0, - step = 2.0 / 127.0, + step = 5.0 / 127.0, width = 100, height = 120, ) { value -> diff --git a/src/jsMain/kotlin/nl/astraeus/vst/chip/ws/WebsocketClient.kt b/src/jsMain/kotlin/nl/astraeus/vst/chip/ws/WebsocketClient.kt index b58b43b..83c939d 100644 --- a/src/jsMain/kotlin/nl/astraeus/vst/chip/ws/WebsocketClient.kt +++ b/src/jsMain/kotlin/nl/astraeus/vst/chip/ws/WebsocketClient.kt @@ -17,7 +17,7 @@ object WebsocketClient { close() websocket = if (window.location.hostname.contains("localhost") || window.location.hostname.contains("192.168")) { - WebSocket("ws://${window.location.hostname}:9000/ws") + WebSocket("ws://${window.location.hostname}:${window.location.port}/ws") } else { WebSocket("wss://${window.location.hostname}/ws") }