Initial commit
This commit is contained in:
@@ -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