Initial commit
This commit is contained in:
48
audio-worklet/build.gradle.kts
Normal file
48
audio-worklet/build.gradle.kts
Normal 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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
5
audio-worklet/settings.gradle.kts
Normal file
5
audio-worklet/settings.gradle.kts
Normal file
@@ -0,0 +1,5 @@
|
||||
apply(from = "../settings.common.gradle.kts")
|
||||
|
||||
include(":common")
|
||||
|
||||
project(":common").projectDir = file("../common")
|
||||
43
audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/Externals.kt
Normal file
43
audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/Externals.kt
Normal 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
|
||||
@@ -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!")
|
||||
}
|
||||
Reference in New Issue
Block a user