diff --git a/.idea/misc.xml b/.idea/misc.xml index 6846d14..cd89a8f 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - 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 427e28d..5b21b54 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 @@ -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) { diff --git a/src/commonMain/kotlin/nl/astraeus/vst/chip/PatchDTO.kt b/src/commonMain/kotlin/nl/astraeus/vst/chip/PatchDTO.kt index 25aadcb..0b9dadd 100644 --- a/src/commonMain/kotlin/nl/astraeus/vst/chip/PatchDTO.kt +++ b/src/commonMain/kotlin/nl/astraeus/vst/chip/PatchDTO.kt @@ -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, ) 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 0554361..6fa5d1b 100644 --- a/src/jsMain/kotlin/nl/astraeus/vst/chip/audio/VstChipWorklet.kt +++ b/src/jsMain/kotlin/nl/astraeus/vst/chip/audio/VstChipWorklet.kt @@ -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, ) } } + 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 9209cb1..cf3270d 100644 --- a/src/jsMain/kotlin/nl/astraeus/vst/chip/view/MainView.kt +++ b/src/jsMain/kotlin/nl/astraeus/vst/chip/view/MainView.kt @@ -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(