Listen to midi

This commit is contained in:
2024-06-17 21:06:39 +02:00
parent 68b7ffffa8
commit 94dec1f636
6 changed files with 160 additions and 46 deletions

View File

@@ -9,6 +9,7 @@ import nl.astraeus.vst.sampleRate
import org.khronos.webgl.ArrayBuffer
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
@@ -79,6 +80,13 @@ class VstChipProcessor : AudioWorkletProcessor() {
}
is ArrayBuffer -> {
}
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)
}
@@ -102,8 +110,12 @@ class VstChipProcessor : AudioWorkletProcessor() {
}
}
}
0x90 -> {
0x80 -> {
if (bytes.length >= 2) {
val note = bytes[1]
noteOff(note)
}
}
}
}
@@ -113,7 +125,6 @@ class VstChipProcessor : AudioWorkletProcessor() {
for (i in 0 until POLYPHONICS) {
if (notes[i].note == note) {
notes[i].retrigger(velocity)
//console.log("Note retriggered", notes[i])
return
}
}
@@ -124,7 +135,9 @@ class VstChipProcessor : AudioWorkletProcessor() {
velocity
)
notes[i].state = NoteState.ON
console.log("Playing note", notes[i])
val n = Note.fromMidi(note)
console.log("Playing note: ${n.sharp} (${n.freq})")
break
}
}
@@ -134,7 +147,6 @@ class VstChipProcessor : AudioWorkletProcessor() {
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
}
}
@@ -163,14 +175,17 @@ class VstChipProcessor : AudioWorkletProcessor() {
note.releaseSamples--
targetVolume *= (note.releaseSamples / 10000f)
}
note.actualVolume += (targetVolume - note.actualVolume) * 0.01f
note.actualVolume += (targetVolume - note.actualVolume) * 0.005f
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
left[i] = left[i] + sin(note.cycleOffset * PI2).toFloat() * note.actualVolume * 0.3f
right[i] = right[i] + sin(note.cycleOffset * PI2).toFloat() * note.actualVolume * 0.3f
//left[i] = left[i] + if (note.cycleOffset < 0.5) { 0.3f } else { -0.3f } * note.actualVolume //sin(note.cycleOffset * PI2).toFloat() * note.actualVolume * 0.3f
//right[i] = right[i] + if (note.cycleOffset < 0.5) { 0.3f } else { -0.3f } * note.actualVolume //sin(note.cycleOffset * PI2).toFloat() * note.actualVolume * 0.3f
note.cycleOffset += sampleDelta
if (note.cycleOffset > 1f) {

View File

@@ -23,7 +23,6 @@ kotlin {
browser {
commonWebpackConfig {
outputFileName = "vst-chip-worklet-ui.js"
//cssSupport.enabled = true
sourceMaps = true
}

View File

@@ -3,6 +3,7 @@ package nl.astraeus.vst
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
import kotlin.math.round
/**
* User: rnentjes
@@ -15,17 +16,17 @@ enum class Note(
val flat: String
) {
NONE("---", "---"),
NO02("",""),
NO03("",""),
NO04("",""),
NO05("",""),
NO06("",""),
NO07("",""),
NO08("",""),
NO09("",""),
NO10("",""),
NO11("",""),
NO12("",""),
No02("C--","C--"),
NO03("C#-","Db-"),
NO04("D--","D--"),
NO05("D#-","Eb-"),
NO06("E--","E--"),
NO07("F--","F--"),
NO08("F#-","Gb-"),
NO09("G--","G--"),
NO10("G#-","Ab-"),
NO11("A#-","Bb-"),
NO12("B--","B--"),
C0("C-0","C-0"),
C0s("C#0","Db0"),
D0("D-0","D-0"),
@@ -152,7 +153,7 @@ enum class Note(
;
// 69 = A4.ordinal
val freq: Double = 440.0 * 2.0.pow((ordinal - 69)/12.0)
val freq: Double = round(440.0 * 2.0.pow((ordinal - 69)/12.0) * 100.0) / 100.0
val cycleLength: Double = 1.0 / freq
var sampleDelta: Double = 0.0
@@ -162,14 +163,12 @@ enum class Note(
result = min(result, G9.ordinal)
result = max(result, C0.ordinal)
entries.firstOrNull { it.ordinal == result } ?: this
fromMidi(result)
} else {
this
}
companion object {
var sampleRate: Int = 44100
fun fromMidi(midi: Int): Note {
// todo: add check
return entries[midi]
@@ -177,10 +176,16 @@ enum class Note(
fun updateSampleRate(rate: Int) {
println("Setting sample rate to $rate")
sampleRate = rate
for (note in Note.entries) {
note.sampleDelta = (1.0 / sampleRate.toDouble()) / note.cycleLength
note.sampleDelta = (1.0 / rate.toDouble()) / note.cycleLength
}
}
}
}
// freq = 10Hz
// cycleLength = 0.1
// sampleRate = 48000
// sampleDelta = 4800
// (1.0 / freq) * sampleRate

View File

@@ -3,10 +3,13 @@ package nl.astraeus.vst.chip
import kotlinx.browser.document
import nl.astraeus.komp.Komponent
import nl.astraeus.vst.chip.channel.Broadcaster
import nl.astraeus.vst.chip.midi.Midi
import nl.astraeus.vst.chip.view.MainView
fun main() {
Komponent.create(document.body!!, MainView)
Broadcaster.start()
Midi.start()
}

View File

@@ -0,0 +1,89 @@
package nl.astraeus.vst.chip.midi
import kotlinx.browser.window
import nl.astraeus.vst.chip.audio.VstChipWorklet
import nl.astraeus.vst.chip.view.MainView
external class MIDIInput {
val connection: String
val id: String
val manufacturer: String
val name: String
val state: String
val type: String
val version: String
var onmidimessage: (dynamic) -> Unit
var onstatechange: (dynamic) -> Unit
fun open()
fun close()
}
external class MIDIOutput {
val connection: String
val id: String
val manufacturer: String
val name: String
val state: String
val type: String
val version: String
fun send(message: dynamic)
}
object Midi {
var inputs = mutableListOf<MIDIInput>()
var outputs = mutableListOf<MIDIInput>()
var currentInput: MIDIInput? = null
fun start() {
val navigator = window.navigator.asDynamic()
navigator.requestMIDIAccess().then(
{ midiAccess ->
val inp = midiAccess.inputs
val outp = midiAccess.outputs
console.log("Midi inputs:", inputs)
console.log("Midi outputs:", outputs)
inp.forEach() { input ->
console.log("Midi input:", input)
inputs.add(input)
console.log("Name: ${(input as? MIDIInput)?.name}")
}
outp.forEach() { output ->
console.log("Midi output:", output)
outputs.add(output)
}
MainView.requestUpdate()
},
{ e ->
println("Failed to get MIDI access - $e")
}
)
}
fun setInput(input: MIDIInput) {
console.log("Setting input", input)
currentInput?.close()
currentInput = input
currentInput?.onstatechange = { message ->
console.log("State change:", message)
}
currentInput?.onmidimessage = { message ->
console.log("Midi message:", message)
VstChipWorklet.postMessage(
message.data
)
}
currentInput?.open()
}
}

View File

@@ -7,14 +7,19 @@ import daw.style.CssId
import daw.style.CssName
import daw.style.hover
import kotlinx.html.FlowContent
import kotlinx.html.P
import kotlinx.html.a
import kotlinx.html.br
import kotlinx.html.classes
import kotlinx.html.div
import kotlinx.html.h1
import kotlinx.html.hr
import kotlinx.html.js.onChangeFunction
import kotlinx.html.js.onClickFunction
import kotlinx.html.js.onMouseDownFunction
import kotlinx.html.js.onMouseUpFunction
import kotlinx.html.option
import kotlinx.html.select
import kotlinx.html.span
import nl.astraeus.css.properties.BoxSizing
import nl.astraeus.css.properties.FontWeight
@@ -27,7 +32,9 @@ import nl.astraeus.komp.Komponent
import nl.astraeus.vst.Note
import nl.astraeus.vst.chip.audio.VstChipWorklet
import nl.astraeus.vst.chip.channel.Broadcaster
import nl.astraeus.vst.chip.midi.Midi
import org.khronos.webgl.Int32Array
import org.w3c.dom.HTMLSelectElement
object MainView : Komponent() {
private var messages: MutableList<String> = ArrayList()
@@ -68,31 +75,27 @@ object MainView : Komponent() {
}
}
div {
a {
href = "#"
+"Send broadcast test message"
onClickFunction = {
Broadcaster.send("Test message")
}
}
}
div {
a {
href = "#"
+"Note on"
onClickFunction = {
VstChipWorklet.postMessage("test_on")
}
}
a {
href = "#"
+"Note off"
onClickFunction = {
VstChipWorklet.postMessage("test_off")
+ "Midi input: "
select {
for (mi in Midi.inputs) {
option {
+mi.name
value = mi.id
}
}
onChangeFunction = { event ->
val target = event.target as HTMLSelectElement
val selected = Midi.inputs.find { it.id == target.value }
if (selected != null) {
Midi.setInput(selected)
}
}
}
}
br {}
hr {}
repeat(9) {