Initial commit

This commit is contained in:
2024-06-16 20:40:05 +02:00
commit 68b7ffffa8
42 changed files with 1729 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackOutput
buildscript {
apply(from = "../common.gradle.kts")
}
plugins {
kotlin("multiplatform")
}
kotlin {
js {
compilerOptions {
target.set("es2015")
}
binaries.executable()
browser {
commonWebpackConfig {
outputFileName = "vst-chip-worklet.js"
sourceMaps = true
}
webpackTask {
output.libraryTarget = KotlinWebpackOutput.Target.VAR
output.library = "vstChipWorklet"
}
distribution {
outputDirectory.set(File("$projectDir/../web/"))
}
}
}
jvm()
sourceSets {
val commonMain by getting {
dependencies {
implementation(project(":common"))
}
}
val jsMain by getting {
dependencies {
implementation(project(":common"))
}
}
}
}

View File

@@ -0,0 +1,5 @@
apply(from = "../settings.common.gradle.kts")
include(":common")
project(":common").projectDir = file("../common")

View File

@@ -0,0 +1,43 @@
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

@@ -0,0 +1,191 @@
@file:OptIn(ExperimentalJsExport::class)
package nl.astraeus.vst.chip
import nl.astraeus.vst.AudioWorkletProcessor
import nl.astraeus.vst.Note
import nl.astraeus.vst.registerProcessor
import nl.astraeus.vst.sampleRate
import org.khronos.webgl.ArrayBuffer
import org.khronos.webgl.Float32Array
import org.khronos.webgl.Int32Array
import org.khronos.webgl.get
import org.khronos.webgl.set
import org.w3c.dom.MessageEvent
import kotlin.math.PI
import kotlin.math.sin
val POLYPHONICS = 10
val PI2 = PI * 2
@ExperimentalJsExport
@JsExport
enum class NoteState {
ON,
RELEASED,
OFF
}
@ExperimentalJsExport
@JsExport
class PlayingNote(
val note: Int,
var velocity: Int = 0
) {
fun retrigger(velocity: Int) {
this.velocity = velocity
state = NoteState.ON
sample = 0
attackSamples = 2500
releaseSamples = 10000
}
var state = NoteState.OFF
var cycleOffset = 0.0
var sample = 0
var attackSamples = 2500
var releaseSamples = 10000
var actualVolume = 0f
}
@ExperimentalJsExport
@JsExport
class VstChipProcessor : AudioWorkletProcessor() {
val notes = Array(POLYPHONICS) {
PlayingNote(
0
)
}
init {
this.port.onmessage = ::handleMessage
Note.updateSampleRate(sampleRate)
}
private fun handleMessage(message: MessageEvent) {
//console.log("VstChipProcessor: Received message", message)
val data = message.data
when (data) {
"test_on" -> {
playMidi(Int32Array(arrayOf(0x90, 60, 64)))
}
"test_off" -> {
playMidi(Int32Array(arrayOf(0x90, 60, 0)))
}
is String -> {
}
is ArrayBuffer -> {
}
is Int32Array -> {
playMidi(data)
}
else ->
console.error("Don't kow how to handle message", message)
}
}
private fun playMidi(bytes: Int32Array) {
if (bytes.length > 0) {
when(bytes[0]) {
0x90 -> {
if (bytes.length == 3) {
val note = bytes[1]
val velocity = bytes[2]
if (velocity > 0) {
noteOn(note, velocity)
} else {
noteOff(note)
}
}
}
0x90 -> {
}
}
}
}
private fun noteOn(note: Int, velocity: Int) {
for (i in 0 until POLYPHONICS) {
if (notes[i].note == note) {
notes[i].retrigger(velocity)
//console.log("Note retriggered", notes[i])
return
}
}
for (i in 0 until POLYPHONICS) {
if (notes[i].state == NoteState.OFF) {
notes[i] = PlayingNote(
note,
velocity
)
notes[i].state = NoteState.ON
console.log("Playing note", notes[i])
break
}
}
}
private fun noteOff(note: Int) {
for (i in 0 until POLYPHONICS) {
if (notes[i].note == note && notes[i].state == NoteState.ON) {
notes[i].state = NoteState.RELEASED
//console.log("Released note", notes[i])
break
}
}
}
override fun process (
inputs: Array<Array<Float32Array>>,
outputs: Array<Array<Float32Array>>,
parameters: dynamic
) : Boolean {
val samples = outputs[0][0].length
val left = outputs[0][0]
val right = outputs[0][1]
for (note in notes) {
if (note.state != NoteState.OFF) {
val sampleDelta = Note.fromMidi(note.note).sampleDelta
for (i in 0 until samples) {
var targetVolume = note.velocity / 127f
if (note.state == NoteState.ON && note.sample < note.attackSamples) {
note.attackSamples--
targetVolume *= ( 1f - (note.attackSamples / 2500f))
} else if (note.state == NoteState.RELEASED) {
note.releaseSamples--
targetVolume *= (note.releaseSamples / 10000f)
}
note.actualVolume += (targetVolume - note.actualVolume) * 0.01f
if (note.state == NoteState.RELEASED && note.actualVolume <= 0) {
note.state = NoteState.OFF
}
left[i] = left[i] + sin(note.cycleOffset * PI2).toFloat() * note.actualVolume
right[i] = right[i] + sin(note.cycleOffset * PI2).toFloat() * note.actualVolume
note.cycleOffset += sampleDelta
if (note.cycleOffset > 1f) {
note.cycleOffset -= 1f
}
}
}
}
return true
}
}
fun main() {
registerProcessor("vst-chip-processor", VstChipProcessor::class.js)
println("VstChipProcessor registered!")
}