From 94dec1f6367c0280481166f2a648effcfac955b2 Mon Sep 17 00:00:00 2001 From: rnentjes Date: Mon, 17 Jun 2024 21:06:39 +0200 Subject: [PATCH] Listen to midi --- .../nl/astraeus/vst/chip/ChipProcessor.kt | 29 ++++-- build.gradle.kts | 1 - .../commonMain/kotlin/nl/astraeus/vst/Note.kt | 39 ++++---- .../kotlin/nl/astraeus/vst/chip/Main.kt | 3 + .../kotlin/nl/astraeus/vst/chip/midi/Midi.kt | 89 +++++++++++++++++++ .../nl/astraeus/vst/chip/view/MainView.kt | 45 +++++----- 6 files changed, 160 insertions(+), 46 deletions(-) create mode 100644 src/jsMain/kotlin/nl/astraeus/vst/chip/midi/Midi.kt diff --git a/audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/chip/ChipProcessor.kt b/audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/chip/ChipProcessor.kt index c169aa8..3721444 100644 --- a/audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/chip/ChipProcessor.kt +++ b/audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/chip/ChipProcessor.kt @@ -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) { diff --git a/build.gradle.kts b/build.gradle.kts index d9de1d9..085b2af 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -23,7 +23,6 @@ kotlin { browser { commonWebpackConfig { outputFileName = "vst-chip-worklet-ui.js" - //cssSupport.enabled = true sourceMaps = true } diff --git a/common/src/commonMain/kotlin/nl/astraeus/vst/Note.kt b/common/src/commonMain/kotlin/nl/astraeus/vst/Note.kt index 9bdea86..c4507ab 100644 --- a/common/src/commonMain/kotlin/nl/astraeus/vst/Note.kt +++ b/common/src/commonMain/kotlin/nl/astraeus/vst/Note.kt @@ -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 diff --git a/src/jsMain/kotlin/nl/astraeus/vst/chip/Main.kt b/src/jsMain/kotlin/nl/astraeus/vst/chip/Main.kt index f2fb097..6bee45c 100644 --- a/src/jsMain/kotlin/nl/astraeus/vst/chip/Main.kt +++ b/src/jsMain/kotlin/nl/astraeus/vst/chip/Main.kt @@ -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() } diff --git a/src/jsMain/kotlin/nl/astraeus/vst/chip/midi/Midi.kt b/src/jsMain/kotlin/nl/astraeus/vst/chip/midi/Midi.kt new file mode 100644 index 0000000..4211319 --- /dev/null +++ b/src/jsMain/kotlin/nl/astraeus/vst/chip/midi/Midi.kt @@ -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() + var outputs = mutableListOf() + 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() + } + +} diff --git a/src/jsMain/kotlin/nl/astraeus/vst/chip/view/MainView.kt b/src/jsMain/kotlin/nl/astraeus/vst/chip/view/MainView.kt index 5015dad..9459376 100644 --- a/src/jsMain/kotlin/nl/astraeus/vst/chip/view/MainView.kt +++ b/src/jsMain/kotlin/nl/astraeus/vst/chip/view/MainView.kt @@ -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 = 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) {