Also search on name when setting midi port

This commit is contained in:
2024-07-13 16:44:33 +02:00
parent f2269c8865
commit 8df6a4fff6
45 changed files with 2387 additions and 934 deletions

View File

@@ -21,13 +21,13 @@ kotlin {
browser {
commonWebpackConfig {
outputFileName = "vst-chip-worklet.js"
outputFileName = "vst-string-worklet.js"
sourceMaps = true
}
webpackTask {
output.libraryTarget = KotlinWebpackOutput.Target.VAR
output.library = "vstChipWorklet"
output.library = "vstStringWorklet"
}
distribution {

View File

@@ -1,393 +0,0 @@
@file:OptIn(ExperimentalJsExport::class)
package nl.astraeus.vst.chip
import nl.astraeus.vst.ADSR
import nl.astraeus.vst.AudioWorkletProcessor
import nl.astraeus.vst.Note
import nl.astraeus.vst.currentTime
import nl.astraeus.vst.registerProcessor
import nl.astraeus.vst.sampleRate
import org.khronos.webgl.Float32Array
import org.khronos.webgl.Int32Array
import org.khronos.webgl.Uint8Array
import org.khronos.webgl.get
import org.khronos.webgl.set
import org.w3c.dom.MessageEvent
import kotlin.math.PI
import kotlin.math.min
import kotlin.math.sin
val POLYPHONICS = 10
val PI2 = PI * 2
@ExperimentalJsExport
@JsExport
class PlayingNote(
val note: Int,
var velocity: Int = 0
) {
fun retrigger(velocity: Int) {
this.velocity = velocity
sample = 0
noteStart = currentTime
noteRelease = null
}
var noteStart = currentTime
var noteRelease: Double? = null
var cycleOffset = 0.0
var sample = 0
var actualVolume = 0f
}
enum class Waveform {
SINE,
SQUARE,
TRIANGLE,
SAWTOOTH
}
@ExperimentalJsExport
@JsExport
enum class RecordingState {
STOPPED,
WAITING_TO_START,
RECORDING
}
@ExperimentalJsExport
@JsExport
class VstChipProcessor : AudioWorkletProcessor() {
var midiChannel = 0
val notes = Array<PlayingNote?>(POLYPHONICS) { null }
var waveform = Waveform.SINE.ordinal
var volume = 0.75f
var dutyCycle = 0.5
var fmFreq = 0.0
var fmAmp = 0.0
var amFreq = 0.0
var amAmp = 0.0
var attack = 0.1
var decay = 0.2
var sustain = 0.5
var release = 0.2
val sampleLength = 1 / sampleRate.toDouble()
val recordingBuffer = Float32Array(sampleRate / 60)
var recordingState = RecordingState.STOPPED
var recordingSample = 0
var recordingStart = 0
init {
this.port.onmessage = ::handleMessage
Note.updateSampleRate(sampleRate)
}
private fun handleMessage(message: MessageEvent) {
//console.log("VstChipProcessor: Received message:", message.data)
val data = message.data
try {
when (data) {
is String -> {
when(data) {
"start_recording" -> {
port.postMessage(recordingBuffer)
if (recordingState == RecordingState.STOPPED) {
recordingState = RecordingState.WAITING_TO_START
recordingSample = 0
}
}
else ->
if (data.startsWith("set_channel")) {
val parts = data.split('\n')
if (parts.size == 2) {
midiChannel = parts[1].toInt()
println("Setting channel: $midiChannel")
}
} else if (data.startsWith("waveform")) {
val parts = data.split('\n')
if (parts.size == 2) {
waveform =parts[1].toInt()
println("Setting waveform: $waveform")
}
}
}
}
is Uint8Array -> {
val data32 = Int32Array(data.length)
for (i in 0 until data.length) {
data32[i] = (data[i].toInt() and 0xff)
}
playMidi(data32)
}
is Int32Array -> {
playMidi(data)
}
else ->
console.error("Don't kow how to handle message", message)
}
} catch(e: Exception) {
console.log(e.message, e)
}
}
private fun playMidi(bytes: Int32Array) {
console.log("playMidi", bytes)
if (bytes.length > 0) {
var cmdByte = bytes[0]
val channelCmd = ((cmdByte shr 4) and 0xf) != 0xf0
val channel = cmdByte and 0xf
println("Channel cmd: $channelCmd")
if (channelCmd && channel != midiChannel) {
console.log("Wrong channel", midiChannel, bytes)
return
}
cmdByte = cmdByte and 0xf0
//console.log("Received", bytes)
when(cmdByte) {
0x90 -> {
if (bytes.length == 3) {
val note = bytes[1]
val velocity = bytes[2]
if (velocity > 0) {
noteOn(note, velocity)
} else {
noteOff(note)
}
}
}
0x80 -> {
if (bytes.length >= 2) {
val note = bytes[1]
noteOff(note)
}
}
0xc9 -> {
if (bytes.length >= 1) {
val waveform = bytes[1]
if (waveform < 4) {
this.waveform = waveform
}
}
}
0xb0 -> {
if (bytes.length == 3) {
val knob = bytes[1]
val value = bytes[2]
when (knob) {
7 -> {
volume = value / 127f
}
0x47 -> {
dutyCycle = value / 127.0
}
0x4a -> {
fmFreq = value / 127.0
}
0x4b -> {
fmAmp = value / 127.0
}
0x4c -> {
amFreq = value / 127.0
}
0x4d -> {
amAmp = value / 127.0
}
0x49 -> {
attack = value / 127.0
}
0x4b -> {
decay = value / 127.0
}
0x46 -> {
sustain = value / 127.0
}
0x48 -> {
release = value / 127.0
}
123 -> {
for (note in notes) {
note?.noteRelease = currentTime
}
}
}
}
}
0xe0 -> {
if (bytes.length == 3) {
val lsb = bytes[1]
val msb = bytes[2]
amFreq = (((msb - 0x40) + (lsb / 127.0)) / 0x40) * 10.0
}
}
}
}
}
private fun noteOn(note: Int, velocity: Int) {
for (i in 0 until POLYPHONICS) {
if (notes[i]?.note == note) {
notes[i]?.retrigger(velocity)
return
}
}
for (i in 0 until POLYPHONICS) {
if (notes[i] == null) {
notes[i] = PlayingNote(
note,
velocity
)
break
}
}
}
private fun noteOff(note: Int) {
for (i in 0 until POLYPHONICS) {
if (notes[i]?.note == note) {
notes[i]?.noteRelease = currentTime
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]
var lowestNote = 200
for (note in notes) {
if (note != null) {
lowestNote = min(lowestNote, note.note)
}
}
if (lowestNote == 200 && recordingState == RecordingState.WAITING_TO_START) {
recordingState = RecordingState.RECORDING
recordingSample = 0
recordingStart = 0
}
for ((index, note) in notes.withIndex()) {
if (note != null) {
val sampleDelta = Note.fromMidi(note.note).sampleDelta
for (i in 0 until samples) {
var targetVolume = note.velocity / 127f
targetVolume *= ADSR.calculate(
attack,
decay,
sustain,
release,
note.noteStart,
currentTime,
note.noteRelease
).toFloat()
note.actualVolume += (targetVolume - note.actualVolume) * 0.01f
if (note.noteRelease != null && note.actualVolume <= 0.01) {
notes[index] = null
}
var cycleOffset = note.cycleOffset
val fmModulation = sampleDelta * sin( fmFreq * 20f * PI2 * (note.sample / sampleRate.toDouble())).toFloat() * fmAmp
val amModulation = 1f + (sin(sampleLength * amFreq * 10f * PI2 * note.sample) * amAmp).toFloat()
cycleOffset = if (cycleOffset < dutyCycle) {
cycleOffset / dutyCycle / 2.0
} else {
0.5 + ((cycleOffset -dutyCycle) / (1.0 - dutyCycle) / 2.0)
}
val waveValue: Float = when (waveform) {
0 -> {
sin(cycleOffset * PI2).toFloat()
}
1 -> {
if (cycleOffset < 0.5) { 1f } else { -1f }
}
2 -> when {
cycleOffset < 0.25 -> 4 * cycleOffset
cycleOffset < 0.75 -> 2 - 4 * cycleOffset
else -> 4 * cycleOffset - 4
}.toFloat()
3 -> {
((cycleOffset * 2f) - 1f).toFloat()
}
else -> {
if (cycleOffset < 0.5) { 1f } else { -1f }
}
}
left[i] = left[i] + waveValue * note.actualVolume * volume * amModulation
right[i] = right[i] + waveValue * note.actualVolume * volume * amModulation
note.cycleOffset += sampleDelta + fmModulation
if (note.cycleOffset > 1f) {
note.cycleOffset -= 1f
if (note.note == lowestNote && recordingState == RecordingState.WAITING_TO_START) {
recordingState = RecordingState.RECORDING
recordingSample = 0
recordingStart = i
}
}
note.sample++
}
}
}
if (recordingState == RecordingState.RECORDING) {
for (i in recordingStart until samples) {
recordingBuffer[recordingSample] = (left[i] + right[i]) / 2f
if (recordingSample < recordingBuffer.length - 1) {
recordingSample++
} else {
recordingState = RecordingState.STOPPED
}
}
recordingStart = 0
}
return true
}
}
fun main() {
registerProcessor("vst-chip-processor", VstChipProcessor::class.js)
println("VstChipProcessor registered!")
}

View File

@@ -0,0 +1,264 @@
@file:OptIn(ExperimentalJsExport::class)
package nl.astraeus.vst.string
import nl.astraeus.vst.AudioWorkletProcessor
import nl.astraeus.vst.Note
import nl.astraeus.vst.currentTime
import nl.astraeus.vst.registerProcessor
import nl.astraeus.vst.sampleRate
import org.khronos.webgl.Float32Array
import org.khronos.webgl.Int32Array
import org.khronos.webgl.Uint8Array
import org.khronos.webgl.get
import org.khronos.webgl.set
import org.w3c.dom.MessageEvent
import kotlin.math.PI
import kotlin.math.min
val POLYPHONICS = 10
val PI2 = PI * 2
@ExperimentalJsExport
@JsExport
class PlayingNote(
val note: Int,
var velocity: Int = 0
) {
fun retrigger(velocity: Int) {
this.velocity = velocity
sample = 0
noteStart = currentTime
noteRelease = null
}
var noteStart = currentTime
var noteRelease: Double? = null
var cycleOffset = 0.0
var sample = 0
var actualVolume = 0f
}
@ExperimentalJsExport
@JsExport
enum class RecordingState {
STOPPED,
WAITING_TO_START,
RECORDING
}
@ExperimentalJsExport
@JsExport
class VstStringProcessor : AudioWorkletProcessor() {
var midiChannel = 0
var volume = 0.75f
var damping = 0.996
val recordingBuffer = Float32Array(sampleRate / 60)
var recordingState = RecordingState.STOPPED
var recordingSample = 0
var recordingStart = 0
val strings = Array(POLYPHONICS) {
PhysicalString(sampleRate, damping)
}
init {
this.port.onmessage = ::handleMessage
Note.updateSampleRate(sampleRate)
}
private fun handleMessage(message: MessageEvent) {
//console.log("VstChipProcessor: Received message:", message.data)
val data = message.data
try {
when (data) {
is String -> {
when (data) {
"start_recording" -> {
port.postMessage(recordingBuffer)
if (recordingState == RecordingState.STOPPED) {
recordingState = RecordingState.WAITING_TO_START
recordingSample = 0
}
}
else ->
if (data.startsWith("set_channel")) {
val parts = data.split('\n')
if (parts.size == 2) {
midiChannel = parts[1].toInt()
println("Setting channel: $midiChannel")
}
}
}
}
is Uint8Array -> {
val data32 = Int32Array(data.length)
for (i in 0 until data.length) {
data32[i] = (data[i].toInt() and 0xff)
}
playMidi(data32)
}
is Int32Array -> {
playMidi(data)
}
else ->
console.error("Don't kow how to handle message", message)
}
} catch (e: Exception) {
console.log(e.message, e)
}
}
private fun playMidi(bytes: Int32Array) {
console.log("playMidi", bytes)
if (bytes.length > 0) {
var cmdByte = bytes[0]
val channelCmd = ((cmdByte shr 4) and 0xf) != 0xf0
val channel = cmdByte and 0xf
println("Channel cmd: $channelCmd")
if (channelCmd && channel != midiChannel) {
console.log("Wrong channel", midiChannel, bytes)
return
}
cmdByte = cmdByte and 0xf0
//console.log("Received", bytes)
when (cmdByte) {
0x90 -> {
if (bytes.length == 3) {
val note = bytes[1]
val velocity = bytes[2]
if (velocity > 0) {
noteOn(note, velocity)
} else {
noteOff(note)
}
}
}
0x80 -> {
if (bytes.length >= 2) {
val note = bytes[1]
noteOff(note)
}
}
0xb0 -> {
if (bytes.length == 3) {
val knob = bytes[1]
val value = bytes[2]
when (knob) {
7 -> {
volume = value / 127f
}
0x47 -> {
damping = 0.8 + value / 127.0
}
}
}
}
}
}
}
private fun noteOn(midiNote: Int, velocity: Int) {
val note = Note.fromMidi(midiNote)
for (i in 0 until POLYPHONICS) {
if (strings[i].currentNote == note) {
strings[i].pluck(note, velocity / 127.0)
strings[i].damping = damping
return
}
}
for (i in 0 until POLYPHONICS) {
if (strings[i].available) {
strings[i].pluck(note, velocity / 127.0)
strings[i].damping = damping
break
}
}
}
private fun noteOff(midiNote: Int) {
val note = Note.fromMidi(midiNote)
for (i in 0 until POLYPHONICS) {
if (strings[i].currentNote.ordinal == note.ordinal) {
strings[i].available = true
strings[i].damping = damping * 0.9
}
}
}
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]
var lowestNote = 200
for (string in strings) {
if (string.available) {
lowestNote = min(lowestNote, string.currentNote.ordinal)
}
}
if (lowestNote == 200 && recordingState == RecordingState.WAITING_TO_START) {
recordingState = RecordingState.RECORDING
recordingSample = 0
recordingStart = 0
}
for (string in strings) {
for (i in 0 until samples) {
val waveValue: Float = string.tick().toFloat()
left[i] = left[i] + waveValue * volume
right[i] = right[i] + waveValue * volume
if (lowestNote == string.currentNote.ordinal && recordingState == RecordingState.WAITING_TO_START && string.index == 0) {
recordingState = RecordingState.RECORDING
recordingSample = 0
recordingStart = 0
}
}
}
if (recordingState == RecordingState.RECORDING) {
for (i in recordingStart until samples) {
recordingBuffer[recordingSample] = (left[i] + right[i]) / 2f
if (recordingSample < recordingBuffer.length - 1) {
recordingSample++
} else {
recordingState = RecordingState.STOPPED
}
}
recordingStart = 0
}
return true
}
}
fun main() {
registerProcessor("vst-string-processor", VstStringProcessor::class.js)
println("VstStringProcessor registered!")
}