Also search on name when setting midi port
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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!")
|
||||
}
|
||||
@@ -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!")
|
||||
}
|
||||
Reference in New Issue
Block a user