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:
1
.idea/misc.xml
generated
1
.idea/misc.xml
generated
@@ -1,4 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="FrameworkDetectionExcludesConfiguration">
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user