Add chorus effect implementation to ChipProcessor

Introduced chorus effect parameters (`rate`, `depth`, `mix`) and integrated real-time processing. Updated `PatchDTO` and `MainView` to support chorus controls and visualization. Enhanced MIDI handling for dynamic chorus adjustments.
This commit is contained in:
2026-07-03 14:22:44 +02:00
parent 887a10ec06
commit 418ce6452e
5 changed files with 189 additions and 7 deletions

1
.idea/misc.xml generated
View File

@@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration">

View File

@@ -5,8 +5,12 @@ package nl.astraeus.vst.chip
import nl.astraeus.midi.message.SortedTimedMidiMessageList
import nl.astraeus.midi.message.TimedMidiMessage
import nl.astraeus.tba.SlicedByteArray
import nl.astraeus.vst.*
import nl.astraeus.vst.ADSR
import nl.astraeus.vst.AudioWorkletProcessor
import nl.astraeus.vst.currentTime
import nl.astraeus.vst.midi.MidiMessageHandler
import nl.astraeus.vst.registerProcessor
import nl.astraeus.vst.sampleRate
import org.khronos.webgl.Float32Array
import org.khronos.webgl.get
import org.khronos.webgl.set
@@ -17,7 +21,13 @@ import kotlin.math.sin
val POLYPHONICS = 20
val PI2 = PI * 2
var chorusRate = 0.25
var chorusDepth = 0.0
var chorusMix = 0.0
var chorusPhase = 0.0
val leftChorusBuffer = Float32Array(sampleRate)
val rightChorusBuffer = Float32Array(sampleRate)
var chorusIndex = 0
@ExperimentalJsExport
@JsExport
class PlayingNote(
@@ -74,6 +84,14 @@ class VstChipProcessor : AudioWorkletProcessor() {
var amFreq = 0.0
var amAmp = 0.0
var chorusRate = 0.25
var chorusDepth = 0.0
var chorusMix = 0.0
var chorusPhase = 0.0
val leftChorusBuffer = Float32Array(sampleRate)
val rightChorusBuffer = Float32Array(sampleRate)
var chorusIndex = 0
var attack = 0.1
var decay = 0.2
var sustain = 0.5
@@ -159,6 +177,20 @@ class VstChipProcessor : AudioWorkletProcessor() {
addHandler(0xb0, 0x50) { b1, b2, b3 ->
feedback = b3 / 127.0
}
addHandler(0xb0, 0x51) { b1, b2, b3 ->
chorusRate = 0.01 + (b3 / 127.0) * 99.99
}
addHandler(0xb0, 0x52) { b1, b2, b3 ->
chorusDepth = b3 / 127.0
}
addHandler(0xb0, 0x53) { b1, b2, b3 ->
chorusMix = b3 / 127.0
}
addHandler(0xb0, 123) { b1, b2, b3 ->
for (note in notes) {
note?.noteRelease = currentTime
}
}
addHandler(0xb0, 123) { b1, b2, b3 ->
for (note in notes) {
note?.noteRelease = currentTime
@@ -418,6 +450,52 @@ class VstChipProcessor : AudioWorkletProcessor() {
}
*/
if (chorusMix > 0.0 && chorusDepth > 0.0) {
val baseDelaySamples = (sampleRate * 0.018).toInt()
val maxModulationSamples = sampleRate * 0.012 * chorusDepth
for (i in 0 until samples) {
val dryLeft = left[i]
val dryRight = right[i]
val leftLfo = sin(chorusPhase)
val rightLfo = sin(chorusPhase + PI)
val leftDelaySamples = baseDelaySamples + (leftLfo * maxModulationSamples).toInt()
val rightDelaySamples = baseDelaySamples + (rightLfo * maxModulationSamples).toInt()
val leftReadIndex =
(chorusIndex + leftChorusBuffer.length - leftDelaySamples) % leftChorusBuffer.length
val rightReadIndex =
(chorusIndex + rightChorusBuffer.length - rightDelaySamples) % rightChorusBuffer.length
val wetLeft = leftChorusBuffer[leftReadIndex]
val wetRight = rightChorusBuffer[rightReadIndex]
left[i] = dryLeft * (1f - chorusMix.toFloat()) + wetLeft * chorusMix.toFloat()
right[i] = dryRight * (1f - chorusMix.toFloat()) + wetRight * chorusMix.toFloat()
leftChorusBuffer[chorusIndex] = dryLeft
rightChorusBuffer[chorusIndex] = dryRight
chorusIndex++
chorusIndex %= leftChorusBuffer.length
chorusPhase += PI2 * chorusRate / sampleRate
if (chorusPhase > PI2) {
chorusPhase -= PI2
}
}
} else {
for (i in 0 until samples) {
leftChorusBuffer[chorusIndex] = left[i]
rightChorusBuffer[chorusIndex] = right[i]
chorusIndex++
chorusIndex %= leftChorusBuffer.length
}
}
val delaySamples = (delay * leftDelayBuffer.length).toInt()
for (i in 0 until samples) {
if (delaySamples > 0) {

View File

@@ -41,4 +41,10 @@ data class PatchDTO(
var delayDepth: Double = 0.0,
@JsName("feedback")
var feedback: Double = 0.0,
@JsName("chorusRate")
var chorusRate: Double = 0.0,
@JsName("chorusDepth")
var chorusDepth: Double = 0.0,
@JsName("chorusMix")
var chorusMix: Double = 0.0,
)

View File

@@ -119,6 +119,30 @@ object VstChipWorklet : AudioNode(
)
}
var chorusRate = 0.0
set(value) {
field = value
super.postMessage(
0xb0 + midiChannel, 0x51, (value * 127).toInt()
)
}
var chorusDepth = 0.0
set(value) {
field = value
super.postMessage(
0xb0 + midiChannel, 0x52, (value * 127).toInt()
)
}
var chorusMix = 0.0
set(value) {
field = value
super.postMessage(
0xb0 + midiChannel, 0x53, (value * 127).toInt()
)
}
var recording: Float32Array? = null
override fun onMessage(message: MessageEvent) {
@@ -214,6 +238,9 @@ object VstChipWorklet : AudioNode(
delay = patch.delay
delayDepth = patch.delayDepth
feedback = patch.feedback
chorusRate = patch.chorusRate
chorusDepth = patch.chorusDepth
chorusMix = patch.chorusMix
}
fun save(): PatchDTO {
@@ -232,8 +259,12 @@ object VstChipWorklet : AudioNode(
release = release,
delay = delay,
delayDepth = delayDepth,
feedback = feedback
feedback = feedback,
chorusRate = chorusRate,
chorusDepth = chorusDepth,
chorusMix = chorusMix,
)
}
}

View File

@@ -4,11 +4,34 @@ package nl.astraeus.vst.chip.view
import kotlinx.browser.document
import kotlinx.browser.window
import kotlinx.html.*
import kotlinx.html.InputType
import kotlinx.html.canvas
import kotlinx.html.classes
import kotlinx.html.div
import kotlinx.html.h1
import kotlinx.html.id
import kotlinx.html.input
import kotlinx.html.js.onChangeFunction
import kotlinx.html.js.onClickFunction
import kotlinx.html.js.onInputFunction
import nl.astraeus.css.properties.*
import kotlinx.html.option
import kotlinx.html.select
import kotlinx.html.span
import kotlinx.html.style
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.Overflow
import nl.astraeus.css.properties.Position
import nl.astraeus.css.properties.Transform
import nl.astraeus.css.properties.em
import nl.astraeus.css.properties.hsla
import nl.astraeus.css.properties.prc
import nl.astraeus.css.properties.px
import nl.astraeus.css.properties.rem
import nl.astraeus.css.style.Style
import nl.astraeus.css.style.cls
import nl.astraeus.komp.HtmlBuilder
@@ -29,7 +52,11 @@ import nl.astraeus.vst.ui.css.Css.noTextSelect
import nl.astraeus.vst.ui.css.CssName
import nl.astraeus.vst.ui.css.hover
import org.khronos.webgl.get
import org.w3c.dom.*
import org.w3c.dom.CanvasRenderingContext2D
import org.w3c.dom.Element
import org.w3c.dom.HTMLCanvasElement
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.HTMLSelectElement
object WaveformView : Komponent() {
@@ -347,6 +374,47 @@ class MainView : Komponent() {
}
)
}
div(ControlsCss.name) {
include(
KnobComponent(
value = VstChipWorklet.chorusRate,
label = "Rate",
minValue = 0.0,
maxValue = 1.0,
step = 2.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.chorusRate = value
}
)
include(
KnobComponent(
value = VstChipWorklet.chorusDepth,
label = "Depth",
minValue = 0.0,
maxValue = 1.0,
step = 2.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.chorusDepth = value
}
)
include(
KnobComponent(
value = VstChipWorklet.chorusMix,
label = "Mix",
minValue = 0.0,
maxValue = 1.0,
step = 2.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.chorusMix = value
}
)
}
div(ControlsCss.name) {
include(
ExpKnobComponent(