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.ArrayBuffer
import org.khronos.webgl.Float32Array import org.khronos.webgl.Float32Array
import org.khronos.webgl.Int32Array import org.khronos.webgl.Int32Array
import org.khronos.webgl.Uint8Array
import org.khronos.webgl.get import org.khronos.webgl.get
import org.khronos.webgl.set import org.khronos.webgl.set
import org.w3c.dom.MessageEvent import org.w3c.dom.MessageEvent
@@ -79,6 +80,13 @@ class VstChipProcessor : AudioWorkletProcessor() {
} }
is ArrayBuffer -> { 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 -> { is Int32Array -> {
playMidi(data) 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) { for (i in 0 until POLYPHONICS) {
if (notes[i].note == note) { if (notes[i].note == note) {
notes[i].retrigger(velocity) notes[i].retrigger(velocity)
//console.log("Note retriggered", notes[i])
return return
} }
} }
@@ -124,7 +135,9 @@ class VstChipProcessor : AudioWorkletProcessor() {
velocity velocity
) )
notes[i].state = NoteState.ON 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 break
} }
} }
@@ -134,7 +147,6 @@ class VstChipProcessor : AudioWorkletProcessor() {
for (i in 0 until POLYPHONICS) { for (i in 0 until POLYPHONICS) {
if (notes[i].note == note && notes[i].state == NoteState.ON) { if (notes[i].note == note && notes[i].state == NoteState.ON) {
notes[i].state = NoteState.RELEASED notes[i].state = NoteState.RELEASED
//console.log("Released note", notes[i])
break break
} }
} }
@@ -163,14 +175,17 @@ class VstChipProcessor : AudioWorkletProcessor() {
note.releaseSamples-- note.releaseSamples--
targetVolume *= (note.releaseSamples / 10000f) 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) { if (note.state == NoteState.RELEASED && note.actualVolume <= 0) {
note.state = NoteState.OFF note.state = NoteState.OFF
} }
left[i] = left[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 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 note.cycleOffset += sampleDelta
if (note.cycleOffset > 1f) { if (note.cycleOffset > 1f) {

View File

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

View File

@@ -3,6 +3,7 @@ package nl.astraeus.vst
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.round
/** /**
* User: rnentjes * User: rnentjes
@@ -15,17 +16,17 @@ enum class Note(
val flat: String val flat: String
) { ) {
NONE("---", "---"), NONE("---", "---"),
NO02("",""), No02("C--","C--"),
NO03("",""), NO03("C#-","Db-"),
NO04("",""), NO04("D--","D--"),
NO05("",""), NO05("D#-","Eb-"),
NO06("",""), NO06("E--","E--"),
NO07("",""), NO07("F--","F--"),
NO08("",""), NO08("F#-","Gb-"),
NO09("",""), NO09("G--","G--"),
NO10("",""), NO10("G#-","Ab-"),
NO11("",""), NO11("A#-","Bb-"),
NO12("",""), NO12("B--","B--"),
C0("C-0","C-0"), C0("C-0","C-0"),
C0s("C#0","Db0"), C0s("C#0","Db0"),
D0("D-0","D-0"), D0("D-0","D-0"),
@@ -152,7 +153,7 @@ enum class Note(
; ;
// 69 = A4.ordinal // 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 val cycleLength: Double = 1.0 / freq
var sampleDelta: Double = 0.0 var sampleDelta: Double = 0.0
@@ -162,14 +163,12 @@ enum class Note(
result = min(result, G9.ordinal) result = min(result, G9.ordinal)
result = max(result, C0.ordinal) result = max(result, C0.ordinal)
entries.firstOrNull { it.ordinal == result } ?: this fromMidi(result)
} else { } else {
this this
} }
companion object { companion object {
var sampleRate: Int = 44100
fun fromMidi(midi: Int): Note { fun fromMidi(midi: Int): Note {
// todo: add check // todo: add check
return entries[midi] return entries[midi]
@@ -177,10 +176,16 @@ enum class Note(
fun updateSampleRate(rate: Int) { fun updateSampleRate(rate: Int) {
println("Setting sample rate to $rate") println("Setting sample rate to $rate")
sampleRate = rate
for (note in Note.entries) { 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 kotlinx.browser.document
import nl.astraeus.komp.Komponent import nl.astraeus.komp.Komponent
import nl.astraeus.vst.chip.channel.Broadcaster import nl.astraeus.vst.chip.channel.Broadcaster
import nl.astraeus.vst.chip.midi.Midi
import nl.astraeus.vst.chip.view.MainView import nl.astraeus.vst.chip.view.MainView
fun main() { fun main() {
Komponent.create(document.body!!, MainView) Komponent.create(document.body!!, MainView)
Broadcaster.start() 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.CssName
import daw.style.hover import daw.style.hover
import kotlinx.html.FlowContent import kotlinx.html.FlowContent
import kotlinx.html.P
import kotlinx.html.a import kotlinx.html.a
import kotlinx.html.br
import kotlinx.html.classes import kotlinx.html.classes
import kotlinx.html.div import kotlinx.html.div
import kotlinx.html.h1 import kotlinx.html.h1
import kotlinx.html.hr import kotlinx.html.hr
import kotlinx.html.js.onChangeFunction
import kotlinx.html.js.onClickFunction import kotlinx.html.js.onClickFunction
import kotlinx.html.js.onMouseDownFunction import kotlinx.html.js.onMouseDownFunction
import kotlinx.html.js.onMouseUpFunction import kotlinx.html.js.onMouseUpFunction
import kotlinx.html.option
import kotlinx.html.select
import kotlinx.html.span import kotlinx.html.span
import nl.astraeus.css.properties.BoxSizing import nl.astraeus.css.properties.BoxSizing
import nl.astraeus.css.properties.FontWeight import nl.astraeus.css.properties.FontWeight
@@ -27,7 +32,9 @@ import nl.astraeus.komp.Komponent
import nl.astraeus.vst.Note import nl.astraeus.vst.Note
import nl.astraeus.vst.chip.audio.VstChipWorklet import nl.astraeus.vst.chip.audio.VstChipWorklet
import nl.astraeus.vst.chip.channel.Broadcaster import nl.astraeus.vst.chip.channel.Broadcaster
import nl.astraeus.vst.chip.midi.Midi
import org.khronos.webgl.Int32Array import org.khronos.webgl.Int32Array
import org.w3c.dom.HTMLSelectElement
object MainView : Komponent() { object MainView : Komponent() {
private var messages: MutableList<String> = ArrayList() private var messages: MutableList<String> = ArrayList()
@@ -68,31 +75,27 @@ object MainView : Komponent() {
} }
} }
div { div {
a { + "Midi input: "
href = "#" select {
+"Send broadcast test message" for (mi in Midi.inputs) {
onClickFunction = { option {
Broadcaster.send("Test message") +mi.name
} value = mi.id
} }
} }
div {
a { onChangeFunction = { event ->
href = "#" val target = event.target as HTMLSelectElement
+"Note on" val selected = Midi.inputs.find { it.id == target.value }
onClickFunction = { if (selected != null) {
VstChipWorklet.postMessage("test_on") Midi.setInput(selected)
} }
}
a {
href = "#"
+"Note off"
onClickFunction = {
VstChipWorklet.postMessage("test_off")
} }
} }
} }
br {}
hr {} hr {}
repeat(9) { repeat(9) {